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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 427e3b20c7b521948d64ca4d6845c660eaebf278
4
- data.tar.gz: 25ccc08885012c0f7269651399d117d1bca07c9c
3
+ metadata.gz: 1fe478db1a3774d9aeaa83d54658eb39137fa001
4
+ data.tar.gz: 3bb17c56babff2a8ab46fe18bde97f39482402ac
5
5
  SHA512:
6
- metadata.gz: a8d5aa36914a6c4a69f6b644defd5c97a34ca38d04c59707b4b74a58d559911509599e51428670d875c1b4a8aef10e8d944be51cdaafbb9b0196cab7ac9b9250
7
- data.tar.gz: 06af7fc7818f88df36c017e92c1033429ad782ab3a0c11eb698b531d649efb7cbb294f1e1865bbaa8eb33de4c023a393f6429edca75d4806f4c501ade2a37b21
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.0
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.0.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
@@ -121,8 +121,6 @@ some trouble.
121
121
  This library aims to support and is [tested against][travis] the following Ruby
122
122
  versions:
123
123
 
124
- * Ruby 2.0.0
125
- * Ruby 2.1.x
126
124
  * Ruby 2.2.x
127
125
  * Ruby 2.3.x
128
126
 
@@ -5,7 +5,7 @@ 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
  gem "sidekiq", "~> 4.0.0"
10
10
 
11
11
  group :test do
@@ -5,7 +5,7 @@ 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
  gem "sidekiq", "~> 4.1.0"
10
10
 
11
11
  group :test do
@@ -5,7 +5,7 @@ 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
  gem "sidekiq"
10
10
 
11
11
  group :test do
@@ -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
- @strictly_ordered_queues = options[:strict]
15
- @queues = options[:queues].map { |q| "queue:#{q}" }
16
- @queues.uniq! if @strictly_ordered_queues
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("queue:#{work.queue_name}", work.job)
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
- Sidekiq.redis { |conn| conn.brpop(*queues, TIMEOUT) }
40
- end
47
+ queues = (@strict ? @queues : @queues.shuffle.uniq)
48
+ queues = QueuesPauser.instance.filter queues
41
49
 
42
- def queues
43
- (@strictly_ordered_queues ? @queues : @queues.shuffle.uniq)
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
@@ -5,6 +5,7 @@ require "sidekiq/throttled/registry"
5
5
  module Sidekiq
6
6
  module Throttled
7
7
  # Server middleware that notifies strategy that job was finished.
8
+ #
8
9
  # @private
9
10
  class Middleware
10
11
  # Called within Sidekiq job processing
@@ -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
@@ -5,6 +5,8 @@ require "sidekiq/throttled/strategy"
5
5
  module Sidekiq
6
6
  module Throttled
7
7
  # Registred strategies.
8
+ #
9
+ # @private
8
10
  module Registry
9
11
  @strategies = {}
10
12
  @aliases = {}
@@ -1,3 +1,13 @@
1
- local r, k, l, t, j = redis, KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), ARGV[3]
2
- if l <= r.call("SCARD", k) and 0 == r.call("SISMEMBER", k, j) then return 1 end
3
- r.call("SADD", k, j); r.call("EXPIRE", k, t); return 0
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.new File.read "#{__dir__}/concurrency.lua"
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, jid.to_s])
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
- # @paral [Logger] logger
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
- # Executes script and returns result of execution
42
- def eval(*args)
43
- Sidekiq.redis { |conn| conn.evalsha(@digest, *args) }
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
- conn.evalsha(@digest, *args)
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 r, k, l, p, t = redis, KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
2
- if l <= r.call("LLEN", k) and t - r.call("LINDEX", k, -1) < p then return 1 end
3
- r.call("LPUSH", k, t); r.call("LTRIM", k, 0, l - 1); r.call("EXPIRE", k, p); return 0
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
@@ -26,7 +26,7 @@ module Sidekiq
26
26
  #
27
27
  # increase!
28
28
  # return 0
29
- SCRIPT = Script.new File.read "#{__dir__}/threshold.lua"
29
+ SCRIPT = Script.read "#{__dir__}/threshold.lua"
30
30
  private_constant :SCRIPT
31
31
 
32
32
  # @param [#to_s] strategy_key
@@ -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
- QUEUE_NAME_PREFIX_RE = /^.*queue:/
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
- queue.sub(QUEUE_NAME_PREFIX_RE, "")
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("queue:#{queue_name}", job) }
52
+ Sidekiq.redis { |conn| conn.rpush(QueueName.expand(queue_name), job) }
30
53
  end
31
54
  end
32
55
  end
@@ -3,6 +3,6 @@
3
3
  module Sidekiq
4
4
  module Throttled
5
5
  # Gem version
6
- VERSION = "0.5.0".freeze
6
+ VERSION = "0.6.0".freeze
7
7
  end
8
8
  end
@@ -3,7 +3,6 @@ module Sidekiq
3
3
  module Throttled
4
4
  module Web
5
5
  # Throttle strategy stats generation helper
6
- # @private
7
6
  class Stats
8
7
  TIME_CONVERSION = [
9
8
  [60 * 60 * 24, "day", "days"],
@@ -13,6 +13,7 @@ require "sidekiq/throttled/web/stats"
13
13
  module Sidekiq
14
14
  module Throttled
15
15
  # Provides Sidekiq tab to monitor and reset throttled stats.
16
+ #
16
17
  # @private
17
18
  module Web
18
19
  class << self
@@ -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
- # @param [String] message JSON payload of job
56
- # @return [TrueClass] if job is not allowed to be processed now
57
- # @return [FalseClass] otherwise
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.5.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-18 00:00:00.000000000 Z
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