build_trace_chunk_spec.rb 9.63 KB
Newer Older
1
2
require 'spec_helper'

3
4
describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
  set(:build) { create(:ci_build, :running) }
5
6
7
  let(:chunk_index) { 0 }
  let(:data_store) { :redis }
  let(:raw_data) { nil }
8

9
10
  let(:build_trace_chunk) do
    described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data)
11
12
  end

13
14
15
16
17
18
  describe 'CHUNK_SIZE' do
    it 'Chunk size can not be changed without special care' do
      expect(described_class::CHUNK_SIZE).to eq(128.kilobytes)
    end
  end

19
  describe '#data' do
20
    subject { build_trace_chunk.data }
21
22
23
24
25

    context 'when data_store is redis' do
      let(:data_store) { :redis }

      before do
26
        build_trace_chunk.send(:redis_set_data, 'Sample data in redis')
27
28
29
30
31
32
33
34
35
36
37
38
39
40
      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' }

      it { is_expected.to eq('Sample data in db') }
    end

    context 'when data_store is others' do
      before do
41
        build_trace_chunk.send(:write_attribute, :data_store, -1)
42
43
44
45
46
47
48
      end

      it { expect { subject }.to raise_error('Unsupported data store') }
    end
  end

  describe '#set_data' do
49
    subject { build_trace_chunk.send(:set_data, value) }
50
51
52
53
54
55
56
57
58
59
60
61
62

    let(:value) { 'Sample data' }

    context 'when value bytesize is bigger than CHUNK_SIZE' do
      let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) }

      it { expect { subject }.to raise_error('too much data') }
    end

    context 'when data_store is redis' do
      let(:data_store) { :redis }

      it do
63
        expect(build_trace_chunk.send(:redis_data)).to be_nil
64
65
66

        subject

67
        expect(build_trace_chunk.send(:redis_data)).to eq(value)
68
69
70
71
72
73
      end

      context 'when fullfilled chunk size' do
        let(:value) { 'a' * described_class::CHUNK_SIZE }

        it 'schedules stashing data' do
74
          expect(BuildTraceSwapChunkWorker).to receive(:perform_async).once
75
76
77
78
79
80
81
82
83
84

          subject
        end
      end
    end

    context 'when data_store is database' do
      let(:data_store) { :db }

      it 'sets data' do
85
        expect(build_trace_chunk.raw_data).to be_nil
86
87
88

        subject

89
90
        expect(build_trace_chunk.raw_data).to eq(value)
        expect(build_trace_chunk.persisted?).to be_truthy
91
92
93
94
      end

      context 'when raw_data is not changed' do
        it 'does not execute UPDATE' do
95
96
          expect(build_trace_chunk.raw_data).to be_nil
          build_trace_chunk.save!
97
98
99

          # First set
          expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0
100
101
          expect(build_trace_chunk.raw_data).to eq(value)
          expect(build_trace_chunk.persisted?).to be_truthy
102
103

          # Second set
104
          build_trace_chunk.reload
105
106
107
108
109
110
          expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0)
        end
      end

      context 'when fullfilled chunk size' do
        it 'does not schedule stashing data' do
111
          expect(BuildTraceSwapChunkWorker).not_to receive(:perform_async)
112
113
114
115
116
117
118
119

          subject
        end
      end
    end

    context 'when data_store is others' do
      before do
120
        build_trace_chunk.send(:write_attribute, :data_store, -1)
121
122
123
124
125
126
127
      end

      it { expect { subject }.to raise_error('Unsupported data store') }
    end
  end

  describe '#truncate' do
128
    subject { build_trace_chunk.truncate(offset) }
129
130
131
132
133

    shared_examples_for 'truncates' do
      context 'when offset is negative' do
        let(:offset) { -1 }

134
        it { expect { subject }.to raise_error('Offset is out of range') }
135
136
137
138
139
      end

      context 'when offset is bigger than data size' do
        let(:offset) { data.bytesize + 1 }

140
141
142
143
144
        it do
          expect_any_instance_of(described_class).not_to receive(:append) { }

          subject
        end
145
146
147
148
149
150
151
152
      end

      context 'when offset is 10' do
        let(:offset) { 10 }

        it 'truncates' do
          subject

153
          expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
154
155
156
157
158
159
160
161
162
        end
      end
    end

    context 'when data_store is redis' do
      let(:data_store) { :redis }
      let(:data) { 'Sample data in redis' }

      before do
163
        build_trace_chunk.send(:redis_set_data, data)
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
      end

      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) { raw_data }

      it_behaves_like 'truncates'
    end
  end

  describe '#append' do
