gitlab-sidekiq-fetcher 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4815e3e75915230d7b2eaf2fe3fa0daa288ec4c670b2cd211cb659aff4838788
4
- data.tar.gz: c8b491f2d1a2678ef40fe856a55bf89341e041070c40941ca693685fa3c048cf
3
+ metadata.gz: 6b9cf610a1915d63141331ba0e4820306235fc1c58e37e0124a9700d19005b99
4
+ data.tar.gz: 3690c4aaff9d47c8ec108ff264984343bc3a4412123ad56ac727cdf1ae3e8fd3
5
5
  SHA512:
6
- metadata.gz: 1667fb3ffb47117ac3c756c07a85867d96871b5489e820823735b62912df0d82b4d6c10be478e1f291d637d2ed15aff99a958f4fd884df98f026ee8c8f9c4c83
7
- data.tar.gz: 5dad56f30515be87e79c58c3b5dce301213d427fe579309bca2a525e2b8664b94f0763c7eabfb2443324c5d8ecf5403f9826a0772e3b09b74e215e72ebfc4226
6
+ metadata.gz: 6d5d61280c6db3b91c8107fca593fa12246db18c727389ef31cf8edf58e923bc78a2cbbe2be505962664ed56c8189513dc0ad0312e545988b70666839f191f66
7
+ data.tar.gz: cd1459179a3f97b3b3194a21e8335367e9021db590cb9441613d6d88030b1b86cc495b00e1ff37dc0cf7ea683d40e6b10d3c511c7d956a70e8c26c67a4bc0903
@@ -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
@@ -7,6 +7,6 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
7
7
  group :test do
8
8
  gem "rspec", '~> 3'
9
9
  gem "pry"
10
- gem "sidekiq", '~> 5.0'
10
+ gem "sidekiq", '~> 6.1'
11
11
  gem 'simplecov', require: false
12
12
  end
@@ -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.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.0.5)
14
- rack-protection (2.0.4)
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 (5.2.2)
31
- connection_pool (~> 2.2, >= 2.2.2)
32
- rack-protection (>= 1.5.0)
33
- redis (>= 3.3.5, < 5)
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 (~> 5.0)
44
+ sidekiq (~> 6.1)
47
45
  simplecov
48
46
 
49
47
  BUNDLED WITH
