gitlab-sidekiq-fetcher 0.5.5 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +0 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +9 -11
- data/README.md +4 -0
- data/gitlab-sidekiq-fetcher.gemspec +2 -2
- data/lib/sidekiq/base_reliable_fetch.rb +76 -72
- data/lib/sidekiq/reliable_fetch.rb +4 -6
- data/spec/base_reliable_fetch_spec.rb +6 -5
- data/spec/fetch_shared_examples.rb +43 -109
- data/tests/reliability/reliability_test.rb +1 -1
- data/tests/reliability/worker.rb +1 -13
- data/tests/support/utils.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7be23d59956ffa44288a1c870bcca66fd0119682f810325d71a3ebaa8b76e80
|
4
|
+
data.tar.gz: 013a7124f61044572ad93335e95c18357c60804dd89024d987485b2d87775787
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92653bc5f9b5729f4dd50a8243a869c20d9621a1a9d25c46d729e735895e0f2d4d940c5a766803f1a5fd908ab0d9340f27d255dbd99f31bab4923e2f539c1882
|
7
|
+
data.tar.gz: d763b8b0ee3c2522752130fac86b83e67e8513faf919dd361aad9896aac684809650959b1627dd38e0440343c063f95b807b67fc31a10217ce1f15c428759803
|
data/.gitlab-ci.yml
CHANGED
@@ -40,7 +40,6 @@ integration_reliable:
|
|
40
40
|
variables:
|
41
41
|
JOB_FETCHER: reliable
|
42
42
|
|
43
|
-
|
44
43
|
integration_basic:
|
45
44
|
extends: .integration
|
46
45
|
allow_failure: yes
|
@@ -63,7 +62,6 @@ term_interruption:
|
|
63
62
|
services:
|
64
63
|
- redis:alpine
|
65
64
|
|
66
|
-
|
67
65
|
# rubocop:
|
68
66
|
# script:
|
69
67
|
# - bundle exec rubocop
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -2,7 +2,7 @@ GEM
|
|
2
2
|
remote: https://rubygems.org/
|
3
3
|
specs:
|
4
4
|
coderay (1.1.2)
|
5
|
-
connection_pool (2.2.
|
5
|
+
connection_pool (2.2.3)
|
6
6
|
diff-lcs (1.3)
|
7
7
|
docile (1.3.1)
|
8
8
|
json (2.1.0)
|
@@ -10,10 +10,8 @@ GEM
|
|
10
10
|
pry (0.11.3)
|
11
11
|
coderay (~> 1.1.0)
|
12
12
|
method_source (~> 0.9.0)
|
13
|
-
rack (2.
|
14
|
-
|
15
|
-
rack
|
16
|
-
redis (4.0.2)
|
13
|
+
rack (2.2.3)
|
14
|
+
redis (4.2.1)
|
17
15
|
rspec (3.8.0)
|
18
16
|
rspec-core (~> 3.8.0)
|
19
17
|
rspec-expectations (~> 3.8.0)
|
@@ -27,10 +25,10 @@ GEM
|
|
27
25
|
diff-lcs (>= 1.2.0, < 2.0)
|
28
26
|
rspec-support (~> 3.8.0)
|
29
27
|
rspec-support (3.8.0)
|
30
|
-
sidekiq (
|
31
|
-
connection_pool (
|
32
|
-
rack
|
33
|
-
redis (>=
|
28
|
+
sidekiq (6.1.0)
|
29
|
+
connection_pool (>= 2.2.2)
|
30
|
+
rack (~> 2.0)
|
31
|
+
redis (>= 4.2.0)
|
34
32
|
simplecov (0.16.1)
|
35
33
|
docile (~> 1.1)
|
36
34
|
json (>= 1.8, < 3)
|
@@ -43,8 +41,8 @@ PLATFORMS
|
|
43
41
|
DEPENDENCIES
|
44
42
|
pry
|
45
43
|
rspec (~> 3)
|
46
|
-
sidekiq (~>
|
44
|
+
sidekiq (~> 6.1)
|
47
45
|
simplecov
|
48
46
|
|
49
47
|
BUNDLED WITH
|
50
|
-
1.17.
|
48
|
+
1.17.2
|
data/README.md
CHANGED
@@ -6,6 +6,10 @@ fetches from Redis.
|
|
6
6
|
|
7
7
|
It's based on https://github.com/TEA-ebook/sidekiq-reliable-fetch.
|
8
8
|
|
9
|
+
**IMPORTANT NOTE:** Since version `0.7.0` this gem works only with `sidekiq >= 6.1` (which introduced Fetch API breaking changes). Please use version `~> 0.5` if you use older version of the `sidekiq` .
|
10
|
+
|
11
|
+
**UPGRADE NOTE:** If upgrading from 0.7.0, strongly consider a full deployed step on 0.7.1 before 0.8.0; that fixes a bug in the queue name validation that will hit if sidekiq nodes running 0.7.0 see working queues named by 0.8.0. See https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/-/merge_requests/22
|
12
|
+
|
9
13
|
There are two strategies implemented: [Reliable fetch](http://redis.io/commands/rpoplpush#pattern-reliable-queue) using `rpoplpush` command and
|
10
14
|
semi-reliable fetch that uses regular `brpop` and `lpush` to pick the job and put it to working queue. The main benefit of "Reliable" strategy is that `rpoplpush` is atomic, eliminating a race condition in which jobs can be lost.
|
11
15
|
However, it comes at a cost because `rpoplpush` can't watch multiple lists at the same time so we need to iterate over the entire queue list which significantly increases pressure on Redis when there are more than a few queues. The "semi-reliable" strategy is much more reliable than the default Sidekiq fetcher, though. Compared to the reliable fetch strategy, it does not increase pressure on Redis significantly.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'gitlab-sidekiq-fetcher'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.8.0'
|
4
4
|
s.authors = ['TEA', 'GitLab']
|
5
5
|
s.email = 'valery@gitlab.com'
|
6
6
|
s.license = 'LGPL-3.0'
|
@@ -10,5 +10,5 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.require_paths = ['lib']
|
11
11
|
s.files = `git ls-files`.split($\)
|
12
12
|
s.test_files = []
|
13
|
-
s.add_dependency 'sidekiq', '~>
|
13
|
+
s.add_dependency 'sidekiq', '~> 6.1'
|
14
14
|
end
|
@@ -45,11 +45,13 @@ module Sidekiq
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def self.setup_reliable_fetch!(config)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
48
|
+
fetch_strategy = if config.options[:semi_reliable_fetch]
|
49
|
+
Sidekiq::SemiReliableFetch
|
50
|
+
else
|
51
|
+
Sidekiq::ReliableFetch
|
52
|
+
end
|
53
|
+
|
54
|
+
config.options[:fetch] = fetch_strategy.new(config.options)
|
53
55
|
|
54
56
|
Sidekiq.logger.info('GitLab reliable fetch activated!')
|
55
57
|
|
@@ -92,7 +94,44 @@ module Sidekiq
|
|
92
94
|
Sidekiq.logger.debug("Heartbeat for #{identity}")
|
93
95
|
end
|
94
96
|
|
95
|
-
def self.
|
97
|
+
def self.worker_dead?(identity, conn)
|
98
|
+
!conn.get(heartbeat_key(identity))
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.heartbeat_key(identity)
|
102
|
+
"reliable-fetcher-heartbeat-#{identity.gsub(':', '-')}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.working_queue_name(queue)
|
106
|
+
"#{WORKING_QUEUE_PREFIX}:#{queue}:#{identity}"
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_reader :cleanup_interval, :last_try_to_take_lease_at, :lease_interval,
|
110
|
+
:queues, :use_semi_reliable_fetch,
|
111
|
+
:strictly_ordered_queues
|
112
|
+
|
113
|
+
def initialize(options)
|
114
|
+
raise ArgumentError, 'missing queue list' unless options[:queues]
|
115
|
+
|
116
|
+
@cleanup_interval = options.fetch(:cleanup_interval, DEFAULT_CLEANUP_INTERVAL)
|
117
|
+
@lease_interval = options.fetch(:lease_interval, DEFAULT_LEASE_INTERVAL)
|
118
|
+
@last_try_to_take_lease_at = 0
|
119
|
+
@strictly_ordered_queues = !!options[:strict]
|
120
|
+
@queues = options[:queues].map { |q| "queue:#{q}" }
|
121
|
+
end
|
122
|
+
|
123
|
+
def retrieve_work
|
124
|
+
clean_working_queues! if take_lease
|
125
|
+
|
126
|
+
retrieve_unit_of_work
|
127
|
+
end
|
128
|
+
|
129
|
+
def retrieve_unit_of_work
|
130
|
+
raise NotImplementedError,
|
131
|
+
"#{self.class} does not implement #{__method__}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def bulk_requeue(inprogress, _options)
|
96
135
|
return if inprogress.empty?
|
97
136
|
|
98
137
|
Sidekiq.redis do |conn|
|
@@ -100,7 +139,7 @@ module Sidekiq
|
|
100
139
|
conn.multi do |multi|
|
101
140
|
preprocess_interrupted_job(unit_of_work.job, unit_of_work.queue, multi)
|
102
141
|
|
103
|
-
multi.lrem(working_queue_name(unit_of_work.queue), 1, unit_of_work.job)
|
142
|
+
multi.lrem(self.class.working_queue_name(unit_of_work.queue), 1, unit_of_work.job)
|
104
143
|
end
|
105
144
|
end
|
106
145
|
end
|
@@ -108,15 +147,9 @@ module Sidekiq
|
|
108
147
|
Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{e.message}")
|
109
148
|
end
|
110
149
|
|
111
|
-
|
112
|
-
Sidekiq.redis do |conn|
|
113
|
-
while job = conn.rpop(working_queue)
|
114
|
-
preprocess_interrupted_job(job, original_queue)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
150
|
+
private
|
118
151
|
|
119
|
-
def
|
152
|
+
def preprocess_interrupted_job(job, queue, conn = nil)
|
120
153
|
msg = Sidekiq.load_json(job)
|
121
154
|
msg['interrupted_count'] = msg['interrupted_count'].to_i + 1
|
122
155
|
|
@@ -127,7 +160,21 @@ module Sidekiq
|
|
127
160
|
end
|
128
161
|
end
|
129
162
|
|
130
|
-
|
163
|
+
# If you want this method to be run in a scope of multi connection
|
164
|
+
# you need to pass it
|
165
|
+
def requeue_job(queue, msg, conn)
|
166
|
+
with_connection(conn) do |conn|
|
167
|
+
conn.lpush(queue, Sidekiq.dump_json(msg))
|
168
|
+
end
|
169
|
+
|
170
|
+
Sidekiq.logger.info(
|
171
|
+
message: "Pushed job #{msg['jid']} back to queue #{queue}",
|
172
|
+
jid: msg['jid'],
|
173
|
+
queue: queue
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
def extract_queue_and_identity(key)
|
131
178
|
# New identity format is "{hostname}:{pid}:{randomhex}
|
132
179
|
# Old identity format is "{hostname}:{pid}"
|
133
180
|
# Queue names may also have colons (namespaced).
|
@@ -142,7 +189,7 @@ module Sidekiq
|
|
142
189
|
|
143
190
|
# Detect "old" jobs and requeue them because the worker they were assigned
|
144
191
|
# to probably failed miserably.
|
145
|
-
def
|
192
|
+
def clean_working_queues!
|
146
193
|
Sidekiq.logger.info('Cleaning working queues')
|
147
194
|
|
148
195
|
Sidekiq.redis do |conn|
|
@@ -151,30 +198,26 @@ module Sidekiq
|
|
151
198
|
|
152
199
|
next if original_queue.nil? || identity.nil?
|
153
200
|
|
154
|
-
clean_working_queue!(original_queue, key) if worker_dead?(identity, conn)
|
201
|
+
clean_working_queue!(original_queue, key) if self.class.worker_dead?(identity, conn)
|
155
202
|
end
|
156
203
|
end
|
157
204
|
end
|
158
205
|
|
159
|
-
def
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
end
|
166
|
-
|
167
|
-
def self.working_queue_name(queue)
|
168
|
-
"#{WORKING_QUEUE_PREFIX}:#{queue}:#{identity}"
|
206
|
+
def clean_working_queue!(original_queue, working_queue)
|
207
|
+
Sidekiq.redis do |conn|
|
208
|
+
while job = conn.rpop(working_queue)
|
209
|
+
preprocess_interrupted_job(job, original_queue)
|
210
|
+
end
|
211
|
+
end
|
169
212
|
end
|
170
213
|
|
171
|
-
def
|
214
|
+
def interruption_exhausted?(msg)
|
172
215
|
return false if max_retries_after_interruption(msg['class']) < 0
|
173
216
|
|
174
217
|
msg['interrupted_count'].to_i >= max_retries_after_interruption(msg['class'])
|
175
218
|
end
|
176
219
|
|
177
|
-
def
|
220
|
+
def max_retries_after_interruption(worker_class)
|
178
221
|
max_retries_after_interruption = nil
|
179
222
|
|
180
223
|
max_retries_after_interruption ||= begin
|
@@ -187,7 +230,7 @@ module Sidekiq
|
|
187
230
|
max_retries_after_interruption
|
188
231
|
end
|
189
232
|
|
190
|
-
def
|
233
|
+
def send_to_quarantine(msg, multi_connection = nil)
|
191
234
|
Sidekiq.logger.warn(
|
192
235
|
class: msg['class'],
|
193
236
|
jid: msg['jid'],
|
@@ -198,52 +241,13 @@ module Sidekiq
|
|
198
241
|
Sidekiq::InterruptedSet.new.put(job, connection: multi_connection)
|
199
242
|
end
|
200
243
|
|
201
|
-
# If you want this method to be run is a scope of multi connection
|
202
|
-
# you need to pass it
|
203
|
-
def self.requeue_job(queue, msg, conn)
|
204
|
-
with_connection(conn) do |conn|
|
205
|
-
conn.lpush(queue, Sidekiq.dump_json(msg))
|
206
|
-
end
|
207
|
-
|
208
|
-
Sidekiq.logger.info(
|
209
|
-
message: "Pushed job #{msg['jid']} back to queue #{queue}",
|
210
|
-
jid: msg['jid'],
|
211
|
-
queue: queue
|
212
|
-
)
|
213
|
-
end
|
214
|
-
|
215
244
|
# Yield block with an existing connection or creates another one
|
216
|
-
def
|
245
|
+
def with_connection(conn)
|
217
246
|
return yield(conn) if conn
|
218
247
|
|
219
|
-
Sidekiq.redis { |
|
220
|
-
end
|
221
|
-
|
222
|
-
attr_reader :cleanup_interval, :last_try_to_take_lease_at, :lease_interval,
|
223
|
-
:queues, :use_semi_reliable_fetch,
|
224
|
-
:strictly_ordered_queues
|
225
|
-
|
226
|
-
def initialize(options)
|
227
|
-
@cleanup_interval = options.fetch(:cleanup_interval, DEFAULT_CLEANUP_INTERVAL)
|
228
|
-
@lease_interval = options.fetch(:lease_interval, DEFAULT_LEASE_INTERVAL)
|
229
|
-
@last_try_to_take_lease_at = 0
|
230
|
-
@strictly_ordered_queues = !!options[:strict]
|
231
|
-
@queues = options[:queues].map { |q| "queue:#{q}" }
|
232
|
-
end
|
233
|
-
|
234
|
-
def retrieve_work
|
235
|
-
self.class.clean_working_queues! if take_lease
|
236
|
-
|
237
|
-
retrieve_unit_of_work
|
248
|
+
Sidekiq.redis { |redis_conn| yield(redis_conn) }
|
238
249
|
end
|
239
250
|
|
240
|
-
def retrieve_unit_of_work
|
241
|
-
raise NotImplementedError,
|
242
|
-
"#{self.class} does not implement #{__method__}"
|
243
|
-
end
|
244
|
-
|
245
|
-
private
|
246
|
-
|
247
251
|
def take_lease
|
248
252
|
return unless allowed_to_take_a_lease?
|
249
253
|
|
@@ -6,23 +6,21 @@ module Sidekiq
|
|
6
6
|
# we inject a regular sleep into the loop.
|
7
7
|
RELIABLE_FETCH_IDLE_TIMEOUT = 5 # seconds
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :queues_size
|
10
10
|
|
11
11
|
def initialize(options)
|
12
12
|
super
|
13
13
|
|
14
|
+
@queues = queues.uniq if strictly_ordered_queues
|
14
15
|
@queues_size = queues.size
|
15
|
-
@queues_iterator = queues.cycle
|
16
16
|
end
|
17
17
|
|
18
18
|
private
|
19
19
|
|
20
20
|
def retrieve_unit_of_work
|
21
|
-
|
22
|
-
|
23
|
-
queues_size.times do
|
24
|
-
queue = queues_iterator.next
|
21
|
+
queues_list = strictly_ordered_queues ? queues : queues.shuffle
|
25
22
|
|
23
|
+
queues_list.each do |queue|
|
26
24
|
work = Sidekiq.redis do |conn|
|
27
25
|
conn.rpoplpush(queue, self.class.working_queue_name(queue))
|
28
26
|
end
|
@@ -39,14 +39,15 @@ describe Sidekiq::BaseReliableFetch do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
describe '
|
42
|
+
describe '#bulk_requeue' do
|
43
|
+
let(:options) { { queues: %w[foo bar] } }
|
43
44
|
let!(:queue1) { Sidekiq::Queue.new('foo') }
|
44
45
|
let!(:queue2) { Sidekiq::Queue.new('bar') }
|
45
46
|
|
46
47
|
it 'requeues the bulk' do
|
47
48
|
uow = described_class::UnitOfWork
|
48
49
|
jobs = [ uow.new('queue:foo', job), uow.new('queue:foo', job), uow.new('queue:bar', job) ]
|
49
|
-
described_class.bulk_requeue(jobs,
|
50
|
+
described_class.new(options).bulk_requeue(jobs, nil)
|
50
51
|
|
51
52
|
expect(queue1.size).to eq 2
|
52
53
|
expect(queue2.size).to eq 1
|
@@ -56,7 +57,7 @@ describe Sidekiq::BaseReliableFetch do
|
|
56
57
|
uow = described_class::UnitOfWork
|
57
58
|
interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
|
58
59
|
jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job) ]
|
59
|
-
described_class.bulk_requeue(jobs,
|
60
|
+
described_class.new(options).bulk_requeue(jobs, nil)
|
60
61
|
|
61
62
|
expect(queue1.size).to eq 1
|
62
63
|
expect(queue2.size).to eq 1
|
@@ -69,7 +70,7 @@ describe Sidekiq::BaseReliableFetch do
|
|
69
70
|
uow = described_class::UnitOfWork
|
70
71
|
interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
|
71
72
|
jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job) ]
|
72
|
-
described_class.bulk_requeue(jobs,
|
73
|
+
described_class.new(options).bulk_requeue(jobs, nil)
|
73
74
|
|
74
75
|
expect(queue1.size).to eq 2
|
75
76
|
expect(queue2.size).to eq 1
|
@@ -80,7 +81,7 @@ describe Sidekiq::BaseReliableFetch do
|
|
80
81
|
end
|
81
82
|
|
82
83
|
it 'sets heartbeat' do
|
83
|
-
config = double(:sidekiq_config, options: {})
|
84
|
+
config = double(:sidekiq_config, options: { queues: %w[foo bar] })
|
84
85
|
|
85
86
|
heartbeat_thread = described_class.setup_reliable_fetch!(config)
|
86
87
|
|
@@ -5,102 +5,16 @@ shared_examples 'a Sidekiq fetcher' do
|
|
5
5
|
|
6
6
|
describe '#retrieve_work' do
|
7
7
|
let(:job) { Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo']) }
|
8
|
-
let(:fetcher) { described_class.new(queues:
|
9
|
-
|
10
|
-
it 'retrieves the job and puts it to working queue' do
|
11
|
-
Sidekiq.redis { |conn| conn.rpush('queue:assigned', job) }
|
12
|
-
|
13
|
-
uow = fetcher.retrieve_work
|
14
|
-
|
15
|
-
expect(working_queue_size('assigned')).to eq 1
|
16
|
-
expect(uow.queue_name).to eq 'assigned'
|
17
|
-
expect(uow.job).to eq job
|
18
|
-
expect(Sidekiq::Queue.new('assigned').size).to eq 0
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'does not retrieve a job from foreign queue' do
|
22
|
-
Sidekiq.redis { |conn| conn.rpush('queue:not_assigned', job) }
|
23
|
-
|
24
|
-
expect(fetcher.retrieve_work).to be_nil
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'requeues jobs from dead working queue with incremented interrupted_count' do
|
28
|
-
Sidekiq.redis do |conn|
|
29
|
-
conn.rpush(other_process_working_queue_name('assigned'), job)
|
30
|
-
end
|
31
|
-
|
32
|
-
expected_job = Sidekiq.load_json(job)
|
33
|
-
expected_job['interrupted_count'] = 1
|
34
|
-
expected_job = Sidekiq.dump_json(expected_job)
|
35
|
-
|
36
|
-
uow = fetcher.retrieve_work
|
37
|
-
|
38
|
-
expect(uow).to_not be_nil
|
39
|
-
expect(uow.job).to eq expected_job
|
40
|
-
|
41
|
-
Sidekiq.redis do |conn|
|
42
|
-
expect(conn.llen(other_process_working_queue_name('assigned'))).to eq 0
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'ignores working queue keys in unknown formats' do
|
47
|
-
# Add a spurious non-numeric char segment at the end; this simulates any other
|
48
|
-
# incorrect form in general
|
49
|
-
malformed_key = "#{other_process_working_queue_name('assigned')}:X"
|
50
|
-
Sidekiq.redis do |conn|
|
51
|
-
conn.rpush(malformed_key, job)
|
52
|
-
end
|
53
|
-
|
54
|
-
uow = fetcher.retrieve_work
|
55
|
-
|
56
|
-
Sidekiq.redis do |conn|
|
57
|
-
expect(conn.llen(malformed_key)).to eq 1
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'requeues jobs from legacy dead working queue with incremented interrupted_count' do
|
62
|
-
Sidekiq.redis do |conn|
|
63
|
-
conn.rpush(legacy_other_process_working_queue_name('assigned'), job)
|
64
|
-
end
|
65
|
-
|
66
|
-
expected_job = Sidekiq.load_json(job)
|
67
|
-
expected_job['interrupted_count'] = 1
|
68
|
-
expected_job = Sidekiq.dump_json(expected_job)
|
69
|
-
|
70
|
-
uow = fetcher.retrieve_work
|
71
|
-
|
72
|
-
expect(uow).to_not be_nil
|
73
|
-
expect(uow.job).to eq expected_job
|
74
|
-
|
75
|
-
Sidekiq.redis do |conn|
|
76
|
-
expect(conn.llen(legacy_other_process_working_queue_name('assigned'))).to eq 0
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
it 'does not requeue jobs from live working queue' do
|
81
|
-
working_queue = live_other_process_working_queue_name('assigned')
|
82
|
-
|
83
|
-
Sidekiq.redis do |conn|
|
84
|
-
conn.rpush(working_queue, job)
|
85
|
-
end
|
86
|
-
|
87
|
-
uow = fetcher.retrieve_work
|
88
|
-
|
89
|
-
expect(uow).to be_nil
|
90
|
-
|
91
|
-
Sidekiq.redis do |conn|
|
92
|
-
expect(conn.llen(working_queue)).to eq 1
|
93
|
-
end
|
94
|
-
end
|
8
|
+
let(:fetcher) { described_class.new(queues: queues) }
|
95
9
|
|
96
10
|
it 'does not clean up orphaned jobs more than once per cleanup interval' do
|
97
11
|
Sidekiq.redis = Sidekiq::RedisConnection.create(url: REDIS_URL, size: 10)
|
98
12
|
|
99
|
-
expect(
|
13
|
+
expect(fetcher).to receive(:clean_working_queues!).once
|
100
14
|
|
101
15
|
threads = 10.times.map do
|
102
16
|
Thread.new do
|
103
|
-
|
17
|
+
fetcher.retrieve_work
|
104
18
|
end
|
105
19
|
end
|
106
20
|
|
@@ -133,13 +47,34 @@ shared_examples 'a Sidekiq fetcher' do
|
|
133
47
|
expect(jobs).to include 'this_job_should_not_stuck'
|
134
48
|
end
|
135
49
|
|
136
|
-
|
137
|
-
let (:queue) { 'namespace:assigned' }
|
50
|
+
shared_examples "basic queue handling" do |queue|
|
138
51
|
let (:fetcher) { described_class.new(queues: [queue]) }
|
139
52
|
|
140
|
-
it '
|
53
|
+
it 'retrieves the job and puts it to working queue' do
|
54
|
+
Sidekiq.redis { |conn| conn.rpush("queue:#{queue}", job) }
|
55
|
+
|
56
|
+
uow = fetcher.retrieve_work
|
57
|
+
|
58
|
+
expect(working_queue_size(queue)).to eq 1
|
59
|
+
expect(uow.queue_name).to eq queue
|
60
|
+
expect(uow.job).to eq job
|
61
|
+
expect(Sidekiq::Queue.new(queue).size).to eq 0
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'does not retrieve a job from foreign queue' do
|
65
|
+
Sidekiq.redis { |conn| conn.rpush("'queue:#{queue}:not", job) }
|
66
|
+
expect(fetcher.retrieve_work).to be_nil
|
67
|
+
|
68
|
+
Sidekiq.redis { |conn| conn.rpush("'queue:not_#{queue}", job) }
|
69
|
+
expect(fetcher.retrieve_work).to be_nil
|
70
|
+
|
71
|
+
Sidekiq.redis { |conn| conn.rpush("'queue:random_name", job) }
|
72
|
+
expect(fetcher.retrieve_work).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'requeues jobs from legacy dead working queue with incremented interrupted_count' do
|
141
76
|
Sidekiq.redis do |conn|
|
142
|
-
conn.rpush(
|
77
|
+
conn.rpush(legacy_other_process_working_queue_name(queue), job)
|
143
78
|
end
|
144
79
|
|
145
80
|
expected_job = Sidekiq.load_json(job)
|
@@ -152,32 +87,26 @@ shared_examples 'a Sidekiq fetcher' do
|
|
152
87
|
expect(uow.job).to eq expected_job
|
153
88
|
|
154
89
|
Sidekiq.redis do |conn|
|
155
|
-
expect(conn.llen(
|
90
|
+
expect(conn.llen(legacy_other_process_working_queue_name(queue))).to eq 0
|
156
91
|
end
|
157
92
|
end
|
158
93
|
|
159
|
-
it '
|
160
|
-
|
161
|
-
|
94
|
+
it 'ignores working queue keys in unknown formats' do
|
95
|
+
# Add a spurious non-numeric char segment at the end; this simulates any other
|
96
|
+
# incorrect form in general
|
97
|
+
malformed_key = "#{other_process_working_queue_name(queue)}:X"
|
162
98
|
Sidekiq.redis do |conn|
|
163
|
-
conn.rpush(
|
99
|
+
conn.rpush(malformed_key, job)
|
164
100
|
end
|
165
101
|
|
166
102
|
uow = fetcher.retrieve_work
|
167
103
|
|
168
|
-
expect(uow).to be_nil
|
169
|
-
|
170
104
|
Sidekiq.redis do |conn|
|
171
|
-
expect(conn.llen(
|
105
|
+
expect(conn.llen(malformed_key)).to eq 1
|
172
106
|
end
|
173
107
|
end
|
174
|
-
end
|
175
|
-
|
176
|
-
context 'with deeper namespaced queues' do
|
177
|
-
let (:queue) { 'deep:namespace:assigned' }
|
178
|
-
let (:fetcher) { described_class.new(queues: [queue]) }
|
179
108
|
|
180
|
-
it 'requeues jobs from dead
|
109
|
+
it 'requeues jobs from dead working queue with incremented interrupted_count' do
|
181
110
|
Sidekiq.redis do |conn|
|
182
111
|
conn.rpush(other_process_working_queue_name(queue), job)
|
183
112
|
end
|
@@ -196,7 +125,7 @@ shared_examples 'a Sidekiq fetcher' do
|
|
196
125
|
end
|
197
126
|
end
|
198
127
|
|
199
|
-
it 'does not requeue jobs
|
128
|
+
it 'does not requeue jobs from live working queue' do
|
200
129
|
working_queue = live_other_process_working_queue_name(queue)
|
201
130
|
|
202
131
|
Sidekiq.redis do |conn|
|
@@ -213,6 +142,12 @@ shared_examples 'a Sidekiq fetcher' do
|
|
213
142
|
end
|
214
143
|
end
|
215
144
|
|
145
|
+
context 'with various queues' do
|
146
|
+
%w[assigned namespace:assigned namespace:deeper:assigned].each do |queue|
|
147
|
+
it_behaves_like "basic queue handling", queue
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
216
151
|
context 'with short cleanup interval' do
|
217
152
|
let(:short_interval) { 1 }
|
218
153
|
let(:fetcher) { described_class.new(queues: queues, lease_interval: short_interval, cleanup_interval: short_interval) }
|
@@ -243,7 +178,6 @@ def legacy_other_process_working_queue_name(queue)
|
|
243
178
|
"#{Sidekiq::BaseReliableFetch::WORKING_QUEUE_PREFIX}:queue:#{queue}:#{Socket.gethostname}:#{::Process.pid + 1}"
|
244
179
|
end
|
245
180
|
|
246
|
-
|
247
181
|
def other_process_working_queue_name(queue)
|
248
182
|
"#{Sidekiq::BaseReliableFetch::WORKING_QUEUE_PREFIX}:queue:#{queue}:#{Socket.gethostname}:#{::Process.pid + 1}:#{::SecureRandom.hex(6)}"
|
249
183
|
end
|
data/tests/reliability/worker.rb
CHANGED
@@ -8,19 +8,7 @@ class ReliabilityTestWorker
|
|
8
8
|
sleep 1
|
9
9
|
|
10
10
|
Sidekiq.redis do |redis|
|
11
|
-
redis.lpush(REDIS_FINISHED_LIST,
|
11
|
+
redis.lpush(REDIS_FINISHED_LIST, jid)
|
12
12
|
end
|
13
13
|
end
|
14
|
-
|
15
|
-
def get_sidekiq_job_id
|
16
|
-
context_data = Thread.current[:sidekiq_context]&.first
|
17
|
-
|
18
|
-
return unless context_data
|
19
|
-
|
20
|
-
index = context_data.index('JID-')
|
21
|
-
|
22
|
-
return unless index
|
23
|
-
|
24
|
-
context_data[index + 4..-1]
|
25
|
-
end
|
26
14
|
end
|
data/tests/support/utils.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-sidekiq-fetcher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TEA
|
8
8
|
- GitLab
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-02
|
12
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sidekiq
|
@@ -17,14 +17,14 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '
|
20
|
+
version: '6.1'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: '
|
27
|
+
version: '6.1'
|
28
28
|
description: Redis reliable queue pattern implemented in Sidekiq
|
29
29
|
email: valery@gitlab.com
|
30
30
|
executables: []
|
@@ -63,7 +63,7 @@ homepage: https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/
|
|
63
63
|
licenses:
|
64
64
|
- LGPL-3.0
|
65
65
|
metadata: {}
|
66
|
-
post_install_message:
|
66
|
+
post_install_message:
|
67
67
|
rdoc_options: []
|
68
68
|
require_paths:
|
69
69
|
- lib
|
@@ -79,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
79
|
version: '0'
|
80
80
|
requirements: []
|
81
81
|
rubygems_version: 3.1.4
|
82
|
-
signing_key:
|
82
|
+
signing_key:
|
83
83
|
specification_version: 4
|
84
84
|
summary: Reliable fetch extension for Sidekiq
|
85
85
|
test_files: []
|