diff options
Diffstat (limited to 'spec/models/ci/build_trace_chunk_spec.rb')
-rw-r--r-- | spec/models/ci/build_trace_chunk_spec.rb | 446 |
1 files changed, 321 insertions, 125 deletions
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 464897de306..774a638b430 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -14,6 +14,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do before do stub_feature_flags(ci_enable_live_trace: true) + stub_artifacts_object_storage end context 'FastDestroyAll' do @@ -37,6 +38,22 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end + describe '.all_stores' do + subject { described_class.all_stores } + + it 'returns a correctly ordered array' do + is_expected.to eq(%w[redis database fog]) + end + + it 'returns redis store as the the lowest precedence' do + expect(subject.first).to eq('redis') + end + + it 'returns fog store as the the highest precedence' do + expect(subject.last).to eq('fog') + end + end + describe '#data' do subject { build_trace_chunk.data } @@ -44,181 +61,269 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data_store) { :redis } before do - build_trace_chunk.send(:redis_set_data, 'Sample data in redis') + build_trace_chunk.send(:unsafe_set_data!, 'Sample data in redis') end it { is_expected.to eq('Sample data in redis') } end context 'when data_store is database' do - let(:data_store) { :db } - let(:raw_data) { 'Sample data in db' } + let(:data_store) { :database } + let(:raw_data) { 'Sample data in database' } - it { is_expected.to eq('Sample data in db') } + it { is_expected.to eq('Sample data in database') } end - end - - describe '#set_data' do - subject { build_trace_chunk.send(:set_data, value) } - let(:value) { 'Sample data' } + context 'when data_store is fog' do + let(:data_store) { :fog } - context 'when value bytesize is bigger than CHUNK_SIZE' do - let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) } + before do + build_trace_chunk.send(:unsafe_set_data!, 'Sample data in fog') + end - it { expect { subject }.to raise_error('too much data') } + it { is_expected.to eq('Sample data in fog') } end + end - context 'when data_store is redis' do - let(:data_store) { :redis } + describe '#append' do + subject { build_trace_chunk.append(new_data, offset) } - it do - expect(build_trace_chunk.send(:redis_data)).to be_nil + let(:new_data) { 'Sample new data' } + let(:offset) { 0 } + let(:merged_data) { data + new_data.to_s } - subject + shared_examples_for 'Appending correctly' do + context 'when offset is negative' do + let(:offset) { -1 } + + it { expect { subject }.to raise_error('Offset is out of range') } + end - expect(build_trace_chunk.send(:redis_data)).to eq(value) + context 'when offset is bigger than data size' do + let(:offset) { data.bytesize + 1 } + + it { expect { subject }.to raise_error('Offset is out of range') } end - context 'when fullfilled chunk size' do - let(:value) { 'a' * described_class::CHUNK_SIZE } + context 'when new data overflows chunk size' do + let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) } - it 'schedules stashing data' do - expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once + it { expect { subject }.to raise_error('Chunk size overflow') } + end + + context 'when offset is EOF' do + let(:offset) { data.bytesize } + it 'appends' do subject + + expect(build_trace_chunk.data).to eq(merged_data) end - end - end - context 'when data_store is database' do - let(:data_store) { :db } + context 'when the other process is appending' do + let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" } - it 'sets data' do - expect(build_trace_chunk.raw_data).to be_nil + before do + stub_exclusive_lease_taken(lease_key) + end - subject + it 'raise an error' do + expect { subject }.to raise_error('Failed to obtain a lock') + end + end - expect(build_trace_chunk.raw_data).to eq(value) - expect(build_trace_chunk.persisted?).to be_truthy - end + context 'when new_data is nil' do + let(:new_data) { nil } - context 'when raw_data is not changed' do - it 'does not execute UPDATE' do - expect(build_trace_chunk.raw_data).to be_nil - build_trace_chunk.save! + it 'raises an error' do + expect { subject }.to raise_error('New data is missing') + end + end - # First set - expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0 - expect(build_trace_chunk.raw_data).to eq(value) - expect(build_trace_chunk.persisted?).to be_truthy + context 'when new_data is empty' do + let(:new_data) { '' } - # Second set - build_trace_chunk.reload - expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0) + it 'does not append' do + subject + + expect(build_trace_chunk.data).to eq(data) + end + + it 'does not execute UPDATE' do + ActiveRecord::QueryRecorder.new { subject }.log.map do |query| + expect(query).not_to include('UPDATE') + end + end end end - context 'when fullfilled chunk size' do - it 'does not schedule stashing data' do - expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async) + context 'when offset is middle of datasize' do + let(:offset) { data.bytesize / 2 } + it 'appends' do subject + + expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data) end end end - end - describe '#truncate' do - subject { build_trace_chunk.truncate(offset) } + shared_examples_for 'Scheduling sidekiq worker to flush data to persist store' do + context 'when new data fullfilled chunk size' do + let(:new_data) { 'a' * described_class::CHUNK_SIZE } - shared_examples_for 'truncates' do - context 'when offset is negative' do - let(:offset) { -1 } + it 'schedules trace chunk flush worker' do + expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once - it { expect { subject }.to raise_error('Offset is out of range') } - end + subject + end - context 'when offset is bigger than data size' do - let(:offset) { data.bytesize + 1 } + it 'migrates data to object storage' do + Sidekiq::Testing.inline! do + subject - it { expect { subject }.to raise_error('Offset is out of range') } + build_trace_chunk.reload + expect(build_trace_chunk.fog?).to be_truthy + expect(build_trace_chunk.data).to eq(new_data) + end + end end + end - context 'when offset is 10' do - let(:offset) { 10 } + shared_examples_for 'Scheduling no sidekiq worker' do + context 'when new data fullfilled chunk size' do + let(:new_data) { 'a' * described_class::CHUNK_SIZE } + + it 'does not schedule trace chunk flush worker' do + expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async) - it 'truncates' do subject + end - expect(build_trace_chunk.data).to eq(data.byteslice(0, offset)) + it 'does not migrate data to object storage' do + Sidekiq::Testing.inline! do + data_store = build_trace_chunk.data_store + + subject + + build_trace_chunk.reload + expect(build_trace_chunk.data_store).to eq(data_store) + end end end end context 'when data_store is redis' do let(:data_store) { :redis } - let(:data) { 'Sample data in redis' } - before do - build_trace_chunk.send(:redis_set_data, data) + context 'when there are no data' do + let(:data) { '' } + + it 'has no data' do + expect(build_trace_chunk.data).to be_empty + end + + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling sidekiq worker to flush data to persist store' end - it_behaves_like 'truncates' - end + context 'when there are some data' do + let(:data) { 'Sample data in redis' } - context 'when data_store is database' do - let(:data_store) { :db } - let(:raw_data) { 'Sample data in db' } - let(:data) { raw_data } + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end - it_behaves_like 'truncates' + it 'has data' do + expect(build_trace_chunk.data).to eq(data) + end + + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling sidekiq worker to flush data to persist store' + end end - end - describe '#append' do - subject { build_trace_chunk.append(new_data, offset) } + context 'when data_store is database' do + let(:data_store) { :database } - let(:new_data) { 'Sample new data' } - let(:offset) { 0 } - let(:total_data) { data + new_data } + context 'when there are no data' do + let(:data) { '' } - shared_examples_for 'appends' do - context 'when offset is negative' do - let(:offset) { -1 } + it 'has no data' do + expect(build_trace_chunk.data).to be_empty + end - it { expect { subject }.to raise_error('Offset is out of range') } + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling no sidekiq worker' end - context 'when offset is bigger than data size' do - let(:offset) { data.bytesize + 1 } + context 'when there are some data' do + let(:raw_data) { 'Sample data in database' } + let(:data) { raw_data } - it { expect { subject }.to raise_error('Offset is out of range') } + it 'has data' do + expect(build_trace_chunk.data).to eq(data) + end + + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling no sidekiq worker' end + end - context 'when offset is bigger than data size' do - let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) } + context 'when data_store is fog' do + let(:data_store) { :fog } - it { expect { subject }.to raise_error('Chunk size overflow') } + context 'when there are no data' do + let(:data) { '' } + + it 'has no data' do + expect(build_trace_chunk.data).to be_empty + end + + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling no sidekiq worker' end - context 'when offset is EOF' do - let(:offset) { data.bytesize } + context 'when there are some data' do + let(:data) { 'Sample data in fog' } - it 'appends' do - subject + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end - expect(build_trace_chunk.data).to eq(total_data) + it 'has data' do + expect(build_trace_chunk.data).to eq(data) end + + it_behaves_like 'Appending correctly' + it_behaves_like 'Scheduling no sidekiq worker' + end + end + end + + describe '#truncate' do + subject { build_trace_chunk.truncate(offset) } + + shared_examples_for 'truncates' do + context 'when offset is negative' do + let(:offset) { -1 } + + it { expect { subject }.to raise_error('Offset is out of range') } + end + + context 'when offset is bigger than data size' do + let(:offset) { data.bytesize + 1 } + + it { expect { subject }.to raise_error('Offset is out of range') } end context 'when offset is 10' do let(:offset) { 10 } - it 'appends' do + it 'truncates' do subject - expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data) + expect(build_trace_chunk.data).to eq(data.byteslice(0, offset)) end end end @@ -228,18 +333,29 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data) { 'Sample data in redis' } before do - build_trace_chunk.send(:redis_set_data, data) + build_trace_chunk.send(:unsafe_set_data!, data) end - it_behaves_like 'appends' + it_behaves_like 'truncates' end context 'when data_store is database' do - let(:data_store) { :db } - let(:raw_data) { 'Sample data in db' } + let(:data_store) { :database } + let(:raw_data) { 'Sample data in database' } let(:data) { raw_data } - it_behaves_like 'appends' + it_behaves_like 'truncates' + end + + context 'when data_store is fog' do + let(:data_store) { :fog } + let(:data) { 'Sample data in fog' } + + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end + + it_behaves_like 'truncates' end end @@ -253,7 +369,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data) { 'Sample data in redis' } before do - build_trace_chunk.send(:redis_set_data, data) + build_trace_chunk.send(:unsafe_set_data!, data) end it { is_expected.to eq(data.bytesize) } @@ -265,10 +381,10 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end context 'when data_store is database' do - let(:data_store) { :db } + let(:data_store) { :database } context 'when data exists' do - let(:raw_data) { 'Sample data in db' } + let(:raw_data) { 'Sample data in database' } let(:data) { raw_data } it { is_expected.to eq(data.bytesize) } @@ -278,10 +394,43 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do it { is_expected.to eq(0) } end end + + context 'when data_store is fog' do + let(:data_store) { :fog } + + context 'when data exists' do + let(:data) { 'Sample data in fog' } + let(:key) { "tmp/builds/#{build.id}/chunks/#{chunk_index}.log" } + + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end + + it { is_expected.to eq(data.bytesize) } + end + + context 'when data does not exist' do + it { is_expected.to eq(0) } + end + end end - describe '#use_database!' do - subject { build_trace_chunk.use_database! } + describe '#persist_data!' do + subject { build_trace_chunk.persist_data! } + + shared_examples_for 'Atomic operation' do + context 'when the other process is persisting' do + let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" } + + before do + stub_exclusive_lease_taken(lease_key) + end + + it 'raise an error' do + expect { subject }.to raise_error('Failed to obtain a lock') + end + end + end context 'when data_store is redis' do let(:data_store) { :redis } @@ -290,46 +439,93 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data) { 'Sample data in redis' } before do - build_trace_chunk.send(:redis_set_data, data) + build_trace_chunk.send(:unsafe_set_data!, data) end - it 'stashes the data' do - expect(build_trace_chunk.data_store).to eq('redis') - expect(build_trace_chunk.send(:redis_data)).to eq(data) - expect(build_trace_chunk.raw_data).to be_nil + it 'persists the data' do + expect(build_trace_chunk.redis?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) subject - expect(build_trace_chunk.data_store).to eq('db') - expect(build_trace_chunk.send(:redis_data)).to be_nil - expect(build_trace_chunk.raw_data).to eq(data) + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) end + + it_behaves_like 'Atomic operation' end context 'when data does not exist' do - it 'does not call UPDATE' do - expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0) + it 'does not persist' do + expect { subject }.to raise_error('Can not persist empty data') end end end context 'when data_store is database' do - let(:data_store) { :db } + let(:data_store) { :database } - it 'does not call UPDATE' do - expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0) + context 'when data exists' do + let(:data) { 'Sample data in database' } + + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end + + it 'persists the data' do + expect(build_trace_chunk.database?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data) + expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound) + + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' end - end - end - describe 'ExclusiveLock' do - before do - stub_exclusive_lease_taken - stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1) + context 'when data does not exist' do + it 'does not persist' do + expect { subject }.to raise_error('Can not persist empty data') + end + end end - it 'raise an error' do - expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock') + context 'when data_store is fog' do + let(:data_store) { :fog } + + context 'when data exists' do + let(:data) { 'Sample data in fog' } + + before do + build_trace_chunk.send(:unsafe_set_data!, data) + end + + it 'does not change data store' do + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + + subject + + expect(build_trace_chunk.fog?).to be_truthy + expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil + expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) + end + + it_behaves_like 'Atomic operation' + end end end |