50
- 1.17.1
48
+ 1.17.2
data/README.md CHANGED
@@ -6,6 +6,8 @@ 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
+
9
11
  There are two strategies implemented: [Reliable fetch](http://redis.io/commands/rpoplpush#pattern-reliable-queue) using `rpoplpush` command and
10
12
  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
13
  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.6.0'
3
+ s.version = '0.7.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', '>= 5', '< 7'
13
+ s.add_dependency 'sidekiq', '~> 6.1'
14
14
  end
@@ -41,10 +41,14 @@ module Sidekiq
41
41
  end
42
42
 
43
43
  def self.setup_reliable_fetch!(config)
44
- fetch = config.options[:semi_reliable_fetch] ? SemiReliableFetch : ReliableFetch
45
- fetch = fetch.new(config.options) if Sidekiq::VERSION >= '6'
44
+ fetch_strategy = if config.options[:semi_reliable_fetch]
45
+ Sidekiq::SemiReliableFetch
46
+ else
47
+ Sidekiq::ReliableFetch
48
+ end
49
+
50
+ config.options[:fetch] = fetch_strategy.new(config.options)
46
51
 
47
- config.options[:fetch] = fetch
48
52
  Sidekiq.logger.info('GitLab reliable fetch activated!')
49
53
 
50
54
  start_heartbeat_thread
@@ -82,11 +86,44 @@ module Sidekiq
82
86
  Sidekiq.logger.debug("Heartbeat for hostname: #{hostname} and pid: #{pid}")
83
87
  end
84
88
 
85
- def bulk_requeue(inprogress, options)
86
- self.class.bulk_requeue(inprogress, options)
89
+ def self.worker_dead?(hostname, pid, conn)
90
+ !conn.get(heartbeat_key(hostname, pid))
91
+ end
92
+
93
+ def self.heartbeat_key(hostname, pid)
94
+ "reliable-fetcher-heartbeat-#{hostname}-#{pid}"
95
+ end
96
+
97
+ def self.working_queue_name(queue)
98
+ "#{WORKING_QUEUE_PREFIX}:#{queue}:#{hostname}:#{pid}"
99
+ end
100
+
101
+ attr_reader :cleanup_interval, :last_try_to_take_lease_at, :lease_interval,
102
+ :queues, :use_semi_reliable_fetch,
103
+ :strictly_ordered_queues
104
+
105
+ def initialize(options)
106
+ raise ArgumentError, 'missing queue list' unless options[:queues]
107
+
108
+ @cleanup_interval = options.fetch(:cleanup_interval, DEFAULT_CLEANUP_INTERVAL)
109
+ @lease_interval = options.fetch(:lease_interval, DEFAULT_LEASE_INTERVAL)
110
+ @last_try_to_take_lease_at = 0
111
+ @strictly_ordered_queues = !!options[:strict]
112
+ @queues = options[:queues].map { |q| "queue:#{q}" }
113
+ end
114
+
115
+ def retrieve_work
116
+ clean_working_queues! if take_lease
117
+
118
+ retrieve_unit_of_work
119
+ end
120
+
121
+ def retrieve_unit_of_work
122
+ raise NotImplementedError,
123
+ "#{self.class} does not implement #{__method__}"
87
124
  end
88
125
 
89
- def self.bulk_requeue(inprogress, _options)
126
+ def bulk_requeue(inprogress, _options)
90
127
  return if inprogress.empty?
91
128
 
92
129
  Sidekiq.redis do |conn|
@@ -94,7 +131,7 @@ module Sidekiq
94
131
  conn.multi do |multi|
95
132
  preprocess_interrupted_job(unit_of_work.job, unit_of_work.queue, multi)
96
133
 
97
- multi.lrem(working_queue_name(unit_of_work.queue), 1, unit_of_work.job)
134
+ multi.lrem(self.class.working_queue_name(unit_of_work.queue), 1, unit_of_work.job)
98
135
  end
99
136
  end
100
137
  end
@@ -102,17 +139,9 @@ module Sidekiq
102
139
  Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{e.message}")
103
140
  end
104
141
 
105
- def self.clean_working_queue!(working_queue)
106
- original_queue = working_queue.gsub(/#{WORKING_QUEUE_PREFIX}:|:[^:]*:[0-9]*\z/, '')
107
-
108
- Sidekiq.redis do |conn|
109
- while job = conn.rpop(working_queue)
110
- preprocess_interrupted_job(job, original_queue)
111
- end
112
- end
113
- end
142
+ private
114
143
 
115
- def self.preprocess_interrupted_job(job, queue, conn = nil)
144
+ def preprocess_interrupted_job(job, queue, conn = nil)
116
145
  msg = Sidekiq.load_json(job)
117
146
  msg['interrupted_count'] = msg['interrupted_count'].to_i + 1
118
147
 
@@ -123,9 +152,23 @@ module Sidekiq
123
152
  end
124
153
  end
125
154
 
155
+ # If you want this method to be run in a scope of multi connection
156
+ # you need to pass it
157
+ def requeue_job(queue, msg, conn)
158
+ with_connection(conn) do |conn|
159
+ conn.lpush(queue, Sidekiq.dump_json(msg))
160
+ end
161
+
162
+ Sidekiq.logger.info(
163
+ message: "Pushed job #{msg['jid']} back to queue #{queue}",
164
+ jid: msg['jid'],
165
+ queue: queue
166
+ )
167
+ end
168
+
126
169
  # Detect "old" jobs and requeue them because the worker they were assigned
127
170
  # to probably failed miserably.
128
- def self.clean_working_queues!
171
+ def clean_working_queues!
129
172
  Sidekiq.logger.info('Cleaning working queues')
130
173
 
131
174
  Sidekiq.redis do |conn|
@@ -135,30 +178,28 @@ module Sidekiq
135
178
 
136
179
  continue if hostname.nil? || pid.nil?
137
180
 
138
- clean_working_queue!(key) if worker_dead?(hostname, pid, conn)
181
+ clean_working_queue!(key) if self.class.worker_dead?(hostname, pid, conn)
139
182
  end
140
183
  end
141
184
  end
142
185
 
143
- def self.worker_dead?(hostname, pid, conn)
144
- !conn.get(heartbeat_key(hostname, pid))
145
- end
146
-
147
- def self.heartbeat_key(hostname, pid)
148
- "reliable-fetcher-heartbeat-#{hostname}-#{pid}"
149
- end
186
+ def clean_working_queue!(working_queue)
187
+ original_queue = working_queue.gsub(/#{WORKING_QUEUE_PREFIX}:|:[^:]*:[0-9]*\z/, '')
150
188
 
151
- def self.working_queue_name(queue)
152
- "#{WORKING_QUEUE_PREFIX}:#{queue}:#{hostname}:#{pid}"
189
+ Sidekiq.redis do |conn|
190
+ while job = conn.rpop(working_queue)
191
+ preprocess_interrupted_job(job, original_queue)
192
+ end
193
+ end
153
194
  end
154
195
 
155
- def self.interruption_exhausted?(msg)
196
+ def interruption_exhausted?(msg)
156
197
  return false if max_retries_after_interruption(msg['class']) < 0
157
198
 
158
199
  msg['interrupted_count'].to_i >= max_retries_after_interruption(msg['class'])
159
200
  end
160
201
 
161
- def self.max_retries_after_interruption(worker_class)
202
+ def max_retries_after_interruption(worker_class)
162
203
  max_retries_after_interruption = nil
163
204
 
164
205
  max_retries_after_interruption ||= begin
@@ -171,7 +212,7 @@ module Sidekiq
171
212
  max_retries_after_interruption
172
213
  end
173
214
 
174
- def self.send_to_quarantine(msg, multi_connection = nil)
215
+ def send_to_quarantine(msg, multi_connection = nil)
175
216
  Sidekiq.logger.warn(
176
217
  class: msg['class'],
177
218
  jid: msg['jid'],
@@ -182,52 +223,13 @@ module Sidekiq
182
223
  Sidekiq::InterruptedSet.new.put(job, connection: multi_connection)
183
224
  end
184
225
 
185
- # If you want this method to be run is a scope of multi connection
186
- # you need to pass it
187
- def self.requeue_job(queue, msg, conn)
188
- with_connection(conn) do |conn|
189
- conn.lpush(queue, Sidekiq.dump_json(msg))
190
- end
191
-
192
- Sidekiq.logger.info(
193
- message: "Pushed job #{msg['jid']} back to queue #{queue}",
194
- jid: msg['jid'],
195
- queue: queue
196
- )
197
- end
198
-
199
226
  # Yield block with an existing connection or creates another one
200
- def self.with_connection(conn, &block)
227
+ def with_connection(conn)
201
228
  return yield(conn) if conn
202
229
 
203
- Sidekiq.redis { |conn| yield(conn) }
204
- end
205
-
206
- attr_reader :cleanup_interval, :last_try_to_take_lease_at, :lease_interval,
207
- :queues, :use_semi_reliable_fetch,
208
- :strictly_ordered_queues
209
-
210
- def initialize(options)
211
- @cleanup_interval = options.fetch(:cleanup_interval, DEFAULT_CLEANUP_INTERVAL)
212
- @lease_interval = options.fetch(:lease_interval, DEFAULT_LEASE_INTERVAL)
213
- @last_try_to_take_lease_at = 0
214
- @strictly_ordered_queues = !!options[:strict]
215
- @queues = options[:queues].map { |q| "queue:#{q}" }
216
- end
217
-
218
- def retrieve_work
219
- self.class.clean_working_queues! if take_lease
220
-
221
- retrieve_unit_of_work
230
+ Sidekiq.redis { |redis_conn| yield(redis_conn) }
222
231
  end
223
232
 
224
- def retrieve_unit_of_work
225
- raise NotImplementedError,
226
- "#{self.class} does not implement #{__method__}"
227
- end
228
-
229
- private
230
-
231
233
  def take_lease
232
234
  return unless allowed_to_take_a_lease?
233
235
 
@@ -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 :queues_iterator, :queues_size
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
- @queues_iterator.rewind if strictly_ordered_queues
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 '.bulk_requeue' do
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, queues: [])
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, queues: [])
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, queues: [])
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: { queues: [] })
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,7 +5,7 @@ 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: ['assigned']) }
8
+ let(:fetcher) { described_class.new(queues: queues) }
9
9
 
