shoryuken 2.1.1 → 3.1.3

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