sidekiq-throttled 0.18.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +314 -0
  3. data/lib/sidekiq/throttled/config.rb +44 -0
  4. data/lib/sidekiq/throttled/cooldown.rb +55 -0
  5. data/lib/sidekiq/throttled/expirable_set.rb +70 -0
  6. data/lib/sidekiq/throttled/job.rb +4 -4
  7. data/lib/sidekiq/throttled/middlewares/server.rb +28 -0
  8. data/lib/sidekiq/throttled/patches/basic_fetch.rb +53 -0
  9. data/lib/sidekiq/throttled/registry.rb +4 -7
  10. data/lib/sidekiq/throttled/strategy/concurrency.rb +2 -4
  11. data/lib/sidekiq/throttled/strategy/threshold.rb +2 -4
  12. data/lib/sidekiq/throttled/strategy.rb +10 -10
  13. data/lib/sidekiq/throttled/strategy_collection.rb +2 -3
  14. data/lib/sidekiq/throttled/version.rb +1 -1
  15. data/lib/sidekiq/throttled/web.rb +2 -45
  16. data/lib/sidekiq/throttled/worker.rb +1 -1
  17. data/lib/sidekiq/throttled.rb +45 -57
  18. metadata +22 -67
  19. data/.coveralls.yml +0 -1
  20. data/.github/dependabot.yml +0 -12
  21. data/.github/workflows/ci.yml +0 -52
  22. data/.gitignore +0 -12
  23. data/.rspec +0 -5
  24. data/.rubocop.yml +0 -20
  25. data/.rubocop_todo.yml +0 -68
  26. data/.travis.yml +0 -37
  27. data/.yardopts +0 -1
  28. data/Appraisals +0 -9
  29. data/CHANGES.md +0 -318
  30. data/Gemfile +0 -34
  31. data/Guardfile +0 -25
  32. data/README.md +0 -297
  33. data/Rakefile +0 -27
  34. data/gemfiles/sidekiq_6.4.gemfile +0 -33
  35. data/gemfiles/sidekiq_6.5.gemfile +0 -33
  36. data/lib/sidekiq/throttled/communicator/callbacks.rb +0 -72
  37. data/lib/sidekiq/throttled/communicator/exception_handler.rb +0 -25
  38. data/lib/sidekiq/throttled/communicator/listener.rb +0 -109
  39. data/lib/sidekiq/throttled/communicator.rb +0 -116
  40. data/lib/sidekiq/throttled/configuration.rb +0 -50
  41. data/lib/sidekiq/throttled/expirable_list.rb +0 -70
  42. data/lib/sidekiq/throttled/fetch/unit_of_work.rb +0 -83
  43. data/lib/sidekiq/throttled/fetch.rb +0 -94
  44. data/lib/sidekiq/throttled/middleware.rb +0 -22
  45. data/lib/sidekiq/throttled/patches/queue.rb +0 -18
  46. data/lib/sidekiq/throttled/queue_name.rb +0 -46
  47. data/lib/sidekiq/throttled/queues_pauser.rb +0 -152
  48. data/lib/sidekiq/throttled/testing.rb +0 -12
  49. data/lib/sidekiq/throttled/utils.rb +0 -19
  50. data/lib/sidekiq/throttled/web/queues.html.erb +0 -49
  51. data/lib/sidekiq/throttled/web/summary_fix.js +0 -10
  52. data/lib/sidekiq/throttled/web/summary_fix.rb +0 -35
  53. data/rubocop/layout.yml +0 -24
  54. data/rubocop/lint.yml +0 -41
  55. data/rubocop/metrics.yml +0 -4
  56. data/rubocop/performance.yml +0 -25
  57. data/rubocop/rspec.yml +0 -3
  58. data/rubocop/style.yml +0 -84
  59. data/sidekiq-throttled.gemspec +0 -36
  60. /data/{LICENSE.md → LICENSE.txt} +0 -0