10
10
  it 'retrieves the job and puts it to working queue' do
11
11
  Sidekiq.redis { |conn| conn.rpush('queue:assigned', job) }
@@ -61,11 +61,11 @@ shared_examples 'a Sidekiq fetcher' do
61
61
  it 'does not clean up orphaned jobs more than once per cleanup interval' do
62
62
  Sidekiq.redis = Sidekiq::RedisConnection.create(url: REDIS_URL, size: 10)
63
63
 
64
- expect(described_class).to receive(:clean_working_queues!).once
64
+ expect(fetcher).to receive(:clean_working_queues!).once
65
65
 
66
66
  threads = 10.times.map do
67
67
  Thread.new do
68
- described_class.new(queues: ['assigned']).retrieve_work
68
+ fetcher.retrieve_work
69
69
  end
70
70
  end
71
71
 
@@ -57,7 +57,7 @@ end
57
57
  def spawn_workers
58
58
  pids = []
59
59
  NUMBER_OF_WORKERS.times do
60
- pids << spawn('sidekiq -r ./config.rb')
60
+ pids << spawn('sidekiq -q default -q low -q high -r ./config.rb')
61
61
  end
62
62
 
63
63
  pids
@@ -8,19 +8,7 @@ class ReliabilityTestWorker
8
8
  sleep 1
