shoryuken 2.0.11 → 3.0.6

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +20 -0
  3. data/.rubocop.yml +8 -2
  4. data/.travis.yml +7 -5
  5. data/CHANGELOG.md +127 -10
  6. data/Gemfile +1 -0
  7. data/README.md +20 -57
  8. data/Rakefile +0 -1
  9. data/bin/cli/base.rb +42 -0
  10. data/bin/cli/sqs.rb +187 -0
  11. data/bin/shoryuken +55 -9
  12. data/examples/default_worker.rb +1 -12
  13. data/lib/shoryuken/client.rb +3 -25
  14. data/lib/shoryuken/default_worker_registry.rb +9 -5
  15. data/lib/shoryuken/environment_loader.rb +29 -67
  16. data/lib/shoryuken/fetcher.rb +22 -53
  17. data/lib/shoryuken/launcher.rb +5 -29
  18. data/lib/shoryuken/manager.rb +77 -180
  19. data/lib/shoryuken/message.rb +4 -13
  20. data/lib/shoryuken/middleware/chain.rb +1 -18
  21. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +21 -18
  22. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +26 -19
  23. data/lib/shoryuken/polling.rb +204 -0
  24. data/lib/shoryuken/processor.rb +9 -14
  25. data/lib/shoryuken/queue.rb +36 -38
  26. data/lib/shoryuken/runner.rb +143 -0
  27. data/lib/shoryuken/util.rb +3 -9
  28. data/lib/shoryuken/version.rb +1 -1
  29. data/lib/shoryuken/worker.rb +1 -1
  30. data/lib/shoryuken.rb +78 -39
  31. data/shoryuken.gemspec +6 -6
  32. data/spec/integration/launcher_spec.rb +4 -3
  33. data/spec/shoryuken/client_spec.rb +2 -43
  34. data/spec/shoryuken/default_worker_registry_spec.rb +12 -10
  35. data/spec/shoryuken/environment_loader_spec.rb +34 -0
  36. data/spec/shoryuken/fetcher_spec.rb +18 -52
  37. data/spec/shoryuken/manager_spec.rb +56 -97
  38. data/spec/shoryuken/middleware/chain_spec.rb +0 -24
  39. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +2 -2
  40. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +7 -3
  41. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +56 -33
  42. data/spec/shoryuken/polling_spec.rb +239 -0
  43. data/spec/shoryuken/processor_spec.rb +7 -5
  44. data/spec/shoryuken/queue_spec.rb +110 -63
  45. data/spec/shoryuken/{cli_spec.rb → runner_spec.rb} +10 -24
  46. data/spec/shoryuken_spec.rb +13 -1
  47. data/spec/spec_helper.rb +8 -20
  48. data/test_workers/endless_interruptive_worker.rb +41 -0
  49. data/test_workers/endless_uninterruptive_worker.rb +44 -0
  50. metadata +34 -35
  51. data/.hound.yml +0 -6
  52. data/lib/shoryuken/cli.rb +0 -210
  53. data/lib/shoryuken/sns_arn.rb +0 -27
  54. data/lib/shoryuken/topic.rb +0 -17
  55. data/spec/shoryuken/sns_arn_spec.rb +0 -42
  56. data/spec/shoryuken/topic_spec.rb +0 -32
  57. data/spec/shoryuken_endpoint.yml +0 -6
  58. /data/{LICENSE.txt → LICENSE} +0 -0
@@ -1,245 +1,142 @@
1
- require 'shoryuken/processor'
2
- require 'shoryuken/fetcher'
3
-
4
1
  module Shoryuken
5
2
  class Manager
6
- include Celluloid
7
3
  include Util
8
4
 
9
- attr_accessor :fetcher
5
+ BATCH_LIMIT = 10
6
+ # See https://github.com/phstc/shoryuken/issues/348#issuecomment-292847028
7
+ MIN_DISPATCH_INTERVAL = 0.1
10
8
 
11
- trap_exit :processor_died
9
+ def initialize(fetcher, polling_strategy)
10
+ @count = Shoryuken.options.fetch(:concurrency, 25)
12
11
 
13
- def initialize(condvar)
14
- @count = Shoryuken.options[:concurrency] || 25
15
12
  raise(ArgumentError, "Concurrency value #{@count} is invalid, it needs to be a positive number") unless @count > 0
13
+
16
14
  @queues = Shoryuken.queues.dup.uniq
17
- @finished = condvar
18
15
 
