shoryuken 3.0.6 → 3.1.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.travis.yml +1 -0
  4. data/CHANGELOG.md +71 -0
  5. data/Gemfile +1 -0
  6. data/README.md +15 -117
  7. data/bin/cli/base.rb +0 -2
  8. data/bin/cli/sqs.rb +18 -1
  9. data/examples/default_worker.rb +1 -1
  10. data/lib/shoryuken/default_worker_registry.rb +2 -2
  11. data/lib/shoryuken/environment_loader.rb +33 -13
  12. data/lib/shoryuken/fetcher.rb +17 -16
  13. data/lib/shoryuken/launcher.rb +86 -7
  14. data/lib/shoryuken/manager.rb +39 -78
  15. data/lib/shoryuken/middleware/server/auto_delete.rb +3 -8
  16. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +4 -4
  17. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +8 -2
  18. data/lib/shoryuken/middleware/server/timing.rb +2 -2
  19. data/lib/shoryuken/options.rb +192 -0
  20. data/lib/shoryuken/polling/base.rb +67 -0
  21. data/lib/shoryuken/polling/strict_priority.rb +77 -0
  22. data/lib/shoryuken/polling/weighted_round_robin.rb +66 -0
  23. data/lib/shoryuken/processor.rb +21 -18
  24. data/lib/shoryuken/queue.rb +27 -6
  25. data/lib/shoryuken/runner.rb +3 -15
  26. data/lib/shoryuken/version.rb +1 -1
  27. data/lib/shoryuken/worker.rb +8 -0
  28. data/lib/shoryuken.rb +39 -172
  29. data/shoryuken.gemspec +1 -1
  30. data/spec/integration/launcher_spec.rb +12 -6
  31. data/spec/shoryuken/environment_loader_spec.rb +3 -12
  32. data/spec/shoryuken/fetcher_spec.rb +30 -15
  33. data/spec/shoryuken/manager_spec.rb +34 -19
  34. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
  35. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  36. data/spec/shoryuken/options_spec.rb +100 -0
  37. data/spec/shoryuken/{polling_spec.rb → polling/strict_priority_spec.rb} +1 -100
  38. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +99 -0
  39. data/spec/shoryuken/processor_spec.rb +20 -39
  40. data/spec/shoryuken/queue_spec.rb +72 -26
  41. data/spec/shoryuken/runner_spec.rb +3 -4
  42. data/spec/shoryuken_spec.rb +0 -59
  43. data/spec/spec_helper.rb +8 -2
  44. data/test_workers/endless_uninterruptive_worker.rb +1 -1
  45. metadata +14 -7
  46. data/lib/shoryuken/polling.rb +0 -204
@@ -6,103 +6,82 @@ module Shoryuken
6
6
  # See https://github.com/phstc/shoryuken/issues/348#issuecomment-292847028
7
7
  MIN_DISPATCH_INTERVAL = 0.1
8
8
 
9
- def initialize(fetcher, polling_strategy)
10
- @count = Shoryuken.options.fetch(:concurrency, 25)
11
-
12
- raise(ArgumentError, "Concurrency value #{@count} is invalid, it needs to be a positive number") unless @count > 0
13
-
14
- @queues = Shoryuken.queues.dup.uniq
15
-
16
- @done = Concurrent::AtomicBoolean.new(false)
17
-
18
- @fetcher = fetcher
9
+ def initialize(fetcher, polling_strategy, concurrency)
10
+ @fetcher = fetcher
19
11
  @polling_strategy = polling_strategy
20
-
21
- @pool = Concurrent::FixedThreadPool.new(@count, max_queue: @count)
22
- @dispatcher_executor = Concurrent::SingleThreadExecutor.new
12
+ @max_processors = concurrency
13
+ @busy_processors = Concurrent::AtomicFixnum.new(0)
14
+ @done = Concurrent::AtomicBoolean.new(false)
23
15
  end
24
16
 
25
17
  def start
