HornsAndHooves-sidekiq-limit_fetch 4.5.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +5 -0
  4. data/.rubocop.yml +34 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +37 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE +22 -0
  10. data/README.md +165 -0
  11. data/Rakefile +14 -0
  12. data/bench/compare.rb +56 -0
  13. data/demo/Gemfile +8 -0
  14. data/demo/README.md +37 -0
  15. data/demo/Rakefile +100 -0
  16. data/demo/app/workers/a_worker.rb +10 -0
  17. data/demo/app/workers/b_worker.rb +10 -0
  18. data/demo/app/workers/c_worker.rb +10 -0
  19. data/demo/app/workers/fast_worker.rb +10 -0
  20. data/demo/app/workers/slow_worker.rb +10 -0
  21. data/demo/config/application.rb +13 -0
  22. data/demo/config/boot.rb +4 -0
  23. data/demo/config/environment.rb +4 -0
  24. data/demo/config/environments/development.rb +11 -0
  25. data/lib/sidekiq/extensions/manager.rb +21 -0
  26. data/lib/sidekiq/extensions/queue.rb +27 -0
  27. data/lib/sidekiq/limit_fetch/global/monitor.rb +83 -0
  28. data/lib/sidekiq/limit_fetch/global/selector.rb +130 -0
  29. data/lib/sidekiq/limit_fetch/global/semaphore.rb +190 -0
  30. data/lib/sidekiq/limit_fetch/instances.rb +29 -0
  31. data/lib/sidekiq/limit_fetch/queues.rb +197 -0
  32. data/lib/sidekiq/limit_fetch/unit_of_work.rb +28 -0
  33. data/lib/sidekiq/limit_fetch.rb +76 -0
  34. data/lib/sidekiq-limit_fetch.rb +3 -0
  35. data/sidekiq-limit_fetch.gemspec +30 -0
  36. data/spec/sidekiq/extensions/manager_spec.rb +13 -0
  37. data/spec/sidekiq/extensions/queue_spec.rb +96 -0
  38. data/spec/sidekiq/limit_fetch/global/monitor_spec.rb +114 -0
  39. data/spec/sidekiq/limit_fetch/queues_spec.rb +127 -0
  40. data/spec/sidekiq/limit_fetch/semaphore_spec.rb +65 -0
  41. data/spec/sidekiq/limit_fetch_spec.rb +58 -0
  42. data/spec/spec_helper.rb +34 -0
  43. metadata +179 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'sidekiq'