19
- @done = false
16
+ @done = Concurrent::AtomicBoolean.new(false)
20
17
 
21
- @busy = []
22
- @ready = @count.times.map { build_processor }
23
- @threads = {}
18
+ @fetcher = fetcher
19
+ @polling_strategy = polling_strategy
20
+
21
+ @pool = Concurrent::FixedThreadPool.new(@count, max_queue: @count)
22
+ @dispatcher_executor = Concurrent::SingleThreadExecutor.new
24
23
  end
25
24
 
26
25
  def start
27
26
  logger.info { 'Starting' }
28
27
 
29
- dispatch
28
+ dispatch_async
30
29
  end
31
30
 
32
31
  def stop(options = {})
33
- watchdog('Manager#stop died') do
34
- @done = true
35
-
36
- if (callback = Shoryuken.stop_callback)
37
- logger.info { 'Calling Shoryuken.on_stop block' }
38
- callback.call
39
- end
40
-
41
- fire_event(:shutdown, true)
42
-
43
- @fetcher.terminate if @fetcher.alive?
44
-
45
- logger.info { "Shutting down #{@ready.size} quiet workers" }
32
+ @done.make_true
46
33
 
47
- @ready.each do |processor|
48
- processor.terminate if processor.alive?
49
- end
50
- @ready.clear
51
-
52
- return after(0) { @finished.signal } if @busy.empty?
53
-
54
- if options[:shutdown]
55
- hard_shutdown_in(options[:timeout])
56
- else
57
- soft_shutdown(options[:timeout])
58
- end
34
+ if (callback = Shoryuken.stop_callback)
35
+ logger.info { 'Calling Shoryuken.on_stop block' }
36
+ callback.call
59
37
  end
60
- end
61
-
62
- def processor_done(queue, processor)
63
- watchdog('Manager#processor_done died') do
64
- logger.debug { "Process done for '#{queue}'" }
65
-
66
- @threads.delete(processor.object_id)
67
- @busy.delete processor
68
38
 
69
- if stopped?
70
- processor.terminate if processor.alive?
71
- return after(0) { @finished.signal } if @busy.empty?
72
- else
73
- @ready << processor
74
- end
75
- end
76
- end
39
+ fire_event(:shutdown, true)
77
40
 
78
- def processor_died(processor, reason)
79
- watchdog("Manager#processor_died died") do
80
- logger.error { "Process died, reason: #{reason}" unless reason.to_s.empty? }
41
+ logger.info { 'Shutting down workers' }
81
42
 
82
- @threads.delete(processor.object_id)
83
- @busy.delete processor
43
+ @dispatcher_executor.kill
84
44
 
85
- if stopped?
86
- return after(0) { @finished.signal } if @busy.empty?
87
- else
88
- @ready << build_processor
89
- end
45
+ if options[:shutdown]
46
+ hard_shutdown_in(options[:timeout])
47
+ else
48
+ soft_shutdown
90
49
  end
91
50
  end
92
51
 
93
- def stopped?
94
- @done
52
+ def processor_failed(ex)
53
+ logger.error ex
54
+ logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
95
55
  end
96
56
 
97
- def assign(queue, sqs_msg)
98
- watchdog('Manager#assign died') do
99
- logger.debug { "Assigning #{sqs_msg.message_id}" }
100
-
101
- processor = @ready.pop
102
- @busy << processor
103
-
104
- processor.async.process(queue, sqs_msg)
105
- end
57
+ def processor_done(queue)
58
+ logger.debug { "Process done for '#{queue}'" }
106
59
  end
107
60
 
108
- def rebalance_queue_weight!(queue)
109
- watchdog('Manager#rebalance_queue_weight! died') do
110
- if (original = original_queue_weight(queue)) > (current = current_queue_weight(queue))
111
- logger.info { "Increasing '#{queue}' weight to #{current + 1}, max: #{original}" }
112
-
113
- @queues << queue
114
- end
115
- end
116
- end
117
-
118
- def pause_queue!(queue)
119
- return if !@queues.include?(queue) || Shoryuken.options[:delay].to_f <= 0
120
-
121
- logger.debug { "Pausing '#{queue}' for #{Shoryuken.options[:delay].to_f} seconds, because it's empty" }
122
-
123
- @queues.delete(queue)
61
+ private
124
62
 
125
- after(Shoryuken.options[:delay].to_f) { async.restart_queue!(queue) }
63
+ def dispatch_async
64
+ @dispatcher_executor.post(&method(:dispatch_now))
126
65
  end
