shoryuken 2.0.11 → 3.0.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 (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 +92 -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 +188 -0
  11. data/bin/shoryuken +47 -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 +72 -184
  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 +6 -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 +5 -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,133 @@
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
+ HEARTBEAT_INTERVAL = 0.1
10
7
 
11
- trap_exit :processor_died
8
+ def initialize(fetcher, polling_strategy)
9
+ @count = Shoryuken.options.fetch(:concurrency, 25)
12
10
 
13
- def initialize(condvar)
14
- @count = Shoryuken.options[:concurrency] || 25
15
11
  raise(ArgumentError, "Concurrency value #{@count} is invalid, it needs to be a positive number") unless @count > 0
12
+
16
13
  @queues = Shoryuken.queues.dup.uniq
17
- @finished = condvar
18
14
 
19
- @done = false
15
+ @done = Concurrent::AtomicBoolean.new(false)
16
+ @dispatching = Concurrent::AtomicBoolean.new(false)
17
+
18
+ @fetcher = fetcher
19
+ @polling_strategy = polling_strategy
20
+
21
+ @heartbeat = Concurrent::TimerTask.new(run_now: true,
22
+ execution_interval: HEARTBEAT_INTERVAL,
23
+ timeout_interval: 60) { dispatch }
20
24
 
21
- @busy = []
22
- @ready = @count.times.map { build_processor }
23
- @threads = {}
25
+ @pool = Concurrent::FixedThreadPool.new(@count, max_queue: @count)
24
26
  end
25
27
 
26
28
  def start
27
29
  logger.info { 'Starting' }
28
30
 
29
- dispatch
31
+ @heartbeat.execute
30
32
  end
31
33
 
32
34
  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
35
+ @done.make_true
61
36
 
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}" unless reason.to_s.empty? }
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
37
+ if (callback = Shoryuken.stop_callback)
38
+ logger.info { 'Calling Shoryuken.on_stop block' }
39
+ callback.call
90
40
  end
91
- end
92
-
93
- def stopped?
94
- @done
95
- end
96
41
 
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
42
+ fire_event(:shutdown, true)
103
43
 
104
- processor.async.process(queue, sqs_msg)
105
- end
106
- end
44
+ logger.info { 'Shutting down workers' }
107
45
 
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}" }
46
+ @heartbeat.kill
112
47
 
113
- @queues << queue
114
- end
48
+ if options[:shutdown]
49
+ hard_shutdown_in(options[:timeout])
50
+ else
51
+ soft_shutdown
115
52
  end
116
53
  end
117
54
 
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)
124
-
125
- after(Shoryuken.options[:delay].to_f) { async.restart_queue!(queue) }
55
+ def processor_done(queue)
56
+ logger.debug { "Process done for '#{queue}'" }
126
57
  end
127
58
 
59
+ private
128
60
 
129
61
  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' }
62
+ return if @done.true?
63
+ return unless @dispatching.make_true
136
64
 
137
- after(1) { dispatch }
65
+ return if ready.zero?
66
+ return unless (queue = @polling_strategy.next_queue)
138
67
 
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' }
68
+ logger.debug { "Ready: #{ready}, Busy: #{busy}, Active Queues: #{@polling_strategy.active_queues}" }
146
69
 
147
- @fetcher_paused = true
148
- end
70
+ batched_queue?(queue) ? dispatch_batch(queue) : dispatch_single_messages(queue)
71
+ ensure
72
+ @dispatching.make_false
149
73
  end
150
74
 
151
- def real_thread(proxy_id, thr)
152
- @threads[proxy_id] = thr
75
+ def busy
76
+ @count - ready
153
77
  end
154
78
 
155
- private
156
-
157
- def build_processor
158
- processor = Processor.new_link(current_actor)
159
- processor.proxy_id = processor.object_id
160
- processor
79
+ def ready
80
+ @pool.remaining_capacity
161
81
  end
162
82
 
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
83
+ def assign(queue, sqs_msg)
84
+ logger.debug { "Assigning #{sqs_msg.message_id}" }
175
85
 
176
- dispatch
177
- end
178
- end
86
+ @pool.post { Processor.new(self).process(queue, sqs_msg) }
179
87
  end
180
88
 
181
- def current_queue_weight(queue)
182
- queue_weight(@queues, queue)
89
+ def dispatch_batch(queue)
90
+ batch = @fetcher.fetch(queue, BATCH_LIMIT)
91
+ @polling_strategy.messages_found(queue.name, batch.size)
92
+ assign(queue.name, patch_batch!(batch))
183
93
  end
184
94
 
185
- def original_queue_weight(queue)
186
- queue_weight(Shoryuken.queues, queue)
95
+ def dispatch_single_messages(queue)
96
+ messages = @fetcher.fetch(queue, ready)
97
+ @polling_strategy.messages_found(queue.name, messages.size)
98
+ messages.each { |message| assign(queue.name, message) }
187
99
  end
188
100
 
189
- def queue_weight(queues, queue)
190
- queues.count { |q| q == queue }
101
+ def batched_queue?(queue)
102
+ Shoryuken.worker_registry.batch_receive_messages?(queue.name)
191
103
  end
192
104
 
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) }
105
+ def soft_shutdown
106
+ @pool.shutdown
107
+ @pool.wait_for_termination
108
+ end
204
109
 
205
- return next_queue
110
+ def hard_shutdown_in(delay)
111
+ if busy > 0
112
+ logger.info { "Pausing up to #{delay} seconds to allow workers to finish..." }
206
113
  end
207
114
 
208
- # add queue back to the end of the list
209
- @queues << queue
210
-
211
- queue
212
- end
115
+ @pool.shutdown
213
116
 
214
- def soft_shutdown(delay)
215
- logger.info { "Waiting for #{@busy.size} busy workers" }
117
+ return if @pool.wait_for_termination(delay)
216
118
 
217
- if @busy.size > 0
218
- after(delay) { soft_shutdown(delay) }
219
- else
220
- @finished.signal
221
- end
119
+ logger.info { "Hard shutting down #{busy} busy workers" }
120
+ @pool.kill
222
121
  end
223
122
 
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
123
+ def patch_batch!(sqs_msgs)
124
+ sqs_msgs.instance_eval do
125
+ def message_id
126
+ "batch-with-#{size}-messages"
241
127
  end
242
128
  end
129
+
130
+ sqs_msgs
243
131
  end
244
132
  end
245
133
  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