sidekiq-throttled 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a601fd050b147fd24ab88416d29273f0cca950c7e540c6303e1c0bb9c47f66d
4
- data.tar.gz: 72ebc4a88c60c09faa9922c4f305fbe4374a90ebcafa62f149b83cbae6a356a6
3
+ metadata.gz: 85023f0d147ed5c41ac214b6f8c907ac4fd8c023271e0def9297659305449b16
4
+ data.tar.gz: 2f6b2c9c97210185dc91539328b85e108ee07b36770da7eaf87e27d3cff4138c
5
5
  SHA512:
6
- metadata.gz: a41505e78b80af968f4aa3ab5076298d8acb62ab4b79046bbb8ff3d7cdb83d456546d066c3c431033418d5754b300cb16c30443b4f66bf82c30b14c5043b3ab4
7
- data.tar.gz: 95b3c4839f01711aa6ff3fca8f390ab49981f7e7cf1cf3cd9c289276f945943ada4a3ec9adf96413cf1435f58fe9ea8c08f2b31fc3133e4fbbfca646d0c677b0
6
+ metadata.gz: 9fe961d74fd88be00803879e6a35197be285a80d7d7c289385bf6268a1c58c9e82a81c32ef0fb89070f8eccab6dc8ee963de2b2109695879be0c4b7d75bdc770
7
+ data.tar.gz: ee02663fa70432081fefbb756b5be9a72917ef3dcee1cf915a221d97c21f7ceb65b04ac0a54d5a3dba0709a484361e04423e3cf7a0750ef7127df5d0d1796f92
data/README.adoc CHANGED
@@ -262,6 +262,33 @@ IMPORTANT: Don't forget to specify `:key_suffix` and make it return different
262
262
  values if you are using dynamic limit/period options. Otherwise, you risk
263
263
  getting into some trouble.
264
264
 
265
+ [source,ruby]
266
+ ----
267
+ class MyJob
268
+ include Sidekiq::Job
269
+ include Sidekiq::Throttled::Job
270
+
271
+ sidekiq_options queue: :my_queue
272
+
273
+ sidekiq_throttle(
274
+ concurrency: { limit: 10 },
275
+ # Allow 500 jobs per minute, 5,000 per hour, and 50,000 per day:
276
+ threshold: [
277
+ { limit: 500, period: 1.minute, key_suffix: "minutely" },
278
+ { limit: 5_000, period: 1.hour, key_suffix: "hourly" },
279
+ { limit: 50_000, period: 1.day, key_suffix: "daily" },
280
+ ]
281
+ )
282
+
283
+ def perform(project_id, user_id)
284
+ # ...
285
+ end
286
+ end
287
+ ----
288
+
289
+ NOTE: `key_suffix` does not have to be a proc/lambda, it can just be a
290
+ string value. This can come in handy to set throttle limits for different
291
+ ranges of time
265
292
 
266
293
  === Concurrency throttling fine-tuning
267
294
 
@@ -287,11 +314,9 @@ sidekiq_throttle(concurrency: { limit: 20, ttl: 1.hour.to_i })
287
314
 
288
315
  This library aims to support and is tested against the following Ruby versions:
289
316
 
290
- * Ruby 2.7.x
291
- * Ruby 3.0.x
292
- * Ruby 3.1.x
293
317
  * Ruby 3.2.x
294
318
  * Ruby 3.3.x
319
+ * Ruby 3.4.x
295
320
 
296
321
  If something doesn't work on one of these versions, it's a bug.
297
322
 
@@ -311,15 +336,11 @@ dropped.
311
336
 
312
337
  This library aims to support and work with following Sidekiq versions:
313
338
 
314
- * Sidekiq 7.0.x
315
- * Sidekiq 7.1.x
316
- * Sidekiq 7.2.x
339
+ * Sidekiq 8.0.x
317
340
 
318
341
  And the following Sidekiq Pro versions:
319
342
 
320
- * Sidekiq Pro 7.0.x
321
- * Sidekiq Pro 7.1.x
322
- * Sidekiq Pro 7.2.x
343
+ * Sidekiq Pro 8.0.x
323
344
 
324
345
  == Development
325
346
 