127
66
 
67
+ def dispatch_now
68
+ return if @done.true?
128
69
 
129
- def dispatch
130
- return if stopped?
131
-
132
- logger.debug { "Ready: #{@ready.size}, Busy: #{@busy.size}, Active Queues: #{unparse_queues(@queues)}" }
133
-
134
- if @ready.empty?
135
- logger.debug { 'Pausing fetcher, because all processors are busy' }
70
+ begin
71
+ if ready.zero? || (queue = @polling_strategy.next_queue).nil?
72
+ sleep MIN_DISPATCH_INTERVAL
73
+ return
74
+ end
136
75
 
137
- after(1) { dispatch }
76
+ logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }
138
77
 
139
- return
140
- end
141
-
142
- if (queue = next_queue)
143
- @fetcher.async.fetch(queue, @ready.size)
144
- else
145
- logger.debug { 'Pausing fetcher, because all queues are paused' }
146
-
147
- @fetcher_paused = true
78
+ batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
79
+ ensure
80
+ dispatch_async
148
81
  end
149
82
  end
150
83
 
151
- def real_thread(proxy_id, thr)
152
- @threads[proxy_id] = thr
84
+ def busy
85
+ @count - ready
153
86
  end
154
87
 
155
- private
156
-
157
- def build_processor
158
- processor = Processor.new_link(current_actor)
159
- processor.proxy_id = processor.object_id
160
- processor
88
+ def ready
89
+ @pool.remaining_capacity
161
90
  end
162
91
 
163
- def restart_queue!(queue)
164
- return if stopped?
165
-
166
- unless @queues.include? queue
167
- logger.debug { "Restarting '#{queue}'" }
168
-
169
- @queues << queue
170
-
171
- if @fetcher_paused
172
- logger.debug { 'Restarting fetcher' }
173
-
174
- @fetcher_paused = false
92
+ def assign(queue, sqs_msg)
93
+ logger.debug { "Assigning #{sqs_msg.message_id}" }
175
94
 
176
- dispatch
177
- end
178
- end
95
+ @pool.post { Processor.new(self).process(queue, sqs_msg) }
179
96
  end
180
97
 
181
- def current_queue_weight(queue)
182
- queue_weight(@queues, queue)
98
+ def dispatch_batch(queue)
99
+ batch = @fetcher.fetch(queue, BATCH_LIMIT)
100
+ @polling_strategy.messages_found(queue.name, batch.size)
101
+ assign(queue.name, patch_batch!(batch))
183
102
  end
184
103
 
185
- def original_queue_weight(queue)
186
- queue_weight(Shoryuken.queues, queue)
104
+ def dispatch_single_messages(queue)
105
+ messages = @fetcher.fetch(queue, ready)
106
+ @polling_strategy.messages_found(queue.name, messages.size)
107
+ messages.each { |message| assign(queue.name, message) }
187
108
  end
188
109
 
189
- def queue_weight(queues, queue)
190
- queues.count { |q| q == queue }
110
+ def batched_queue?(queue)
111
+ Shoryuken.worker_registry.batch_receive_messages?(queue.name)
191
112
  end
192
113
 
193
- def next_queue
194
- return nil if @queues.empty?
195
-
196
- # get/remove the first queue in the list
197
- queue = @queues.shift
198
-
199
- unless defined?(::ActiveJob) || !Shoryuken.worker_registry.workers(queue).empty?
200
- # when no worker registered pause the queue to avoid endless recursion
201
- logger.debug { "Pausing '#{queue}' for #{Shoryuken.options[:delay].to_f} seconds, because no workers registered" }
202
-
203
- after(Shoryuken.options[:delay].to_f) { async.restart_queue!(queue) }
114
+ def soft_shutdown
115
+ @pool.shutdown
116
+ @pool.wait_for_termination
117
+ end
204
118
 
205
- return next_queue
119
+ def hard_shutdown_in(delay)
120
+ if busy > 0
121
+ logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
206
122
  end
207
123
 
208
- # add queue back to the end of the list
209
- @queues << queue
210
-
211
- queue
212
- end
124
+ @pool.shutdown
213
125
 
214
- def soft_shutdown(delay)
215
- logger.info { "Waiting for #{@busy.size} busy workers" }
126
+ return if @pool.wait_for_termination(delay)
216
127
 