26
- logger.info { 'Starting' }
27
-
28
- dispatch_async
18
+ dispatch
29
19
  end
30
20
 
31
- def stop(options = {})
21
+ def stop
32
22
  @done.make_true
23
+ end
33
24
 
34
- if (callback = Shoryuken.stop_callback)
35
- logger.info { 'Calling Shoryuken.on_stop block' }
36
- callback.call
37
- end
38
-
39
- fire_event(:shutdown, true)
25
+ private
40
26
 
41
- logger.info { 'Shutting down workers' }
27
+ def stopped?
28
+ @done.true?
29
+ end
42
30
 
43
- @dispatcher_executor.kill
31
+ def dispatch
32
+ return if stopped?
44
33
 
45
- if options[:shutdown]
46
- hard_shutdown_in(options[:timeout])
47
- else
48
- soft_shutdown
34
+ if ready <= 0 || (queue = @polling_strategy.next_queue).nil?
35
+ return dispatch_later
49
36
  end
50
- end
51
37
 
52
- def processor_failed(ex)
53
- logger.error ex
54
- logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
55
- end
38
+ fire_event(:dispatch)
56
39
 
57
- def processor_done(queue)
58
- logger.debug { "Process done for '#{queue}'" }
59
- end
40
+ logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }
60
41
 
61
- private
42
+ batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
62
43
 
63
- def dispatch_async
64
- @dispatcher_executor.post(&method(:dispatch_now))
44
+ dispatch
65
45
  end
66
46
 
67
- def dispatch_now
68
- return if @done.true?
69
-
70
- begin
71
- if ready.zero? || (queue = @polling_strategy.next_queue).nil?
72
- sleep MIN_DISPATCH_INTERVAL
73
- return
74
- end
75
-
76
- logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }
77
-
78
- batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
79
- ensure
80
- dispatch_async
81
- end
47
+ def dispatch_later
48
+ sleep(MIN_DISPATCH_INTERVAL)
49
+ dispatch
82
50
  end
83
51
 
84
52
  def busy
85
- @count - ready
53
+ @busy_processors.value
86
54
  end
87
55
 
88
56
  def ready
89
- @pool.remaining_capacity
57
+ @max_processors - busy
58
+ end
59
+
60
+ def processor_done
61
+ @busy_processors.decrement
90
62
  end
91
63
 
92
- def assign(queue, sqs_msg)
64
+ def assign(queue_name, sqs_msg)
65
+ return if stopped?
66
+
93
67
  logger.debug { "Assigning #{sqs_msg.message_id}" }
94
68
 
95
- @pool.post { Processor.new(self).process(queue, sqs_msg) }
69
+ @busy_processors.increment
70
+
71
+ Concurrent::Promise.execute {
72
+ Processor.new(queue_name, sqs_msg).process
73
+ }.then { processor_done }.rescue { processor_done }
96
74
  end
97
75
 
98
76
  def dispatch_batch(queue)
99
- batch = @fetcher.fetch(queue, BATCH_LIMIT)
77
+ return if (batch = @fetcher.fetch(queue, BATCH_LIMIT)).none?
100
78
  @polling_strategy.messages_found(queue.name, batch.size)
101
79
  assign(queue.name, patch_batch!(batch))
102
80
  end
103
81
 
104
82
  def dispatch_single_messages(queue)
105
83
  messages = @fetcher.fetch(queue, ready)
84
+
106
85
  @polling_strategy.messages_found(queue.name, messages.size)
107
86
  messages.each { |message| assign(queue.name, message) }
108
87
  end
@@ -111,24 +90,6 @@ module Shoryuken
111
90
  Shoryuken.worker_registry.batch_receive_messages?(queue.name)
112
91
  end
113
92
 
