gitlab-org--gitlab-foss/app/models/ci/job_trace_chunk.rb

146 lines
3.1 KiB
Ruby
Raw Normal View History

2018-03-26 11:45:18 +00:00
module Ci
class JobTraceChunk < ActiveRecord::Base
extend Gitlab::Ci::Model
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
2018-04-04 10:19:17 +00:00
default_value_for :data_store, :redis
2018-04-06 15:08:35 +00:00
WriteError = Class.new(StandardError)
2018-04-05 11:39:35 +00:00
CHUNK_SIZE = 128.kilobytes
2018-04-06 13:43:12 +00:00
CHUNK_REDIS_TTL = 1.week
2018-04-06 15:08:35 +00:00
LOCK_RETRY = 100
LOCK_SLEEP = 1
LOCK_TTL = 5.minutes
2018-04-04 10:19:17 +00:00
enum data_store: {
redis: 1,
2018-04-05 15:57:05 +00:00
db: 2
2018-04-04 10:19:17 +00:00
}
def data
2018-04-05 15:57:05 +00:00
if redis?
2018-04-04 10:19:17 +00:00
redis_data
2018-04-05 15:57:05 +00:00
elsif db?
2018-04-04 10:19:17 +00:00
raw_data
else
raise 'Unsupported data store'
end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
2018-04-04 10:19:17 +00:00
end
def set_data(value)
raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
2018-04-06 15:08:35 +00:00
in_lock do
2018-04-06 15:08:35 +00:00
if redis?
redis_set_data(value)
elsif db?
self.raw_data = value
else
raise 'Unsupported data store'
end
save! if changed?
2018-04-04 10:19:17 +00:00
end
schedule_to_db if fullfilled?
2018-04-04 10:19:17 +00:00
end
def truncate(offset = 0)
self.append("", offset)
end
def append(new_data, offset)
current_data = self.data.to_s
raise ArgumentError, 'Offset is out of bound' if offset > current_data.bytesize || offset < 0
raise ArgumentError, 'Outside of chunk size' if CHUNK_SIZE < offset + new_data.bytesize
2018-04-04 10:19:17 +00:00
self.set_data(current_data.byteslice(0, offset) + new_data)
end
def size
data&.bytesize.to_i
end
def start_offset
chunk_index * CHUNK_SIZE
end
def end_offset
start_offset + size
end
def range
(start_offset...end_offset)
end
def use_database!
2018-04-06 15:08:35 +00:00
in_lock do
2018-04-23 06:20:55 +00:00
break if db?
break unless size > 0
2018-04-04 10:19:17 +00:00
2018-04-06 15:08:35 +00:00
self.update!(raw_data: data, data_store: :db)
redis_delete_data
end
2018-04-04 10:19:17 +00:00
end
private
def schedule_to_db
return if db?
BuildTraceSwapChunkWorker.perform_async(id)
2018-04-04 10:19:17 +00:00
end
def fullfilled?
size == CHUNK_SIZE
end
def redis_data
Gitlab::Redis::SharedState.with do |redis|
2018-04-06 15:08:35 +00:00
redis.get(redis_data_key)
2018-04-04 10:19:17 +00:00
end
end
def redis_set_data(data)
Gitlab::Redis::SharedState.with do |redis|
2018-04-06 15:08:35 +00:00
redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
2018-04-04 10:19:17 +00:00
end
end
def redis_delete_data
Gitlab::Redis::SharedState.with do |redis|
2018-04-06 15:08:35 +00:00
redis.del(redis_data_key)
2018-04-04 10:19:17 +00:00
end
end
2018-04-06 15:08:35 +00:00
def redis_data_key
"gitlab:ci:trace:#{job_id}:chunks:#{chunk_index}:data"
end
def redis_lock_key
"gitlab:ci:trace:#{job_id}:chunks:#{chunk_index}:lock"
end
def in_lock
lease = Gitlab::ExclusiveLease.new(redis_lock_key, timeout: LOCK_TTL)
retry_count = 0
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. To prevent hammering Redis too
# much we'll wait for a bit between retries.
sleep(LOCK_SLEEP)
break if LOCK_RETRY < (retry_count += 1)
end
raise WriteError, 'Failed to obtain write lock' unless uuid
self.reload if self.persisted?
return yield
ensure
Gitlab::ExclusiveLease.cancel(redis_lock_key, uuid)
2018-04-04 10:19:17 +00:00
end
2018-03-26 11:45:18 +00:00
end
end