9
9
 
10
10
  Sidekiq.redis do |redis|
11
- redis.lpush(REDIS_FINISHED_LIST, get_sidekiq_job_id)
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
@@ -11,7 +11,7 @@ def spawn_workers(number)
11
11
  pids = []
12
12
 
13
13
  number.times do
14
- pids << spawn('sidekiq -r ./config.rb')
14
+ pids << spawn('sidekiq -q default -q high -q low -r ./config.rb')
15
15
  end
16
16
 
17
17
  pids
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-sidekiq-fetcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TEA
@@ -9,28 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-07-22 00:00:00.000000000 Z
12
+ date: 2020-07-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ">="
18
+ - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '5'
21
- - - "<"
22
- - !ruby/object:Gem::Version
23
- version: '7'
20
+ version: '6.1'
24
21
  type: :runtime
25
22
  prerelease: false
26
23
  version_requirements: !ruby/object:Gem::Requirement
27
24
  requirements:
28
- - - ">="
29
- - !ruby/object:Gem::Version
30
- version: '5'
31
- - - "<"
25
+ - - "~>"
32
26
  - !ruby/object:Gem::Version
33
- version: '7'
27
+ version: '6.1'
34
28
  description: Redis reliable queue pattern implemented in Sidekiq
35
29
  email: valery@gitlab.com
36
30
  executables: []
@@ -84,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
78
  - !ruby/object:Gem::Version
85
79
  version: '0'
86
80
  requirements: []
87
- rubygems_version: 3.0.3
81
+ rubygems_version: 3.0.6
88
82
  signing_key:
89
83
  specification_version: 4
90
84
  summary: Reliable fetch extension for Sidekiq