sidekiq-throttled 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|