217
- if @busy.size > 0
218
- after(delay) { soft_shutdown(delay) }
219
- else
220
- @finished.signal
221
- end
128
+ logger.info { "Hard shutting down #{busy} busy workers" }
129
+ @pool.kill
222
130
  end
223
131
 
224
- def hard_shutdown_in(delay)
225
- logger.info { "Waiting for #{@busy.size} busy workers" }
226
- logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
227
-
228
- after(delay) do
229
- watchdog('Manager#hard_shutdown_in died') do
230
- if @busy.size > 0
231
- logger.info { "Hard shutting down #{@busy.size} busy workers" }
232
-
233
- @busy.each do |processor|
234
- if processor.alive? && t = @threads.delete(processor.object_id)
235
- t.raise Shutdown
236
- end
237
- end
238
- end
239
-
240
- @finished.signal
132
+ def patch_batch!(sqs_msgs)
133
+ sqs_msgs.instance_eval do
134
+ def message_id
135
+ "batch-with-#{size}-messages"
241
136
  end
242
137
  end
138
+
139
+ sqs_msgs
243
140
  end
244
141
  end
245
142
  end
@@ -3,19 +3,10 @@ module Shoryuken
3
3
  attr_accessor :client, :queue_url, :queue_name, :data
4
4
 
5
5
  def initialize(client, queue, data)
6
- self.client = client
7
- self.data = data
8
-
9
- if queue.is_a?(Shoryuken::Queue)
10
- self.queue_url = queue.url
11
- self.queue_name = queue.name
12
- else
13
- # TODO: Remove next major release
14
- Shoryuken.logger.warn do
15
- '[DEPRECATION] Passing a queue url into Shoryuken::Message is deprecated, please pass the queue itself'
16
- end
17
- self.queue_url = queue
18
- end
6
+ self.client = client
7
+ self.data = data
8
+ self.queue_url = queue.url
9
+ self.queue_name = queue.name
19
10
  end
20
11
 
21
12
  def delete
@@ -102,32 +102,15 @@ module Shoryuken
102
102
 
103
103
  class Entry
104
104
  attr_reader :klass
105
+
105
106
  def initialize(klass, *args)
106
107
  @klass = klass
107
108
  @args = args
108
-
109
- patch_deprecated_middleware!(klass)
110
109
  end
111
110
 
112
111
  def make_new
113
112
  @klass.new(*@args)
114
113
  end
115
-
116
- private
117
-
118
- def patch_deprecated_middleware!(klass)
119
- if klass.instance_method(:call).arity == 3
120
- Shoryuken.logger.warn { "[DEPRECATION] #{klass.name}#call(worker_instance, queue, sqs_msg) is deprecated. Please use #{klass.name}#call(worker_instance, queue, sqs_msg, body)" }
121
-
122
- klass.class_eval do
123
- alias_method :deprecated_call, :call
124
-
125
- def call(worker_instance, queue, sqs_msg, body = nil, &block)
126
- deprecated_call(worker_instance, queue, sqs_msg, &block)
127
- end
128
- end
129
- end
130
- end
131
114
  end
132
115
  end
133
116
  end
@@ -1,51 +1,54 @@
1
- require 'celluloid' unless defined?(Celluloid)
2
-
3
1
  module Shoryuken
4
2
  module Middleware
5
3
  module Server
6
4
  class AutoExtendVisibility
5
+ include Util
6
+
7
7
  EXTEND_UPFRONT_SECONDS = 5
8
8
 
9
9
  def call(worker, queue, sqs_msg, body)
10
- timer = auto_visibility_timer(queue, sqs_msg, worker.class)
11
- begin
12
- yield
13
- ensure
14
- timer.cancel if timer
10
+ if sqs_msg.is_a?(Array)
11
+ logger.warn { "Auto extend visibility isn't supported for batch workers" }
12
+ return yield
15
13
  end
14
+
15
+ timer = auto_visibility_timer(worker, queue, sqs_msg, body)
16
+ yield
17
+ ensure
18
+ timer.kill if timer
16
19
  end
17
20
 
18
21
  private
19
22
 
20
23
  class MessageVisibilityExtender
21
- include Celluloid
22
24
  include Util
23
25
 
24
- def auto_extend(queue, sqs_msg, worker_class)
26
+ def auto_extend(worker, queue, sqs_msg, body)
25
27
  queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout
26
28
 