@@ -334,6 +355,7 @@ If you're working on Sidekiq-Pro support make sure that you have Sidekiq-Pro
334
355
  license set either in the global config, or in `BUNDLE_GEMS\__CONTRIBSYS__COM`
335
356
  environment variable.
336
357
 
358
+
337
359
  == Contributing
338
360
 
339
361
  * Fork sidekiq-throttled on GitHub
@@ -51,7 +51,7 @@ module Sidekiq
51
51
 
52
52
  # @!attribute [w] default_requeue_options
53
53
  def default_requeue_options=(options)
54
- requeue_with = options.delete(:with).intern || :enqueue
54
+ requeue_with = options.delete(:with)&.to_sym || :enqueue
55
55
 
56
56
  @default_requeue_options = options.merge({ with: requeue_with })
57
57
  end
@@ -88,8 +88,8 @@ module Sidekiq
88
88
  # @param [Hash] requeue What to do with jobs that are throttled
89
89
  # @see Registry.add
90
90
  # @return [void]
91
- def sidekiq_throttle(**kwargs)
92
- Registry.add(self, **kwargs)
91
+ def sidekiq_throttle(**)
92
+ Registry.add(self, **)
93
93
  end
94
94
 
95
95
  # Adds current worker to preconfigured throttling strategy. Allows
@@ -17,10 +17,10 @@ module Sidekiq
17
17
  #
18
18
  # @param (see Strategy#initialize)
19
19
  # @return [Strategy]
20
- def add(name, **kwargs)
20
+ def add(name, **)
21
21
  name = name.to_s
22
22
 
23
- @strategies[name] = Strategy.new(name, **kwargs)
23
+ @strategies[name] = Strategy.new(name, **)
24
24
  end
25
25
 
26
26
  # Adds alias for existing strategy.
@@ -58,6 +58,8 @@ module Sidekiq
58
58
  return 0.0 if !job_limit || count(*job_args) < job_limit
59
59
 
60
60
  oldest_jid_with_score = Sidekiq.redis { |redis| redis.zrange(key(job_args), 0, 0, withscores: true) }.first
61
+ return 0.0 unless oldest_jid_with_score
62
+
61
63
  expiry_time = oldest_jid_with_score.last.to_f
62
64
  expiry_time - Time.now.to_f
63
65
  end
@@ -143,24 +143,27 @@ module Sidekiq
143
143
  when NilClass
144
144
  work.queue
145
145
  when String, Symbol
146
- requeue_to.to_s
146
+ requeue_to
147
147
  else
148
148
  raise ArgumentError, "Invalid argument for `to`"
149
149
  end
150
150
 
151
151
  target = work.queue if target.nil? || target.empty?
152
152
 
153
- target.start_with?("queue:") ? target : "queue:#{target}"
153
+ target.to_s
154
154
  end
155
155
 
156
156
  # Push the job back to the head of the queue.
157
+ # The queue name is expected to include the "queue:" prefix, so we add it if it's missing.
157
158
  def re_enqueue_throttled(work, target_queue)
159
+ target_queue = "queue:#{target_queue}" unless target_queue.start_with?("queue:")
160
+
158
161
  case work.class.name
159
162
  when "Sidekiq::Pro::SuperFetch::UnitOfWork"
160
163
  # Calls SuperFetch UnitOfWork's requeue to remove the job from the
161
164
  # temporary queue and push job back to the head of the target queue, so that
162
165
  # the job won't be tried immediately after it was requeued (in most cases).
163
- work.queue = target_queue if target_queue
166
+ work.queue = target_queue
164
167
  work.requeue
165
168
  else
166
169
  # This is the same operation Sidekiq performs upon `Sidekiq::Worker.perform_async` call.
@@ -168,10 +171,13 @@ module Sidekiq
168
171
  end
169
172
  end
170
173
 
174
+ # Reschedule the job to be executed later in the target queue.
175
+ # The queue name should NOT include the "queue:" prefix, so we remove it if it's present.
171
176
  def reschedule_throttled(work, target_queue)
172
- message = JSON.parse(work.job)
173
- job_class = message.fetch("wrapped") { message.fetch("class") { return false } }
174
- job_args = message["args"]
177
+ target_queue = target_queue.delete_prefix("queue:")
178
+ message = JSON.parse(work.job)
179
+ job_class = message.fetch("wrapped") { message.fetch("class") { return false } }
180
+ job_args = message["args"]
175
181
 
