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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +314 -0
  3. data/lib/sidekiq/throttled/config.rb +44 -0
  4. data/lib/sidekiq/throttled/cooldown.rb +55 -0
  5. data/lib/sidekiq/throttled/expirable_set.rb +70 -0
  6. data/lib/sidekiq/throttled/job.rb +4 -4
  7. data/lib/sidekiq/throttled/middlewares/server.rb +28 -0
  8. data/lib/sidekiq/throttled/patches/basic_fetch.rb +53 -0
  9. data/lib/sidekiq/throttled/registry.rb +4 -7
  10. data/lib/sidekiq/throttled/strategy/concurrency.rb +4 -6
  11. data/lib/sidekiq/throttled/strategy/threshold.rb +4 -6
  12. data/lib/sidekiq/throttled/strategy.rb +10 -10
  13. data/lib/sidekiq/throttled/strategy_collection.rb +2 -3
  14. data/lib/sidekiq/throttled/version.rb +1 -1
  15. data/lib/sidekiq/throttled/web.rb +2 -45
  16. data/lib/sidekiq/throttled/worker.rb +1 -1
  17. data/lib/sidekiq/throttled.rb +45 -57
  18. metadata +25 -70
  19. data/.coveralls.yml +0 -1
  20. data/.github/dependabot.yml +0 -12
  21. data/.github/workflows/ci.yml +0 -52
  22. data/.gitignore +0 -12
  23. data/.rspec +0 -5
  24. data/.rubocop.yml +0 -20
  25. data/.rubocop_todo.yml +0 -68
  26. data/.travis.yml +0 -37
  27. data/.yardopts +0 -1
  28. data/Appraisals +0 -9
  29. data/CHANGES.md +0 -300
  30. data/Gemfile +0 -34
  31. data/Guardfile +0 -25
  32. data/README.md +0 -297
  33. data/Rakefile +0 -27
  34. data/gemfiles/sidekiq_6.4.gemfile +0 -33
  35. data/gemfiles/sidekiq_6.5.gemfile +0 -33
  36. data/lib/sidekiq/throttled/communicator/callbacks.rb +0 -72
  37. data/lib/sidekiq/throttled/communicator/exception_handler.rb +0 -25
  38. data/lib/sidekiq/throttled/communicator/listener.rb +0 -109
  39. data/lib/sidekiq/throttled/communicator.rb +0 -116
  40. data/lib/sidekiq/throttled/configuration.rb +0 -50
  41. data/lib/sidekiq/throttled/expirable_list.rb +0 -70
  42. data/lib/sidekiq/throttled/fetch/unit_of_work.rb +0 -83
  43. data/lib/sidekiq/throttled/fetch.rb +0 -94
  44. data/lib/sidekiq/throttled/middleware.rb +0 -22
  45. data/lib/sidekiq/throttled/patches/queue.rb +0 -18
  46. data/lib/sidekiq/throttled/queue_name.rb +0 -46
  47. data/lib/sidekiq/throttled/queues_pauser.rb +0 -152
  48. data/lib/sidekiq/throttled/testing.rb +0 -12
  49. data/lib/sidekiq/throttled/utils.rb +0 -19
  50. data/lib/sidekiq/throttled/web/queues.html.erb +0 -49
  51. data/lib/sidekiq/throttled/web/summary_fix.js +0 -10
  52. data/lib/sidekiq/throttled/web/summary_fix.rb +0 -35
  53. data/rubocop/layout.yml +0 -24
  54. data/rubocop/lint.yml +0 -41
  55. data/rubocop/metrics.yml +0 -4
  56. data/rubocop/performance.yml +0 -25
  57. data/rubocop/rspec.yml +0 -3
  58. data/rubocop/style.yml +0 -84
  59. data/sidekiq-throttled.gemspec +0 -36
  60. /data/{LICENSE.md → LICENSE.txt} +0 -0
