sidekiq-throttled 0.5.0 → 0.6.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/.coveralls.yml +1 -0
- data/.travis.yml +4 -10
- data/CHANGES.md +14 -0
- data/Gemfile +2 -2
- data/README.md +0 -2
- data/gemfiles/sidekiq_4.0.gemfile +1 -1
- data/gemfiles/sidekiq_4.1.gemfile +1 -1
- data/gemfiles/sidekiq_latest.gemfile +1 -1
- data/lib/sidekiq/throttled/communicator/callbacks.rb +74 -0
- data/lib/sidekiq/throttled/communicator/listener.rb +109 -0
- data/lib/sidekiq/throttled/communicator.rb +116 -0
- data/lib/sidekiq/throttled/fetch.rb +20 -8
- data/lib/sidekiq/throttled/middleware.rb +1 -0
- data/lib/sidekiq/throttled/queue_name.rb +46 -0
- data/lib/sidekiq/throttled/queues_pauser.rb +105 -0
- data/lib/sidekiq/throttled/registry.rb +2 -0
- data/lib/sidekiq/throttled/strategy/concurrency.lua +13 -3
- data/lib/sidekiq/throttled/strategy/concurrency.rb +2 -2
- data/lib/sidekiq/throttled/strategy/script.rb +23 -14
- data/lib/sidekiq/throttled/strategy/threshold.lua +14 -3
- data/lib/sidekiq/throttled/strategy/threshold.rb +1 -1
- data/lib/sidekiq/throttled/strategy.rb +2 -0
- data/lib/sidekiq/throttled/unit_of_work.rb +28 -5
- data/lib/sidekiq/throttled/version.rb +1 -1
- data/lib/sidekiq/throttled/web/stats.rb +0 -1
- data/lib/sidekiq/throttled/web.rb +1 -0
- data/lib/sidekiq/throttled.rb +25 -3
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fe478db1a3774d9aeaa83d54658eb39137fa001
|
4
|
+
data.tar.gz: 3bb17c56babff2a8ab46fe18bde97f39482402ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81b3a563dc78de58619d964afc2a940c0b0f93789b0e39ed1ea390cb3e1991c0b4445b9572a60464e0dc2d7330251d7a4049a1dbed5111fac42444f0ed146894
|
7
|
+
data.tar.gz: 8be93fdcaa16dfec95b12f91a2bb81c580cc617761308cceaba6c588b9cc0e78e822091fbbee9c56895dbff7d6d1df2255aeed771bc842447da257456fbb5576
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.travis.yml
CHANGED
@@ -1,22 +1,16 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 2.
|
4
|
-
- 2.1
|
5
|
-
- 2.2
|
6
|
-
- 2.3.0
|
3
|
+
- 2.2.5
|
7
4
|
- 2.3.1
|
8
5
|
- ruby-head
|
9
|
-
- jruby-9.0.
|
10
|
-
- jruby-9.0.1.0
|
6
|
+
- jruby-9.0.5.0
|
11
7
|
- jruby-head
|
12
|
-
- ruby-head
|
13
8
|
- rbx-2
|
14
9
|
matrix:
|
15
10
|
allow_failures:
|
16
|
-
- rvm: jruby-9.0.0.0
|
17
|
-
- rvm: jruby-9.0.1.0
|
18
|
-
- rvm: jruby-head
|
19
11
|
- rvm: ruby-head
|
12
|
+
- rvm: jruby-9.0.5.0
|
13
|
+
- rvm: jruby-head
|
20
14
|
- rvm: rbx-2
|
21
15
|
fast_finish: true
|
22
16
|
gemfile:
|
data/CHANGES.md
CHANGED
@@ -1,9 +1,23 @@
|
|
1
|
+
## 0.6.0 (2016-08-27)
|
2
|
+
|
3
|
+
* [#21](https://github.com/sensortower/sidekiq-throttled/pull/21)
|
4
|
+
Allow pause/unpause queues.
|
5
|
+
([@ixti])
|
6
|
+
|
7
|
+
|
1
8
|
## 0.5.0 (2016-08-18)
|
2
9
|
|
3
10
|
* Drop Sidekiq 3.x support.
|
4
11
|
([@ixti])
|
5
12
|
|
6
13
|
|
14
|
+
## 0.4.1 (2016-08-18)
|
15
|
+
|
16
|
+
* [#15](https://github.com/sensortower/sidekiq-throttled/pull/15)
|
17
|
+
Fix throttled web UI on older versions of sidekiq.
|
18
|
+
([@palanglung])
|
19
|
+
|
20
|
+
|
7
21
|
## 0.4.0 (2016-05-17)
|
8
22
|
|
9
23
|
* [#14](https://github.com/sensortower/sidekiq-throttled/pull/14)
|
data/Gemfile
CHANGED
@@ -5,10 +5,10 @@ source "https://rubygems.org"
|
|
5
5
|
gem "appraisal"
|
6
6
|
gem "rake"
|
7
7
|
gem "rspec"
|
8
|
-
gem "rubocop"
|
8
|
+
gem "rubocop", "~> 0.42.0", :require => false
|
9
9
|
|
10
10
|
group :test do
|
11
|
-
gem "coveralls"
|
11
|
+
gem "coveralls", :require => false
|
12
12
|
gem "rack-test"
|
13
13
|
gem "simplecov", ">= 0.9"
|
14
14
|
gem "sinatra", "~> 1.4", ">= 1.4.6"
|
data/README.md
CHANGED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fiber"
|
4
|
+
require "thread"
|
5
|
+
|
6
|
+
require "sidekiq/exception_handler"
|
7
|
+
|
8
|
+
module Sidekiq
|
9
|
+
module Throttled
|
10
|
+
class Communicator
|
11
|
+
# Callbacks registry and runner. Runs registered callbacks in dedicated
|
12
|
+
# Fiber solving issue with ConnectionPool and Redis client in subscriber
|
13
|
+
# mode.
|
14
|
+
#
|
15
|
+
# Once Redis entered subscriber mode `#subscribe` method, it can't be used
|
16
|
+
# for any command but pub/sub or quit, making it impossible to use for
|
17
|
+
# anything else. ConnectionPool binds reserved client to Thread, thus
|
18
|
+
# nested `#with` calls inside same thread result into a same connection.
|
19
|
+
# That makes it impossible to issue any normal Redis commands from
|
20
|
+
# within listener Thread.
|
21
|
+
#
|
22
|
+
# @private
|
23
|
+
class Callbacks
|
24
|
+
include ExceptionHandler
|
25
|
+
|
26
|
+
# Initializes callbacks registry.
|
27
|
+
def initialize
|
28
|
+
@mutex = Mutex.new
|
29
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Registers handler of given event.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
#
|
36
|
+
# callbacks.on "and out comes wolves" do |who|
|
37
|
+
# puts "#{who} let the dogs out?!"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @param [#to_s] event
|
41
|
+
# @raise [ArgumentError] if no handler block given
|
42
|
+
# @yield [*args] Runs given block upon `event`
|
43
|
+
# @yieldreturn [void]
|
44
|
+
# @return [self]
|
45
|
+
def on(event, &handler)
|
46
|
+
raise ArgumentError, "No block given" unless handler
|
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.new do
|
59
|
+
@handlers[event.to_s].each do |callback|
|
60
|
+
begin
|
61
|
+
callback.call(payload)
|
62
|
+
rescue => e
|
63
|
+
handle_exception(e, {
|
64
|
+
:context => "sidekiq:throttled".freeze
|
65
|
+
})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end.resume
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq/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
|
66
|
+
subscribe
|
67
|
+
rescue Sidekiq::Shutdown
|
68
|
+
@terminated = true
|
69
|
+
@subscribed = false
|
70
|
+
rescue StandardError => e
|
71
|
+
@subscribed = false
|
72
|
+
handle_exception(e, { :context => "sidekiq:throttled".freeze })
|
73
|
+
sleep 1
|
74
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
75
|
+
@terminated = true
|
76
|
+
@subscribed = false
|
77
|
+
handle_exception(e, { :context => "sidekiq:throttled".freeze })
|
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
|
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)
|
101
|
+
@callbacks.run("message:#{message}", payload)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
require "singleton"
|
5
|
+
|
6
|
+
require "sidekiq/exception_handler"
|
7
|
+
require "sidekiq/throttled/communicator/listener"
|
8
|
+
require "sidekiq/throttled/communicator/callbacks"
|
9
|
+
|
10
|
+
module Sidekiq
|
11
|
+
module Throttled
|
12
|
+
# Inter-process communication for sidekiq. It starts listener thread on
|
13
|
+
# sidekiq server and listens for incoming messages.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
#
|
17
|
+
# # Add incoming message handler for server
|
18
|
+
# Communicator.instance.receive "knock" do |who|
|
19
|
+
# puts "#{who}'s knocking on the door"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # Emit message from console
|
23
|
+
# Sidekiq.redis do |conn|
|
24
|
+
# Communicator.instance.transmit(conn, "knock", "ixti")
|
25
|
+
class Communicator
|
26
|
+
include Singleton
|
27
|
+
include ExceptionHandler
|
28
|
+
|
29
|
+
# Redis PUB/SUB channel name
|
30
|
+
#
|
31
|
+
# @see http://redis.io/topics/pubsub
|
32
|
+
CHANNEL_NAME = "sidekiq:throttled".freeze
|
33
|
+
private_constant :CHANNEL_NAME
|
34
|
+
|
35
|
+
# Initializes singleton instance.
|
36
|
+
def initialize
|
37
|
+
@callbacks = Callbacks.new
|
38
|
+
@listener = nil
|
39
|
+
@mutex = Mutex.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# Starts listener thread.
|
43
|
+
#
|
44
|
+
# @return [void]
|
45
|
+
def start_listener
|
46
|
+
@mutex.synchronize do
|
47
|
+
@listener ||= Listener.new(CHANNEL_NAME, @callbacks)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Stops listener thread.
|
52
|
+
#
|
53
|
+
# @return [void]
|
54
|
+
def stop_listener
|
55
|
+
@mutex.synchronize do
|
56
|
+
@listener.stop if @listener
|
57
|
+
@listener = nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Configures Sidekiq server to start/stop listener thread.
|
62
|
+
#
|
63
|
+
# @private
|
64
|
+
# @return [void]
|
65
|
+
def setup!
|
66
|
+
Sidekiq.configure_server do |config|
|
67
|
+
config.on(:startup) { start_listener }
|
68
|
+
config.on(:quiet) { stop_listener }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Transmit message to listeners.
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
#
|
76
|
+
# Sidekiq.redis do |conn|
|
77
|
+
# Communicator.instance.transmit(conn, "knock")
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @param [Redis] redis Redis client
|
81
|
+
# @param [#to_s] message
|
82
|
+
# @param [Object] payload
|
83
|
+
# @return [void]
|
84
|
+
def transmit(redis, message, payload = nil)
|
85
|
+
redis.publish(CHANNEL_NAME, Marshal.dump([message.to_s, payload]))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Add incoming message handler.
|
89
|
+
#
|
90
|
+
# @example
|
91
|
+
#
|
92
|
+
# Communicator.instance.receive "knock" do |payload|
|
93
|
+
# # do something upon `knock` message
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# @param [#to_s] message
|
97
|
+
# @yield [payload] Runs given block everytime `message` being received.
|
98
|
+
# @yieldparam [Object, nil] payload Payload that was transmitted
|
99
|
+
# @yieldreturn [void]
|
100
|
+
# @return [void]
|
101
|
+
def receive(message, &handler)
|
102
|
+
@callbacks.on("message:#{message}", &handler)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Communicator readiness hook.
|
106
|
+
#
|
107
|
+
# @yield Runs given block every time listener thread subscribes
|
108
|
+
# to Redis pub/sub channel.
|
109
|
+
# @return [void]
|
110
|
+
def ready(&handler)
|
111
|
+
@callbacks.on("ready", &handler)
|
112
|
+
yield if @listener && @listener.ready?
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -2,18 +2,24 @@
|
|
2
2
|
|
3
3
|
require "sidekiq"
|
4
4
|
require "sidekiq/throttled/unit_of_work"
|
5
|
+
require "sidekiq/throttled/queues_pauser"
|
6
|
+
require "sidekiq/throttled/queue_name"
|
5
7
|
|
6
8
|
module Sidekiq
|
7
9
|
module Throttled
|
8
10
|
# Throttled fetch strategy.
|
11
|
+
#
|
12
|
+
# @private
|
9
13
|
class Fetch
|
10
14
|
TIMEOUT = 2
|
11
15
|
private_constant :TIMEOUT
|
12
16
|
|
17
|
+
# Initializes fetcher instance.
|
13
18
|
def initialize(options)
|
14
|
-
@
|
15
|
-
@queues = options[:queues].map { |q|
|
16
|
-
|
19
|
+
@strict = options[:strict]
|
20
|
+
@queues = options[:queues].map { |q| QueueName.expand q }
|
21
|
+
|
22
|
+
@queues.uniq! if @strict
|
17
23
|
end
|
18
24
|
|
19
25
|
# @return [Sidekiq::Throttled::UnitOfWork, nil]
|
@@ -25,7 +31,7 @@ module Sidekiq
|
|
25
31
|
return work unless Throttled.throttled? work.job
|
26
32
|
|
27
33
|
Sidekiq.redis do |conn|
|
28
|
-
conn.lpush(
|
34
|
+
conn.lpush(QueueName.expand(work.queue_name), work.job)
|
29
35
|
end
|
30
36
|
|
31
37
|
nil
|
@@ -34,13 +40,19 @@ module Sidekiq
|
|
34
40
|
private
|
35
41
|
|
36
42
|
# Tries to pop pair of `queue` and job `message` out of sidekiq queue.
|
43
|
+
#
|
44
|
+
# @see http://redis.io/commands/brpop
|
37
45
|
# @return [Array<String, String>, nil]
|
38
46
|
def brpop
|
39
|
-
|
40
|
-
|
47
|
+
queues = (@strict ? @queues : @queues.shuffle.uniq)
|
48
|
+
queues = QueuesPauser.instance.filter queues
|
41
49
|
|
42
|
-
|
43
|
-
|
50
|
+
if queues.empty?
|
51
|
+
sleep TIMEOUT
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
Sidekiq.redis { |conn| conn.brpop(*queues, TIMEOUT) }
|
44
56
|
end
|
45
57
|
end
|
46
58
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Throttled
|
5
|
+
# Queue name utility belt.
|
6
|
+
#
|
7
|
+
# @private
|
8
|
+
module QueueName
|
9
|
+
# RegExp used to stip out any redisr-namespace prefixes with `queue:`.
|
10
|
+
QUEUE_NAME_PREFIX_RE = /^.*queue:/
|
11
|
+
private_constant :QUEUE_NAME_PREFIX_RE
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Strips redis-namespace and `queue:` prefix from given queue name.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
#
|
18
|
+
# QueueName.normalize "queue:default"
|
19
|
+
# # => "default"
|
20
|
+
#
|
21
|
+
# QueueName.normalize "queue:queue:default"
|
22
|
+
# # => "default"
|
23
|
+
#
|
24
|
+
# QueueName.normalize "foo:bar:queue:default"
|
25
|
+
# # => "default"
|
26
|
+
#
|
27
|
+
# @param [String]
|
28
|
+
# @return [String]
|
29
|
+
def normalize(queue)
|
30
|
+
queue.sub(QUEUE_NAME_PREFIX_RE, "".freeze)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Prepends `queue:` prefix to given `queue` name.
|
34
|
+
#
|
35
|
+
# @note It does not normalizes queue before expanding it, thus
|
36
|
+
# double-call of this method will potentially do some harm.
|
37
|
+
#
|
38
|
+
# @param [String] queue Queue name
|
39
|
+
# @return [String]
|
40
|
+
def expand(queue)
|
41
|
+
"queue:#{queue}".freeze
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "singleton"
|
5
|
+
|
6
|
+
require "sidekiq/throttled/communicator"
|
7
|
+
require "sidekiq/throttled/queue_name"
|
8
|
+
|
9
|
+
module Sidekiq
|
10
|
+
module Throttled
|
11
|
+
# Singleton class used to pause queues from being processed.
|
12
|
+
# For the sake of efficiency it uses {Communicator} behind the scene
|
13
|
+
# to notify all processes about paused/resumed queues.
|
14
|
+
#
|
15
|
+
# @private
|
16
|
+
class QueuesPauser
|
17
|
+
include Singleton
|
18
|
+
|
19
|
+
# Redis key of Set with paused queues.
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
PAUSED_QUEUES = "throttled:X:paused_queues".freeze
|
23
|
+
private_constant :PAUSED_QUEUES
|
24
|
+
|
25
|
+
# {Communicator} message used to notify that queue needs to be paused.
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
PAUSE_MESSAGE = "pause".freeze
|
29
|
+
private_constant :PAUSE_MESSAGE
|
30
|
+
|
31
|
+
# {Communicator} message used to notify that queue needs to be resumed.
|
32
|
+
#
|
33
|
+
# @return [String]
|
34
|
+
RESUME_MESSAGE = "resume".freeze
|
35
|
+
private_constant :RESUME_MESSAGE
|
36
|
+
|
37
|
+
# Initializes singleton instance.
|
38
|
+
def initialize
|
39
|
+
@paused_queues = Set.new
|
40
|
+
@communicator = Communicator.instance
|
41
|
+
end
|
42
|
+
|
43
|
+
# Configures Sidekiq server to keep actual list of paused queues.
|
44
|
+
#
|
45
|
+
# @private
|
46
|
+
# @return [void]
|
47
|
+
def setup!
|
48
|
+
Sidekiq.configure_server do
|
49
|
+
@communicator.receive PAUSE_MESSAGE do |queue|
|
50
|
+
@paused_queues << QueueName.expand(queue)
|
51
|
+
end
|
52
|
+
|
53
|
+
@communicator.receive RESUME_MESSAGE do |queue|
|
54
|
+
@paused_queues.delete QueueName.expand(queue)
|
55
|
+
end
|
56
|
+
|
57
|
+
@communicator.ready do
|
58
|
+
@paused_queues.replace paused_queues.map { |q| QueueName.expand q }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns queues list with paused queues being stripped out.
|
64
|
+
#
|
65
|
+
# @private
|
66
|
+
# @return [Array<String>]
|
67
|
+
def filter(queues)
|
68
|
+
queues - @paused_queues.to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns list of paused queues.
|
72
|
+
#
|
73
|
+
# @return [Array<String>]
|
74
|
+
def paused_queues
|
75
|
+
Sidekiq.redis { |conn| conn.smembers(PAUSED_QUEUES).to_a }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Pauses given `queue`.
|
79
|
+
#
|
80
|
+
# @param [#to_s] queue
|
81
|
+
# @return [void]
|
82
|
+
def pause!(queue)
|
83
|
+
queue = QueueName.normalize queue.to_s
|
84
|
+
|
85
|
+
Sidekiq.redis do |conn|
|
86
|
+
conn.sadd(PAUSED_QUEUES, queue)
|
87
|
+
@communicator.transmit(conn, PAUSE_MESSAGE, queue)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Resumes given `queue`.
|
92
|
+
#
|
93
|
+
# @param [#to_s] queue
|
94
|
+
# @return [void]
|
95
|
+
def resume!(queue)
|
96
|
+
queue = QueueName.normalize queue.to_s
|
97
|
+
|
98
|
+
Sidekiq.redis do |conn|
|
99
|
+
conn.srem(PAUSED_QUEUES, queue)
|
100
|
+
@communicator.transmit(conn, RESUME_MESSAGE, queue)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -1,3 +1,13 @@
|
|
1
|
-
local
|
2
|
-
|
3
|
-
|
1
|
+
local key = KEYS[1]
|
2
|
+
local jid = KEYS[2]
|
3
|
+
local lmt = tonumber(ARGV[1])
|
4
|
+
local ttl = tonumber(ARGV[2])
|
5
|
+
|
6
|
+
if lmt <= redis.call("SCARD", key) and 0 == redis.call("SISMEMBER", key, jid) then
|
7
|
+
return 1
|
8
|
+
end
|
9
|
+
|
10
|
+
redis.call("SADD", key, jid)
|
11
|
+
redis.call("EXPIRE", key, ttl)
|
12
|
+
|
13
|
+
return 0
|
@@ -14,7 +14,7 @@ module Sidekiq
|
|
14
14
|
#
|
15
15
|
# PUSH(@key, @jid)
|
16
16
|
# return 0
|
17
|
-
SCRIPT = Script.
|
17
|
+
SCRIPT = Script.read "#{__dir__}/concurrency.lua"
|
18
18
|
private_constant :SCRIPT
|
19
19
|
|
20
20
|
# @param [#to_s] strategy_key
|
@@ -44,7 +44,7 @@ module Sidekiq
|
|
44
44
|
|
45
45
|
# @return [Boolean] whenever job is throttled or not
|
46
46
|
def throttled?(jid, *job_args)
|
47
|
-
1 == SCRIPT.eval([key(job_args)], [limit(job_args), @ttl
|
47
|
+
1 == SCRIPT.eval([key(job_args), jid.to_s], [limit(job_args), @ttl])
|
48
48
|
end
|
49
49
|
|
50
50
|
# @return [Integer] Current count of jobs
|
@@ -31,25 +31,16 @@ module Sidekiq
|
|
31
31
|
attr_reader :digest
|
32
32
|
|
33
33
|
# @param [#to_s] source Lua script
|
34
|
-
# @
|
34
|
+
# @param [Logger] logger
|
35
35
|
def initialize(source, logger: Sidekiq.logger)
|
36
36
|
@source = source.to_s.strip.freeze
|
37
37
|
@digest = Digest::SHA1.hexdigest(@source).freeze
|
38
38
|
@logger = logger
|
39
39
|
end
|
40
40
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
rescue => e
|
45
|
-
raise unless e.message.include? NOSCRIPT
|
46
|
-
load_and_eval(*args)
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# Loads script into redis cache and executes it.
|
52
|
-
def load_and_eval(*args)
|
41
|
+
# Loads script to redis
|
42
|
+
# @return [void]
|
43
|
+
def bootstrap!
|
53
44
|
Sidekiq.redis do |conn|
|
54
45
|
digest = conn.script(LOAD, @source)
|
55
46
|
|
@@ -64,10 +55,28 @@ module Sidekiq
|
|
64
55
|
|
65
56
|
@digest = digest.freeze
|
66
57
|
end
|
58
|
+
end
|
59
|
+
end
|
67
60
|
|
68
|
-
|
61
|
+
# Executes script and returns result of execution
|
62
|
+
# @return Result of script execution
|
63
|
+
def eval(*args)
|
64
|
+
Sidekiq.redis do |conn|
|
65
|
+
begin
|
66
|
+
conn.evalsha(@digest, *args)
|
67
|
+
rescue => e
|
68
|
+
raise unless e.message.include? NOSCRIPT
|
69
|
+
bootstrap!
|
70
|
+
conn.evalsha(@digest, *args)
|
71
|
+
end
|
69
72
|
end
|
70
73
|
end
|
74
|
+
|
75
|
+
# Reads given file and returns new {Script} with its contents.
|
76
|
+
# @return [Script]
|
77
|
+
def self.read(file)
|
78
|
+
new File.read file
|
79
|
+
end
|
71
80
|
end
|
72
81
|
end
|
73
82
|
end
|
@@ -1,3 +1,14 @@
|
|
1
|
-
local
|
2
|
-
|
3
|
-
|
1
|
+
local key = KEYS[1]
|
2
|
+
local lmt = tonumber(ARGV[1])
|
3
|
+
local ttl = tonumber(ARGV[2])
|
4
|
+
local now = tonumber(ARGV[3])
|
5
|
+
|
6
|
+
if lmt <= redis.call("LLEN", key) and now - redis.call("LINDEX", key, -1) < ttl then
|
7
|
+
return 1
|
8
|
+
end
|
9
|
+
|
10
|
+
redis.call("LPUSH", key, now)
|
11
|
+
redis.call("LTRIM", key, 0, lmt - 1)
|
12
|
+
redis.call("EXPIRE", key, ttl)
|
13
|
+
|
14
|
+
return 0
|
@@ -7,6 +7,8 @@ require "sidekiq/throttled/strategy/threshold"
|
|
7
7
|
module Sidekiq
|
8
8
|
module Throttled
|
9
9
|
# Meta-strategy that couples {Concurrency} and {Threshold} strategies.
|
10
|
+
#
|
11
|
+
# @private
|
10
12
|
class Strategy
|
11
13
|
# @!attribute [r] concurrency
|
12
14
|
# @return [Strategy::Concurrency, nil]
|
@@ -2,31 +2,54 @@
|
|
2
2
|
|
3
3
|
require "sidekiq"
|
4
4
|
|
5
|
+
require "sidekiq/throttled/queue_name"
|
6
|
+
|
5
7
|
module Sidekiq
|
6
8
|
module Throttled
|
9
|
+
# BRPOP response envelope.
|
10
|
+
#
|
11
|
+
# @see Throttled::Fetch
|
12
|
+
# @private
|
7
13
|
class UnitOfWork
|
8
|
-
|
9
|
-
private_constant :QUEUE_NAME_PREFIX_RE
|
10
|
-
|
14
|
+
# @return [String] Redis key where job was pulled from
|
11
15
|
attr_reader :queue
|
12
16
|
|
17
|
+
# @return [String] Job's JSON payload
|
13
18
|
attr_reader :job
|
14
19
|
|
20
|
+
# @param [String] queue Redis key where job was pulled from
|
21
|
+
# @param [String] job Job's JSON payload
|
15
22
|
def initialize(queue, job)
|
16
23
|
@queue = queue
|
17
24
|
@job = job
|
18
25
|
end
|
19
26
|
|
27
|
+
# Callback that is called by `Sidekiq::Processor` when job was
|
28
|
+
# succeccfully processed. Most this is used by `ReliableFetch`
|
29
|
+
# of Sidekiq Pro/Enterprise to remove job from running queue.
|
30
|
+
#
|
31
|
+
# @return [void]
|
20
32
|
def acknowledge
|
21
33
|
# do nothing
|
22
34
|
end
|
23
35
|
|
36
|
+
# Normalized `queue` name.
|
37
|
+
#
|
38
|
+
# @see QueueName.normalize
|
39
|
+
# @return [String]
|
24
40
|
def queue_name
|
25
|
-
|
41
|
+
@queue_name ||= QueueName.normalize queue
|
26
42
|
end
|
27
43
|
|
44
|
+
# Pushes job back to the queue.
|
45
|
+
#
|
46
|
+
# @note This is triggered when job was not finished and Sidekiq server
|
47
|
+
# process was terminated (shutdowned). Thus it should be reverse of
|
48
|
+
# whatever fetcher was doing to pull the job out of queue.
|
49
|
+
#
|
50
|
+
# @return [void]
|
28
51
|
def requeue
|
29
|
-
Sidekiq.redis { |conn| conn.rpush(
|
52
|
+
Sidekiq.redis { |conn| conn.rpush(QueueName.expand(queue_name), job) }
|
30
53
|
end
|
31
54
|
end
|
32
55
|
end
|
data/lib/sidekiq/throttled.rb
CHANGED
@@ -4,6 +4,8 @@ require "sidekiq"
|
|
4
4
|
|
5
5
|
# internal
|
6
6
|
require "sidekiq/version"
|
7
|
+
require "sidekiq/throttled/communicator"
|
8
|
+
require "sidekiq/throttled/queues_pauser"
|
7
9
|
require "sidekiq/throttled/registry"
|
8
10
|
require "sidekiq/throttled/worker"
|
9
11
|
|
@@ -39,8 +41,12 @@ module Sidekiq
|
|
39
41
|
module Throttled
|
40
42
|
class << self
|
41
43
|
# Hooks throttler into sidekiq.
|
44
|
+
#
|
42
45
|
# @return [void]
|
43
46
|
def setup!
|
47
|
+
Communicator.instance.setup!
|
48
|
+
QueuesPauser.instance.setup!
|
49
|
+
|
44
50
|
Sidekiq.configure_server do |config|
|
45
51
|
require "sidekiq/throttled/fetch"
|
46
52
|
Sidekiq.options[:fetch] = Sidekiq::Throttled::Fetch
|
@@ -52,9 +58,25 @@ module Sidekiq
|
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
61
|
+
# (see QueuesPauser#pause!)
|
62
|
+
def pause!(queue)
|
63
|
+
QueuesPauser.instance.pause!(queue)
|
64
|
+
end
|
65
|
+
|
66
|
+
# (see QueuesPauser#resume!)
|
67
|
+
def resume!(queue)
|
68
|
+
QueuesPauser.instance.resume!(queue)
|
69
|
+
end
|
70
|
+
|
71
|
+
# (see QueuesPauser#paused_queues)
|
72
|
+
def paused_queues
|
73
|
+
QueuesPauser.instance.paused_queues
|
74
|
+
end
|
75
|
+
|
76
|
+
# Tells whenever job is throttled or not.
|
77
|
+
#
|
78
|
+
# @param [String] message Job's JSON payload
|
79
|
+
# @return [Boolean]
|
58
80
|
def throttled?(message)
|
59
81
|
message = JSON.parse message
|
60
82
|
job = message.fetch("class".freeze) { return false }
|
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: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexey V Zapparov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sidekiq
|
@@ -45,6 +45,7 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
+
- ".coveralls.yml"
|
48
49
|
- ".gitignore"
|
49
50
|
- ".rspec"
|
50
51
|
- ".rubocop.yml"
|
@@ -60,9 +61,14 @@ files:
|
|
60
61
|
- gemfiles/sidekiq_4.1.gemfile
|
61
62
|
- gemfiles/sidekiq_latest.gemfile
|
62
63
|
- lib/sidekiq/throttled.rb
|
64
|
+
- lib/sidekiq/throttled/communicator.rb
|
65
|
+
- lib/sidekiq/throttled/communicator/callbacks.rb
|
66
|
+
- lib/sidekiq/throttled/communicator/listener.rb
|
63
67
|
- lib/sidekiq/throttled/errors.rb
|
64
68
|
- lib/sidekiq/throttled/fetch.rb
|
65
69
|
- lib/sidekiq/throttled/middleware.rb
|
70
|
+
- lib/sidekiq/throttled/queue_name.rb
|
71
|
+
- lib/sidekiq/throttled/queues_pauser.rb
|
66
72
|
- lib/sidekiq/throttled/registry.rb
|
67
73
|
- lib/sidekiq/throttled/strategy.rb
|
68
74
|
- lib/sidekiq/throttled/strategy/concurrency.lua
|