gitlab-sidekiq-fetcher 0.1.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 +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: []
|