sidekiq-throttled 2.0.0 → 2.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 +4 -4
- data/README.adoc +71 -3
- data/lib/sidekiq/throttled/strategy/base.rb +5 -1
- data/lib/sidekiq/throttled/strategy/concurrency.lua +51 -6
- data/lib/sidekiq/throttled/strategy/concurrency.rb +56 -13
- data/lib/sidekiq/throttled/version.rb +1 -1
- data/lib/sidekiq/throttled/web/stats.rb +7 -6
- data/web/views/index.html.erb +1 -0
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcec267e317bfaeabe891a21b776cbddc083cba27a163c82a5e5e0839390fb8d
|
|
4
|
+
data.tar.gz: '09706d9f91fdc770f01a5fb7835d8c35706c2852dfe798ec3e60dcd76bdeb8e9'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c90a776bb731f2a140469a03092e8307997602d7723f73517ed804f0f3973cbfed9692baf8f0f98e47320e0b5c4316bbaa15ffc36c70637d68d553e9a537d25
|
|
7
|
+
data.tar.gz: e52c211802a6e740dcd8b1b77ef3d7cad8b39d6a5aa714b7f1619c9ec2391d893140a636562cb6bafdc326ce4bcbd42904bfe053e6410ceec215720d72100b4a
|
data/README.adoc
CHANGED
|
@@ -79,6 +79,30 @@ class MyWorker
|
|
|
79
79
|
end
|
|
80
80
|
----
|
|
81
81
|
|
|
82
|
+
=== Requeue Strategy
|
|
83
|
+
The default requeue strategy `:enqueue` puts jobs immiediately back on the queue if they are being limited, with a potential `cooldown_period`. This is often an appropriate strategy but in some situations may cause the system to repeatedly dequeue and reenque the same job causing excessive redis CPU usage.
|
|
84
|
+
|
|
85
|
+
An alternative requeue strategy `:schedule` is available that schedules the work for the future. This can be less appropriate for highly concurrent jobs, but may be a good way to reduce redis load when the number of jobs to be processed is modest.
|
|
86
|
+
|
|
87
|
+
The alternative `:schedule` strategy may be configured globally with `config.default_requeue_options = { with: :schedule }`
|
|
88
|
+
|
|
89
|
+
It may be configured on a per job basis with:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
sidekiq_throttle(
|
|
93
|
+
# Allow 5 jobs to processed within 1 minute, using the
|
|
94
|
+
threshold: { limit: 5, period: 1.minute, requeue: {with: :schedule} }
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It is also possible to requeue jobs to another queue:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
sidekiq_throttle(
|
|
102
|
+
# Allow 5 jobs to processed within 1 minute, using the
|
|
103
|
+
threshold: { limit: 5, period: 1.minute, requeue: {to: :other_queue, with: :schedule} }
|
|
104
|
+
)
|
|
105
|
+
```
|
|
82
106
|
|
|
83
107
|
=== Web UI
|
|
84
108
|
|
|
@@ -246,8 +270,8 @@ class MyJob
|
|
|
246
270
|
sidekiq_throttle(
|
|
247
271
|
# Allow maximum 10 concurrent jobs per project at a time and maximum 2 jobs per user
|
|
248
272
|
concurrency: [
|
|
249
|
-
{ limit: 10, key_suffix: -> (project_id, user_id) { project_id } },
|
|
250
|
-
{ limit: 2, key_suffix: -> (project_id, user_id) { user_id } }
|
|
273
|
+
{ limit: 10, key_suffix: -> (project_id, user_id) { "project_id:#{project_id}" } },
|
|
274
|
+
{ limit: 2, key_suffix: -> (project_id, user_id) { "user_id:#{user_id}" } }
|
|
251
275
|
]
|
|
252
276
|
# For :threshold it works the same
|
|
253
277
|
)
|
|
@@ -309,6 +333,47 @@ lock TTL to fit your needs:
|
|
|
309
333
|
sidekiq_throttle(concurrency: { limit: 20, ttl: 1.hour.to_i })
|
|
310
334
|
----
|
|
311
335
|
|
|
336
|
+
=== Scheduling based concurrency tuning
|
|
337
|
+
|
|
338
|
+
The default concurrency throttling algorithm immediately requeues throttled
|
|
339
|
+
jobs. This can lead to a lot of wasted work picking up the same set of still
|
|
340
|
+
throttled jobs repeatedly. This churn also often starves lower priority
|
|
341
|
+
jobs/queues. The `:schedule` requeue strategy delays checking the runability of
|
|
342
|
+
throttled jobs until likely to be runnable. This future time is estimated based
|
|
343
|
+
on the expected runtime of the job and current number of throttled jobs. This
|
|
344
|
+
eliminates -- or greatly reduces -- the negative impacts to non-throttled job
|
|
345
|
+
types and queues and reduces wasted work constantly rechecking the same still
|
|
346
|
+
throttled jobs.
|
|
347
|
+
|
|
348
|
+
**Config items**
|
|
349
|
+
|
|
350
|
+
- `limit` — Maximum number of this job allowed to run simultaneously.
|
|
351
|
+
- `avg_job_duration` — Expected runtime (in seconds) for this job type.
|
|
352
|
+
Choose a value on the high end of what’s plausible; if you set this too low under heavy load, job scheduling will become sub-optimal.
|
|
353
|
+
- `lost_job_threshold` — Duration (in seconds) representing how long a job “owns” its concurrency slot before being considered lost.
|
|
354
|
+
- `ttl` — Deprecated alias for `lost_job_threshold`.
|
|
355
|
+
- `max_delay` — The maximum number of seconds to delay a job when it is throttled.
|
|
356
|
+
Prevents excessively long scheduling delays as the backlog grows.
|
|
357
|
+
_Default: the smaller of 30 minutes or `10 × avg_job_duration`._
|
|
358
|
+
|
|
359
|
+
[source,ruby]
|
|
360
|
+
----
|
|
361
|
+
sidekiq_throttle(
|
|
362
|
+
concurrency: {
|
|
363
|
+
# only run 10 of this job at a time
|
|
364
|
+
limit: 10,
|
|
365
|
+
# these jobs finish in less than 30 seconds
|
|
366
|
+
avg_job_duration: 30,
|
|
367
|
+
# if it doesn't release its lease in 2 minutes it's considered lost
|
|
368
|
+
lost_job_threshold: 120,
|
|
369
|
+
# maximum delay allowed when throttled
|
|
370
|
+
max_delay: 300
|
|
371
|
+
},
|
|
372
|
+
# requeue using Sidekiq's scheduler
|
|
373
|
+
requeue: { with: :schedule }
|
|
374
|
+
)
|
|
375
|
+
----
|
|
376
|
+
|
|
312
377
|
|
|
313
378
|
== Supported Ruby Versions
|
|
314
379
|
|
|
@@ -317,6 +382,7 @@ This library aims to support and is tested against the following Ruby versions:
|
|
|
317
382
|
* Ruby 3.2.x
|
|
318
383
|
* Ruby 3.3.x
|
|
319
384
|
* Ruby 3.4.x
|
|
385
|
+
* Ruby 4.0.x
|
|
320
386
|
|
|
321
387
|
If something doesn't work on one of these versions, it's a bug.
|
|
322
388
|
|
|
@@ -337,10 +403,12 @@ dropped.
|
|
|
337
403
|
This library aims to support and work with following Sidekiq versions:
|
|
338
404
|
|
|
339
405
|
* Sidekiq 8.0.x
|
|
406
|
+
* Sidekiq 8.1.x
|
|
340
407
|
|
|
341
|
-
And the following Sidekiq Pro versions:
|
|
408
|
+
And (might work with) the following Sidekiq Pro versions:
|
|
342
409
|
|
|
343
410
|
* Sidekiq Pro 8.0.x
|
|
411
|
+
* Sidekiq Pro 8.1.x
|
|
344
412
|
|
|
345
413
|
== Development
|
|
346
414
|
|
|
@@ -14,7 +14,11 @@ module Sidekiq
|
|
|
14
14
|
key = @base_key.dup
|
|
15
15
|
return key unless @key_suffix
|
|
16
16
|
|
|
17
|
-
key << ":#{
|
|
17
|
+
key << ":#{key_suffix(job_args)}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def key_suffix(job_args)
|
|
21
|
+
@key_suffix.respond_to?(:call) ? @key_suffix.call(*job_args) : @key_suffix
|
|
18
22
|
rescue StandardError => e
|
|
19
23
|
Sidekiq.logger.error "Failed to get key suffix: #{e}"
|
|
20
24
|
raise e
|
|
@@ -1,16 +1,61 @@
|
|
|
1
|
-
local
|
|
1
|
+
local in_progress_jobs_key = KEYS[1]
|
|
2
|
+
local backlog_info_key = KEYS[2]
|
|
2
3
|
local jid = ARGV[1]
|
|
3
4
|
local lmt = tonumber(ARGV[2])
|
|
4
|
-
local
|
|
5
|
+
local lost_job_threshold = tonumber(ARGV[3])
|
|
5
6
|
local now = tonumber(ARGV[4])
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
-- supporting functions
|
|
9
|
+
local function over_limit()
|
|
10
|
+
return lmt <= redis.call("ZCARD", in_progress_jobs_key)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
local function job_already_in_progress()
|
|
14
|
+
return redis.call("ZSCORE", in_progress_jobs_key, jid)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
-- Estimates current backlog size. This function tends to underestimate
|
|
18
|
+
-- the actual backlog. This is intentional. Overestimates are bad as it
|
|
19
|
+
-- can cause unnecessary delays in job processing. Underestimates are much
|
|
20
|
+
-- safer as they only increase workload of sidekiq processors.
|
|
21
|
+
local function est_current_backlog_size()
|
|
22
|
+
local old_size = tonumber(redis.call("HGET", backlog_info_key, "size")) or 0
|
|
23
|
+
local old_timestamp = tonumber(redis.call("HGET", backlog_info_key, "timestamp")) or now
|
|
24
|
+
|
|
25
|
+
local jobs_lost_since_old_timestamp = (now - old_timestamp) / lost_job_threshold * lmt
|
|
26
|
+
|
|
27
|
+
return math.max(old_size - jobs_lost_since_old_timestamp, 0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
local function change_backlog_size(delta)
|
|
32
|
+
local curr_backlog_size = est_current_backlog_size()
|
|
33
|
+
|
|
34
|
+
redis.call("HSET", backlog_info_key, "size", curr_backlog_size + delta)
|
|
35
|
+
redis.call("HSET", backlog_info_key, "timestamp", now)
|
|
36
|
+
redis.call("EXPIRE", backlog_info_key, math.ceil((lost_job_threshold * curr_backlog_size) + 1 / lmt))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
local function register_job_in_progress()
|
|
40
|
+
redis.call("ZADD", in_progress_jobs_key, now + lost_job_threshold , jid)
|
|
41
|
+
redis.call("EXPIRE", in_progress_jobs_key, lost_job_threshold)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
local function clear_stale_in_progress_jobs()
|
|
45
|
+
local cleared_count = redis.call("ZREMRANGEBYSCORE", in_progress_jobs_key, "-inf", "(" .. now)
|
|
46
|
+
change_backlog_size(-cleared_count)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
-- END supporting functions
|
|
50
|
+
|
|
51
|
+
clear_stale_in_progress_jobs()
|
|
8
52
|
|
|
9
|
-
if
|
|
53
|
+
if over_limit() and not job_already_in_progress() then
|
|
54
|
+
change_backlog_size(1)
|
|
10
55
|
return 1
|
|
11
56
|
end
|
|
12
57
|
|
|
13
|
-
|
|
14
|
-
|
|
58
|
+
register_job_in_progress()
|
|
59
|
+
change_backlog_size(-1)
|
|
15
60
|
|
|
16
61
|
return 0
|
|
@@ -26,13 +26,26 @@ module Sidekiq
|
|
|
26
26
|
# @param [#to_s] strategy_key
|
|
27
27
|
# @param [#to_i, #call] limit Amount of allowed concurrent jobs
|
|
28
28
|
# per processors running for given key.
|
|
29
|
-
# @param [#to_i]
|
|
29
|
+
# @param [#to_i] avg_job_duration Average number of seconds needed
|
|
30
|
+
# to complete a job of this type. Default: 300 or 1/3 of lost_job_threshold
|
|
31
|
+
# @param [#to_i] lost_job_threshold Seconds to wait before considering
|
|
32
|
+
# a job lost or dead. Default: 900 or 3 * avg_job_duration
|
|
30
33
|
# @param [Proc] key_suffix Dynamic key suffix generator.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
# @param [#to_i] max_delay Maximum number of seconds to delay a job when it
|
|
35
|
+
# throttled. This prevents jobs from being schedule very far in the future
|
|
36
|
+
# when the backlog is large. Default: the smaller of 30 minutes or 10 * avg_job_duration
|
|
37
|
+
# @deprecated @param [#to_i] ttl Obsolete alias for `lost_job_threshold`.
|
|
38
|
+
# Default: 900 or 3 * avg_job_duration
|
|
39
|
+
def initialize(strategy_key, limit:, avg_job_duration: nil, ttl: nil, # rubocop:disable Metrics/ParameterLists
|
|
40
|
+
lost_job_threshold: ttl, key_suffix: nil, max_delay: nil)
|
|
41
|
+
@base_key = "#{strategy_key}:concurrency.v2"
|
|
42
|
+
@limit = limit
|
|
43
|
+
@avg_job_duration, @lost_job_threshold = interp_duration_args(avg_job_duration, lost_job_threshold)
|
|
35
44
|
@key_suffix = key_suffix
|
|
45
|
+
@max_delay = max_delay || [(10 * @avg_job_duration), 1_800].min
|
|
46
|
+
|
|
47
|
+
raise(ArgumentError, "lost_job_threshold must be greater than avg_job_duration") if
|
|
48
|
+
@lost_job_threshold <= @avg_job_duration
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
# @return [Boolean] Whenever strategy has dynamic config
|
|
@@ -46,8 +59,8 @@ module Sidekiq
|
|
|
46
59
|
return false unless job_limit
|
|
47
60
|
return true if job_limit <= 0
|
|
48
61
|
|
|
49
|
-
keys = [key(job_args)]
|
|
50
|
-
argv = [jid.to_s, job_limit, @
|
|
62
|
+
keys = [key(job_args), backlog_info_key(job_args)]
|
|
63
|
+
argv = [jid.to_s, job_limit, @lost_job_threshold, Time.now.to_f]
|
|
51
64
|
|
|
52
65
|
Sidekiq.redis { |redis| 1 == SCRIPT.call(redis, keys: keys, argv: argv) }
|
|
53
66
|
end
|
|
@@ -57,11 +70,8 @@ module Sidekiq
|
|
|
57
70
|
job_limit = limit(job_args)
|
|
58
71
|
return 0.0 if !job_limit || count(*job_args) < job_limit
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
expiry_time = oldest_jid_with_score.last.to_f
|
|
64
|
-
expiry_time - Time.now.to_f
|
|
73
|
+
(estimated_backlog_size(job_args) * @avg_job_duration / limit(job_args))
|
|
74
|
+
.then { |delay_sec| @max_delay * (1 - Math.exp(-delay_sec / @max_delay)) } # limit to max_delay
|
|
65
75
|
end
|
|
66
76
|
|
|
67
77
|
# @return [Integer] Current count of jobs
|
|
@@ -78,7 +88,40 @@ module Sidekiq
|
|
|
78
88
|
# Remove jid from the pool of jobs in progress
|
|
79
89
|
# @return [void]
|
|
80
90
|
def finalize!(jid, *job_args)
|
|
81
|
-
Sidekiq.redis
|
|
91
|
+
Sidekiq.redis do |conn|
|
|
92
|
+
conn.zrem(key(job_args), jid.to_s)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def backlog_info_key(job_args)
|
|
99
|
+
"#{key(job_args)}.backlog_info"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def estimated_backlog_size(job_args)
|
|
103
|
+
old_size_str, old_timestamp_str =
|
|
104
|
+
Sidekiq.redis { |conn| conn.hmget(backlog_info_key(job_args), "size", "timestamp") }
|
|
105
|
+
old_size = (old_size_str || 0).to_f
|
|
106
|
+
old_timestamp = (old_timestamp_str || Time.now).to_f
|
|
107
|
+
|
|
108
|
+
(old_size - jobs_lost_since(old_timestamp, job_args)).clamp(0, Float::INFINITY)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def jobs_lost_since(timestamp, job_args)
|
|
112
|
+
(Time.now.to_f - timestamp) / @lost_job_threshold * limit(job_args)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def interp_duration_args(avg_job_duration, lost_job_threshold)
|
|
116
|
+
if avg_job_duration && lost_job_threshold
|
|
117
|
+
[avg_job_duration.to_i, lost_job_threshold.to_i]
|
|
118
|
+
elsif avg_job_duration && lost_job_threshold.nil?
|
|
119
|
+
[avg_job_duration.to_i, avg_job_duration.to_i * 3]
|
|
120
|
+
elsif avg_job_duration.nil? && lost_job_threshold
|
|
121
|
+
[lost_job_threshold.to_i / 3, lost_job_threshold.to_i]
|
|
122
|
+
else
|
|
123
|
+
[300, 900]
|
|
124
|
+
end
|
|
82
125
|
end
|
|
83
126
|
end
|
|
84
127
|
end
|
|
@@ -62,12 +62,13 @@ module Sidekiq
|
|
|
62
62
|
|
|
63
63
|
# @return [String]
|
|
64
64
|
def humanize_integer(int)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
int.to_s.chars
|
|
66
|
+
.reverse
|
|
67
|
+
.each_slice(3)
|
|
68
|
+
.map(&:reverse)
|
|
69
|
+
.reverse
|
|
70
|
+
.map(&:join)
|
|
71
|
+
.join(",")
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
index.erb
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sidekiq-throttled
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexey Zapparov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: concurrent-ruby
|
|
@@ -52,7 +51,6 @@ dependencies:
|
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '8.0'
|
|
55
|
-
description:
|
|
56
54
|
email:
|
|
57
55
|
- alexey@zapparov.com
|
|
58
56
|
executables: []
|
|
@@ -85,16 +83,16 @@ files:
|
|
|
85
83
|
- lib/sidekiq/throttled/web/stats.rb
|
|
86
84
|
- lib/sidekiq/throttled/worker.rb
|
|
87
85
|
- web/views/index.erb
|
|
86
|
+
- web/views/index.html.erb
|
|
88
87
|
homepage: https://github.com/ixti/sidekiq-throttled
|
|
89
88
|
licenses:
|
|
90
89
|
- MIT
|
|
91
90
|
metadata:
|
|
92
91
|
homepage_uri: https://github.com/ixti/sidekiq-throttled
|
|
93
|
-
source_code_uri: https://github.com/ixti/sidekiq-throttled/tree/v2.
|
|
92
|
+
source_code_uri: https://github.com/ixti/sidekiq-throttled/tree/v2.1.0
|
|
94
93
|
bug_tracker_uri: https://github.com/ixti/sidekiq-throttled/issues
|
|
95
|
-
changelog_uri: https://github.com/ixti/sidekiq-throttled/blob/v2.
|
|
94
|
+
changelog_uri: https://github.com/ixti/sidekiq-throttled/blob/v2.1.0/CHANGES.md
|
|
96
95
|
rubygems_mfa_required: 'true'
|
|
97
|
-
post_install_message:
|
|
98
96
|
rdoc_options: []
|
|
99
97
|
require_paths:
|
|
100
98
|
- lib
|
|
@@ -109,8 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
107
|
- !ruby/object:Gem::Version
|
|
110
108
|
version: '0'
|
|
111
109
|
requirements: []
|
|
112
|
-
rubygems_version: 3.
|
|
113
|
-
signing_key:
|
|
110
|
+
rubygems_version: 3.6.9
|
|
114
111
|
specification_version: 4
|
|
115
112
|
summary: Concurrency and rate-limit throttling for Sidekiq
|
|
116
113
|
test_files: []
|