5
+ require 'sidekiq/manager'
6
+ require 'sidekiq/api'
7
+
8
+ module Sidekiq
9
+ module LimitFetch
10
+ autoload :UnitOfWork, 'sidekiq/limit_fetch/unit_of_work'
11
+
12
+ require_relative 'limit_fetch/instances'
13
+ require_relative 'limit_fetch/queues'
14
+ require_relative 'limit_fetch/global/semaphore'
15
+ require_relative 'limit_fetch/global/selector'
16
+ require_relative 'limit_fetch/global/monitor'
17
+ require_relative 'extensions/queue'
18
+ require_relative 'extensions/manager'
19
+
20
+ TIMEOUT = Sidekiq::BasicFetch::TIMEOUT
21
+
22
+ extend self
23
+
24
+ RedisBaseConnectionError = RedisClient::ConnectionError
25
+ RedisCommandError = RedisClient::CommandError
26
+
27
+ def new(_)
28
+ self
29
+ end
30
+
31
+ def retrieve_work
32
+ queue, job = redis_brpop(Queues.acquire)
33
+ Queues.release_except(queue)
34
+ UnitOfWork.new(queue, job, capsule) if job
35
+ end
36
+
37
+ def capsule
38
+ Sidekiq.default_configuration.default_capsule
39
+ end
40
+
41
+ def bulk_requeue(*args)
42
+ Sidekiq::BasicFetch.new(capsule).bulk_requeue(*args)
43
+ end
44
+
45
+ def redis_retryable
46
+ yield
47
+ rescue RedisBaseConnectionError
48
+ sleep TIMEOUT
49
+ retry
50
+ rescue RedisCommandError => e
51
+ # If Redis was restarted and is still loading its snapshot,
52
+ # then we should treat this as a temporary connection error too.
53
+ raise unless e.message =~ /^LOADING/
54
+
55
+ sleep TIMEOUT
56
+ retry
57
+ end
58
+
59
+ private
60
+
61
+ # rubocop:disable Metrics/MethodLength
62
+ def redis_brpop(queues)
63
+ if queues.empty?
64
+ sleep TIMEOUT # there are no queues to handle, so lets sleep
65
+ [] # and return nothing
66
+ else
67
+ redis_retryable do
68
+ Sidekiq.redis do |it|
69
+ it.blocking_call(false, 'brpop', *queues, TIMEOUT)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ # rubocop:enable Metrics/MethodLength
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sidekiq/limit_fetch'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'HornsAndHooves-sidekiq-limit_fetch'
6
+ gem.version = '4.5.0'
7
+ gem.license = 'MIT'
8
+ gem.authors = ['HornsAndHooves', 'Peter Maneykowski']
9
+ gem.email = ['maneyko@integracredit.com']
10
+ gem.summary = 'Sidekiq strategy to support queue limits'
11
+ gem.homepage = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch'
12
+ gem.description = 'Sidekiq strategy to restrict number of workers which are able to run specified ' \
13
+ 'queues simultaneously.'
14
+
15
+ gem.metadata['homepage_uri'] = gem.homepage
16
+ gem.metadata['source_code_uri'] = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch'
17
+ gem.metadata['changelog_uri'] = 'https://github.com/HornsAndHooves/sidekiq-limit_fetch/blob/master/CHANGELOG.md'
18
+
19
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
20
+ gem.require_paths = %w[lib]
21
+
22
+ gem.required_ruby_version = '>= 2.7.0'
23
+
24
+ gem.add_dependency 'sidekiq', '>= 8'
25
+ gem.add_development_dependency 'rake'
26
+ gem.add_development_dependency 'redis-namespace', '~> 1.5', '>= 1.5.2'
27
+ gem.add_development_dependency 'rspec'
28
+ gem.add_development_dependency 'rubocop'
29
+ gem.add_development_dependency 'simplecov'
30
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Sidekiq::Manager do
4
+ let(:capsule_or_options) do
5
+ Sidekiq.default_configuration.default_capsule
6
+ end
7
+
8
+ it 'can be instantiated' do
9
+ expect(described_class).to be < Sidekiq::Manager::InitLimitFetch
10
+ manager = described_class.new(capsule_or_options)
11
+ expect(manager).to respond_to(:start)
12
+ end
13
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Sidekiq::Queue do
4
+ context 'singleton' do
5
+ shared_examples :constructor do
6
+ it 'with default name' do
7
+ new_object = -> { described_class.send constructor }
8
+ expect(new_object.call).to eq new_object.call
9
+ end
10
+
11
+ it 'with given name' do
12
+ new_object = ->(name) { described_class.send constructor, name }
13
+ expect(new_object.call('name')).to eq new_object.call('name')
14
+ end
15
+ end
16
+
17
+ context '.new' do
18
+ let(:constructor) { :new }
19
+ it_behaves_like :constructor
20
+ end
21
+
22
+ context '.[]' do
23
+ let(:constructor) { :[] }
24
+ it_behaves_like :constructor
25
+ end
26
+
27
+ context '#lock' do
28
+ let(:name) { 'example' }
29
+ let(:queue) { Sidekiq::Queue[name] }
30
+
31
+ it 'should be available' do
32
+ expect(queue.acquire).to be
33
+ end
34
+
35
+ it 'should be pausable' do
36
+ queue.pause
37
+ expect(queue.acquire).not_to be
38
+ end
39
+
40
+ it 'should be continuable' do
41
+ queue.pause
42
+ queue.unpause
43
+ expect(queue.acquire).to be
44
+ end
45
+
46
+ it 'should be limitable' do
47
+ queue.limit = 1
48
+ expect(queue.acquire).to be
49
+ expect(queue.acquire).not_to be
50
+ end
51
+
52
+ it 'should be resizable' do
53
+ queue.limit = 0
54
+ expect(queue.acquire).not_to be
55
+ queue.limit = nil
56
+ expect(queue.acquire).to be
57
+ end
58
+
59
+ it 'should be countable' do
60
+ queue.limit = 3
61
+ 5.times { queue.acquire }
62
+ expect(queue.probed).to eq 3
63
+ end
64
+
65
+ it 'should be releasable' do
66
+ queue.acquire
67
+ expect(queue.probed).to eq 1
68
+ queue.release
69
+ expect(queue.probed).to eq 0
70
+ end
71
+
72
+ it 'should tell if paused' do
73
+ expect(queue).not_to be_paused
74
+ queue.pause
75
+ expect(queue).to be_paused
76
+ queue.unpause
77
+ expect(queue).not_to be_paused
78
+ end
79
+
80
+ it 'should tell if blocking' do
81
+ expect(queue).not_to be_blocking
82
+ queue.block
83
+ expect(queue).to be_blocking
84
+ queue.unblock
85
+ expect(queue).not_to be_blocking
86
+ end
87
+
88
+ it 'should be marked as changed' do
89
+ queue = Sidekiq::Queue["uniq_#{name}"]
90
+ expect(queue).not_to be_limit_changed
91
+ queue.limit = 3
92
+ expect(queue).to be_limit_changed
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Sidekiq::LimitFetch::Global::Monitor do
4
+ let(:monitor) { described_class.start! ttl, timeout }
5
+ let(:ttl) { 1 }
6
+ let(:queue) { Sidekiq::Queue[name] }
7
+ let(:name) { 'default' }
8
+ let(:timeout) { 0.5 }
9
+
10
+ after { monitor.kill }
11
+
12
+ context 'old locks' do
13
+ before { monitor }
14
+
15
+ it 'should remove invalidated old locks' do
16
+ 2.times { queue.acquire }
17
+ sleep ttl * 2
18
+ expect(queue.probed).to eq 2
19
+
20
+ allow(described_class).to receive(:update_heartbeat)
21
+ sleep ttl * 2
22
+ expect(queue.probed).to eq 0
23
+ end
24
+
25
+ it 'should remove invalid locks' do
26
+ 2.times { queue.acquire }
27
+ allow(described_class).to receive(:update_heartbeat)
28
+ Sidekiq.redis do |it|
29
+ it.del Sidekiq::LimitFetch::Global::Monitor::PROCESS_SET
30
+ end
31
+ sleep ttl * 2
32
+ expect(queue.probed).to eq 0
33
+ end
34
+ end
35
+
36
+ context 'dynamic queue' do
37
+ let(:limits) do
38
+ {
39
+ 'queue1' => 3,
40
+ 'queue2' => 3
41
+ }
42
+ end
43
+ let(:queues) { %w[queue1 queue2] }
44
+ let(:queue) { Sidekiq::LimitFetch::Queues }
45
+
46
+ let(:config) { Sidekiq::Config.new(options) }
47
+ let(:capsule) do
48
+ config.capsule('default') do |cap|
49
+ cap.concurrency = 1
50
+ cap.queues = config[:queues]
51
+ end
52
+ end
53
+
54
+ let(:capsule_or_options) do
55
+ capsule
56
+ end
57
+
58
+ context 'without excluded queue' do
59
+ let(:options) do
60
+ {
61
+ limits: limits,
62
+ queues: queues,
63
+ dynamic: true
64
+ }
65
+ end
66
+
67
+ it 'should add dynamic queue' do
68
+ queue.start(capsule_or_options)
69
+ monitor
70
+
71
+ expect(queue.instance_variable_get(:@queues)).not_to include('queue3')
72
+
73
+ Sidekiq.redis do |it|
74
+ it.sadd 'queues', 'queue3'
75
+ end
76
+
77
+ sleep ttl * 2
78
+ expect(queue.instance_variable_get(:@queues)).to include('queue3')
79
+
80
+ Sidekiq.redis do |it|
81
+ it.srem 'queues', 'queue3'
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'with excluded queue' do
87
+ let(:options) do
88
+ {
89
+ limits: limits,
90
+ queues: queues,
91
+ dynamic: { exclude: ['queue4'] }
92
+ }
93
+ end
94
+
95
+ it 'should exclude excluded dynamic queue' do
96
+ queue.start(capsule_or_options)
97
+ monitor
98
+
99
+ expect(queue.instance_variable_get(:@queues)).not_to include('queue4')
100
+
101
+ Sidekiq.redis do |it|
102
+ it.sadd 'queues', 'queue4'
103
+ end
104
+
105
+ sleep ttl * 2
106
+ expect(queue.instance_variable_get(:@queues)).not_to include('queue4')
107
+
108
+ Sidekiq.redis do |it|
109
+ it.srem 'queues', 'queue4'
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Sidekiq::LimitFetch::Queues do
4
+ let(:queues) { %w[queue1 queue2] }
5
+ let(:limits) { { 'queue1' => 3 } }
6
+ let(:strict) { true }
7
+ let(:blocking) { nil }
8
+ let(:process_limits) { { 'queue2' => 3 } }
9
+
10
+ let(:options) do
11
+ { queues: queues,
12
+ limits: limits,
13
+ strict: strict,
14
+ blocking: blocking,
15
+ process_limits: process_limits }
16
+ end
17
+
18
+ let(:config) { Sidekiq::Config.new(options) }
19
+ let(:capsule) do
20
+ config.capsule('default') do |cap|
21
+ cap.concurrency = 1
22
+ cap.queues = config[:queues]
23
+ end
24
+ end
25
+
26
+ let(:capsule_or_options) do
27
+ capsule
28
+ end
29
+
30
+ before do
31
+ subject.start(capsule_or_options)
32
+ end
33
+
34
+ def in_thread(&block)
35
+ thr = Thread.new(&block)
36
+ thr.join
37
+ end
38
+
39
+ it 'should acquire queues' do
40
+ in_thread { subject.acquire }
41
+ expect(Sidekiq::Queue['queue1'].probed).to eq 1
42
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
43
+ end
44
+
45
+ it 'should acquire dynamically blocking queues' do
46
+ in_thread { subject.acquire }
47
+ expect(Sidekiq::Queue['queue1'].probed).to eq 1
48
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
49
+
50
+ Sidekiq::Queue['queue1'].block
51
+
52
+ in_thread { subject.acquire }
53
+ expect(Sidekiq::Queue['queue1'].probed).to eq 2
54
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
55
+ end
56
+
57
+ it 'should block except given queues' do
58
+ Sidekiq::Queue['queue1'].block_except 'queue2'
59
+ in_thread { subject.acquire }
60
+ expect(Sidekiq::Queue['queue1'].probed).to eq 1
61
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
62
+
63
+ Sidekiq::Queue['queue1'].block_except 'queue404'
64
+ in_thread { subject.acquire }
65
+ expect(Sidekiq::Queue['queue1'].probed).to eq 2
66
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
67
+ end
68
+
69
+ it 'should release queues' do
70
+ in_thread do
71
+ subject.acquire
72
+ subject.release_except nil
73
+ end
74
+ expect(Sidekiq::Queue['queue1'].probed).to eq 0
75
+ expect(Sidekiq::Queue['queue2'].probed).to eq 0
76
+ end
77
+
78
+ it 'should release queues except selected' do
79
+ in_thread do
80
+ subject.acquire
81
+ subject.release_except 'queue:queue1'
82
+ end
83
+ expect(Sidekiq::Queue['queue1'].probed).to eq 1
84
+ expect(Sidekiq::Queue['queue2'].probed).to eq 0
85
+ end
86
+
87
+ it 'should release when no queues was acquired' do
88
+ queues.each { |name| Sidekiq::Queue[name].pause }
89
+ in_thread do
90
+ subject.acquire
91
+ expect { subject.release_except nil }.not_to raise_exception
92
+ end
93
+ end
94
+
95
+ context 'blocking' do
96
+ let(:blocking) { %w[queue1] }
97
+
98
+ it 'should acquire blocking queues' do
99
+ 3.times { in_thread { subject.acquire } }
100
+ expect(Sidekiq::Queue['queue1'].probed).to eq 3
101
+ expect(Sidekiq::Queue['queue2'].probed).to eq 1
102
+ end
103
+ end
104
+
105
+ it 'should set limits' do
106
+ subject
107
+ expect(Sidekiq::Queue['queue1'].limit).to eq 3
108
+ expect(Sidekiq::Queue['queue2'].limit).not_to be
109
+ end
110
+
111
+ it 'should set process_limits' do
112
+ subject
113
+ expect(Sidekiq::Queue['queue2'].process_limit).to eq 3
114
+ end
115
+
116
+ context 'without strict flag' do
117
+ let(:strict) { false }
118
+
119
+ it 'should retrieve weighted queues' do
120
+ expect(subject.ordered_queues).to match_array(%w[queue1 queue2])
121
+ end
122
+ end
123
+
124
+ it 'with strict flag should retrieve strictly ordered queues' do
125
+ expect(subject.ordered_queues).to eq %w[queue1 queue2]
126
+ end
127
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'semaphore' do
4
+ let(:name) { 'default' }
5
+ subject { Sidekiq::LimitFetch::Global::Semaphore.new name }
6
+
7
+ it 'should have no limit by default' do
8
+ expect(subject.limit).not_to be
9
+ end
10
+
11
+ it 'should set limit' do
12
+ subject.limit = 4
13
+ expect(subject.limit).to eq 4
14
+ end
15
+
16
+ it 'should acquire and count active tasks' do
17
+ 3.times { subject.acquire }
18
+ expect(subject.probed).to eq 3
19
+ end
20
+
21
+ it 'should acquire tasks with regard to limit' do
22
+ subject.limit = 4
23
+ 6.times { subject.acquire }
24
+ expect(subject.probed).to eq 4
25
+ end
26
+
27
+ it 'should acquire tasks with regard to process limit' do
28
+ subject.process_limit = 4
29
+ 6.times { subject.acquire }
30
+ expect(subject.probed).to eq 4
31
+ end
32
+
33
+ it 'should release active tasks' do
34
+ 6.times { subject.acquire }
35
+ 3.times { subject.release }
36
+ expect(subject.probed).to eq 3
37
+ end
38
+
39
+ it 'should pause tasks' do
40
+ 3.times { subject.acquire }
41
+ subject.pause
42
+ 2.times { subject.acquire }
43
+ expect(subject.probed).to eq 3
44
+ 2.times { subject.release }
45
+ expect(subject.probed).to eq 1
46
+ end
47
+
48
+ it 'should unpause tasks' do
49
+ subject.pause
50
+ 3.times { subject.acquire }
51
+ subject.unpause
52
+ 2.times { subject.acquire }
53
+ expect(subject.probed).to eq 2
54
+ end
55
+
56
+ it 'should pause tasks for a limited time' do
57
+ 3.times { subject.acquire }
58
+ subject.pause_for_ms 50
59
+ 2.times { subject.acquire }
60
+ expect(subject.probed).to eq 3
61
+ sleep(100.0 / 1000)
62
+ 2.times { subject.acquire }
63
+ expect(subject.probed).to eq 5
64
+ end
65
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ Thread.abort_on_exception = true
4
+
5
+ RSpec.describe Sidekiq::LimitFetch do
6
+ let(:options) { { queues: queues, limits: limits } }
7
+ let(:queues) { %w[queue1 queue1 queue2 queue2] }
8
+ let(:limits) { { 'queue1' => 1, 'queue2' => 2 } }
9
+ let(:config) { Sidekiq::Config.new(options) }
10
+ let(:capsule) do
11
+ config.capsule('default') do |cap|
12
+ cap.concurrency = 1
13
+ cap.queues = config[:queues]
14
+ end
15
+ end
16
+ let(:capsule_or_config) { capsule }
17
+
18
+ before do
19
+ subject::Queues.start(capsule_or_config)
20
+
21
+ Sidekiq.redis do |it|
22
+ it.del 'queue:queue1'
23
+ it.lpush 'queue:queue1', 'task1'
24
+ it.lpush 'queue:queue1', 'task2'
25
+ it.expire 'queue:queue1', 30
26
+ end
27
+ end
28
+
29
+ it 'should acquire lock on queue for execution' do
30
+ work = subject.retrieve_work
31
+ expect(work.queue_name).to eq 'queue1'
32
+ expect(work.job).to eq 'task1'
33
+
34
+ expect(Sidekiq::Queue['queue1'].busy).to eq 1
35
+ expect(Sidekiq::Queue['queue2'].busy).to eq 0
36
+
37
+ expect(subject.retrieve_work).not_to be
38
+ work.requeue
39
+
40
+ expect(Sidekiq::Queue['queue1'].busy).to eq 0
41
+ expect(Sidekiq::Queue['queue2'].busy).to eq 0
42
+
43
+ work = subject.retrieve_work
44
+ expect(work.job).to eq 'task1'
45
+
46
+ expect(Sidekiq::Queue['queue1'].busy).to eq 1
47
+ expect(Sidekiq::Queue['queue2'].busy).to eq 0
48
+
49
+ expect(subject.retrieve_work).not_to be
50
+ work.acknowledge
51
+
52
+ expect(Sidekiq::Queue['queue1'].busy).to eq 0
53
+ expect(Sidekiq::Queue['queue2'].busy).to eq 0
54
+
55
+ work = subject.retrieve_work
56
+ expect(work.job).to eq 'task2'
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'sidekiq/limit_fetch'
7
+
8
+ Sidekiq.configure_embed do |config|
9
+ config.logger = nil
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ config.order = :random
14
+ config.disable_monkey_patching!
15
+ config.raise_errors_for_deprecations!
16
+ config.before do
17
+ Sidekiq::Queue.reset_instances!
18
+ Sidekiq.redis do |it|
19
+ clean_redis = lambda do |queue|
20
+ it.pipelined do |pipeline|
21
+ pipeline.del "limit_fetch:limit:#{queue}"
22
+ pipeline.del "limit_fetch:process_limit:#{queue}"
23
+ pipeline.del "limit_fetch:busy:#{queue}"
24
+ pipeline.del "limit_fetch:probed:#{queue}"
25
+ pipeline.del "limit_fetch:pause:#{queue}"
26
+ pipeline.del "limit_fetch:block:#{queue}"
27
+ end
28
+ end
29
+
30
+ clean_redis.call(name) if defined?(name)
31
+ queues.each(&clean_redis) if defined?(queues) && queues.is_a?(Array)
32
+ end
33
+ end
34
+ end