data/README.md DELETED
@@ -1,297 +0,0 @@
1
- # Sidekiq::Throttled
2
-
3
- [![CI Status](https://github.com/ixti/sidekiq-throttled/actions/workflows/ci.yml/badge.svg)](https://github.com/ixti/sidekiq-throttled/actions/workflows/ci.yml)
4
- [![Latest Version](https://badge.fury.io/rb/sidekiq-throttled.svg)](http://rubygems.org/gems/sidekiq-throttled)
5
- [![API Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/sidekiq-throttled)
6
-
7
- Concurrency and threshold throttling for [Sidekiq][sidekiq].
8
-
9
- ## Installation
10
-
11
- Add this line to your application's Gemfile:
12
-
13
- ``` ruby
14
- gem "sidekiq-throttled"
15
- ```
16
-
17
- And then execute:
18
-
19
- $ bundle
20
-
21
- Or install it yourself as:
22
-
23
- $ gem install sidekiq-throttled
24
-
25
-
26
- ## Usage
27
-
28
- Add somewhere in your app's bootstrap (e.g. `config/initializers/sidekiq.rb` if
29
- you are using Rails):
30
-
31
- ``` ruby
32
- require "sidekiq/throttled"
33
- Sidekiq::Throttled.setup!
34
- ```
35
-
36
- Load order can be an issue if you are using other Sidekiq plugins and/or middleware.
37
- To prevent any problems, add the `.setup!` call to the bottom of your init file.
38
-
39
- Once you've done that you can include `Sidekiq::Throttled::Job` to your
40
- job classes and configure throttling:
41
-
42
- ``` ruby
43
- class MyJob
44
- include Sidekiq::Job
45
- include Sidekiq::Throttled::Job
46
-
47
- sidekiq_options :queue => :my_queue
48
-
49
- sidekiq_throttle(
50
- # Allow maximum 10 concurrent jobs of this class at a time.
51
- :concurrency => { :limit => 10 },
52
- # Allow maximum 1K jobs being processed within one hour window.
53
- :threshold => { :limit => 1_000, :period => 1.hour }
54
- )
55
-
56
- def perform
57
- # ...
58
- end
59
- end
60
- ```
61
-
62
- **NOTE:** `Sidekiq::Throttled::Job` is aliased as `Sidekiq::Throttled::Worker`,
63
- thus if you're using Sidekiq prior 6.3.0 version, or you using `Sidekiq::Worker`
64
- naming convention, you can use the alias for consistency:
65
-
66
- ``` ruby
67
- class MyWorker
68
- include Sidekiq::Worker
69
- include Sidekiq::Throttled::Worker
70
-
71
- # ...
72
- end
73
- ```
74
-
75
-
76
- ### Observer
77
-
78
- You can specify an observer that will be called on throttling. To do so pass an
79
- `:observer` option with callable object:
80
-
81
- ``` ruby
82
- class MyJob
83
- include Sidekiq::Job
84
- include Sidekiq::Throttled::Job
85
-
86
- MY_OBSERVER = lambda do |strategy, *args|
87
- # do something
88
- end
89
-
90
- sidekiq_options :queue => :my_queue
91
-
92
- sidekiq_throttle(
93
- :concurrency => { :limit => 10 },
94
- :threshold => { :limit => 100, :period => 1.hour }
95
- :observer => MY_OBSERVER
96
- )
97
-
98
- def perform(*args)
99
- # ...
100
- end
101
- end
102
- ```
103
-
104
- Observer will receive `strategy, *args` arguments, where `strategy` is a Symbol
105
- `:concurrency` or `:threshold`, and `*args` are the arguments that were passed
106
- to the job.
107
-
108
-
109
- ### Dynamic throttling
110
-
111
- You can throttle jobs dynamically with `:key_suffix` option:
112
-
113
- ``` ruby
114
- class MyJob
115
- include Sidekiq::Job
116
- include Sidekiq::Throttled::Job
117
-
118
- sidekiq_options :queue => :my_queue
119
-
120
- sidekiq_throttle(
121
- # Allow maximum 10 concurrent jobs per user at a time.
122
- :concurrency => { :limit => 10, :key_suffix => -> (user_id) { user_id } }
123
- )
124
-
125
- def perform(user_id)
126
- # ...
127
- end
128
- end
129
- ```
130
-
131
- You can also supply dynamic values for limits and periods by supplying a proc
132
- for these values. The proc will be evaluated at the time the job is fetched
133
- and will receive the same arguments that are passed to the job.
134
-
135
- ``` ruby
136
- class MyJob
137
- include Sidekiq::Job
138
- include Sidekiq::Throttled::Job
139
-
140
- sidekiq_options :queue => :my_queue
141
-
142
- sidekiq_throttle(
143
- # Allow maximum 1000 concurrent jobs of this class at a time for VIPs and 10 for all other users.
144
- :concurrency => {
145
- :limit => ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
146
- :key_suffix => ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
147
- },
148
- # Allow 1000 jobs/hour to be processed for VIPs and 10/day for all others
149
- :threshold => {
150
- :limit => ->(user_id) { User.vip?(user_id) ? 1_000 : 10 },
151
- :period => ->(user_id) { User.vip?(user_id) ? 1.hour : 1.day },
152
- :key_suffix => ->(user_id) { User.vip?(user_id) ? "vip" : "std" }
153
- )
154
-
155
- def perform(user_id)
156
- # ...
157
- end
158
- end
159
- ```
160
-
161
- You also can use several different keys to throttle one worker.
162
-
163
- ``` ruby
164
- class MyJob
165
- include Sidekiq::Job
166
- include Sidekiq::Throttled::Job
167
-
168
- sidekiq_options :queue => :my_queue
169
-
170
- sidekiq_throttle(
171
- # Allow maximum 10 concurrent jobs per project at a time and maximum 2 jobs per user
172
- :concurrency => [
173
- { :limit => 10, :key_suffix => -> (project_id, user_id) { project_id } },
174
- { :limit => 2, :key_suffix => -> (project_id, user_id) { user_id } }
175
- ]
176
- # For :threshold it works the same
177
- )
178
-
179
- def perform(project_id, user_id)
180
- # ...
181
- end
182
- end
183
- ```
184
-
185
- **NB** Don't forget to specify `:key_suffix` and make it return different values
186
- if you are using dynamic limit/period options. Otherwise you risk getting into
187
- some trouble.
188
-
189
-
190
- ### Concurrency throttling fine-tuning
191
-
192
- Concurrency throttling is based on distributed locks. Those locks have default
193
- time to live (TTL) set to 15 minutes. If your job takes more than 15 minutes
194
- to finish, lock will be released and you might end up with more jobs running
195
- concurrently than you expect.
196
-
197
- This is done to avoid deadlocks - when by any reason (e.g. Sidekiq process was
198
- OOM-killed) cleanup middleware wasn't executed and locks were not released.
199
-
200
- If your job takes more than 15 minutes to complete, you can tune concurrency
201
- lock TTL to fit your needs:
202
-
203
- ``` ruby
204
- # Set concurrency strategy lock TTL to 1 hour.
205
- sidekiq_throttle(:concurrency => { :limit => 20, :ttl => 1.hour.to_i })
206
- ```
207
-
208
-
209
- ## Enhanced Queues list
210
-
211
- This gem provides ability to pause/resume queues from processing by workers.
212
- So you may simply pause particular queue without need to stop and reconfigure
213
- workers by simply pushing a button on sidekiq web UI.
214
-
215
- By default we add *Enhanced Queues* tab with this functionality. But if you
216
- want you can override default *Queues* tab completely (notice that page will
217
- still be available using it's URL, but tab will be pointing enhanced version).
218
- To do so, just call `Sidekiq::Throttled::Web.enhance_queues_tab!` somewhere
219
- in your initializer/bootstrap code. If you are using rails, you might want to
220
- add it right into your `config/routes.rb` file:
221
-
222
- ``` ruby
223
- # file config/routes.rb
224
-
225
- require "sidekiq/web"
226
- require "sidekiq/throttled/web"
227
-
228
- Rails.application.routes.draw do
229
- # ...
230
-
231
- # Replace Sidekiq Queues with enhanced version!
232
- Sidekiq::Throttled::Web.enhance_queues_tab!
233
-
234
- # Mount Sidekiq Web UI to `/sidekiq` endpoint
235
- mount Sidekiq::Web => "/sidekiq"
236
-
237
- # ...
238
- end
239
- ```
240
-
241
-
242
- ## Supported Ruby Versions
243
-
244
- This library aims to support and is [tested against][ci] the following Ruby
245
- versions:
246
-
247
- * Ruby 2.7.x
248
- * Ruby 3.0.x
249
- * Ruby 3.1.x
250
-
251
- If something doesn't work on one of these versions, it's a bug.
252
-
253
- This library may inadvertently work (or seem to work) on other Ruby versions,
254
- however support will only be provided for the versions listed above.
255
-
256
- If you would like this library to support another Ruby version or
257
- implementation, you may volunteer to be a maintainer. Being a maintainer
258
- entails making sure all tests run and pass on that implementation. When
259
- something breaks on your implementation, you will be responsible for providing
260
- patches in a timely fashion. If critical issues for a particular implementation
261
- exist at the time of a major release, support for that Ruby version may be
262
- dropped.
263
-
264
-
265
- ## Supported Sidekiq Versions
266
-
267
- This library aims to support work with following [Sidekiq][sidekiq] versions:
268
-
269
- * Sidekiq 6.4.x
270
- * Sidekiq 6.5.x
271
-
272
-
273
- ## Contributing
274
-
275
- * Fork sidekiq-throttled on GitHub
276
- * Make your changes
277
- * Ensure all tests pass (`bundle exec rake`)
278
- * Send a pull request
279
- * If we like them we'll merge them
280
- * If we've accepted a patch, feel free to ask for commit access!
281
-
282
-
283
- ## Development
284
-
285
- ```
286
- bundle update
287
- bundle exec appraisal install # install dependencies for all gemfiles
288
- bundle exec appraisal update # update dependencies for all gemfiles
289
- bundle exec appraisal rspec # run rspec against each gemfile
290
- bundle exec rubocop # run static code analysis
291
- ```
292
-
293
- Don't forget to run `appraisal update` after any changes to `Gemfile`.
294
-
295
-
296
- [ci]: https://github.com/ixti/sidekiq-throttled/actions/workflows/ci.yml
297
- [sidekiq]: https://github.com/mperham/sidekiq
data/Rakefile DELETED
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
-
5
- require "rspec/core/rake_task"
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- desc "Run RuboCop"
9
- task :rubocop do
10
- require "rubocop"
11
- result = RuboCop::CLI.new.run([])
12
- abort("RuboCop failed!") if result.nonzero?
13
- end
14
-
15
- namespace :rubocop do
16
- desc "Auto-correct RuboCop offenses"
17
- task :autocorrect do
18
- require "rubocop"
19
- result = RuboCop::CLI.new.run(["--auto-correct"])
20
- abort("RuboCop failed!") if result.nonzero?
21
- end
22
- end
23
-
24
- default_suite = ENV["CI"] ? :spec : %i[spec rubocop]
25
- named_suites = { "rubocop" => :rubocop, "rspec" => :spec }
26
-
27
- task :default => named_suites.fetch(ENV.fetch("SUITE", nil), default_suite)
@@ -1,33 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "rake"
7
- gem "rspec"
8
- gem "sidekiq", "~> 6.4.0"
9
-
10
- group :development do
11
- gem "byebug"
12
- gem "guard", require: false
13
- gem "guard-rspec", require: false
14
- gem "guard-rubocop", require: false
15
- end
16
-
17
- group :test do
18
- gem "apparition"
19
- gem "capybara"
20
- gem "puma"
21
- gem "rack-test"
22
- gem "sinatra"
23
- gem "timecop"
24
- end
25
-
26
- group :lint do
27
- gem "rubocop", require: false
28
- gem "rubocop-performance", require: false
29
- gem "rubocop-rake", require: false
30
- gem "rubocop-rspec", require: false
31
- end
32
-
33
- gemspec path: "../"
@@ -1,33 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "rake"
7
- gem "rspec"
8
- gem "sidekiq", "~> 6.5.0"
9
-
10
- group :development do
11
- gem "byebug"
12
- gem "guard", require: false
13
- gem "guard-rspec", require: false
14
- gem "guard-rubocop", require: false
15
- end
16
-
17
- group :test do
18
- gem "apparition"
19
- gem "capybara"
20
- gem "puma"
21
- gem "rack-test"
22
- gem "sinatra"
23
- gem "timecop"
24
- end
25
-
26
- group :lint do
27
- gem "rubocop", require: false
28
- gem "rubocop-performance", require: false
29
- gem "rubocop-rake", require: false
30
- gem "rubocop-rspec", require: false
31
- end
32
-
33
- gemspec path: "../"
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fiber"
4
-
5
- require "sidekiq/throttled/communicator/exception_handler"
6
-
7
- module Sidekiq
8
- module Throttled
9
- class Communicator
10
- # Callbacks registry and runner. Runs registered callbacks in dedicated
11
- # Fiber solving issue with ConnectionPool and Redis client in subscriber
12
- # mode.
13
- #
14
- # Once Redis entered subscriber mode `#subscribe` method, it can't be used
15
- # for any command but pub/sub or quit, making it impossible to use for
16
- # anything else. ConnectionPool binds reserved client to Thread, thus
17
- # nested `#with` calls inside same thread result into a same connection.
18
- # That makes it impossible to issue any normal Redis commands from
19
- # within listener Thread.
20
- #
21
- # @private
22
- class Callbacks
23
- include ExceptionHandler
24
-
25
- # Initializes callbacks registry.
26
- def initialize
27
- @mutex = Mutex.new
28
- @handlers = Hash.new { |h, k| h[k] = [] }
29
- end
30
-
31
- # Registers handler of given event.
32
- #
33
- # @example
34
- #
35
- # callbacks.on "and out comes wolves" do |who|
36
- # puts "#{who} let the dogs out?!"
37
- # end
38
- #
39
- # @param [#to_s] event
40
- # @raise [ArgumentError] if no handler block given
41
- # @yield [*args] Runs given block upon `event`
42
- # @yieldreturn [void]
43
- # @return [self]
44
- def on(event, &handler)
45
- raise ArgumentError, "No block given" unless handler
46
-
47
- @mutex.synchronize { @handlers[event.to_s] << handler }
48
- self
49
- end
50
-
51
- # Runs event handlers with given args.
52
- #
53
- # @param [#to_s] event
54
- # @param [Object] payload
55
- # @return [void]
56
- def run(event, payload = nil)
57
- @mutex.synchronize do
58
- fiber = Fiber.new do
59
- @handlers[event.to_s].each do |callback|
60
- callback.call(payload)
61
- rescue => e
62
- handle_exception(e, :context => "sidekiq:throttled")
63
- end
64
- end
65
-
66
- fiber.resume
67
- end
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq"
4
- require "sidekiq/version"
5
-
6
- module Sidekiq
7
- module Throttled
8
- class Communicator
9
- if Sidekiq::VERSION >= "6.5.0"
10
- module ExceptionHandler
11
- def handle_exception(*args)
12
- Sidekiq.handle_exception(*args)
13
- end
14
- end
15
-
16
- # NOTE: `Sidekiq.default_error_handler` is private API
17
- Sidekiq.error_handlers << Sidekiq.method(:default_error_handler)
18
- else
19
- require "sidekiq/exception_handler"
20
-
21
- ExceptionHandler = ::Sidekiq::ExceptionHandler
22
- end
23
- end
24
- end
25
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq/throttled/communicator/exception_handler"
4
-
5
- module Sidekiq
6
- module Throttled
7
- class Communicator
8
- # Redis subscription listener thread.
9
- #
10
- # @private
11
- class Listener < Thread
12
- include ExceptionHandler
13
-
14
- # Starts listener thread.
15
- #
16
- # @param [String] channel Redis pub/sub channel to listen
17
- # @param [Callbacks] callbacks Message callbacks registry
18
- def initialize(channel, callbacks)
19
- @channel = channel
20
- @callbacks = callbacks
21
- @terminated = false
22
- @subscribed = false
23
-
24
- super { listen until @terminated }
25
- end
26
-
27
- # Whenever underlying redis client subscribed to pub/sup channel.
28
- #
29
- # @return [Boolean]
30
- def ready?
31
- @subscribed
32
- end
33
-
34
- # Whenever main loop is still running.
35
- #
36
- # @return [Boolean]
37
- def listening?
38
- !@terminated
39
- end
40
-
41
- # Stops listener.
42
- #
43
- # @return [void]
44
- def stop
45
- # Raising exception while client is in subscription mode makes
46
- # redis close connection and thus causing ConnectionPool reopen
47
- # it (normal mode). Otherwise subscription mode client will be
48
- # pushed back to ConnectionPool causing problems.
49
- raise Sidekiq::Shutdown
50
- end
51
-
52
- private
53
-
54
- # Wraps {#subscribe} with exception handlers:
55
- #
56
- # - `Sidekiq::Shutdown` exception marks listener as stopped and returns
57
- # making `while` loop of listener thread terminate.
58
- #
59
- # - `StandardError` got recorded to the log and swallowed,
60
- # making `while` loop of the listener thread restart.
61
- #
62
- # - `Exception` is recorded to the log and re-raised.
63
- #
64
- # @return [void]
65
- def listen # rubocop:disable Metrics/MethodLength
66
- subscribe
67
- rescue Sidekiq::Shutdown
68
- @terminated = true
69
- @subscribed = false
70
- rescue StandardError => e # rubocop:disable Style/RescueStandardError
71
- @subscribed = false
72
- handle_exception(e, { :context => "sidekiq:throttled" })
73
- sleep 1
74
- rescue Exception => e # rubocop:disable Lint/RescueException
75
- @terminated = true
76
- @subscribed = false
77
- handle_exception(e, { :context => "sidekiq:throttled" })
78
- raise
79
- end
80
-
81
- # Subscribes to channel and triggers all registerd handlers for
82
- # received messages.
83
- #
84
- # @note Puts thread's Redis connection to subscription mode and
85
- # locks thread.
86
- #
87
- # @see http://redis.io/topics/pubsub
88
- # @see http://redis.io/commands/subscribe
89
- # @see Callbacks#run
90
- # @return [void]
91
- def subscribe # rubocop:disable Metrics/MethodLength
92
- Sidekiq.redis do |conn|
93
- conn.subscribe @channel do |on|
94
- on.subscribe do
95
- @subscribed = true
96
- @callbacks.run("ready")
97
- end
98
-
99
- on.message do |_channel, data|
100
- message, payload = Marshal.load(data) # rubocop:disable Security/MarshalLoad:
101
- @callbacks.run("message:#{message}", payload)
102
- end
103
- end
104
- end
105
- end
106
- end
107
- end
108
- end
109
- end