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 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