27
- every(queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
29
+ Concurrent::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
28
30
  begin
29
31
  logger.debug do
30
- "Extending message #{worker_name(worker_class, sqs_msg)}/#{queue}/#{sqs_msg.message_id} " \
31
- "visibility timeout by #{queue_visibility_timeout}s."
32
+ "Extending message #{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id} " \
33
+ "visibility timeout by #{queue_visibility_timeout}s."
32
34
  end
33
35
 
34
36
  sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
35
37
  rescue => e
36
38
  logger.error do
37
- "Could not auto extend the message #{worker_class}/#{queue}/#{sqs_msg.message_id} " \
38
- "visibility timeout. Error: #{e.message}"
39
+ 'Could not auto extend the message ' \
40
+ "#{worker_name(worker.class, sqs_msg, body)}/#{queue}/#{sqs_msg.message_id} " \
41
+ "visibility timeout. Error: #{e.message}"
39
42
  end
40
43
  end
41
44
  end
42
45
  end
43
46
  end
44
47
 
45
- def auto_visibility_timer(queue, sqs_msg, worker_class)
46
- return unless worker_class.auto_visibility_timeout?
47
- @visibility_extender ||= MessageVisibilityExtender.new_link
48
- @visibility_extender.auto_extend(queue, sqs_msg, worker_class)
48
+ def auto_visibility_timer(worker, queue, sqs_msg, body)
49
+ return unless worker.class.auto_visibility_timeout?
50
+
51
+ MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body).tap(&:execute)
49
52
  end
50
53
  end
51
54
  end
@@ -5,12 +5,17 @@ module Shoryuken
5
5
  include Util
6
6
 
7
7
  def call(worker, queue, sqs_msg, body)
8
+ if sqs_msg.is_a?(Array)
9
+ logger.warn { "Exponential backoff isn't supported for batch workers" }
10
+ return yield
11
+ end
12
+
8
13
  started_at = Time.now
9
14
  yield
10
15
  rescue
11
- retry_intervals = Array(worker.class.get_shoryuken_options['retry_intervals'])
16
+ retry_intervals = worker.class.get_shoryuken_options['retry_intervals']
12
17
 
13
- if retry_intervals.empty? || !handle_failure(sqs_msg, started_at, retry_intervals)
18
+ if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
14
19
  # Re-raise the exception if the job is not going to be exponential backoff retried.
15
20
  # This allows custom middleware (like exception notifiers) to be aware of the unhandled failure.
16
21
  raise
@@ -19,30 +24,32 @@ module Shoryuken
19
24
 
20
25
  private
21
26
 
22
- def handle_failure(sqs_msg, started_at, retry_intervals)
23
- attempts = sqs_msg.attributes['ApproximateReceiveCount']
27
+ def get_interval(retry_intervals, attempts)
28
+ return retry_intervals.call(attempts) if retry_intervals.respond_to?(:call)
24
29
 
25
- return unless attempts
30
+ if attempts <= (retry_intervals = Array(retry_intervals)).size
31
+ retry_intervals[attempts - 1]
32
+ else
33
+ retry_intervals.last
34
+ end
35
+ end
26
36
 
27
- attempts = attempts.to_i - 1
37
+ def next_visibility_timeout(interval, started_at)
38
+ max_timeout = 43_200 - (Time.now - started_at).ceil - 1
39
+ interval = max_timeout if interval > max_timeout
40
+ interval.to_i
41
+ end
28
42
 
29
- interval = if attempts < retry_intervals.size
30
- retry_intervals[attempts]
31
- else
32
- retry_intervals.last
33
- end
43
+ def handle_failure(sqs_msg, started_at, retry_intervals)
44
+ receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i
34
45
 
35
- # Visibility timeouts are limited to a total 12 hours, starting from the receipt of the message.
36
- # We calculate the maximum timeout by subtracting the amount of time since the receipt of the message.
37
- #
38
- # From the docs: "Amazon SQS restarts the timeout period using the new value."
39
- # http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/AboutVT.html#AboutVT-extending-message-visibility-timeout
40
- max_timeout = 43200 - (Time.now - started_at).ceil - 1
41
- interval = max_timeout if interval > max_timeout
46
+ return false unless (interval = get_interval(retry_intervals, receive_count))
42
47
 
43
- sqs_msg.change_visibility(visibility_timeout: interval.to_i)
48
+ sqs_msg.change_visibility(visibility_timeout: next_visibility_timeout(interval.to_i, started_at))
44
49
 
45
50
  logger.info { "Message #{sqs_msg.message_id} failed, will be retried in #{interval} seconds." }
51
+
52
+ true
46
53
  end
47
54
  end
48
55
  end