sidekiq-throttled 0.17.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.adoc +314 -0
- data/lib/sidekiq/throttled/config.rb +44 -0
- data/lib/sidekiq/throttled/cooldown.rb +55 -0
- data/lib/sidekiq/throttled/expirable_set.rb +70 -0
- data/lib/sidekiq/throttled/job.rb +4 -4
- data/lib/sidekiq/throttled/middlewares/server.rb +28 -0
- data/lib/sidekiq/throttled/patches/basic_fetch.rb +53 -0
- data/lib/sidekiq/throttled/registry.rb +4 -7
- data/lib/sidekiq/throttled/strategy/concurrency.rb +4 -6
- data/lib/sidekiq/throttled/strategy/threshold.rb +4 -6
- data/lib/sidekiq/throttled/strategy.rb +10 -10
- data/lib/sidekiq/throttled/strategy_collection.rb +2 -3
- data/lib/sidekiq/throttled/version.rb +1 -1
- data/lib/sidekiq/throttled/web.rb +2 -45
- data/lib/sidekiq/throttled/worker.rb +1 -1
- data/lib/sidekiq/throttled.rb +45 -57
- metadata +25 -70
- data/.coveralls.yml +0 -1
- data/.github/dependabot.yml +0 -12
- data/.github/workflows/ci.yml +0 -52
- data/.gitignore +0 -12
- data/.rspec +0 -5
- data/.rubocop.yml +0 -20
- data/.rubocop_todo.yml +0 -68
- data/.travis.yml +0 -37
- data/.yardopts +0 -1
- data/Appraisals +0 -9
- data/CHANGES.md +0 -300
- data/Gemfile +0 -34
- data/Guardfile +0 -25
- data/README.md +0 -297
- data/Rakefile +0 -27
- data/gemfiles/sidekiq_6.4.gemfile +0 -33
- data/gemfiles/sidekiq_6.5.gemfile +0 -33
- data/lib/sidekiq/throttled/communicator/callbacks.rb +0 -72
- data/lib/sidekiq/throttled/communicator/exception_handler.rb +0 -25
- data/lib/sidekiq/throttled/communicator/listener.rb +0 -109
- data/lib/sidekiq/throttled/communicator.rb +0 -116
- data/lib/sidekiq/throttled/configuration.rb +0 -50
- data/lib/sidekiq/throttled/expirable_list.rb +0 -70
- data/lib/sidekiq/throttled/fetch/unit_of_work.rb +0 -83
- data/lib/sidekiq/throttled/fetch.rb +0 -94
- data/lib/sidekiq/throttled/middleware.rb +0 -22
- data/lib/sidekiq/throttled/patches/queue.rb +0 -18
- data/lib/sidekiq/throttled/queue_name.rb +0 -46
- data/lib/sidekiq/throttled/queues_pauser.rb +0 -152
- data/lib/sidekiq/throttled/testing.rb +0 -12
- data/lib/sidekiq/throttled/utils.rb +0 -19
- data/lib/sidekiq/throttled/web/queues.html.erb +0 -49
- data/lib/sidekiq/throttled/web/summary_fix.js +0 -10
- data/lib/sidekiq/throttled/web/summary_fix.rb +0 -35
- data/rubocop/layout.yml +0 -24
- data/rubocop/lint.yml +0 -41
- data/rubocop/metrics.yml +0 -4
- data/rubocop/performance.yml +0 -25
- data/rubocop/rspec.yml +0 -3
- data/rubocop/style.yml +0 -84
- data/sidekiq-throttled.gemspec +0 -36
- /data/{LICENSE.md → LICENSE.txt} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31c4673002a4919f71a18009e82812f21a777d241072bc095994e4a67662a49e
|
4
|
+
data.tar.gz: fa2c768c9ea4b023e16864c77eabe4949e9e3301a20aab5cdb5acdbdd3de9702
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 636e41cc593e6f164061ff901ab6dab4eba7c65acf1bb41febbca13be55e9336bd86413ac50db80469429ff3bd8d9eaccd62b4e9f397a7692c184c7c70f0b98f
|
7
|
+
data.tar.gz: 7055289ad5e059da619ede7c4d9ec73876894dc4a3913851e55e256e81be9bb08c3671199c81eafaf6865fe50b0f60ad670fa2e74460de45c89b20af9514a90e
|
data/README.adoc
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
= Sidekiq::Throttled
|
2
|
+
:ci-link: https://github.com/ixti/sidekiq-throttled/actions/workflows/ci.yml
|
3
|
+
:ci-badge: https://img.shields.io/github/actions/workflow/status/ixti/sidekiq-throttled/ci.yml?branch=main&style=for-the-badge
|
4
|
+
:gem-link: http://rubygems.org/gems/sidekiq-throttled
|
5
|
+
:gem-badge: https://img.shields.io/gem/v/sidekiq-throttled?style=for-the-badge
|
6
|
+
:doc-link: http://www.rubydoc.info/gems/sidekiq-throttled
|
7
|
+
:doc-badge: https://img.shields.io/badge/Documentation-API-blue?style=for-the-badge
|
8
|
+
|
9
|
+
****
|
10
|
+
{ci-link}[image:{ci-badge}[CI Status]]
|
11
|
+
{gem-link}[image:{gem-badge}[Latest Version]]
|
12
|
+
{doc-link}[image:{doc-badge}[API Documentation]]
|
13
|
+
****
|
14
|
+
|
15
|
+
NOTE: This is the 1.x *development* branch. For the 0.x *stable* branch, please
|
16
|
+
see: https://github.com/ixti/sidekiq-throttled/tree/0-x-stable[0-x-stable]
|
17
|
+
|
18
|
+
Concurrency and threshold throttling for https://github.com/mperham/sidekiq[Sidekiq].
|
19
|
+
|
20
|
+
|
21
|
+
== Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
[source,ruby]
|
26
|
+
----
|
27
|
+
gem "sidekiq-throttled"
|
28
|
+
----
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install sidekiq-throttled
|
37
|
+
|
38
|
+
|
39
|
+
== Usage
|
40
|
+
|
41
|
+
Add somewhere in your app's bootstrap (e.g. `config/initializers/sidekiq.rb` if
|
42
|
+
you are using Rails):
|
43
|
+
|
44
|
+
[source,ruby]
|
45
|
+
----
|
46
|
+
require "sidekiq/throttled"
|
47
|
+
----
|
48
|
+
|
49
|
+
Once you've done that you can include `Sidekiq::Throttled::Job` to your
|
50
|
+
job classes and configure throttling:
|
51
|
+
|
52
|
+
[source,ruby]
|
53
|
+
----
|
54
|
+
class MyJob
|
55
|
+
include Sidekiq::Job
|
56
|
+
include Sidekiq::Throttled::Job
|
57
|
+
|
58
|
+
sidekiq_options :queue => :my_queue
|
59
|
+
|
60
|
+
sidekiq_throttle(
|
61
|
+
# Allow maximum 10 concurrent jobs of this class at a time.
|
62
|
+
concurrency: { limit: 10 },
|
63
|
+
# Allow maximum 1K jobs being processed within one hour window.
|
64
|
+
threshold: { limit: 1_000, period: 1.hour }
|
65
|
+
)
|
66
|
+
|
67
|
+
def perform
|
68
|
+
# ...
|
69
|
+
end
|
70
|
+
end
|
71
|
+
----
|
72
|
+
|
73
|
+
TIP: `Sidekiq::Throttled::Job` is aliased as `Sidekiq::Throttled::Worker`,
|
74
|
+
thus if you're using `Sidekiq::Worker` naming convention, you can use the
|
75
|
+
alias for consistency:
|
76
|
+
|
77
|
+
[source,ruby]
|
78
|
+
----
|
79
|
+
class MyWorker
|
80
|
+
include Sidekiq::Worker
|
81
|
+
include Sidekiq::Throttled::Worker
|
82
|
+
|
83
|
+
# ...
|
84
|
+
end
|
85
|
+
----
|
86
|
+
|
87
|
+
|
88
|
+
=== Middleware(s)
|
89
|
+
|
90
|
+
`Sidekiq::Throttled` relies on following bundled middlewares:
|
91
|
+
|
92
|
+
* `Sidekiq::Throttled::Middlewares::Server`
|
93
|
+
|
94
|
+
The middleware is automatically injected when you require `sidekiq/throttled`.
|
95
|
+
In rare cases this might be an issue. You can change to order manually:
|
96
|
+
|
97
|
+
[source,ruby]
|
98
|
+
----
|
99
|
+
Sidekiq.configure_server do |config|
|
100
|
+
# ...
|
101
|
+
|
102
|
+
config.server_middleware do |chain|
|
103
|
+
chain.remove(Sidekiq::Throttled::Middlewares::Server)
|
104
|
+
chain.add(Sidekiq::Throttled::Middlewares::Server)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
----
|
108
|
+
|
109
|
+
=== Configuration
|
110
|
+
|
111
|
+
[source,ruby]
|
112
|
+
----
|
113
|
+
Sidekiq::Throttled.configure do |config|
|
114
|
+
# Period in seconds to exclude queue from polling in case it returned
|
115
|
+
# {config.cooldown_threshold} amount of throttled jobs in a row. Set
|
116
|
+
# this value to `nil` to disable cooldown manager completely.
|
117
|
+
# Default: 2.0
|
118
|
+
config.cooldown_period = 2.0
|
119
|
+
|
120
|
+
# Exclude queue from polling after it returned given amount of throttled
|
121
|
+
# jobs in a row.
|
122
|
+
# Default: 1 (cooldown after first throttled job)
|
123
|
+
config.cooldown_threshold = 1
|
124
|
+
end
|
125
|
+
----
|
126
|
+
|
127
|
+
|
128
|
+
=== Observer
|
129
|
+
|
130
|
+
You can specify an observer that will be called on throttling. To do so pass an
|
131
|
+
`:observer` option with callable object:
|
132
|
+
|
133
|
+
[source,ruby]
|
134
|
+
----
|
135
|
+
class MyJob
|
136
|
+
include Sidekiq::Job
|
137
|
+
include Sidekiq::Throttled::Job
|
138
|
+
|
139
|
+
MY_OBSERVER = lambda do |strategy, *args|
|
140
|
+
# do something
|
141
|
+
end
|
142
|
+
|
143
|
+
sidekiq_options queue: :my_queue
|
144
|
+
|
145
|
+
sidekiq_throttle(
|
146
|
+
concurrency: { limit: 10 },
|
147
|
+
threshold: { limit: 100, period: 1.hour },
|
148
|
+
observer: MY_OBSERVER
|
149
|
+
)
|
150
|
+
|
151
|
+
def perform(*args)
|
152
|
+
# ...
|
153
|
+
end
|
154
|
+
end
|
155
|
+
----
|
156
|
+
|
157
|
+
Observer will receive `strategy, *args` arguments, where `strategy` is a Symbol
|
158
|
+
`:concurrency` or `:threshold`, and `*args` are the arguments that were passed
|
159
|
+
to the job.
|
160
|
+
|
161
|
+
|
162
|
+
=== Dynamic throttling
|
163
|
+
|
164
|
+
You can throttle jobs dynamically with `:key_suffix` option:
|
165
|
+
|
166
|
+
[source,ruby]
|
167
|
+
----
|
168
|
+
class MyJob
|
169
|
+
include Sidekiq::Job
|
170
|
+
include Sidekiq::Throttled::Job
|
171
|
+
|
172
|
+
sidekiq_options queue: :my_queue
|
173
|
+
|
174
|
+
sidekiq_throttle(
|
175
|
+
# Allow maximum 10 concurrent jobs per user at a time.
|
176
|
+
concurrency: { limit: 10, key_suffix: -> (user_id) { user_id } }
|
177
|
+
)
|
178
|
+
|
179
|
+
def perform(user_id)
|
180
|
+
# ...
|
181
|
+
end
|
182
|
+
end
|
183
|
+
----
|
184
|
+
|
185
|
+
You can also supply dynamic values for limits and periods by supplying a proc
|
186
|
+
for these values. The proc will be evaluated at the time the job is fetched
|
187
|
+
and will receive the same arguments that are passed to the job.
|
188
|
+
|
189
|
+
[source,ruby]
|
190
|
+
----
|
191
|
+
class MyJob
|
192
|
+
include Sidekiq::Job
|
193
|
+
include Sidekiq::Throttled::Job
|
194
|
+
|
195
|
+
sidekiq_options queue: :my_queue
|
196
|
+
|
197
|
+
sidekiq_throttle(
|
198
|
+
# Allow maximum 1000 concurrent jobs of this class at a time for VIPs and 10 for all other users.
|
199
|
+
concurrency: {
|
200
|
+
limit: ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
|
201
|
+
key_suffix: ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
|
202
|
+
},
|
203
|
+
# Allow 1000 jobs/hour to be processed for VIPs and 10/day for all others
|
204
|
+
threshold: {
|
205
|
+
limit: ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
|
206
|
+
period: ->(user_id) { User.vip?(user_id) ? 1.hour : 1.day },
|
207
|
+
key_suffix: ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
|
208
|
+
}
|
209
|
+
)
|
210
|
+
|
211
|
+
def perform(user_id)
|
212
|
+
# ...
|
213
|
+
end
|
214
|
+
end
|
215
|
+
----
|
216
|
+
|
217
|
+
You also can use several different keys to throttle one worker.
|
218
|
+
|
219
|
+
[source,ruby]
|
220
|
+
----
|
221
|
+
class MyJob
|
222
|
+
include Sidekiq::Job
|
223
|
+
include Sidekiq::Throttled::Job
|
224
|
+
|
225
|
+
sidekiq_options queue: :my_queue
|
226
|
+
|
227
|
+
sidekiq_throttle(
|
228
|
+
# Allow maximum 10 concurrent jobs per project at a time and maximum 2 jobs per user
|
229
|
+
concurrency: [
|
230
|
+
{ limit: 10, key_suffix: -> (project_id, user_id) { project_id } },
|
231
|
+
{ limit: 2, key_suffix: -> (project_id, user_id) { user_id } }
|
232
|
+
]
|
233
|
+
# For :threshold it works the same
|
234
|
+
)
|
235
|
+
|
236
|
+
def perform(project_id, user_id)
|
237
|
+
# ...
|
238
|
+
end
|
239
|
+
end
|
240
|
+
----
|
241
|
+
|
242
|
+
IMPORTANT: Don't forget to specify `:key_suffix` and make it return different
|
243
|
+
values if you are using dynamic limit/period options. Otherwise, you risk
|
244
|
+
getting into some trouble.
|
245
|
+
|
246
|
+
|
247
|
+
=== Concurrency throttling fine-tuning
|
248
|
+
|
249
|
+
Concurrency throttling is based on distributed locks. Those locks have default
|
250
|
+
time to live (TTL) set to 15 minutes. If your job takes more than 15 minutes
|
251
|
+
to finish, lock will be released and you might end up with more jobs running
|
252
|
+
concurrently than you expect.
|
253
|
+
|
254
|
+
This is done to avoid deadlocks - when by any reason (e.g. Sidekiq process was
|
255
|
+
OOM-killed) cleanup middleware wasn't executed and locks were not released.
|
256
|
+
|
257
|
+
If your job takes more than 15 minutes to complete, you can tune concurrency
|
258
|
+
lock TTL to fit your needs:
|
259
|
+
|
260
|
+
[source,ruby]
|
261
|
+
----
|
262
|
+
# Set concurrency strategy lock TTL to 1 hour.
|
263
|
+
sidekiq_throttle(concurrency: { limit: 20, ttl: 1.hour.to_i })
|
264
|
+
----
|
265
|
+
|
266
|
+
|
267
|
+
== Supported Ruby Versions
|
268
|
+
|
269
|
+
This library aims to support and is tested against the following Ruby versions:
|
270
|
+
|
271
|
+
* Ruby 3.0.x
|
272
|
+
* Ruby 3.1.x
|
273
|
+
* Ruby 3.2.x
|
274
|
+
|
275
|
+
If something doesn't work on one of these versions, it's a bug.
|
276
|
+
|
277
|
+
This library may inadvertently work (or seem to work) on other Ruby versions,
|
278
|
+
however support will only be provided for the versions listed above.
|
279
|
+
|
280
|
+
If you would like this library to support another Ruby version or
|
281
|
+
implementation, you may volunteer to be a maintainer. Being a maintainer
|
282
|
+
entails making sure all tests run and pass on that implementation. When
|
283
|
+
something breaks on your implementation, you will be responsible for providing
|
284
|
+
patches in a timely fashion. If critical issues for a particular implementation
|
285
|
+
exist at the time of a major release, support for that Ruby version may be
|
286
|
+
dropped.
|
287
|
+
|
288
|
+
|
289
|
+
== Supported Sidekiq Versions
|
290
|
+
|
291
|
+
This library aims to support and work with following Sidekiq versions:
|
292
|
+
|
293
|
+
* Sidekiq 6.5.x
|
294
|
+
* Sidekiq 7.0.x
|
295
|
+
* Sidekiq 7.1.x
|
296
|
+
* Sidekiq 7.2.x
|
297
|
+
|
298
|
+
|
299
|
+
== Development
|
300
|
+
|
301
|
+
bundle install
|
302
|
+
bundle exec appraisal generate
|
303
|
+
bundle exec appraisal install
|
304
|
+
bundle exec rake
|
305
|
+
|
306
|
+
|
307
|
+
== Contributing
|
308
|
+
|
309
|
+
* Fork sidekiq-throttled on GitHub
|
310
|
+
* Make your changes
|
311
|
+
* Ensure all tests pass (`bundle exec rake`)
|
312
|
+
* Send a pull request
|
313
|
+
* If we like them we'll merge them
|
314
|
+
* If we've accepted a patch, feel free to ask for commit access!
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Throttled
|
5
|
+
# Configuration object.
|
6
|
+
class Config
|
7
|
+
# Period in seconds to exclude queue from polling in case it returned
|
8
|
+
# {#cooldown_threshold} amount of throttled jobs in a row.
|
9
|
+
#
|
10
|
+
# Set this to `nil` to disable cooldown completely.
|
11
|
+
#
|
12
|
+
# @return [Float, nil]
|
13
|
+
attr_reader :cooldown_period
|
14
|
+
|
15
|
+
# Amount of throttled jobs returned from the queue subsequently after
|
16
|
+
# which queue will be excluded from polling for the durations of
|
17
|
+
# {#cooldown_period}.
|
18
|
+
#
|
19
|
+
# @return [Integer]
|
20
|
+
attr_reader :cooldown_threshold
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@cooldown_period = 2.0
|
24
|
+
@cooldown_threshold = 1
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!attribute [w] cooldown_period
|
28
|
+
def cooldown_period=(value)
|
29
|
+
raise TypeError, "unexpected type #{value.class}" unless value.nil? || value.is_a?(Float)
|
30
|
+
raise ArgumentError, "period must be positive" unless value.nil? || value.positive?
|
31
|
+
|
32
|
+
@cooldown_period = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# @!attribute [w] cooldown_threshold
|
36
|
+
def cooldown_threshold=(value)
|
37
|
+
raise TypeError, "unexpected type #{value.class}" unless value.is_a?(Integer)
|
38
|
+
raise ArgumentError, "threshold must be positive" unless value.positive?
|
39
|
+
|
40
|
+
@cooldown_threshold = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
require_relative "./expirable_set"
|
6
|
+
|
7
|
+
module Sidekiq
|
8
|
+
module Throttled
|
9
|
+
# @api internal
|
10
|
+
#
|
11
|
+
# Queues cooldown manager. Tracks list of queues that should be temporarily
|
12
|
+
# (for the duration of {Config#cooldown_period}) excluded from polling.
|
13
|
+
class Cooldown
|
14
|
+
class << self
|
15
|
+
# Returns new {Cooldown} instance if {Config#cooldown_period} is not `nil`.
|
16
|
+
#
|
17
|
+
# @param config [Config]
|
18
|
+
# @return [Cooldown, nil]
|
19
|
+
def [](config)
|
20
|
+
new(config) if config.cooldown_period
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param config [Config]
|
25
|
+
def initialize(config)
|
26
|
+
@queues = ExpirableSet.new(config.cooldown_period)
|
27
|
+
@threshold = config.cooldown_threshold
|
28
|
+
@tracker = Concurrent::Map.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Notify that given queue returned job that was throttled.
|
32
|
+
#
|
33
|
+
# @param queue [String]
|
34
|
+
# @return [void]
|
35
|
+
def notify_throttled(queue)
|
36
|
+
@queues.add(queue) if @threshold <= @tracker.merge_pair(queue, 1, &:succ)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Notify that given queue returned job that was not throttled.
|
40
|
+
#
|
41
|
+
# @param queue [String]
|
42
|
+
# @return [void]
|
43
|
+
def notify_admitted(queue)
|
44
|
+
@tracker.delete(queue)
|
45
|
+
end
|
46
|
+
|
47
|
+
# List of queues that should not be polled
|
48
|
+
#
|
49
|
+
# @return [Array<String>]
|
50
|
+
def queues
|
51
|
+
@queues.to_a
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module Throttled
|
7
|
+
# @api internal
|
8
|
+
#
|
9
|
+
# Set of elements with expirations.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# set = ExpirableSet.new(10.0)
|
13
|
+
# set.add("a")
|
14
|
+
# sleep(5)
|
15
|
+
# set.add("b")
|
16
|
+
# set.to_a # => ["a", "b"]
|
17
|
+
# sleep(5)
|
18
|
+
# set.to_a # => ["b"]
|
19
|
+
class ExpirableSet
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
# @param ttl [Float] expiration is seconds
|
23
|
+
# @raise [ArgumentError] if `ttl` is not positive Float
|
24
|
+
def initialize(ttl)
|
25
|
+
raise ArgumentError, "ttl must be positive Float" unless ttl.is_a?(Float) && ttl.positive?
|
26
|
+
|
27
|
+
@elements = Concurrent::Map.new
|
28
|
+
@ttl = ttl
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param element [Object]
|
32
|
+
# @return [ExpirableSet] self
|
33
|
+
def add(element)
|
34
|
+
# cleanup expired elements to avoid mem-leak
|
35
|
+
horizon = now
|
36
|
+
expired = @elements.each_pair.select { |(_, sunset)| expired?(sunset, horizon) }
|
37
|
+
expired.each { |pair| @elements.delete_pair(*pair) }
|
38
|
+
|
39
|
+
# add new element
|
40
|
+
@elements[element] = now + @ttl
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# @yield [Object] Gives each live (not expired) element to the block
|
46
|
+
def each
|
47
|
+
return to_enum __method__ unless block_given?
|
48
|
+
|
49
|
+
horizon = now
|
50
|
+
|
51
|
+
@elements.each_pair do |element, sunset|
|
52
|
+
yield element unless expired?(sunset, horizon)
|
53
|
+
end
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# @return [Float]
|
61
|
+
def now
|
62
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
63
|
+
end
|
64
|
+
|
65
|
+
def expired?(sunset, horizon)
|
66
|
+
sunset <= horizon
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# internal
|
4
|
-
|
4
|
+
require_relative "./registry"
|
5
5
|
|
6
6
|
module Sidekiq
|
7
7
|
module Throttled
|
@@ -13,7 +13,7 @@ module Sidekiq
|
|
13
13
|
# include Sidekiq::Job
|
14
14
|
# include Sidekiq::Throttled::Job
|
15
15
|
#
|
16
|
-
#
|
16
|
+
# sidekiq_options :queue => :my_queue
|
17
17
|
# sidekiq_throttle :threshold => { :limit => 123, :period => 1.hour }
|
18
18
|
#
|
19
19
|
# def perform
|
@@ -29,8 +29,8 @@ module Sidekiq
|
|
29
29
|
# in order to make API inline with `include Sidekiq::Job`.
|
30
30
|
#
|
31
31
|
# @private
|
32
|
-
def self.included(
|
33
|
-
|
32
|
+
def self.included(base)
|
33
|
+
base.extend(ClassMethods)
|
34
34
|
end
|
35
35
|
|
36
36
|
# Helper methods added to the singleton class of destination
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# internal
|
4
|
+
require_relative "../registry"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Throttled
|
8
|
+
module Middlewares
|
9
|
+
# Server middleware required for Sidekiq::Throttled functioning.
|
10
|
+
class Server
|
11
|
+
include Sidekiq::ServerMiddleware
|
12
|
+
|
13
|
+
def call(_worker, msg, _queue)
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
job = msg.fetch("wrapped") { msg["class"] }
|
17
|
+
jid = msg["jid"]
|
18
|
+
|
19
|
+
if job && jid
|
20
|
+
Registry.get job do |strategy|
|
21
|
+
strategy.finalize!(jid, *msg["args"])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
require "sidekiq/fetch"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Throttled
|
8
|
+
module Patches
|
9
|
+
module BasicFetch
|
10
|
+
# Retrieves job from redis.
|
11
|
+
#
|
12
|
+
# @return [Sidekiq::Throttled::UnitOfWork, nil]
|
13
|
+
def retrieve_work
|
14
|
+
work = super
|
15
|
+
|
16
|
+
if work && Throttled.throttled?(work.job)
|
17
|
+
Throttled.cooldown&.notify_throttled(work.queue)
|
18
|
+
requeue_throttled(work)
|
19
|
+
return nil
|
20
|
+
end
|
21
|
+
|
22
|
+
Throttled.cooldown&.notify_admitted(work.queue) if work
|
23
|
+
|
24
|
+
work
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Pushes job back to the head of the queue, so that job won't be tried
|
30
|
+
# immediately after it was requeued (in most cases).
|
31
|
+
#
|
32
|
+
# @note This is triggered when job is throttled. So it is same operation
|
33
|
+
# Sidekiq performs upon `Sidekiq::Worker.perform_async` call.
|
34
|
+
#
|
35
|
+
# @return [void]
|
36
|
+
def requeue_throttled(work)
|
37
|
+
redis { |conn| conn.lpush(work.queue, work.job) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns list of queues to try to fetch jobs from.
|
41
|
+
#
|
42
|
+
# @note It may return an empty array.
|
43
|
+
# @param [Array<String>] queues
|
44
|
+
# @return [Array<String>]
|
45
|
+
def queues_cmd
|
46
|
+
super - (Throttled.cooldown&.queues || [])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Sidekiq::BasicFetch.prepend(Sidekiq::Throttled::Patches::BasicFetch)
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# internal
|
4
|
-
|
5
|
-
require "sidekiq/throttled/utils"
|
4
|
+
require_relative "./strategy"
|
6
5
|
|
7
6
|
module Sidekiq
|
8
7
|
module Throttled
|
@@ -14,8 +13,6 @@ module Sidekiq
|
|
14
13
|
@aliases = {}
|
15
14
|
|
16
15
|
class << self
|
17
|
-
include Utils
|
18
|
-
|
19
16
|
# Adds strategy to the registry.
|
20
17
|
#
|
21
18
|
# @param (see Strategy#initialize)
|
@@ -105,9 +102,7 @@ module Sidekiq
|
|
105
102
|
# @param name [Class, #to_s]
|
106
103
|
# @return [Strategy, nil]
|
107
104
|
def find_by_class(name)
|
108
|
-
|
109
|
-
|
110
|
-
const = name.is_a?(Class) ? name : constantize(name)
|
105
|
+
const = name.is_a?(Class) ? name : Object.const_get(name)
|
111
106
|
return unless const.is_a?(Class)
|
112
107
|
|
113
108
|
const.ancestors.each do |m|
|
@@ -115,6 +110,8 @@ module Sidekiq
|
|
115
110
|
return strategy if strategy
|
116
111
|
end
|
117
112
|
|
113
|
+
nil
|
114
|
+
rescue NameError
|
118
115
|
nil
|
119
116
|
end
|
120
117
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "redis_prescription"
|
4
4
|
|
5
|
-
|
5
|
+
require_relative "./base"
|
6
6
|
|
7
7
|
module Sidekiq
|
8
8
|
module Throttled
|
@@ -20,7 +20,7 @@ module Sidekiq
|
|
20
20
|
# PUSH(@key, @jid)
|
21
21
|
# return 0
|
22
22
|
# end
|
23
|
-
SCRIPT =
|
23
|
+
SCRIPT = RedisPrescription.new(File.read("#{__dir__}/concurrency.lua"))
|
24
24
|
private_constant :SCRIPT
|
25
25
|
|
26
26
|
# @param [#to_s] strategy_key
|
@@ -49,9 +49,7 @@ module Sidekiq
|
|
49
49
|
keys = [key(job_args)]
|
50
50
|
argv = [jid.to_s, job_limit, @ttl, Time.now.to_f]
|
51
51
|
|
52
|
-
Sidekiq.redis
|
53
|
-
1 == SCRIPT.eval(redis, :keys => keys, :argv => argv)
|
54
|
-
end
|
52
|
+
Sidekiq.redis { |redis| 1 == SCRIPT.call(redis, keys: keys, argv: argv) }
|
55
53
|
end
|
56
54
|
|
57
55
|
# @return [Integer] Current count of jobs
|