114
- def soft_shutdown
115
- @pool.shutdown
116
- @pool.wait_for_termination
117
- end
118
-
119
- def hard_shutdown_in(delay)
120
- if busy > 0
121
- logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
122
- end
123
-
124
- @pool.shutdown
125
-
126
- return if @pool.wait_for_termination(delay)
127
-
128
- logger.info { "Hard shutting down #{busy} busy workers" }
129
- @pool.kill
130
- end
131
-
132
93
  def patch_batch!(sqs_msgs)
133
94
  sqs_msgs.instance_eval do
134
95
  def message_id
@@ -5,18 +5,13 @@ module Shoryuken
5
5
  def call(worker, queue, sqs_msg, body)
6
6
  yield
7
7
 
8
- auto_delete = worker.class.get_shoryuken_options['delete'] || worker.class.get_shoryuken_options['auto_delete']
8
+ return unless worker.class.auto_delete?
9
9
 
10
- if auto_delete
11
- entries = [sqs_msg].flatten.map.with_index do |message, i|
12
- { id: i.to_s, receipt_handle: message.receipt_handle }
13
- end
10
+ entries = [sqs_msg].flatten.map.with_index { |message, i| { id: i.to_s, receipt_handle: message.receipt_handle } }
14
11
 
15
- Shoryuken::Client.queues(queue).delete_messages(entries: entries)
16
- end
12
+ Shoryuken::Client.queues(queue).delete_messages(entries: entries)
17
13
  end
18
14
  end
19
15
  end
20
16
  end
21
17
  end
22
-
@@ -7,6 +7,8 @@ module Shoryuken
7
7
  EXTEND_UPFRONT_SECONDS = 5
8
8
 
9
9
  def call(worker, queue, sqs_msg, body)
10
+ return yield unless worker.class.auto_visibility_timeout?
11
+
10
12
  if sqs_msg.is_a?(Array)
11
13
  logger.warn { "Auto extend visibility isn't supported for batch workers" }
12
14
  return yield
@@ -34,11 +36,11 @@ module Shoryuken
34
36
  end
35
37
 
36
38
  sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
37
- rescue => e
39
+ rescue => ex
38
40
  logger.error do
39
41
  'Could not auto extend the message ' \
40
42
  "#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id} " \
41
- "visibility timeout. Error: #{e.message}"
43
+ "visibility timeout. Error: #{ex.message}"
42
44
  end
43
45
  end
44
46
  end
@@ -46,8 +48,6 @@ module Shoryuken
46
48
  end
47
49
 
48
50
  def auto_visibility_timer(worker, queue, sqs_msg, body)
49
- return unless worker.class.auto_visibility_timeout?
50
-
51
51
  MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body).tap(&:execute)
52
52
  end
53
53
  end
@@ -5,6 +5,8 @@ module Shoryuken
5
5
  include Util
6
6
 
7
7
  def call(worker, queue, sqs_msg, body)
8
+ return yield unless worker.class.exponential_backoff?
9
+
8
10
  if sqs_msg.is_a?(Array)
9
11
  logger.warn { "Exponential backoff isn't supported for batch workers" }
10
12
  return yield
@@ -12,7 +14,7 @@ module Shoryuken
12
14
 
13
15
  started_at = Time.now
14
16
  yield
15
- rescue
17
+ rescue => ex
16
18
  retry_intervals = worker.class.get_shoryuken_options['retry_intervals']
17
19
 
18
20
  if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
@@ -20,6 +22,10 @@ module Shoryuken
20
22
  # This allows custom middleware (like exception notifiers) to be aware of the unhandled failure.
21
23
  raise
22
24
  end
25
+
26
+ logger.warn { "Message #{sqs_msg.message_id} will attempt retry due to error: #{ex.message}" }
27
+ # since we didn't raise, lets log the backtrace for debugging purposes.
28
+ logger.debug { ex.backtrace.join("\n") } unless ex.backtrace.nil?
23
29
  end
24
30
 
25
31
  private
@@ -47,7 +53,7 @@ module Shoryuken
47
53
 
48
54
  sqs_msg.change_visibility(visibility_timeout: next_visibility_timeout(interval.to_i, started_at))
49
55
 
