1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::OptimisticLocking do
let!(:pipeline) { create(:ci_pipeline) }
let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
let(:histogram) { spy('prometheus metric') }
before do
allow(described_class)
.to receive(:retry_lock_histogram)
.and_return(histogram)
end
describe '#retry_lock' do
let(:name) { 'optimistic_locking_spec' }
it 'does not change current_scope', :aggregate_failures do
instance = Class.new { include Gitlab::OptimisticLocking }.new
relation = pipeline.cancelable_statuses
expected_scope = Ci::Build.current_scope&.to_sql
instance.send(:retry_lock, relation, name: :test) do
expect(Ci::Build.current_scope&.to_sql).to eq(expected_scope)
end
expect(Ci::Build.current_scope&.to_sql).to eq(expected_scope)
end
context 'when state changed successfully without retries' do
subject do
described_class.retry_lock(pipeline, name: name) do |lock_subject|
lock_subject.succeed
end
end
it 'does not reload object' do
expect(pipeline).not_to receive(:reset)
expect(pipeline).to receive(:succeed).and_call_original
subject
end
it 'does not create log record' do
expect(described_class.retry_lock_logger).not_to receive(:info)
subject
end
it 'adds number of retries to histogram' do
subject
expect(histogram).to have_received(:observe).with({}, 0)
end
end
context 'when at least one retry happened, the change succeeded' do
subject do
described_class.retry_lock(pipeline2, name: 'optimistic_locking_spec') do |lock_subject|
lock_subject.drop
end
end
before do
pipeline.succeed
end
it 'completes the action' do
expect(pipeline2).to receive(:reset).and_call_original
expect(pipeline2).to receive(:drop).twice.and_call_original
subject
end
it 'creates a single log record' do
expect(described_class.retry_lock_logger)
.to receive(:info)
.once
.with(hash_including(:time_s, name: name, retries: 1))
subject
end
it 'adds number of retries to histogram' do
subject
expect(histogram).to have_received(:observe).with({}, 1)
end
end
context 'when MAX_RETRIES attempts exceeded' do
subject do
described_class.retry_lock(pipeline, max_retries, name: name) do |lock_subject|
lock_subject.lock_version = 100
lock_subject.drop
end
end
let(:max_retries) { 2 }
it 'raises an exception' do
expect(pipeline).to receive(:drop).exactly(max_retries + 1).times.and_call_original
expect { subject }.to raise_error(ActiveRecord::StaleObjectError)
end
it 'creates a single log record' do
expect(described_class.retry_lock_logger)
.to receive(:info)
.once
.with(hash_including(:time_s, name: name, retries: max_retries))
expect { subject }.to raise_error(ActiveRecord::StaleObjectError)
end
it 'adds number of retries to histogram' do
expect { subject }.to raise_error(ActiveRecord::StaleObjectError)
expect(histogram).to have_received(:observe).with({}, max_retries)
end
end
end
describe '#retry_optimistic_lock' do
context 'when locking module is mixed in' do
let(:unlockable) do
Class.new.include(described_class).new
end
it 'is an alias for retry_lock' do
expect(unlockable.method(:retry_optimistic_lock))
.to eq unlockable.method(:retry_lock)
end
end
end
end
|