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

186 lines
4.5 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2018-03-26 11:45:18 +00:00
module Ci
class BuildTraceChunk < ApplicationRecord
2018-05-01 08:06:44 +00:00
include FastDestroyAll
2018-07-03 07:20:27 +00:00
include ::Gitlab::ExclusiveLeaseHelpers
2018-03-26 11:45:18 +00:00
extend Gitlab::Ci::Model
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
2018-04-04 10:19:17 +00:00
default_value_for :data_store, :redis
2018-04-05 11:39:35 +00:00
CHUNK_SIZE = 128.kilobytes
2018-05-07 08:34:47 +00:00
WRITE_LOCK_RETRY = 10
2018-05-07 09:45:38 +00:00
WRITE_LOCK_SLEEP = 0.01.seconds
2018-05-07 08:34:47 +00:00
WRITE_LOCK_TTL = 1.minute
2018-04-04 10:19:17 +00:00
2018-11-28 12:27:25 +00:00
FailedToPersistDataError = Class.new(StandardError)
# Note: The ordering of this enum is related to the precedence of persist store.
# The bottom item takes the highest precedence, and the top item takes the lowest precedence.
2018-04-04 10:19:17 +00:00
enum data_store: {
redis: 1,
database: 2,
fog: 3
2018-04-04 10:19:17 +00:00
}
2018-05-01 08:06:44 +00:00
class << self
def all_stores
@all_stores ||= self.data_stores.keys
end
def persistable_store
# get first available store from the back of the list
all_stores.reverse.find { |store| get_store_class(store).available? }
end
def get_store_class(store)
@stores ||= {}
@stores[store] ||= "Ci::BuildTraceChunks::#{store.capitalize}".constantize.new
end
2018-05-03 08:08:05 +00:00
##
# FastDestroyAll concerns
def begin_fast_destroy
all_stores.each_with_object({}) do |store, result|
relation = public_send(store) # rubocop:disable GitlabSecurity/PublicSend
keys = get_store_class(store).keys(relation)
result[store] = keys if keys.present?
end
2018-05-03 08:08:05 +00:00
end
##
# FastDestroyAll concerns
def finalize_fast_destroy(keys)
keys.each do |store, value|
get_store_class(store).delete_keys(value)
end
2018-05-03 08:08:05 +00:00
end
end
##
# Data is memoized for optimizing #size and #end_offset
2018-04-04 10:19:17 +00:00
def data
@data ||= get_data.to_s
2018-04-04 10:19:17 +00:00
end
def truncate(offset = 0)
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
return if offset == size # Skip the following process as it doesn't affect anything
self.append("", offset)
2018-04-04 10:19:17 +00:00
end
def append(new_data, offset)
2018-07-03 05:48:00 +00:00
raise ArgumentError, 'New data is missing' unless new_data
raise ArgumentError, 'Offset is out of range' if offset < 0 || offset > size
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
2018-04-04 10:19:17 +00:00
in_lock(*lock_params) { unsafe_append_data!(new_data, offset) }
schedule_to_persist if full?
2018-04-04 10:19:17 +00:00
end
def size
@size ||= current_store.size(self) || data&.bytesize
2018-04-04 10:19:17 +00:00
end
def start_offset
chunk_index * CHUNK_SIZE
end
def end_offset
start_offset + size
end
def range
(start_offset...end_offset)
end
2018-06-18 08:56:16 +00:00
def persist_data!
2018-11-23 16:25:11 +00:00
in_lock(*lock_params) do # Write operation is atomic
unsafe_persist_to!(self.class.persistable_store)
2018-04-06 15:08:35 +00:00
end
2018-04-04 10:19:17 +00:00
end
private
2018-06-25 07:19:40 +00:00
def unsafe_persist_to!(new_store)
return if data_store == new_store.to_s
2018-11-28 12:27:25 +00:00
current_data = get_data
2018-11-28 12:27:25 +00:00
unless current_data&.bytesize.to_i == CHUNK_SIZE
raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
end
old_store_class = current_store
2018-11-28 12:27:25 +00:00
self.raw_data = nil
self.data_store = new_store
unsafe_set_data!(current_data)
old_store_class.delete_data(self)
end
def get_data
current_store.data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
end
def unsafe_set_data!(value)
2018-06-25 07:19:40 +00:00
raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
current_store.set_data(self, value)
@data = value
@size = value.bytesize
save! if changed?
end
def unsafe_append_data!(value, offset)
new_size = value.bytesize + offset
if new_size > CHUNK_SIZE
raise ArgumentError, 'New data size exceeds chunk size'
end
current_store.append_data(self, value, offset).then do |stored|
raise ArgumentError, 'Trace appended incorrectly' if stored != new_size
end
@data = nil
@size = new_size
save! if changed?
end
def schedule_to_persist
2018-06-18 08:56:16 +00:00
return if data_persisted?
2018-04-04 10:19:17 +00:00
Ci::BuildTraceChunkFlushWorker.perform_async(id)
2018-04-04 10:19:17 +00:00
end
2018-06-25 07:19:40 +00:00
def data_persisted?
!redis?
end
2018-05-07 08:34:47 +00:00
def full?
2018-04-04 10:19:17 +00:00
size == CHUNK_SIZE
end
def current_store
self.class.get_store_class(data_store)
end
def lock_params
["trace_write:#{build_id}:chunks:#{chunk_index}",
{ ttl: WRITE_LOCK_TTL,
2018-07-04 04:29:47 +00:00
retries: WRITE_LOCK_RETRY,
sleep_sec: WRITE_LOCK_SLEEP }]
2018-04-04 10:19:17 +00:00
end
2018-03-26 11:45:18 +00:00
end
end