@@ -1,33 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "rake"
7
- gem "rspec"
8
- gem "sidekiq", "~> 6.4.0"
9
-
10
- group :development do
11
- gem "byebug"
12
- gem "guard", require: false
13
- gem "guard-rspec", require: false
14
- gem "guard-rubocop", require: false
15
- end
16
-
17
- group :test do
18
- gem "apparition"
19
- gem "capybara"
20
- gem "puma"
21
- gem "rack-test"
22
- gem "sinatra"
23
- gem "timecop"
24
- end
25
-
26
- group :lint do
27
- gem "rubocop", require: false
28
- gem "rubocop-performance", require: false
29
- gem "rubocop-rake", require: false
30
- gem "rubocop-rspec", require: false
31
- end
32
-
33
- gemspec path: "../"
@@ -1,33 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "appraisal"
6
- gem "rake"
7
- gem "rspec"
8
- gem "sidekiq", "~> 6.5.0"
9
-
10
- group :development do
11
- gem "byebug"
12
- gem "guard", require: false
13
- gem "guard-rspec", require: false
14
- gem "guard-rubocop", require: false
15
- end
16
-
17
- group :test do
18
- gem "apparition"
19
- gem "capybara"
20
- gem "puma"
21
- gem "rack-test"
22
- gem "sinatra"
23
- gem "timecop"
24
- end
25
-
26
- group :lint do
27
- gem "rubocop", require: false
28
- gem "rubocop-performance", require: false
29
- gem "rubocop-rake", require: false
30
- gem "rubocop-rspec", require: false
31
- end
32
-
33
- gemspec path: "../"
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fiber"
4
-
5
- require "sidekiq/throttled/communicator/exception_handler"
6
-
7
- module Sidekiq
8
- module Throttled
9
- class Communicator
10
- # Callbacks registry and runner. Runs registered callbacks in dedicated
11
- # Fiber solving issue with ConnectionPool and Redis client in subscriber
12
- # mode.
13
- #
14
- # Once Redis entered subscriber mode `#subscribe` method, it can't be used
15
- # for any command but pub/sub or quit, making it impossible to use for
16
- # anything else. ConnectionPool binds reserved client to Thread, thus
17
- # nested `#with` calls inside same thread result into a same connection.
18
- # That makes it impossible to issue any normal Redis commands from
19
- # within listener Thread.
20
- #
21
- # @private
22
- class Callbacks
23
- include ExceptionHandler
24
-
25
- # Initializes callbacks registry.
26
- def initialize
27
- @mutex = Mutex.new
28
- @handlers = Hash.new { |h, k| h[k] = [] }
29
- end
30
-
31
- # Registers handler of given event.
32
- #
33
- # @example
34
- #
35
- # callbacks.on "and out comes wolves" do |who|
36
- # puts "#{who} let the dogs out?!"
37
- # end
38
- #
39
- # @param [#to_s] event
40
- # @raise [ArgumentError] if no handler block given
41
- # @yield [*args] Runs given block upon `event`
42
- # @yieldreturn [void]
43
- # @return [self]
44
- def on(event, &handler)
45
- raise ArgumentError, "No block given" unless handler
46
-
47
- @mutex.synchronize { @handlers[event.to_s] << handler }
48
- self
49
- end
50
-
51
- # Runs event handlers with given args.
52
- #
53
- # @param [#to_s] event
54
- # @param [Object] payload
55
- # @return [void]
56
- def run(event, payload = nil)
57
- @mutex.synchronize do
58
- fiber = Fiber.new do
59
- @handlers[event.to_s].each do |callback|
60
- callback.call(payload)
61
- rescue => e
62
- handle_exception(e, :context => "sidekiq:throttled")
63
- end
64
- end
65
-
66
- fiber.resume
67
- end
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq"
4
- require "sidekiq/version"
5
-
6
- module Sidekiq
7
- module Throttled
8
- class Communicator
9
- if Sidekiq::VERSION >= "6.5.0"
10
- module ExceptionHandler
11
- def handle_exception(*args)
12
- Sidekiq.handle_exception(*args)
13
- end
14
- end
15
-
16
- # NOTE: `Sidekiq.default_error_handler` is private API
17
- Sidekiq.error_handlers << Sidekiq.method(:default_error_handler)
18
- else
19
- require "sidekiq/exception_handler"
20
-
21
- ExceptionHandler = ::Sidekiq::ExceptionHandler
22
- end
23
- end
24
- end
25
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq/throttled/communicator/exception_handler"
4
-
5
- module Sidekiq
6
- module Throttled
7
- class Communicator
8
- # Redis subscription listener thread.
9
- #
10
- # @private
11
- class Listener < Thread
12
- include ExceptionHandler
13
-
14
- # Starts listener thread.
15
- #
16
- # @param [String] channel Redis pub/sub channel to listen
17
- # @param [Callbacks] callbacks Message callbacks registry
18
- def initialize(channel, callbacks)
19
- @channel = channel
20
- @callbacks = callbacks
21
- @terminated = false
22
- @subscribed = false
23
-
24
- super { listen until @terminated }
25
- end
26
-
27
- # Whenever underlying redis client subscribed to pub/sup channel.
28
- #
29
- # @return [Boolean]
30
- def ready?
31
- @subscribed
32
- end
33
-
34
- # Whenever main loop is still running.
35
- #
36
- # @return [Boolean]
37
- def listening?
38
- !@terminated
39
- end
40
-
41
- # Stops listener.
42
- #
43
- # @return [void]
44
- def stop
45
- # Raising exception while client is in subscription mode makes
46
- # redis close connection and thus causing ConnectionPool reopen
47
- # it (normal mode). Otherwise subscription mode client will be
48
- # pushed back to ConnectionPool causing problems.
49
- raise Sidekiq::Shutdown
50
- end
51
-
52
- private
53
-
54
- # Wraps {#subscribe} with exception handlers:
55
- #
56
- # - `Sidekiq::Shutdown` exception marks listener as stopped and returns
57
- # making `while` loop of listener thread terminate.
58
- #
59
- # - `StandardError` got recorded to the log and swallowed,
60
- # making `while` loop of the listener thread restart.
61
- #
62
- # - `Exception` is recorded to the log and re-raised.
63
- #
64
- # @return [void]
65
- def listen # rubocop:disable Metrics/MethodLength
66
- subscribe
67
- rescue Sidekiq::Shutdown
68
- @terminated = true
69
- @subscribed = false
70
- rescue StandardError => e # rubocop:disable Style/RescueStandardError
71
- @subscribed = false
72
- handle_exception(e, { :context => "sidekiq:throttled" })
73
- sleep 1
74
- rescue Exception => e # rubocop:disable Lint/RescueException
75
- @terminated = true
76
- @subscribed = false
77
- handle_exception(e, { :context => "sidekiq:throttled" })
78
- raise
79
- end
80
-
81
- # Subscribes to channel and triggers all registerd handlers for
82
- # received messages.
83
- #
84
- # @note Puts thread's Redis connection to subscription mode and
85
- # locks thread.
86
- #
87
- # @see http://redis.io/topics/pubsub
88
- # @see http://redis.io/commands/subscribe
89
- # @see Callbacks#run
90
- # @return [void]
91
- def subscribe # rubocop:disable Metrics/MethodLength
92
- Sidekiq.redis do |conn|
93
- conn.subscribe @channel do |on|
94
- on.subscribe do
95
- @subscribed = true
96
- @callbacks.run("ready")
97
- end
98
-
99
- on.message do |_channel, data|
100
- message, payload = Marshal.load(data) # rubocop:disable Security/MarshalLoad:
101
- @callbacks.run("message:#{message}", payload)
102
- end
103
- end
104
- end
105
- end
106
- end
107
- end
108
- end
109
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "singleton"
4
-
5
- require "sidekiq/throttled/communicator/exception_handler"
6
- require "sidekiq/throttled/communicator/listener"
7
- require "sidekiq/throttled/communicator/callbacks"
8
-
9
- module Sidekiq
10
- module Throttled
11
- # Inter-process communication for sidekiq. It starts listener thread on
12
- # sidekiq server and listens for incoming messages.
13
- #
14
- # @example
15
- #
16
- # # Add incoming message handler for server
17
- # Communicator.instance.receive "knock" do |who|
18
- # puts "#{who}'s knocking on the door"
19
- # end
20
- #
21
- # # Emit message from console
22
- # Sidekiq.redis do |conn|
23
- # Communicator.instance.transmit(conn, "knock", "ixti")
24
- # end
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"
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
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&.ready?
113
- end
114
- end
115
- end
116
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sidekiq
4
- module Throttled
5
- # Configuration holder.
6
- class Configuration
7
- # Class constructor.
8
- def initialize
9
- reset!
10
- end
11
-
12
- # Reset configuration to defaults.
13
- #
14
- # @return [self]
15
- def reset!
16
- @inherit_strategies = false
17
-
18
- self
19
- end
20
-
21
- # Instructs throttler to lookup strategies in parent classes, if there's
22
- # no own strategy:
23
- #
24
- # class FooJob
25
- # include Sidekiq::Job
26
- # include Sidekiq::Throttled::Job
27
- #
28
- # sidekiq_throttle :concurrency => { :limit => 42 }
29
- # end
30
- #
31
- # class BarJob < FooJob
32
- # end
33
- #
34
- # By default in the example above, `Bar` won't have throttling options.
35
- # Set this flag to `true` to enable this lookup in initializer, after
36
- # that `Bar` will use `Foo` throttling bucket.
37
- def inherit_strategies=(value)
38
- @inherit_strategies = value ? true : false
39
- end
40
-
41
- # Whenever throttled workers should inherit parent's strategies or not.
42
- # Default: `false`.
43
- #
44
- # @return [Boolean]
45
- def inherit_strategies?
46
- @inherit_strategies
47
- end
48
- end
49
- end
50
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "monitor"
4
-
5
- module Sidekiq
6
- module Throttled
7
- # List that tracks when elements were added and enumerates over those not
8
- # older than `ttl` seconds ago.
9
- #
10
- # ## Implementation
11
- #
12
- # Internally list holds an array of arrays. Thus each element is a tuple of
13
- # monotonic timestamp (when element was added) and element itself:
14
- #
15
- # [
16
- # [ 123456.7890, "default" ],
17
- # [ 123456.7891, "urgent" ],
18
- # [ 123457.9621, "urgent" ],
19
- # ...
20
- # ]
21
- #
22
- # It does not deduplicates elements. Eviction happens only upon elements
23
- # retrieval (see {#each}).
24
- #
25
- # @see https://ruby-doc.org/core/Process.html#method-c-clock_gettime
26
- # @see https://linux.die.net/man/3/clock_gettime
27
- #
28
- # @private
29
- class ExpirableList
30
- include Enumerable
31
-
32
- # @param ttl [Float] elements time-to-live in seconds
33
- def initialize(ttl)
34
- @ttl = ttl.to_f
35
- @arr = []
36
- @mon = Monitor.new
37
- end
38
-
39
- # Pushes given element into the list.
40
- #
41
- # @params element [Object]
42
- # @return [ExpirableList] self
43
- def <<(element)
44
- @mon.synchronize { @arr << [::Process.clock_gettime(::Process::CLOCK_MONOTONIC), element] }
45
- self
46
- end
47
-
48
- # Evicts expired elements and calls the given block once for each element
49
- # left, passing that element as a parameter.
50
- #
51
- # @yield [element]
52
- # @return [Enumerator] if no block given
53
- # @return [ExpirableList] self if block given
54
- def each
55
- return to_enum __method__ unless block_given?
56
-
57
- @mon.synchronize do
58
- horizon = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @ttl
59
-
60
- # drop all elements older than horizon
61
- @arr.shift while @arr[0] && @arr[0][0] < horizon
62
-
63
- @arr.each { |x| yield x[1] }
64
- end
65
-
66
- self
67
- end
68
- end
69
- end
70
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq"
4
-
5
- require "sidekiq/throttled/queue_name"
6
-
7
- module Sidekiq
8
- module Throttled
9
- class Fetch
10
- # BRPOP response envelope.
11
- #
12
- # @see Throttled::Fetch
13
- # @private
14
- class UnitOfWork
15
- # @return [String] Redis key where job was pulled from
16
- attr_reader :queue
17
-
18
- # @return [String] Job's JSON payload
19
- attr_reader :job
20
-
21
- # @param [String] queue Redis key where job was pulled from
22
- # @param [String] job Job's JSON payload
23
- def initialize(queue, job)
24
- @queue = queue
25
- @job = job
26
- end
27
-
28
- # Callback that is called by `Sidekiq::Processor` when job was
29
- # succeccfully processed. Most likely this is used by `ReliableFetch`
30
- # of Sidekiq Pro/Enterprise to remove job from running queue.
31
- #
32
- # @return [void]
33
- def acknowledge
34
- # do nothing
35
- end
36
-
37
- # Normalized `queue` name.
38
- #
39
- # @see QueueName.normalize
40
- # @return [String]
41
- def queue_name
42
- @queue_name ||= QueueName.normalize queue
43
- end
44
-
45
- # Pushes job back to the tail of the queue, so that it will be popped
46
- # first next time fetcher will pull job.
47
- #
48
- # @note This is triggered when job was not finished and Sidekiq server
49
- # process was terminated. It is a reverse of whatever fetcher was
50
- # doing to pull the job out of queue.
51
- #
52
- # @param [Redis] pipelined connection for requeing via Redis#pipelined
53
- # @return [void]
54
- def requeue(pipeline = nil)
55
- if pipeline
56
- pipeline.rpush(QueueName.expand(queue_name), job)
57
- else
58
- Sidekiq.redis { |conn| conn.rpush(QueueName.expand(queue_name), job) }
59
- end
60
- end
61
-
62
- # Pushes job back to the head of the queue, so that job won't be tried
63
- # immediately after it was requeued (in most cases).
64
- #
65
- # @note This is triggered when job is throttled. So it is same operation
66
- # Sidekiq performs upon `Sidekiq::Worker.perform_async` call.
67
- #
68
- # @return [void]
69
- def requeue_throttled
70
- Sidekiq.redis { |conn| conn.lpush(QueueName.expand(queue_name), job) }
71
- end
72
-
73
- # Tells whenever job should be pushed back to queue (throttled) or not.
74
- #
75
- # @see Sidekiq::Throttled.throttled?
76
- # @return [Boolean]
77
- def throttled?
78
- Throttled.throttled? job
79
- end
80
- end
81
- end
82
- end
83
- end
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq"
4
- require "sidekiq/throttled/expirable_list"
5
- require "sidekiq/throttled/fetch/unit_of_work"
6
- require "sidekiq/throttled/queues_pauser"
7
- require "sidekiq/throttled/queue_name"
8
-
9
- module Sidekiq
10
- module Throttled
11
- # Throttled fetch strategy.
12
- #
13
- # @private
14
- class Fetch
15
- # Timeout to sleep between fetch retries in case of no job received,
16
- # as well as timeout to wait for redis to give us something to work.
17
- TIMEOUT = 2
18
-
19
- # Initializes fetcher instance.
20
- # @param options [Hash]
21
- # @option options [Integer] :throttled_queue_cooldown (TIMEOUT)
22
- # Min delay in seconds before queue will be polled again after
23
- # throttled job.
24
- # @option options [Boolean] :strict (false)
25
- # @option options [Array<#to_s>] :queue
26
- def initialize(options)
27
- @paused = ExpirableList.new(options.fetch(:throttled_queue_cooldown, TIMEOUT))
28
-
29
- @strict = options.fetch(:strict, false)
30
- @queues = options.fetch(:queues).map { |q| QueueName.expand q }
31
-
32
- raise ArgumentError, "empty :queues" if @queues.empty?
33
-
34
- @queues.uniq! if @strict
35
- end
36
-
37
- # Retrieves job from redis.
38
- #
39
- # @return [Sidekiq::Throttled::UnitOfWork, nil]
40
- def retrieve_work
41
- work = brpop
42
- return unless work
43
-
44
- work = UnitOfWork.new(*work)
45
- return work unless work.throttled?
46
-
47
- work.requeue_throttled
48
- @paused << QueueName.expand(work.queue_name)
49
-
50
- nil
51
- end
52
-
53
- def bulk_requeue(units, _options)
54
- return if units.empty?
55
-
56
- Sidekiq.logger.debug { "Re-queueing terminated jobs" }
57
- Sidekiq.redis do |conn|
58
- conn.pipelined do |pipeline|
59
- units.each { |unit| unit.requeue(pipeline) }
60
- end
61
- end
62
- Sidekiq.logger.info("Pushed #{units.size} jobs back to Redis")
63
- rescue => e
64
- Sidekiq.logger.warn("Failed to requeue #{units.size} jobs: #{e}")
65
- end
66
-
67
- private
68
-
69
- # Tries to pop pair of `queue` and job `message` out of sidekiq queues.
70
- #
71
- # @see http://redis.io/commands/brpop
72
- # @return [Array(String, String), nil]
73
- def brpop
74
- queues = filter_queues(@strict ? @queues : @queues.shuffle.uniq)
75
-
76
- if queues.empty?
77
- sleep TIMEOUT
78
- return
79
- end
80
-
81
- Sidekiq.redis { |conn| conn.brpop(*queues, :timeout => TIMEOUT) }
82
- end
83
-
84
- # Returns list of queues to try to fetch jobs from.
85
- #
86
- # @note It may return an empty array.
87
- # @param [Array<String>] queues
88
- # @return [Array<String>]
89
- def filter_queues(queues)
90
- QueuesPauser.instance.filter(queues) - @paused.to_a
91
- end
92
- end
93
- end
94
- end