176
182
  # Re-enqueue the job to the target queue at another time as a NEW unit of work
177
183
  # AND THEN mark this work as done, so SuperFetch doesn't think this instance is orphaned
@@ -179,6 +185,7 @@ module Sidekiq
179
185
  # but your job should be idempotent anyway, right?
180
186
  # The job running twice was already a risk with SuperFetch anyway and this doesn't really increase that risk.
181
187
  Sidekiq::Client.enqueue_to_in(target_queue, retry_in(work), Object.const_get(job_class), *job_args)
188
+
182
189
  work.acknowledge
183
190
  end
184
191
 
@@ -43,7 +43,7 @@ module Sidekiq
43
43
 
44
44
  # @return [Float] How long, in seconds, before we'll next be able to take on jobs
45
45
  def retry_in(*args)
46
- max { |s| s.retry_in(*args) }
46
+ map { |s| s.retry_in(*args) }.max
47
47
  end
48
48
 
49
49
  # Marks job as being processed.
@@ -3,6 +3,6 @@
3
3
  module Sidekiq
4
4
  module Throttled
5
5
  # Gem version
6
- VERSION = "1.5.1"
6
+ VERSION = "2.0.0"
7
7
  end
8
8
  end
@@ -1,43 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # stdlib
4
3
  require "pathname"
5
4
 
6
- # 3rd party
7
5
  require "sidekiq"
8
6
  require "sidekiq/web"
9
7
 
10
- # internal
11
8
  require_relative "./registry"
12
9
  require_relative "./web/stats"
13
10
 
14
11
  module Sidekiq
15
12
  module Throttled
16
- # Provides Sidekiq tab to monitor and reset throttled stats.
17
13
  module Web
18
- VIEWS = Pathname.new(__dir__).join("web")
19
- THROTTLED_TPL = VIEWS.join("throttled.html.erb").read.freeze
14
+ ROOT = Pathname.new(__dir__).join("../../../web").expand_path.realpath.freeze
15
+ VIEWS = ROOT.join("views").freeze
20
16
 
21
- class << self
22
- # @api private
23
- def registered(app)
24
- register_throttled_tab app
17
+ def self.registered(app)
18
+ app.get("/throttled") do
19
+ erb :index, views: VIEWS
25
20
  end
26
21
 
27
- private
28
-
29
- def register_throttled_tab(app)
30
- app.get("/throttled") { erb THROTTLED_TPL.dup }
31
-
32
- app.post("/throttled/:id/reset") do
33
- Registry.get(params[:id], &:reset!)
34
- redirect "#{root_path}throttled"
35
- end
22
+ app.post("/throttled/:id/reset") do
23
+ Registry.get(route_params(:id), &:reset!)
24
+ redirect "#{root_path}throttled"
36
25
  end
37
26
  end
38
27
  end
39
28
  end
40
29
  end
41
30
 
42
- Sidekiq::Web.register Sidekiq::Throttled::Web
43
- Sidekiq::Web.tabs["Throttled"] = "throttled"
31
+ Sidekiq::Web.configure do |config|
32
+ config.register_extension(
33
+ Sidekiq::Throttled::Web,
34
+ name: "throttled",
35
+ tab: %w[Throttled],
36
+ index: %w[throttled],
37
+ root_dir: Sidekiq::Throttled::Web::ROOT.to_s
38
+ )
39
+ end
@@ -0,0 +1,41 @@
1
+ <section>
2
+ <header>
3
+ <h1>Throttled</h1>
4
+ </header>
5
+
6
+ <div class="table_container">
7
+ <table class="throttled">
8
+ <thead>
9
+ <tr>
10
+ <th>Name</th>
11
+ <th>Concurrency</th>
12
+ <th>Threshold</th>
13
+ <th>Actions</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% Sidekiq::Throttled::Registry.each_with_static_keys do |name, strategy| %>
18
+ <tr>
19
+ <td style="vertical-align:middle;"><%= name %></td>
20
+ <td style="vertical-align:middle;text-align:center;">
21
+ <% strategy.concurrency.each do |concurrency| %>
22
+ <%= Sidekiq::Throttled::Web::Stats.new(concurrency).to_html %>
23
+ <% end %>
24
+ </td>
25
+ <td style="vertical-align:middle;text-align:center;">
26
+ <% strategy.threshold.each do |threshold| %>
27
+ <%= Sidekiq::Throttled::Web::Stats.new(threshold).to_html %>
28
+ <% end %>
29
+ </td>
30
+ <td style="vertical-align:middle;text-align:center;">
31
+ <form action="<%= root_path %>throttled/<%= CGI.escape name %>/reset" method="post">
32
+ <%= csrf_tag %>
33
+ <button class="btn btn-danger" type="submit">Reset</button>
34
+ </form>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ </section>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-throttled
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Zapparov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-09 00:00:00.000000000 Z
11
+ date: 2025-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '6.5'
47
+ version: '8.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '6.5'
54
+ version: '8.0'
55
55
  description:
56
56
  email:
57
57
  - alexey@zapparov.com
@@ -83,16 +83,16 @@ files:
83
83
  - lib/sidekiq/throttled/version.rb
84
84
  - lib/sidekiq/throttled/web.rb
85
85
  - lib/sidekiq/throttled/web/stats.rb
86
- - lib/sidekiq/throttled/web/throttled.html.erb
87
86
  - lib/sidekiq/throttled/worker.rb
87
+ - web/views/index.erb
88
88
  homepage: https://github.com/ixti/sidekiq-throttled
89
89
  licenses:
90
90
  - MIT
91
91
  metadata:
92
92
  homepage_uri: https://github.com/ixti/sidekiq-throttled
93
- source_code_uri: https://github.com/ixti/sidekiq-throttled/tree/v1.5.1
93
+ source_code_uri: https://github.com/ixti/sidekiq-throttled/tree/v2.0.0
94
94
  bug_tracker_uri: https://github.com/ixti/sidekiq-throttled/issues
95
- changelog_uri: https://github.com/ixti/sidekiq-throttled/blob/v1.5.1/CHANGES.md
95
+ changelog_uri: https://github.com/ixti/sidekiq-throttled/blob/v2.0.0/CHANGES.md
96
96
  rubygems_mfa_required: 'true'
97
97
  post_install_message:
98
98
  rdoc_options: []
@@ -102,14 +102,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
102
102
  requirements:
103
103
  - - ">="
104
104
  - !ruby/object:Gem::Version
105
- version: '2.7'
105
+ version: '3.2'
106
106
  required_rubygems_version: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  requirements: []
112
- rubygems_version: 3.4.22
112
+ rubygems_version: 3.4.19
113
113
  signing_key:
114
114
  specification_version: 4
115
115
  summary: Concurrency and rate-limit throttling for Sidekiq
@@ -1,35 +0,0 @@
1
- <h3>Throttled</h3>
2
-
3
- <div class="table_container">
4
- <table class="table table-hover table-bordered table-striped table-white">
5
- <thead>
6
- <tr>
7
- <th>Name</th>
8
- <th style="text-align:center;">Concurrency</th>
9
- <th style="text-align:center;">Threshold</th>
10
- <th style="text-align:center;">Actions</th>
11
- </tr>
12
- </thead>
13
- <% Sidekiq::Throttled::Registry.each_with_static_keys do |name, strategy| %>
14
- <tr>
15
- <td style="vertical-align:middle;"><%= name %></td>
16
- <td style="vertical-align:middle;text-align:center;">
17
- <% strategy.concurrency.each do |concurrency| %>
18
- <%= Sidekiq::Throttled::Web::Stats.new(concurrency).to_html %>
19
- <% end %>
20
- </td>
21
- <td style="vertical-align:middle;text-align:center;">
22
- <% strategy.threshold.each do |threshold| %>
23
- <%= Sidekiq::Throttled::Web::Stats.new(threshold).to_html %>
24
- <% end %>
25
- </td>
26
- <td style="vertical-align:middle;text-align:center;">
27
- <form action="<%= root_path %>throttled/<%= CGI.escape name %>/reset" method="post">
28
- <%= csrf_tag %>
29
- <button class="btn btn-danger" type="submit">Reset</button>
30
- </form>
31
- </td>
32
- </tr>
33
- <% end %>
34
- </table>
35
- </div>