179
    subject { build_trace_chunk.append(new_data, offset) }
180
181
182
183
184
185
186
187
188

    let(:new_data) { 'Sample new data' }
    let(:offset) { 0 }
    let(:total_data) { data + new_data }

    shared_examples_for 'appends' do
      context 'when offset is negative' do
        let(:offset) { -1 }

189
        it { expect { subject }.to raise_error('Offset is out of range') }
190
191
192
193
194
      end

      context 'when offset is bigger than data size' do
        let(:offset) { data.bytesize + 1 }

195
        it { expect { subject }.to raise_error('Offset is out of range') }
196
197
198
199
200
      end

      context 'when offset is bigger than data size' do
        let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }

201
        it { expect { subject }.to raise_error('Chunk size overflow') }
202
203
204
205
206
207
208
209
      end

      context 'when offset is EOF' do
        let(:offset) { data.bytesize }

        it 'appends' do
          subject

210
          expect(build_trace_chunk.data).to eq(total_data)
211
212
213
214
215
216
217
218
219
        end
      end

      context 'when offset is 10' do
        let(:offset) { 10 }

        it 'appends' do
          subject

220
          expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
221
222
223
224
225
226
227
228
229
        end
      end
    end

    context 'when data_store is redis' do
      let(:data_store) { :redis }
      let(:data) { 'Sample data in redis' }

      before do
230
        build_trace_chunk.send(:redis_set_data, data)
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
      end

      it_behaves_like 'appends'
    end

    context 'when data_store is database' do
      let(:data_store) { :db }
      let(:raw_data) { 'Sample data in db' }
      let(:data) { raw_data }

      it_behaves_like 'appends'
    end
  end

  describe '#size' do
246
    subject { build_trace_chunk.size }
247
248
249
250
251
252
253
254

    context 'when data_store is redis' do
      let(:data_store) { :redis }

      context 'when data exists' do
        let(:data) { 'Sample data in redis' }

        before do
255
          build_trace_chunk.send(:redis_set_data, data)
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
        end

        it { is_expected.to eq(data.bytesize) }
      end

      context 'when data exists' do
        it { is_expected.to eq(0) }
      end
    end

    context 'when data_store is database' do
      let(:data_store) { :db }

      context 'when data exists' do
        let(:raw_data) { 'Sample data in db' }
        let(:data) { raw_data }

        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
283
    subject { build_trace_chunk.use_database! }
284
285
286
287
288
289
290
291

    context 'when data_store is redis' do
      let(:data_store) { :redis }

      context 'when data exists' do
        let(:data) { 'Sample data in redis' }

        before do
292
          build_trace_chunk.send(:redis_set_data, data)
293
294
295
        end

        it 'stashes the data' do
296
297
298
          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
299
300
301

          subject

302
303
304
          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)
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
        end
      end

      context 'when data does not exist' do
        it 'does not call UPDATE' do
          expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
        end
      end
    end

    context 'when data_store is database' do
      let(:data_store) { :db }

      it 'does not call UPDATE' do
        expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
      end
    end
  end
323

324
325
326
  describe 'ExclusiveLock' do
    before do
      allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
327
      stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
328
329
330
    end

    it 'raise an error' do
331
      expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock')
332
333
334
    end
  end

335
  describe 'deletes data in redis after a parent record destroyed' do
336
337
338
339
    let(:project) { create(:project) }

    before do
      pipeline = create(:ci_pipeline, project: project)
340
341
342
      create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
      create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
      create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
343
344
    end

345
    shared_examples_for 'deletes all build_trace_chunk and data in redis' do
346
      it do
347
348
        Gitlab::Redis::SharedState.with do |redis|
          expect(redis.scan_each(match: "gitlab:ci:trace:?:chunks:?").to_a.count).to eq(3)
349
350
        end

351
        expect(described_class.count).to eq(3)
352
353
354
355
356

        subject

        expect(described_class.count).to eq(0)

357
358
        Gitlab::Redis::SharedState.with do |redis|
          expect(redis.scan_each(match: "gitlab:ci:trace:?:chunks:?").to_a.count).to eq(0)
359
360
361
362
        end
      end
    end

363
    context 'when build is destroyed' do
364
365
366
367
      let(:subject) do
        project.builds.destroy_all
      end

368
      it_behaves_like 'deletes all build_trace_chunk and data in redis'
369
370
371
372
373
374
375
    end

    context 'when project is destroyed' do
      let(:subject) do
        project.destroy!
      end

376
      it_behaves_like 'deletes all build_trace_chunk and data in redis'
377
378
    end
  end
379
end