50
- logger.info { "Message #{sqs_msg.message_id} failed, will be retried in #{interval} seconds." }
56
+ logger.info { "Message #{sqs_msg.message_id} failed, will be retried in #{interval} seconds" }
51
57
 
52
58
  true
53
59
  end
@@ -20,9 +20,9 @@ module Shoryuken
20
20
  end
21
21
 
22
22
  logger.info { "completed in: #{total_time} ms" }
23
- rescue => e
23
+ rescue
24
24
  logger.info { "failed in: #{elapsed(started_at)} ms" }
25
- raise e
25
+ raise
26
26
  end
27
27
  end
28
28
  end
@@ -0,0 +1,192 @@
1
+ module Shoryuken
2
+ class Options
3
+ DEFAULTS = {
4
+ concurrency: 25,
5
+ queues: [],
6
+ aws: {},
7
+ delay: 0,
8
+ timeout: 8,
9
+ lifecycle_events: {
10
+ startup: [],
11
+ dispatch: [],
12
+ quiet: [],
13
+ shutdown: []
14
+ }
15
+ }.freeze
16
+
17
+ @@groups = {}
18
+ @@worker_registry = DefaultWorkerRegistry.new
19
+ @@active_job_queue_name_prefixing = false
20
+ @@sqs_client = nil
21
+ @@sqs_client_receive_message_opts = {}
22
+ @@start_callback = nil
23
+ @@stop_callback = nil
24
+
25
+ class << self
26
+ def add_group(group, concurrency)
27
+ groups[group] ||= {
28
+ concurrency: concurrency,
29
+ queues: []
30
+ }
31
+ end
32
+
33
+ def groups
34
+ @@groups
35
+ end
36
+
37
+ def add_queue(queue, weight, group)
38
+ weight.times do
39
+ groups[group][:queues] << queue
40
+ end
41
+ end
42
+
43
+ def ungrouped_queues
44
+ groups.values.flat_map { |options| options[:queues] }
45
+ end
46
+
47
+ def worker_registry
48
+ @@worker_registry
49
+ end
50
+
51
+ def worker_registry=(worker_registry)
52
+ @@worker_registry = worker_registry
53
+ end
54
+
55
+ def polling_strategy(group)
56
+ options[group].to_h.fetch(:polling_strategy, Polling::WeightedRoundRobin)
57
+ end
58
+
59
+ def start_callback
60
+ @@start_callback
61
+ end
62
+
63
+ def start_callback=(start_callback)
64
+ @@start_callback = start_callback
65
+ end
66
+
67
+ def stop_callback
68
+ @@stop_callback
69
+ end
70
+
71
+ def stop_callback=(stop_callback)
72
+ @@stop_callback = stop_callback
73
+ end
74
+
75
+ def active_job_queue_name_prefixing
76
+ @@active_job_queue_name_prefixing
77
+ end
78
+
79
+ def active_job_queue_name_prefixing=(active_job_queue_name_prefixing)
80
+ @@active_job_queue_name_prefixing = active_job_queue_name_prefixing
81
+ end
82
+
83
+ def sqs_client
84
+ @@sqs_client ||= Aws::SQS::Client.new
85
+ end
86
+
87
+ def sqs_client=(sqs_client)
88
+ @@sqs_client = sqs_client
89
+ end
90
+
91
+ def sqs_client_receive_message_opts
92
+ @@sqs_client_receive_message_opts
93
+ end
94
+
95
+ def sqs_client_receive_message_opts=(sqs_client_receive_message_opts)
96
+ @@sqs_client_receive_message_opts['default'] = sqs_client_receive_message_opts
97
+ end
98
+
99
+ def options
100
+ @@options ||= DEFAULTS.dup
101
+ end
102
+
103
+ def logger
104
+ Shoryuken::Logging.logger
105
+ end
106
+
107
+ def register_worker(*args)
108
+ @@worker_registry.register_worker(*args)
109
+ end
110
+
111
+ def configure_server
112
+ yield self if server?
113
+ end
114
+
115
+ def server_middleware
116
+ @@server_chain ||= default_server_middleware
117
+ yield @@server_chain if block_given?
118
+ @@server_chain
119
+ end
120
+
121
+ def configure_client
122
+ yield self unless server?
123
+ end
124
+
125
+ def client_middleware
126
+ @@client_chain ||= default_client_middleware
127
+ yield @@client_chain if block_given?
128
+ @@client_chain
129
+ end
130
+
131
+ def default_worker_options
132
+ @@default_worker_options ||= {
133
+ 'queue' => 'default',
134
+ 'delete' => false,
135
+ 'auto_delete' => false,
136
+ 'auto_visibility_timeout' => false,
137
+ 'retry_intervals' => nil,
138
+ 'batch' => false
139
+ }
140
+ end
141
+
142
+ def default_worker_options=(default_worker_options)
143
+ @@default_worker_options = default_worker_options
144
+ end
145
+
146
+ def on_start(&block)
147
+ @@start_callback = block
148
+ end
149
+
150
+ def on_stop(&block)
151
+ @@stop_callback = block
152
+ end
153
+
154
+ # Register a block to run at a point in the Shoryuken lifecycle.
155
+ # :startup, :quiet or :shutdown are valid events.
156
+ #
157
+ # Shoryuken.configure_server do |config|
158
+ # config.on(:shutdown) do
159
+ # puts "Goodbye cruel world!"
160
+ # end
161
+ # end
162
+ def on(event, &block)
163
+ fail ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
164
+ fail ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
165
+ options[:lifecycle_events][event] << block
166
+ end
167
+
168
+ private
169
+
170
+ def default_server_middleware
171
+ Middleware::Chain.new do |m|
172
+ m.add Middleware::Server::Timing
173
+ m.add Middleware::Server::ExponentialBackoffRetry
174
+ m.add Middleware::Server::AutoDelete
175
+ m.add Middleware::Server::AutoExtendVisibility
176
+ if defined?(::ActiveRecord::Base)
177
+ require 'shoryuken/middleware/server/active_record'
178
+ m.add Middleware::Server::ActiveRecord
179
+ end
180
+ end
181
+ end
182
+
183
+ def default_client_middleware
184
+ Middleware::Chain.new
185
+ end
186
+
187
+ def server?
188
+ defined?(Shoryuken::CLI)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,67 @@
1
+ module Shoryuken
2
+ module Polling
3
+ QueueConfiguration = Struct.new(:name, :options) do
4
+ def hash
5
+ name.hash
6
+ end
7
+
8
+ def ==(other)
9
+ case other
10
+ when String
11
+ if options.empty?
12
+ name == other
13
+ else
14
+ false
15
+ end
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ alias_method :eql?, :==
22
+
23
+ def to_s
24
+ if options.empty?
25
+ name
26
+ else
27
+ "#<QueueConfiguration #{name} options=#{options.inspect}>"
28
+ end
29
+ end
30
+ end
31
+
32
+ class BaseStrategy
33
+ include Util
34
+
35
+ def next_queue
36
+ fail NotImplementedError
37
+ end
38
+
39
+ def messages_found(queue, messages_found)
40
+ fail NotImplementedError
41
+ end
42
+
43
+ def active_queues
44
+ fail NotImplementedError
45
+ end
46
+
47
+ def ==(other)
48
+ case other
49
+ when Array
50
+ @queues == other
51
+ else
52
+ if other.respond_to?(:active_queues)
53
+ active_queues == other.active_queues
54
+ else
55
+ false
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def delay
63
+ Shoryuken.options[:delay].to_f
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,77 @@
1
+ module Shoryuken
2
+ module Polling
3
+ class StrictPriority < BaseStrategy
4
+ def initialize(queues)
5
+ # Priority ordering of the queues, highest priority first
6
+ @queues = queues
7
+ .group_by { |q| q }
8
+ .sort_by { |_, qs| -qs.count }
9
+ .map(&:first)
10
+
11
+ # Pause status of the queues, default to past time (unpaused)
12
+ @paused_until = queues
13
+ .each_with_object(Hash.new) { |queue, h| h[queue] = Time.at(0) }
14
+
15
+ # Start queues at 0
16
+ reset_next_queue
17
+ end
18
+
19
+ def next_queue
20
+ next_queue = next_active_queue
21
+ next_queue.nil? ? nil : QueueConfiguration.new(next_queue, {})
22
+ end
23
+
24
+ def messages_found(queue, messages_found)
25
+ if messages_found == 0
26
+ pause(queue)
27
+ else
28
+ reset_next_queue
29
+ end
30
+ end
31
+
32
+ def active_queues
33
+ @queues
34
+ .reverse
35
+ .map.with_index(1)
36
+ .reject { |q, _| queue_paused?(q) }
37
+ .reverse
38
+ end
39
+
40
+ private
41
+
42
+ def next_active_queue
43
+ reset_next_queue if queues_unpaused_since?
44
+
45
+ size = @queues.length
46
+ size.times do
47
+ queue = @queues[@next_queue_index]
48
+ @next_queue_index = (@next_queue_index + 1) % size
49
+ return queue unless queue_paused?(queue)
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def queues_unpaused_since?
56
+ last = @last_unpause_check
57
+ now = @last_unpause_check = Time.now
58
+
59
+ last && @paused_until.values.any? { |t| t > last && t <= now }
60
+ end
61
+
62
+ def reset_next_queue
63
+ @next_queue_index = 0
64
+ end
65
+
66
+ def queue_paused?(queue)
67
+ @paused_until[queue] > Time.now
68
+ end
69
+
70
+ def pause(queue)
71
+ return unless delay > 0
72
+ @paused_until[queue] = Time.now + delay
73
+ logger.debug "Paused #{queue}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,66 @@
1
+ module Shoryuken
2
+ module Polling
3
+ class WeightedRoundRobin < BaseStrategy
4
+ def initialize(queues)
5
+ @initial_queues = queues
6
+ @queues = queues.dup.uniq
7
+ @paused_queues = []
8
+ end
9
+
10
+ def next_queue
11
+ unpause_queues
12
+ queue = @queues.shift
13
+ return nil if queue.nil?
14
+
15
+ @queues << queue
16
+ QueueConfiguration.new(queue, {})
17
+ end
18
+
19
+ def messages_found(queue, messages_found)
20
+ if messages_found == 0
21
+ pause(queue)
22
+ return
23
+ end
24
+
25
+ maximum_weight = maximum_queue_weight(queue)
26
+ current_weight = current_queue_weight(queue)
27
+ if maximum_weight > current_weight
28
+ logger.info { "Increasing #{queue} weight to #{current_weight + 1}, max: #{maximum_weight}" }
29
+ @queues << queue
30
+ end
31
+ end
32
+
33
+ def active_queues
34
+ unparse_queues(@queues)
35
+ end
36
+
37
+ private
38
+
39
+ def pause(queue)
40
+ return unless @queues.delete(queue)
41
+ @paused_queues << [Time.now + delay, queue]
42
+ logger.debug "Paused #{queue}"
43
+ end
44
+
45
+ def unpause_queues
46
+ return if @paused_queues.empty?
47
+ return if Time.now < @paused_queues.first[0]
48
+ pause = @paused_queues.shift
49
+ @queues << pause[1]
50
+ logger.debug "Unpaused #{pause[1]}"
51
+ end
52
+
53
+ def current_queue_weight(queue)
54
+ queue_weight(@queues, queue)
55
+ end
56
+
57
+ def maximum_queue_weight(queue)
58
+ queue_weight(@initial_queues, queue)
59
+ end
60
+
61
+ def queue_weight(queues, queue)
62
+ queues.count { |q| q == queue }
63
+ end
64
+ end
65
+ end
66
+ end