shoryuken 3.0.4 → 3.1.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 +4 -4
- data/.rubocop.yml +1 -1
- data/.travis.yml +1 -0
- data/CHANGELOG.md +79 -0
- data/Gemfile +1 -0
- data/README.md +15 -117
- data/bin/cli/base.rb +0 -2
- data/bin/cli/sqs.rb +18 -1
- data/bin/shoryuken +9 -1
- data/examples/default_worker.rb +1 -1
- data/lib/shoryuken/default_worker_registry.rb +2 -2
- data/lib/shoryuken/environment_loader.rb +33 -13
- data/lib/shoryuken/fetcher.rb +17 -16
- data/lib/shoryuken/launcher.rb +86 -7
- data/lib/shoryuken/manager.rb +42 -72
- data/lib/shoryuken/middleware/server/auto_delete.rb +3 -8
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +4 -4
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +8 -2
- data/lib/shoryuken/middleware/server/timing.rb +2 -2
- data/lib/shoryuken/options.rb +192 -0
- data/lib/shoryuken/polling/base.rb +67 -0
- data/lib/shoryuken/polling/strict_priority.rb +77 -0
- data/lib/shoryuken/polling/weighted_round_robin.rb +66 -0
- data/lib/shoryuken/processor.rb +23 -17
- data/lib/shoryuken/queue.rb +27 -6
- data/lib/shoryuken/runner.rb +3 -15
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker.rb +8 -0
- data/lib/shoryuken.rb +39 -172
- data/shoryuken.gemspec +1 -1
- data/spec/integration/launcher_spec.rb +12 -6
- data/spec/shoryuken/environment_loader_spec.rb +3 -12
- data/spec/shoryuken/fetcher_spec.rb +30 -15
- data/spec/shoryuken/manager_spec.rb +12 -13
- data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
- data/spec/shoryuken/options_spec.rb +100 -0
- data/spec/shoryuken/{polling_spec.rb → polling/strict_priority_spec.rb} +1 -100
- data/spec/shoryuken/polling/weighted_round_robin_spec.rb +99 -0
- data/spec/shoryuken/processor_spec.rb +20 -37
- data/spec/shoryuken/queue_spec.rb +72 -26
- data/spec/shoryuken/runner_spec.rb +3 -4
- data/spec/shoryuken_spec.rb +0 -59
- data/spec/spec_helper.rb +8 -2
- data/test_workers/endless_uninterruptive_worker.rb +1 -1
- metadata +14 -7
- data/lib/shoryuken/polling.rb +0 -204
data/lib/shoryuken/manager.rb
CHANGED
|
@@ -3,97 +3,85 @@ module Shoryuken
|
|
|
3
3
|
include Util
|
|
4
4
|
|
|
5
5
|
BATCH_LIMIT = 10
|
|
6
|
+
# See https://github.com/phstc/shoryuken/issues/348#issuecomment-292847028
|
|
7
|
+
MIN_DISPATCH_INTERVAL = 0.1
|
|
6
8
|
|
|
7
|
-
def initialize(fetcher, polling_strategy)
|
|
8
|
-
@
|
|
9
|
-
|
|
10
|
-
raise(ArgumentError, "Concurrency value #{@count} is invalid, it needs to be a positive number") unless @count > 0
|
|
11
|
-
|
|
12
|
-
@queues = Shoryuken.queues.dup.uniq
|
|
13
|
-
|
|
14
|
-
@done = Concurrent::AtomicBoolean.new(false)
|
|
15
|
-
|
|
16
|
-
@fetcher = fetcher
|
|
9
|
+
def initialize(fetcher, polling_strategy, concurrency)
|
|
10
|
+
@fetcher = fetcher
|
|
17
11
|
@polling_strategy = polling_strategy
|
|
18
|
-
|
|
19
|
-
@
|
|
20
|
-
@
|
|
12
|
+
@max_processors = concurrency
|
|
13
|
+
@busy_processors = Concurrent::AtomicFixnum.new(0)
|
|
14
|
+
@done = Concurrent::AtomicBoolean.new(false)
|
|
21
15
|
end
|
|
22
16
|
|
|
23
17
|
def start
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
dispatch_async
|
|
18
|
+
dispatch
|
|
27
19
|
end
|
|
28
20
|
|
|
29
|
-
def stop
|
|
21
|
+
def stop
|
|
30
22
|
@done.make_true
|
|
23
|
+
end
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
logger.info { 'Calling Shoryuken.on_stop block' }
|
|
34
|
-
callback.call
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
fire_event(:shutdown, true)
|
|
38
|
-
|
|
39
|
-
logger.info { 'Shutting down workers' }
|
|
40
|
-
|
|
41
|
-
@dispatcher_executor.kill
|
|
25
|
+
private
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
else
|
|
46
|
-
soft_shutdown
|
|
47
|
-
end
|
|
27
|
+
def stopped?
|
|
28
|
+
@done.true? || !Concurrent.global_io_executor.running?
|
|
48
29
|
end
|
|
49
30
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
end
|
|
31
|
+
def dispatch
|
|
32
|
+
return if stopped?
|
|
53
33
|
|
|
54
|
-
|
|
34
|
+
if !ready.positive? || (queue = @polling_strategy.next_queue).nil?
|
|
35
|
+
return dispatch_later
|
|
36
|
+
end
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
@dispatcher_executor.post(&method(:dispatch_now))
|
|
58
|
-
end
|
|
38
|
+
fire_event(:dispatch)
|
|
59
39
|
|
|
60
|
-
|
|
61
|
-
return if @done.true?
|
|
40
|
+
logger.info { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }
|
|
62
41
|
|
|
63
|
-
|
|
64
|
-
return if ready.zero?
|
|
65
|
-
return unless (queue = @polling_strategy.next_queue)
|
|
42
|
+
batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
|
|
66
43
|
|
|
67
|
-
|
|
44
|
+
dispatch
|
|
45
|
+
end
|
|
68
46
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
47
|
+
def dispatch_later
|
|
48
|
+
sleep(MIN_DISPATCH_INTERVAL)
|
|
49
|
+
dispatch
|
|
73
50
|
end
|
|
74
51
|
|
|
75
52
|
def busy
|
|
76
|
-
@
|
|
53
|
+
@busy_processors.value
|
|
77
54
|
end
|
|
78
55
|
|
|
79
56
|
def ready
|
|
80
|
-
@
|
|
57
|
+
@max_processors - busy
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def processor_done
|
|
61
|
+
@busy_processors.decrement
|
|
81
62
|
end
|
|
82
63
|
|
|
83
|
-
def assign(
|
|
64
|
+
def assign(queue_name, sqs_msg)
|
|
65
|
+
return if stopped?
|
|
66
|
+
|
|
84
67
|
logger.debug { "Assigning #{sqs_msg.message_id}" }
|
|
85
68
|
|
|
86
|
-
@
|
|
69
|
+
@busy_processors.increment
|
|
70
|
+
|
|
71
|
+
Concurrent::Promise.execute {
|
|
72
|
+
Processor.new(queue_name, sqs_msg).process
|
|
73
|
+
}.then { processor_done }.rescue { processor_done }
|
|
87
74
|
end
|
|
88
75
|
|
|
89
76
|
def dispatch_batch(queue)
|
|
90
|
-
batch = @fetcher.fetch(queue, BATCH_LIMIT)
|
|
77
|
+
return if (batch = @fetcher.fetch(queue, BATCH_LIMIT)).none?
|
|
91
78
|
@polling_strategy.messages_found(queue.name, batch.size)
|
|
92
79
|
assign(queue.name, patch_batch!(batch))
|
|
93
80
|
end
|
|
94
81
|
|
|
95
82
|
def dispatch_single_messages(queue)
|
|
96
83
|
messages = @fetcher.fetch(queue, ready)
|
|
84
|
+
|
|
97
85
|
@polling_strategy.messages_found(queue.name, messages.size)
|
|
98
86
|
messages.each { |message| assign(queue.name, message) }
|
|
99
87
|
end
|
|
@@ -102,24 +90,6 @@ module Shoryuken
|
|
|
102
90
|
Shoryuken.worker_registry.batch_receive_messages?(queue.name)
|
|
103
91
|
end
|
|
104
92
|
|
|
105
|
-
def soft_shutdown
|
|
106
|
-
@pool.shutdown
|
|
107
|
-
@pool.wait_for_termination
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def hard_shutdown_in(delay)
|
|
111
|
-
if busy > 0
|
|
112
|
-
logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
@pool.shutdown
|
|
116
|
-
|
|
117
|
-
return if @pool.wait_for_termination(delay)
|
|
118
|
-
|
|
119
|
-
logger.info { "Hard shutting down #{busy} busy workers" }
|
|
120
|
-
@pool.kill
|
|
121
|
-
end
|
|
122
|
-
|
|
123
93
|
def patch_batch!(sqs_msgs)
|
|
124
94
|
sqs_msgs.instance_eval do
|
|
125
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
|
-
|
|
8
|
+
return unless worker.class.auto_delete?
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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 =>
|
|
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: #{
|
|
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
|
|
@@ -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
|