gitlab-sidekiq-fetcher 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.gitlab-ci.yml +53 -0
- data/.rspec +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +50 -0
- data/LICENSE +165 -0
- data/README.md +46 -0
- data/RELEASE-GITLAB.md +9 -0
- data/gitlab-sidekiq-fetcher.gemspec +14 -0
- data/lib/sidekiq-reliable-fetch.rb +5 -0
- data/lib/sidekiq/base_reliable_fetch.rb +185 -0
- data/lib/sidekiq/reliable_fetch.rb +40 -0
- data/lib/sidekiq/semi_reliable_fetch.rb +44 -0
- data/spec/base_reliable_fetch_spec.rb +73 -0
- data/spec/fetch_shared_examples.rb +118 -0
- data/spec/reliable_fetch_spec.rb +7 -0
- data/spec/semi_reliable_fetch_spec.rb +7 -0
- data/spec/spec_helper.rb +115 -0
- data/test/README.md +34 -0
- data/test/config.rb +31 -0
- data/test/reliability_test.rb +116 -0
- data/test/worker.rb +26 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f10159f679879ed622c3bbd336bb04d0da0c54d536b7270c8c81dcc1243cedfc
|
4
|
+
data.tar.gz: cbca779aacb710b4f8de222799ca88860f3745666d22b51ea3d7da7f5677f8a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd8376c3c379c325db87519e43f3ea36f96063b03b8884dc45363ff949cf3cda5341b20641f27dbb90be8a2188c0588ec92eb048b4055702aac6f7419f595a49
|
7
|
+
data.tar.gz: 02b0b4746cbe42960b89fc3b1b7f861f87b45317f6ae1f1f76d737333f80ac67c1553f6a8fdd3d44cacda87c08de88f9dee9b227a6d4b021d8bd58cacaa7fdee
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
image: "ruby:2.5"
|
2
|
+
|
3
|
+
before_script:
|
4
|
+
- ruby -v
|
5
|
+
- which ruby
|
6
|
+
- gem install bundler --no-ri --no-rdoc
|
7
|
+
- bundle install --jobs $(nproc) "${FLAGS[@]}"
|
8
|
+
|
9
|
+
variables:
|
10
|
+
REDIS_URL: "redis://redis"
|
11
|
+
|
12
|
+
rspec:
|
13
|
+
stage: test
|
14
|
+
coverage: '/LOC \((\d+\.\d+%)\) covered.$/'
|
15
|
+
script:
|
16
|
+
- bundle exec rspec
|
17
|
+
services:
|
18
|
+
- redis:alpine
|
19
|
+
artifacts:
|
20
|
+
expire_in: 31d
|
21
|
+
when: always
|
22
|
+
paths:
|
23
|
+
- coverage/
|
24
|
+
|
25
|
+
.integration:
|
26
|
+
stage: test
|
27
|
+
script:
|
28
|
+
- cd test
|
29
|
+
- bundle exec ruby reliability_test.rb
|
30
|
+
services:
|
31
|
+
- redis:alpine
|
32
|
+
|
33
|
+
integration_semi:
|
34
|
+
extends: .integration
|
35
|
+
variables:
|
36
|
+
JOB_FETCHER: semi
|
37
|
+
|
38
|
+
integration_reliable:
|
39
|
+
extends: .integration
|
40
|
+
variables:
|
41
|
+
JOB_FETCHER: reliable
|
42
|
+
|
43
|
+
|
44
|
+
integration_basic:
|
45
|
+
extends: .integration
|
46
|
+
allow_failure: yes
|
47
|
+
variables:
|
48
|
+
JOB_FETCHER: basic
|
49
|
+
|
50
|
+
|
51
|
+
# rubocop:
|
52
|
+
# script:
|
53
|
+
# - bundle exec rubocop
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
group :test do
|
8
|
+
gem "rspec", '~> 3'
|
9
|
+
gem "pry"
|
10
|
+
gem "sidekiq", '~> 5.0'
|
11
|
+
gem 'simplecov', require: false
|
12
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.1.2)
|
5
|
+
connection_pool (2.2.2)
|
6
|
+
diff-lcs (1.3)
|
7
|
+
docile (1.3.1)
|
8
|
+
json (2.1.0)
|
9
|
+
method_source (0.9.0)
|
10
|
+
pry (0.11.3)
|
11
|
+
coderay (~> 1.1.0)
|
12
|
+
method_source (~> 0.9.0)
|
13
|
+
rack (2.0.5)
|
14
|
+
rack-protection (2.0.4)
|
15
|
+
rack
|
16
|
+
redis (4.0.2)
|
17
|
+
rspec (3.8.0)
|
18
|
+
rspec-core (~> 3.8.0)
|
19
|
+
rspec-expectations (~> 3.8.0)
|
20
|
+
rspec-mocks (~> 3.8.0)
|
21
|
+
rspec-core (3.8.0)
|
22
|
+
rspec-support (~> 3.8.0)
|
23
|
+
rspec-expectations (3.8.1)
|
24
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
25
|
+
rspec-support (~> 3.8.0)
|
26
|
+
rspec-mocks (3.8.0)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.8.0)
|
29
|
+
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)
|
34
|
+
simplecov (0.16.1)
|
35
|
+
docile (~> 1.1)
|
36
|
+
json (>= 1.8, < 3)
|
37
|
+
simplecov-html (~> 0.10.0)
|
38
|
+
simplecov-html (0.10.2)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
pry
|
45
|
+
rspec (~> 3)
|
46
|
+
sidekiq (~> 5.0)
|
47
|
+
simplecov
|
48
|
+
|
49
|
+
BUNDLED WITH
|
50
|
+
1.17.1
|
data/LICENSE
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
2
|
+
Version 3, 29 June 2007
|
3
|
+
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
6
|
+
of this license document, but changing it is not allowed.
|
7
|
+
|
8
|
+
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
11
|
+
License, supplemented by the additional permissions listed below.
|
12
|
+
|
13
|
+
0. Additional Definitions.
|
14
|
+
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
17
|
+
General Public License.
|
18
|
+
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
20
|
+
other than an Application or a Combined Work as defined below.
|
21
|
+
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
25
|
+
of using an interface provided by the Library.
|
26
|
+
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
28
|
+
Application with the Library. The particular version of the Library
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
30
|
+
Version".
|
31
|
+
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
35
|
+
based on the Application, and not on the Linked Version.
|
36
|
+
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
38
|
+
object code and/or source code for the Application, including any data
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
41
|
+
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
43
|
+
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
46
|
+
|
47
|
+
2. Conveying Modified Versions.
|
48
|
+
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
51
|
+
that uses the facility (other than as an argument passed when the
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
53
|
+
version:
|
54
|
+
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
56
|
+
ensure that, in the event an Application does not supply the
|
57
|
+
function or data, the facility still operates, and performs
|
58
|
+
whatever part of its purpose remains meaningful, or
|
59
|
+
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
61
|
+
this License applicable to that copy.
|
62
|
+
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
64
|
+
|
65
|
+
The object code form of an Application may incorporate material from
|
66
|
+
a header file that is part of the Library. You may convey such object
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
68
|
+
material is not limited to numerical parameters, data structure
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
71
|
+
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
73
|
+
Library is used in it and that the Library and its use are
|
74
|
+
covered by this License.
|
75
|
+
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
77
|
+
document.
|
78
|
+
|
79
|
+
4. Combined Works.
|
80
|
+
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
82
|
+
taken together, effectively do not restrict modification of the
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
85
|
+
the following:
|
86
|
+
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
88
|
+
the Library is used in it and that the Library and its use are
|
89
|
+
covered by this License.
|
90
|
+
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
92
|
+
document.
|
93
|
+
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
95
|
+
execution, include the copyright notice for the Library among
|
96
|
+
these notices, as well as a reference directing the user to the
|
97
|
+
copies of the GNU GPL and this license document.
|
98
|
+
|
99
|
+
d) Do one of the following:
|
100
|
+
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
102
|
+
License, and the Corresponding Application Code in a form
|
103
|
+
suitable for, and under terms that permit, the user to
|
104
|
+
recombine or relink the Application with a modified version of
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
107
|
+
Corresponding Source.
|
108
|
+
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
111
|
+
a copy of the Library already present on the user's computer
|
112
|
+
system, and (b) will operate properly with a modified version
|
113
|
+
of the Library that is interface-compatible with the Linked
|
114
|
+
Version.
|
115
|
+
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
117
|
+
be required to provide such information under section 6 of the
|
118
|
+
GNU GPL, and only to the extent that such information is
|
119
|
+
necessary to install and execute a modified version of the
|
120
|
+
Combined Work produced by recombining or relinking the
|
121
|
+
Application with a modified version of the Linked Version. (If
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
126
|
+
for conveying Corresponding Source.)
|
127
|
+
|
128
|
+
5. Combined Libraries.
|
129
|
+
|
130
|
+
You may place library facilities that are a work based on the
|
131
|
+
Library side by side in a single library together with other library
|
132
|
+
facilities that are not Applications and are not covered by this
|
133
|
+
License, and convey such a combined library under terms of your
|
134
|
+
choice, if you do both of the following:
|
135
|
+
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
137
|
+
on the Library, uncombined with any other library facilities,
|
138
|
+
conveyed under the terms of this License.
|
139
|
+
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
141
|
+
is a work based on the Library, and explaining where to find the
|
142
|
+
accompanying uncombined form of the same work.
|
143
|
+
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
145
|
+
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
148
|
+
versions will be similar in spirit to the present version, but may
|
149
|
+
differ in detail to address new problems or concerns.
|
150
|
+
|
151
|
+
Each version is given a distinguishing version number. If the
|
152
|
+
Library as you received it specifies that a certain numbered version
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
154
|
+
applies to it, you have the option of following the terms and
|
155
|
+
conditions either of that published version or of any later version
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
160
|
+
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
164
|
+
permanent authorization for you to choose that version for the
|
165
|
+
Library.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
gitlab-sidekiq-fetcher
|
2
|
+
======================
|
3
|
+
|
4
|
+
`gitlab-sidekiq-fetcher` is an extension to Sidekiq that adds support for reliable
|
5
|
+
fetches from Redis.
|
6
|
+
|
7
|
+
It's based on https://github.com/TEA-ebook/sidekiq-reliable-fetch.
|
8
|
+
|
9
|
+
There are two strategies implemented: [Reliable fetch](http://redis.io/commands/rpoplpush#pattern-reliable-queue) using `rpoplpush` command and
|
10
|
+
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
|
+
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.
|
12
|
+
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add the following to your `Gemfile`:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'gitlab-sidekiq-fetcher', require: 'sidekiq-reliable-fetch'
|
20
|
+
```
|
21
|
+
|
22
|
+
## Configuration
|
23
|
+
|
24
|
+
Enable reliable fetches by calling this gem from your Sidekiq configuration:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
Sidekiq.configure_server do |config|
|
28
|
+
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
|
29
|
+
|
30
|
+
# …
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
There is an additional parameter `config.options[:semi_reliable_fetch]` you can use to switch between two strategies:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
Sidekiq.configure_server do |config|
|
38
|
+
config.options[:semi_reliable_fetch] = true # Default value is false
|
39
|
+
|
40
|
+
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
LGPL-3.0, see the LICENSE file.
|
data/RELEASE-GITLAB.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
gitlab-sidekiq-fetcher
|
2
|
+
======================
|
3
|
+
|
4
|
+
# How to publish a new release?
|
5
|
+
|
6
|
+
1. Dev-commit cycle
|
7
|
+
2. Update the version in the gemspec file, commit and tag
|
8
|
+
3. Build the gem: `gem build gitlab-sidekiq-fetcher.gemspec`
|
9
|
+
4. Upload the gem: `gem push gitlab-sidekiq-fetcher-X.X.X.gem`
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'gitlab-sidekiq-fetcher'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.authors = ['TEA', 'GitLab']
|
5
|
+
s.email = 'valery@gitlab.com'
|
6
|
+
s.license = 'LGPL-3.0'
|
7
|
+
s.homepage = 'https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/'
|
8
|
+
s.summary = 'Reliable fetch extension for Sidekiq'
|
9
|
+
s.description = 'Redis reliable queue pattern implemented in Sidekiq'
|
10
|
+
s.require_paths = ['lib']
|
11
|
+
s.files = `git ls-files`.split($\)
|
12
|
+
s.test_files = []
|
13
|
+
s.add_dependency 'sidekiq', '~> 5'
|
14
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class BaseReliableFetch
|
5
|
+
DEFAULT_CLEANUP_INTERVAL = 60 * 60 # 1 hour
|
6
|
+
HEARTBEAT_INTERVAL = 20 # seconds
|
7
|
+
HEARTBEAT_LIFESPAN = 60 # seconds
|
8
|
+
HEARTBEAT_RETRY_DELAY = 1 # seconds
|
9
|
+
WORKING_QUEUE_PREFIX = 'working'
|
10
|
+
|
11
|
+
# Defines how often we try to take a lease to not flood our
|
12
|
+
# Redis server with SET requests
|
13
|
+
DEFAULT_LEASE_INTERVAL = 2 * 60 # seconds
|
14
|
+
LEASE_KEY = 'reliable-fetcher-cleanup-lock'
|
15
|
+
|
16
|
+
# Defines the COUNT parameter that will be passed to Redis SCAN command
|
17
|
+
SCAN_COUNT = 1000
|
18
|
+
|
19
|
+
UnitOfWork = Struct.new(:queue, :job) do
|
20
|
+
def acknowledge
|
21
|
+
Sidekiq.redis { |conn| conn.lrem(Sidekiq::BaseReliableFetch.working_queue_name(queue), 1, job) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def queue_name
|
25
|
+
queue.sub(/.*queue:/, '')
|
26
|
+
end
|
27
|
+
|
28
|
+
def requeue
|
29
|
+
Sidekiq.redis do |conn|
|
30
|
+
conn.multi do |multi|
|
31
|
+
multi.lpush(queue, job)
|
32
|
+
multi.lrem(Sidekiq::BaseReliableFetch.working_queue_name(queue), 1, job)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.setup_reliable_fetch!(config)
|
39
|
+
config.options[:fetch] = if config.options[:semi_reliable_fetch]
|
40
|
+
Sidekiq::SemiReliableFetch
|
41
|
+
else
|
42
|
+
Sidekiq::ReliableFetch
|
43
|
+
end
|
44
|
+
|
45
|
+
Sidekiq.logger.info('GitLab reliable fetch activated!')
|
46
|
+
|
47
|
+
start_heartbeat_thread
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.start_heartbeat_thread
|
51
|
+
Thread.new do
|
52
|
+
loop do
|
53
|
+
begin
|
54
|
+
heartbeat
|
55
|
+
|
56
|
+
sleep HEARTBEAT_INTERVAL
|
57
|
+
rescue => e
|
58
|
+
Sidekiq.logger.error("Heartbeat thread error: #{e.message}")
|
59
|
+
|
60
|
+
sleep HEARTBEAT_RETRY_DELAY
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.pid
|
67
|
+
@pid ||= ::Process.pid
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.hostname
|
71
|
+
@hostname ||= Socket.gethostname
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.heartbeat
|
75
|
+
Sidekiq.redis do |conn|
|
76
|
+
conn.set(heartbeat_key(hostname, pid), 1, ex: HEARTBEAT_LIFESPAN)
|
77
|
+
end
|
78
|
+
|
79
|
+
Sidekiq.logger.debug("Heartbeat for hostname: #{hostname} and pid: #{pid}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.bulk_requeue(inprogress, _options)
|
83
|
+
return if inprogress.empty?
|
84
|
+
|
85
|
+
Sidekiq.logger.debug('Re-queueing terminated jobs')
|
86
|
+
|
87
|
+
Sidekiq.redis do |conn|
|
88
|
+
inprogress.each do |unit_of_work|
|
89
|
+
conn.multi do |multi|
|
90
|
+
multi.lpush(unit_of_work.queue, unit_of_work.job)
|
91
|
+
multi.lrem(working_queue_name(unit_of_work.queue), 1, unit_of_work.job)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
|
97
|
+
rescue => e
|
98
|
+
Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{e.message}")
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.heartbeat_key(hostname, pid)
|
102
|
+
"reliable-fetcher-heartbeat-#{hostname}-#{pid}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.working_queue_name(queue)
|
106
|
+
"#{WORKING_QUEUE_PREFIX}:#{queue}:#{hostname}:#{pid}"
|
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
|
+
@cleanup_interval = options.fetch(:cleanup_interval, DEFAULT_CLEANUP_INTERVAL)
|
115
|
+
@lease_interval = options.fetch(:lease_interval, DEFAULT_LEASE_INTERVAL)
|
116
|
+
@last_try_to_take_lease_at = 0
|
117
|
+
@strictly_ordered_queues = !!options[:strict]
|
118
|
+
@queues = options[:queues].map { |q| "queue:#{q}" }
|
119
|
+
end
|
120
|
+
|
121
|
+
def retrieve_work
|
122
|
+
clean_working_queues! if take_lease
|
123
|
+
|
124
|
+
retrieve_unit_of_work
|
125
|
+
end
|
126
|
+
|
127
|
+
def retrieve_unit_of_work
|
128
|
+
raise NotImplementedError,
|
129
|
+
"#{self.class} does not implement #{__method__}"
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def clean_working_queue!(working_queue)
|
135
|
+
original_queue = working_queue.gsub(/#{WORKING_QUEUE_PREFIX}:|:[^:]*:[0-9]*\z/, '')
|
136
|
+
|
137
|
+
Sidekiq.redis do |conn|
|
138
|
+
count = 0
|
139
|
+
|
140
|
+
while conn.rpoplpush(working_queue, original_queue) do
|
141
|
+
count += 1
|
142
|
+
end
|
143
|
+
|
144
|
+
Sidekiq.logger.info("Requeued #{count} dead jobs to #{original_queue}")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Detect "old" jobs and requeue them because the worker they were assigned
|
149
|
+
# to probably failed miserably.
|
150
|
+
def clean_working_queues!
|
151
|
+
Sidekiq.logger.info("Cleaning working queues")
|
152
|
+
|
153
|
+
Sidekiq.redis do |conn|
|
154
|
+
conn.scan_each(match: "#{WORKING_QUEUE_PREFIX}:queue:*", count: SCAN_COUNT) do |key|
|
155
|
+
# Example: "working:name_of_the_job:queue:{hostname}:{PID}"
|
156
|
+
hostname, pid = key.scan(/:([^:]*):([0-9]*)\z/).flatten
|
157
|
+
|
158
|
+
continue if hostname.nil? || pid.nil?
|
159
|
+
|
160
|
+
clean_working_queue!(key) if worker_dead?(hostname, pid)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def worker_dead?(hostname, pid)
|
166
|
+
Sidekiq.redis do |conn|
|
167
|
+
!conn.get(self.class.heartbeat_key(hostname, pid))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def take_lease
|
172
|
+
return unless allowed_to_take_a_lease?
|
173
|
+
|
174
|
+
@last_try_to_take_lease_at = Time.now.to_f
|
175
|
+
|
176
|
+
Sidekiq.redis do |conn|
|
177
|
+
conn.set(LEASE_KEY, 1, nx: true, ex: cleanup_interval)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def allowed_to_take_a_lease?
|
182
|
+
Time.now.to_f - last_try_to_take_lease_at > lease_interval
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class ReliableFetch < BaseReliableFetch
|
5
|
+
# For reliable fetch we don't use Redis' blocking operations so
|
6
|
+
# we inject a regular sleep into the loop.
|
7
|
+
RELIABLE_FETCH_IDLE_TIMEOUT = 5 # seconds
|
8
|
+
|
9
|
+
attr_reader :queues_iterator, :queues_size
|
10
|
+
|
11
|
+
def initialize(options)
|
12
|
+
super
|
13
|
+
|
14
|
+
@queues_size = queues.size
|
15
|
+
@queues_iterator = queues.cycle
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
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
|
25
|
+
|
26
|
+
work = Sidekiq.redis do |conn|
|
27
|
+
conn.rpoplpush(queue, self.class.working_queue_name(queue))
|
28
|
+
end
|
29
|
+
|
30
|
+
return UnitOfWork.new(queue, work) if work
|
31
|
+
end
|
32
|
+
|
33
|
+
# We didn't find a job in any of the configured queues. Let's sleep a bit
|
34
|
+
# to avoid uselessly burning too much CPU
|
35
|
+
sleep(RELIABLE_FETCH_IDLE_TIMEOUT)
|
36
|
+
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class SemiReliableFetch < BaseReliableFetch
|
5
|
+
# We want the fetch operation to timeout every few seconds so the thread
|
6
|
+
# can check if the process is shutting down. This constant is only used
|
7
|
+
# for semi-reliable fetch.
|
8
|
+
SEMI_RELIABLE_FETCH_TIMEOUT = 2 # seconds
|
9
|
+
|
10
|
+
def initialize(options)
|
11
|
+
super
|
12
|
+
|
13
|
+
if strictly_ordered_queues
|
14
|
+
@queues = @queues.uniq
|
15
|
+
@queues << SEMI_RELIABLE_FETCH_TIMEOUT
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def retrieve_unit_of_work
|
22
|
+
work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
|
23
|
+
return unless work
|
24
|
+
|
25
|
+
unit_of_work = UnitOfWork.new(*work)
|
26
|
+
|
27
|
+
Sidekiq.redis do |conn|
|
28
|
+
conn.lpush(self.class.working_queue_name(unit_of_work.queue), unit_of_work.job)
|
29
|
+
end
|
30
|
+
|
31
|
+
unit_of_work
|
32
|
+
end
|
33
|
+
|
34
|
+
def queues_cmd
|
35
|
+
if strictly_ordered_queues
|
36
|
+
@queues
|
37
|
+
else
|
38
|
+
queues = @queues.shuffle.uniq
|
39
|
+
queues << SEMI_RELIABLE_FETCH_TIMEOUT
|
40
|
+
queues
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fetch_shared_examples'
|
3
|
+
require 'sidekiq/base_reliable_fetch'
|
4
|
+
require 'sidekiq/reliable_fetch'
|
5
|
+
require 'sidekiq/semi_reliable_fetch'
|
6
|
+
|
7
|
+
describe Sidekiq::BaseReliableFetch do
|
8
|
+
before { Sidekiq.redis(&:flushdb) }
|
9
|
+
|
10
|
+
describe 'UnitOfWork' do
|
11
|
+
let(:fetcher) { Sidekiq::ReliableFetch.new(queues: ['foo']) }
|
12
|
+
|
13
|
+
describe '#requeue' do
|
14
|
+
it 'requeues job' do
|
15
|
+
Sidekiq.redis { |conn| conn.rpush('queue:foo', 'msg') }
|
16
|
+
|
17
|
+
uow = fetcher.retrieve_work
|
18
|
+
|
19
|
+
uow.requeue
|
20
|
+
|
21
|
+
expect(Sidekiq::Queue.new('foo').size).to eq 1
|
22
|
+
expect(working_queue_size('foo')).to eq 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#acknowledge' do
|
27
|
+
it 'acknowledges job' do
|
28
|
+
Sidekiq.redis { |conn| conn.rpush('queue:foo', 'msg') }
|
29
|
+
|
30
|
+
uow = fetcher.retrieve_work
|
31
|
+
|
32
|
+
expect { uow.acknowledge }
|
33
|
+
.to change { working_queue_size('foo') }.by(-1)
|
34
|
+
|
35
|
+
expect(Sidekiq::Queue.new('foo').size).to eq 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.bulk_requeue' do
|
41
|
+
it 'requeues the bulk' do
|
42
|
+
queue1 = Sidekiq::Queue.new('foo')
|
43
|
+
queue2 = Sidekiq::Queue.new('bar')
|
44
|
+
|
45
|
+
expect(queue1.size).to eq 0
|
46
|
+
expect(queue2.size).to eq 0
|
47
|
+
|
48
|
+
uow = described_class::UnitOfWork
|
49
|
+
jobs = [ uow.new('queue:foo', 'bob'), uow.new('queue:foo', 'bar'), uow.new('queue:bar', 'widget') ]
|
50
|
+
described_class.bulk_requeue(jobs, queues: [])
|
51
|
+
|
52
|
+
expect(queue1.size).to eq 2
|
53
|
+
expect(queue2.size).to eq 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'sets heartbeat' do
|
58
|
+
config = double(:sidekiq_config, options: {})
|
59
|
+
|
60
|
+
heartbeat_thread = described_class.setup_reliable_fetch!(config)
|
61
|
+
|
62
|
+
Sidekiq.redis do |conn|
|
63
|
+
sleep 0.2 # Give the time to heartbeat thread to make a loop
|
64
|
+
|
65
|
+
heartbeat_key = described_class.heartbeat_key(Socket.gethostname, ::Process.pid)
|
66
|
+
heartbeat = conn.get(heartbeat_key)
|
67
|
+
|
68
|
+
expect(heartbeat).not_to be_nil
|
69
|
+
end
|
70
|
+
|
71
|
+
heartbeat_thread.kill
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
shared_examples 'a Sidekiq fetcher' do
|
2
|
+
let(:queues) { ['assigned'] }
|
3
|
+
|
4
|
+
before { Sidekiq.redis(&:flushdb) }
|
5
|
+
|
6
|
+
describe '#retrieve_work' do
|
7
|
+
let(:fetcher) { described_class.new(queues: ['assigned']) }
|
8
|
+
|
9
|
+
it 'retrieves the job and puts it to working queue' do
|
10
|
+
Sidekiq.redis { |conn| conn.rpush('queue:assigned', 'msg') }
|
11
|
+
|
12
|
+
uow = fetcher.retrieve_work
|
13
|
+
|
14
|
+
expect(working_queue_size('assigned')).to eq 1
|
15
|
+
expect(uow.queue_name).to eq 'assigned'
|
16
|
+
expect(uow.job).to eq 'msg'
|
17
|
+
expect(Sidekiq::Queue.new('assigned').size).to eq 0
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not retrieve a job from foreign queue' do
|
21
|
+
Sidekiq.redis { |conn| conn.rpush('queue:not_assigned', 'msg') }
|
22
|
+
|
23
|
+
expect(fetcher.retrieve_work).to be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'requeues jobs from dead working queue' do
|
27
|
+
Sidekiq.redis do |conn|
|
28
|
+
conn.rpush(other_process_working_queue_name('assigned'), 'msg')
|
29
|
+
end
|
30
|
+
|
31
|
+
uow = fetcher.retrieve_work
|
32
|
+
|
33
|
+
expect(uow.job).to eq 'msg'
|
34
|
+
|
35
|
+
Sidekiq.redis do |conn|
|
36
|
+
expect(conn.llen(other_process_working_queue_name('assigned'))).to eq 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not requeue jobs from live working queue' do
|
41
|
+
working_queue = live_other_process_working_queue_name('assigned')
|
42
|
+
|
43
|
+
Sidekiq.redis do |conn|
|
44
|
+
conn.rpush(working_queue, 'msg')
|
45
|
+
end
|
46
|
+
|
47
|
+
uow = fetcher.retrieve_work
|
48
|
+
|
49
|
+
expect(uow).to be_nil
|
50
|
+
|
51
|
+
Sidekiq.redis do |conn|
|
52
|
+
expect(conn.llen(working_queue)).to eq 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does not clean up orphaned jobs more than once per cleanup interval' do
|
57
|
+
Sidekiq.redis = Sidekiq::RedisConnection.create(url: REDIS_URL, size: 10)
|
58
|
+
|
59
|
+
expect_any_instance_of(described_class)
|
60
|
+
.to receive(:clean_working_queues!).once
|
61
|
+
|
62
|
+
threads = 10.times.map do
|
63
|
+
Thread.new do
|
64
|
+
described_class.new(queues: ['assigned']).retrieve_work
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
threads.map(&:join)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'retrieves by order when strictly order is enabled' do
|
72
|
+
fetcher = described_class.new(strict: true, queues: ['first', 'second'])
|
73
|
+
|
74
|
+
Sidekiq.redis do |conn|
|
75
|
+
conn.rpush('queue:first', ['msg3', 'msg2', 'msg1'])
|
76
|
+
conn.rpush('queue:second', 'msg4')
|
77
|
+
end
|
78
|
+
|
79
|
+
jobs = (1..4).map { fetcher.retrieve_work.job }
|
80
|
+
|
81
|
+
expect(jobs).to eq ['msg1', 'msg2', 'msg3', 'msg4']
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not starve any queue when queues are not strictly ordered' do
|
85
|
+
fetcher = described_class.new(queues: ['first', 'second'])
|
86
|
+
|
87
|
+
Sidekiq.redis do |conn|
|
88
|
+
conn.rpush('queue:first', (1..200).map { |i| "msg#{i}" })
|
89
|
+
conn.rpush('queue:second', 'this_job_should_not_stuck')
|
90
|
+
end
|
91
|
+
|
92
|
+
jobs = (1..100).map { fetcher.retrieve_work.job }
|
93
|
+
|
94
|
+
expect(jobs).to include 'this_job_should_not_stuck'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def working_queue_size(queue_name)
|
100
|
+
Sidekiq.redis do |c|
|
101
|
+
c.llen(Sidekiq::BaseReliableFetch.working_queue_name("queue:#{queue_name}"))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def other_process_working_queue_name(queue)
|
106
|
+
"#{Sidekiq::BaseReliableFetch::WORKING_QUEUE_PREFIX}:queue:#{queue}:#{Socket.gethostname}:#{::Process.pid + 1}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def live_other_process_working_queue_name(queue)
|
110
|
+
pid = ::Process.pid + 1
|
111
|
+
hostname = Socket.gethostname
|
112
|
+
|
113
|
+
Sidekiq.redis do |conn|
|
114
|
+
conn.set(Sidekiq::BaseReliableFetch.heartbeat_key(hostname, pid), 1)
|
115
|
+
end
|
116
|
+
|
117
|
+
"#{Sidekiq::BaseReliableFetch::WORKING_QUEUE_PREFIX}:queue:#{queue}:#{hostname}:#{pid}"
|
118
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'sidekiq/util'
|
3
|
+
require 'sidekiq/api'
|
4
|
+
require 'pry'
|
5
|
+
require 'simplecov'
|
6
|
+
|
7
|
+
SimpleCov.start
|
8
|
+
|
9
|
+
REDIS_URL = ENV['REDIS_URL'] || 'redis://localhost:6379/10'
|
10
|
+
|
11
|
+
Sidekiq.configure_client do |config|
|
12
|
+
config.redis = { url: REDIS_URL }
|
13
|
+
end
|
14
|
+
|
15
|
+
Sidekiq.logger.level = Logger::ERROR
|
16
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
17
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
18
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
19
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
20
|
+
# files.
|
21
|
+
#
|
22
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
23
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
24
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
25
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
26
|
+
# a separate helper file that requires the additional dependencies and performs
|
27
|
+
# the additional setup, and require it from the spec files that actually need
|
28
|
+
# it.
|
29
|
+
#
|
30
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
31
|
+
RSpec.configure do |config|
|
32
|
+
# rspec-expectations config goes here. You can use an alternate
|
33
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
34
|
+
# assertions if you prefer.
|
35
|
+
config.expect_with :rspec do |expectations|
|
36
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
37
|
+
# and `failure_message` of custom matchers include text for helper methods
|
38
|
+
# defined using `chain`, e.g.:
|
39
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
40
|
+
# # => "be bigger than 2 and smaller than 4"
|
41
|
+
# ...rather than:
|
42
|
+
# # => "be bigger than 2"
|
43
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
44
|
+
end
|
45
|
+
|
46
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
47
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
48
|
+
config.mock_with :rspec do |mocks|
|
49
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
50
|
+
# a real object. This is generally recommended, and will default to
|
51
|
+
# `true` in RSpec 4.
|
52
|
+
mocks.verify_partial_doubles = true
|
53
|
+
end
|
54
|
+
|
55
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
56
|
+
# have no way to turn it off -- the option exists only for backwards
|
57
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
58
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
59
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
60
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
61
|
+
|
62
|
+
# The settings below are suggested to provide a good initial experience
|
63
|
+
# with RSpec, but feel free to customize to your heart's content.
|
64
|
+
=begin
|
65
|
+
# This allows you to limit a spec run to individual examples or groups
|
66
|
+
# you care about by tagging them with `:focus` metadata. When nothing
|
67
|
+
# is tagged with `:focus`, all examples get run. RSpec also provides
|
68
|
+
# aliases for `it`, `describe`, and `context` that include `:focus`
|
69
|
+
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
70
|
+
config.filter_run_when_matching :focus
|
71
|
+
|
72
|
+
# Allows RSpec to persist some state between runs in order to support
|
73
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
74
|
+
# you configure your source control system to ignore this file.
|
75
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
76
|
+
|
77
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
78
|
+
# recommended. For more details, see:
|
79
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
80
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
81
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
82
|
+
config.disable_monkey_patching!
|
83
|
+
|
84
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
85
|
+
# be too noisy due to issues in dependencies.
|
86
|
+
config.warnings = true
|
87
|
+
|
88
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
89
|
+
# file, and it's useful to allow more verbose output when running an
|
90
|
+
# individual spec file.
|
91
|
+
if config.files_to_run.one?
|
92
|
+
# Use the documentation formatter for detailed output,
|
93
|
+
# unless a formatter has already been configured
|
94
|
+
# (e.g. via a command-line flag).
|
95
|
+
config.default_formatter = "doc"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Print the 10 slowest examples and example groups at the
|
99
|
+
# end of the spec run, to help surface which specs are running
|
100
|
+
# particularly slow.
|
101
|
+
config.profile_examples = 10
|
102
|
+
|
103
|
+
# Run specs in random order to surface order dependencies. If you find an
|
104
|
+
# order dependency and want to debug it, you can fix the order by providing
|
105
|
+
# the seed, which is printed after each run.
|
106
|
+
# --seed 1234
|
107
|
+
config.order = :random
|
108
|
+
|
109
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
110
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
111
|
+
# test failures related to randomization by passing the same `--seed` value
|
112
|
+
# as the one that triggered the failure.
|
113
|
+
Kernel.srand config.seed
|
114
|
+
=end
|
115
|
+
end
|
data/test/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# How to run
|
2
|
+
|
3
|
+
```
|
4
|
+
cd test
|
5
|
+
bundle exec ruby reliability_test.rb
|
6
|
+
```
|
7
|
+
|
8
|
+
You can adjust some parameters of the test in the `config.rb`
|
9
|
+
|
10
|
+
|
11
|
+
# How it works
|
12
|
+
|
13
|
+
This tool spawns configured number of Sidekiq workers and when the amount of processed jobs is about half of origin
|
14
|
+
number it will kill all the workers with `kill -9` and then it will spawn new workers again until all the jobs are processed. To track the process and counters we use Redis keys/counters.
|
15
|
+
|
16
|
+
# How to run tests
|
17
|
+
|
18
|
+
To run rspec:
|
19
|
+
|
20
|
+
```
|
21
|
+
bundle exec rspec
|
22
|
+
```
|
23
|
+
|
24
|
+
To run performance tests:
|
25
|
+
|
26
|
+
```
|
27
|
+
cd test
|
28
|
+
JOB_FETCHER=semi bundle exec ruby reliability_test.rb
|
29
|
+
```
|
30
|
+
|
31
|
+
JOB_FETCHER can be set to one of these values: `semi`, `reliable`, `basic`
|
32
|
+
|
33
|
+
To run both kind of tests you need to have redis server running on default HTTP port `6379`. To use other HTTP port, you can define
|
34
|
+
`REDIS_URL` environment varible with the port you need(example: `REDIS_URL="redis://localhost:9999"`).
|
data/test/config.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/sidekiq/base_reliable_fetch'
|
4
|
+
require_relative '../lib/sidekiq/reliable_fetch'
|
5
|
+
require_relative '../lib/sidekiq/semi_reliable_fetch'
|
6
|
+
require_relative 'worker'
|
7
|
+
|
8
|
+
REDIS_FINISHED_LIST = 'reliable-fetcher-finished-jids'
|
9
|
+
|
10
|
+
NUMBER_OF_WORKERS = ENV['NUMBER_OF_WORKERS'] || 10
|
11
|
+
NUMBER_OF_JOBS = ENV['NUMBER_OF_JOBS'] || 1000
|
12
|
+
JOB_FETCHER = (ENV['JOB_FETCHER'] || :semi).to_sym # :basic, :semi, :reliable
|
13
|
+
TEST_CLEANUP_INTERVAL = 20
|
14
|
+
TEST_LEASE_INTERVAL = 5
|
15
|
+
WAIT_CLEANUP = TEST_CLEANUP_INTERVAL +
|
16
|
+
TEST_LEASE_INTERVAL +
|
17
|
+
Sidekiq::ReliableFetch::HEARTBEAT_LIFESPAN
|
18
|
+
|
19
|
+
Sidekiq.configure_server do |config|
|
20
|
+
if %i[semi reliable].include?(JOB_FETCHER)
|
21
|
+
config.options[:semi_reliable_fetch] = (JOB_FETCHER == :semi)
|
22
|
+
|
23
|
+
# We need to override these parameters to not wait too long
|
24
|
+
# The default values are good for production use only
|
25
|
+
# These will be ignored for :basic
|
26
|
+
config.options[:cleanup_interval] = TEST_CLEANUP_INTERVAL
|
27
|
+
config.options[:lease_interval] = TEST_LEASE_INTERVAL
|
28
|
+
|
29
|
+
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
require 'sidekiq/util'
|
5
|
+
require 'sidekiq/cli'
|
6
|
+
require_relative 'config'
|
7
|
+
|
8
|
+
def spawn_workers_and_stop_them_on_a_half_way
|
9
|
+
pids = spawn_workers
|
10
|
+
|
11
|
+
wait_until do |queue_size|
|
12
|
+
queue_size < NUMBER_OF_JOBS / 2
|
13
|
+
end
|
14
|
+
|
15
|
+
first_half_pids, second_half_pids = split_array(pids)
|
16
|
+
|
17
|
+
puts 'Killing half of the workers...'
|
18
|
+
signal_to_workers('KILL', first_half_pids)
|
19
|
+
|
20
|
+
puts 'Stopping another half of the workers...'
|
21
|
+
signal_to_workers('TERM', second_half_pids)
|
22
|
+
end
|
23
|
+
|
24
|
+
def spawn_workers_and_let_them_finish
|
25
|
+
puts 'Spawn workers and let them finish...'
|
26
|
+
|
27
|
+
pids = spawn_workers
|
28
|
+
|
29
|
+
wait_until do |queue_size|
|
30
|
+
queue_size.zero?
|
31
|
+
end
|
32
|
+
|
33
|
+
if %i[semi reliable].include? JOB_FETCHER
|
34
|
+
puts 'Waiting for clean up process that will requeue dead jobs...'
|
35
|
+
sleep WAIT_CLEANUP
|
36
|
+
end
|
37
|
+
|
38
|
+
signal_to_workers('TERM', pids)
|
39
|
+
end
|
40
|
+
|
41
|
+
def wait_until
|
42
|
+
loop do
|
43
|
+
sleep 3
|
44
|
+
|
45
|
+
queue_size = current_queue_size
|
46
|
+
puts "Jobs in the queue:#{queue_size}"
|
47
|
+
|
48
|
+
break if yield(queue_size)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def signal_to_workers(signal, pids)
|
53
|
+
pids.each { |pid| Process.kill(signal, pid) }
|
54
|
+
pids.each { |pid| Process.wait(pid) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def spawn_workers
|
58
|
+
pids = []
|
59
|
+
NUMBER_OF_WORKERS.times do
|
60
|
+
pids << spawn('sidekiq -r ./config.rb')
|
61
|
+
end
|
62
|
+
|
63
|
+
pids
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_queue_size
|
67
|
+
Sidekiq.redis { |c| c.llen('queue:default') }
|
68
|
+
end
|
69
|
+
|
70
|
+
def duplicates
|
71
|
+
Sidekiq.redis { |c| c.llen(REDIS_FINISHED_LIST) }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Splits array into two halves
|
75
|
+
def split_array(arr)
|
76
|
+
first_arr = arr.take(arr.size / 2)
|
77
|
+
second_arr = arr - first_arr
|
78
|
+
[first_arr, second_arr]
|
79
|
+
end
|
80
|
+
|
81
|
+
##########################################################
|
82
|
+
|
83
|
+
puts '########################################'
|
84
|
+
puts "Mode: #{JOB_FETCHER}"
|
85
|
+
puts '########################################'
|
86
|
+
|
87
|
+
Sidekiq.redis(&:flushdb)
|
88
|
+
|
89
|
+
jobs = []
|
90
|
+
|
91
|
+
NUMBER_OF_JOBS.times do
|
92
|
+
jobs << TestWorker.perform_async
|
93
|
+
end
|
94
|
+
|
95
|
+
puts "Queued #{NUMBER_OF_JOBS} jobs"
|
96
|
+
|
97
|
+
spawn_workers_and_stop_them_on_a_half_way
|
98
|
+
spawn_workers_and_let_them_finish
|
99
|
+
|
100
|
+
jobs_lost = 0
|
101
|
+
|
102
|
+
Sidekiq.redis do |redis|
|
103
|
+
jobs.each do |job|
|
104
|
+
next if redis.lrem(REDIS_FINISHED_LIST, 1, job) == 1
|
105
|
+
jobs_lost += 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "Remaining unprocessed: #{jobs_lost}"
|
110
|
+
puts "Duplicates found: #{duplicates}"
|
111
|
+
|
112
|
+
if jobs_lost.zero? && duplicates.zero?
|
113
|
+
exit 0
|
114
|
+
else
|
115
|
+
exit 1
|
116
|
+
end
|
data/test/worker.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TestWorker
|
4
|
+
include Sidekiq::Worker
|
5
|
+
|
6
|
+
def perform
|
7
|
+
# To mimic long running job and to increase the probability of losing the job
|
8
|
+
sleep 1
|
9
|
+
|
10
|
+
Sidekiq.redis do |redis|
|
11
|
+
redis.lpush(REDIS_FINISHED_LIST, get_sidekiq_job_id)
|
12
|
+
end
|
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
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gitlab-sidekiq-fetcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TEA
|
8
|
+
- GitLab
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-12-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sidekiq
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '5'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '5'
|
28
|
+
description: Redis reliable queue pattern implemented in Sidekiq
|
29
|
+
email: valery@gitlab.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- ".gitlab-ci.yml"
|
36
|
+
- ".rspec"
|
37
|
+
- Gemfile
|
38
|
+
- Gemfile.lock
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- RELEASE-GITLAB.md
|
42
|
+
- gitlab-sidekiq-fetcher.gemspec
|
43
|
+
- lib/sidekiq-reliable-fetch.rb
|
44
|
+
- lib/sidekiq/base_reliable_fetch.rb
|
45
|
+
- lib/sidekiq/reliable_fetch.rb
|
46
|
+
- lib/sidekiq/semi_reliable_fetch.rb
|
47
|
+
- spec/base_reliable_fetch_spec.rb
|
48
|
+
- spec/fetch_shared_examples.rb
|
49
|
+
- spec/reliable_fetch_spec.rb
|
50
|
+
- spec/semi_reliable_fetch_spec.rb
|
51
|
+
- spec/spec_helper.rb
|
52
|
+
- test/README.md
|
53
|
+
- test/config.rb
|
54
|
+
- test/reliability_test.rb
|
55
|
+
- test/worker.rb
|
56
|
+
homepage: https://gitlab.com/gitlab-org/sidekiq-reliable-fetch/
|
57
|
+
licenses:
|
58
|
+
- LGPL-3.0
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 2.7.6
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Reliable fetch extension for Sidekiq
|
80
|
+
test_files: []
|