shoryuken 7.0.1 → 7.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/push.yml +3 -3
- data/.github/workflows/specs.yml +37 -10
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.gitignore +0 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +120 -0
- data/README.md +1 -1
- data/bin/{clean_localstack → clean_sqs} +4 -4
- data/bin/cli/sqs.rb +7 -0
- data/bin/integrations +52 -34
- data/docker-compose.yml +4 -20
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +3 -1
- data/lib/shoryuken/active_job/current_attributes.rb +35 -7
- data/lib/shoryuken/environment_loader.rb +1 -1
- data/lib/shoryuken/fetcher.rb +7 -1
- data/lib/shoryuken/helpers/timer_task.rb +19 -2
- data/lib/shoryuken/launcher.rb +21 -5
- data/lib/shoryuken/manager.rb +38 -5
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +32 -2
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +8 -3
- data/lib/shoryuken/middleware/server/non_retryable_exception.rb +17 -8
- data/lib/shoryuken/options.rb +22 -3
- data/lib/shoryuken/polling/strict_priority.rb +26 -14
- data/lib/shoryuken/polling/weighted_round_robin.rb +41 -27
- data/lib/shoryuken/queue.rb +8 -1
- data/lib/shoryuken/util.rb +4 -1
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker.rb +5 -1
- data/lib/shoryuken.rb +2 -0
- data/renovate.json +26 -2
- data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/complex_types_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/cross_job_reset_spec.rb +47 -0
- data/spec/integration/active_job/current_attributes/empty_context_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/full_context_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/incremental_persist_spec.rb +76 -0
- data/spec/integration/active_job/current_attributes/partial_context_spec.rb +1 -1
- data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +1 -1
- data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +1 -1
- data/spec/integration/active_job/fifo_dedup_opt_out/fifo_dedup_opt_out_spec.rb +67 -0
- data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +1 -1
- data/spec/integration/active_job/retry/discard_on_spec.rb +1 -1
- data/spec/integration/active_job/retry/retry_on_spec.rb +1 -1
- data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +2 -2
- data/spec/integration/active_job/scheduled/scheduled_spec.rb +1 -1
- data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +1 -1
- data/spec/integration/auto_delete/auto_delete_spec.rb +1 -1
- data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +1 -1
- data/spec/integration/auto_extend_visibility/short_visibility_timeout_spec.rb +52 -0
- data/spec/integration/aws_config/aws_config_spec.rb +3 -3
- data/spec/integration/batch_processing/batch_processing_spec.rb +1 -1
- data/spec/integration/body_parser/json_parser_spec.rb +1 -1
- data/spec/integration/body_parser/proc_parser_spec.rb +1 -1
- data/spec/integration/body_parser/text_parser_spec.rb +1 -1
- data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +1 -1
- data/spec/integration/concurrent_processing/processor_accounting_spec.rb +94 -0
- data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
- data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +1 -1
- data/spec/integration/exception_handlers/exception_handlers_spec.rb +1 -1
- data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +1 -1
- data/spec/integration/fifo_ordering/fifo_max_messages_cap_spec.rb +96 -0
- data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +1 -1
- data/spec/integration/large_payloads/large_payloads_spec.rb +1 -1
- data/spec/integration/launcher/double_graceful_stop_spec.rb +71 -0
- data/spec/integration/launcher/embedded_dispatch_error_spec.rb +85 -0
- data/spec/integration/launcher/global_executor_preserved_spec.rb +76 -0
- data/spec/integration/launcher/graceful_stop_timeout_spec.rb +74 -0
- data/spec/integration/launcher/launcher_spec.rb +1 -1
- data/spec/integration/message_attributes/message_attributes_spec.rb +1 -1
- data/spec/integration/message_operations/message_operations_spec.rb +1 -1
- data/spec/integration/message_operations/partial_batch_delete_spec.rb +67 -0
- data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +2 -2
- data/spec/integration/non_retryable_exception/with_retry_intervals_spec.rb +115 -0
- data/spec/integration/polling_strategies/polling_strategies_spec.rb +1 -1
- data/spec/integration/queue_operations/queue_operations_spec.rb +1 -1
- data/spec/integration/retry_behavior/retry_behavior_spec.rb +1 -1
- data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +1 -1
- data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +1 -1
- data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +1 -1
- data/spec/integration/worker_groups/worker_groups_spec.rb +1 -1
- data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +1 -1
- data/spec/integrations_helper.rb +13 -12
- data/spec/lib/shoryuken/fetcher_spec.rb +13 -0
- data/spec/lib/shoryuken/helpers/timer_task_spec.rb +24 -0
- data/spec/lib/shoryuken/launcher_spec.rb +38 -0
- data/spec/lib/shoryuken/manager_spec.rb +99 -0
- data/spec/lib/shoryuken/middleware/server/auto_extend_visibility_spec.rb +35 -0
- data/spec/lib/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +56 -0
- data/spec/lib/shoryuken/options_spec.rb +43 -0
- data/spec/lib/shoryuken/polling/strict_priority_spec.rb +25 -0
- data/spec/lib/shoryuken/polling/weighted_round_robin_spec.rb +50 -0
- data/spec/lib/shoryuken/queue_spec.rb +37 -0
- data/spec/lib/shoryuken/util_spec.rb +26 -0
- data/spec/shared_examples_for_active_job.rb +18 -0
- data/spec/spec_helper.rb +26 -7
- metadata +29 -3
data/lib/shoryuken/launcher.rb
CHANGED
|
@@ -43,8 +43,7 @@ module Shoryuken
|
|
|
43
43
|
# Don't await here so the timeout below is not delayed
|
|
44
44
|
stop_new_dispatching
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
|
|
46
|
+
shutdown_executor
|
|
48
47
|
|
|
49
48
|
fire_event(:stopped)
|
|
50
49
|
end
|
|
@@ -61,8 +60,7 @@ module Shoryuken
|
|
|
61
60
|
stop_new_dispatching
|
|
62
61
|
await_dispatching_in_progress
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
executor.wait_for_termination
|
|
63
|
+
shutdown_executor
|
|
66
64
|
|
|
67
65
|
fire_event(:stopped)
|
|
68
66
|
end
|
|
@@ -93,11 +91,29 @@ module Shoryuken
|
|
|
93
91
|
@managers.each(&:await_dispatching_in_progress)
|
|
94
92
|
end
|
|
95
93
|
|
|
94
|
+
# Shuts the executor down, giving in-flight workers up to the configured
|
|
95
|
+
# timeout to finish before force-killing them so the process can exit.
|
|
96
|
+
# Used by both the graceful ({#stop}) and immediate ({#stop!}) shutdowns:
|
|
97
|
+
# a graceful stop still waits for workers, but must not block forever on a
|
|
98
|
+
# hung one.
|
|
99
|
+
#
|
|
100
|
+
# @return [void]
|
|
101
|
+
def shutdown_executor
|
|
102
|
+
executor.shutdown
|
|
103
|
+
executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
|
|
104
|
+
end
|
|
105
|
+
|
|
96
106
|
# Returns the executor for running async operations
|
|
97
107
|
#
|
|
108
|
+
# Owns a dedicated executor rather than borrowing Concurrent.global_io_executor:
|
|
109
|
+
# {#stop} and {#stop!} shut down and kill this executor, and destroying the
|
|
110
|
+
# process-global pool would break anything else relying on it (including
|
|
111
|
+
# Shoryuken's own ShoryukenConcurrentSendAdapter) and prevent a fresh launcher
|
|
112
|
+
# from starting in the same process.
|
|
113
|
+
#
|
|
98
114
|
# @return [Concurrent::ExecutorService] the executor service
|
|
99
115
|
def executor
|
|
100
|
-
@_executor ||= Shoryuken.launcher_executor || Concurrent.
|
|
116
|
+
@_executor ||= Shoryuken.launcher_executor || Concurrent::CachedThreadPool.new(auto_terminate: true)
|
|
101
117
|
end
|
|
102
118
|
|
|
103
119
|
# Starts all managers in parallel futures
|
data/lib/shoryuken/manager.rb
CHANGED
|
@@ -57,6 +57,9 @@ module Shoryuken
|
|
|
57
57
|
# There might still be a dispatching on-going, as the response from SQS could take some time
|
|
58
58
|
# We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
|
|
59
59
|
# We use a queue, as the dispatch_loop is running on another thread, and this is a efficient way of communicating between threads.
|
|
60
|
+
# The dispatch loop closes the queue when it observes the stop flag; pop on a closed queue
|
|
61
|
+
# returns immediately, so this stays safe when stop is requested more than once
|
|
62
|
+
# (e.g. TSTP followed by USR1, which both trigger a graceful stop).
|
|
60
63
|
@dispatching_release_signal.pop
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -74,11 +77,23 @@ module Shoryuken
|
|
|
74
77
|
# @return [void]
|
|
75
78
|
def dispatch_loop
|
|
76
79
|
if @stop_new_dispatching.true? || !running?
|
|
77
|
-
|
|
80
|
+
# Close (instead of push) so every pending and future
|
|
81
|
+
# await_dispatching_in_progress call returns, not just the first one
|
|
82
|
+
@dispatching_release_signal.close
|
|
78
83
|
return
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
@executor.post { dispatch }
|
|
87
|
+
rescue Concurrent::RejectedExecutionError
|
|
88
|
+
# The executor was shut down between the running? check and the post
|
|
89
|
+
# (e.g. a hard stop racing the dispatch loop); release any waiters as
|
|
90
|
+
# the dispatch chain ends here
|
|
91
|
+
@dispatching_release_signal.close
|
|
92
|
+
rescue StandardError
|
|
93
|
+
# Unexpected error from post (e.g. ThreadError if the OS thread limit is
|
|
94
|
+
# hit); the dispatch chain ends here so release waiters before re-raising
|
|
95
|
+
@dispatching_release_signal.close
|
|
96
|
+
raise
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
# Dispatches messages from a queue
|
|
@@ -129,6 +144,12 @@ module Shoryuken
|
|
|
129
144
|
return unless @polling_strategy.respond_to?(:message_processed)
|
|
130
145
|
|
|
131
146
|
@polling_strategy.message_processed(queue)
|
|
147
|
+
rescue => e
|
|
148
|
+
# Swallow (but log) failures from the SQS lookups or the strategy callback:
|
|
149
|
+
# the busy counter was already decremented above and the caller's ensure
|
|
150
|
+
# must not run completion twice
|
|
151
|
+
logger.error { "Processor completion failed for #{queue}: #{e.message}" }
|
|
152
|
+
logger.debug { e.backtrace.join("\n") } unless e.backtrace.nil?
|
|
132
153
|
end
|
|
133
154
|
|
|
134
155
|
# Assigns a message to a processor
|
|
@@ -144,6 +165,11 @@ module Shoryuken
|
|
|
144
165
|
@busy_processors.increment
|
|
145
166
|
fire_utilization_update_event
|
|
146
167
|
|
|
168
|
+
# Completion runs in an ensure so it executes exactly once whether
|
|
169
|
+
# processing succeeds or raises. The previous `.then { processor_done }
|
|
170
|
+
# .rescue { processor_done }` chain ran completion twice when
|
|
171
|
+
# processor_done itself raised, double decrementing the busy counter
|
|
172
|
+
# and silently breaking the concurrency limit
|
|
147
173
|
Concurrent::Promise
|
|
148
174
|
.execute(executor: @executor) do
|
|
149
175
|
original_priority = Thread.current.priority
|
|
@@ -152,10 +178,9 @@ module Shoryuken
|
|
|
152
178
|
Processor.process(queue_name, sqs_msg)
|
|
153
179
|
ensure
|
|
154
180
|
Thread.current.priority = original_priority
|
|
181
|
+
processor_done(queue_name)
|
|
155
182
|
end
|
|
156
183
|
end
|
|
157
|
-
.then { processor_done(queue_name) }
|
|
158
|
-
.rescue { processor_done(queue_name) }
|
|
159
184
|
end
|
|
160
185
|
|
|
161
186
|
# Dispatches a batch of messages from a queue
|
|
@@ -208,9 +233,17 @@ module Shoryuken
|
|
|
208
233
|
logger.error { "Manager failed: #{ex.message}" }
|
|
209
234
|
logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
|
|
210
235
|
|
|
211
|
-
|
|
212
|
-
|
|
236
|
+
# Stop this manager first so Launcher#healthy? surfaces the failure to
|
|
237
|
+
# whoever is supervising us (the CLI Runner, or an embedding application).
|
|
213
238
|
@running.make_false
|
|
239
|
+
|
|
240
|
+
# In server (CLI) mode the Runner traps USR1 and turns it into a graceful
|
|
241
|
+
# shutdown of the whole process, so a process supervisor can restart us.
|
|
242
|
+
# When embedded (no Runner), USR1 keeps its default disposition and would
|
|
243
|
+
# terminate the host process - and any in-flight workers - so we must not
|
|
244
|
+
# send it. The stopped manager above is enough for Launcher#healthy? to
|
|
245
|
+
# report the failure to the embedding application.
|
|
246
|
+
Process.kill('USR1', Process.pid) if Shoryuken.server?
|
|
214
247
|
end
|
|
215
248
|
|
|
216
249
|
# Fires a utilization update event
|
|
@@ -48,8 +48,17 @@ module Shoryuken
|
|
|
48
48
|
# @return [Shoryuken::Helpers::TimerTask] the timer task
|
|
49
49
|
def auto_extend(_worker, queue, sqs_msg, _body)
|
|
50
50
|
queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout
|
|
51
|
+
execution_interval = extension_interval(queue_visibility_timeout)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
unless execution_interval
|
|
54
|
+
logger.warn do
|
|
55
|
+
"Not auto-extending visibility for #{queue}/#{sqs_msg.message_id}: queue visibility " \
|
|
56
|
+
"timeout (#{queue_visibility_timeout}s) is too short. Increase it to use auto_visibility_timeout."
|
|
57
|
+
end
|
|
58
|
+
return nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Shoryuken::Helpers::TimerTask.new(execution_interval: execution_interval) do
|
|
53
62
|
logger.debug do
|
|
54
63
|
"Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
|
|
55
64
|
end
|
|
@@ -61,6 +70,25 @@ module Shoryuken
|
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Returns a positive interval at which to re-extend the message's
|
|
77
|
+
# visibility before it expires, or nil when the visibility timeout is
|
|
78
|
+
# too short to schedule one. Normally this is EXTEND_UPFRONT_SECONDS
|
|
79
|
+
# before expiry, but for short timeouts (<= EXTEND_UPFRONT_SECONDS) it
|
|
80
|
+
# falls back to half the timeout so the timer still fires in time
|
|
81
|
+
# instead of TimerTask raising on a non-positive interval.
|
|
82
|
+
#
|
|
83
|
+
# @param visibility_timeout [Integer] the queue's visibility timeout in seconds
|
|
84
|
+
# @return [Float, Integer, nil] the execution interval, or nil if too short
|
|
85
|
+
def extension_interval(visibility_timeout)
|
|
86
|
+
return nil if visibility_timeout <= 0
|
|
87
|
+
|
|
88
|
+
interval = visibility_timeout - EXTEND_UPFRONT_SECONDS
|
|
89
|
+
interval = visibility_timeout / 2.0 if interval <= 0
|
|
90
|
+
interval
|
|
91
|
+
end
|
|
64
92
|
end
|
|
65
93
|
|
|
66
94
|
# Creates and starts a visibility extension timer
|
|
@@ -71,7 +99,9 @@ module Shoryuken
|
|
|
71
99
|
# @param body [Object] the parsed message body
|
|
72
100
|
# @return [Shoryuken::Helpers::TimerTask] the started timer
|
|
73
101
|
def auto_visibility_timer(worker, queue, sqs_msg, body)
|
|
74
|
-
MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body)
|
|
102
|
+
timer = MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body)
|
|
103
|
+
timer&.execute
|
|
104
|
+
timer
|
|
75
105
|
end
|
|
76
106
|
end
|
|
77
107
|
end
|
|
@@ -17,8 +17,8 @@ module Shoryuken
|
|
|
17
17
|
# @param _body [Object] the parsed message body (unused)
|
|
18
18
|
# @yield continues to the next middleware in the chain
|
|
19
19
|
# @return [void]
|
|
20
|
-
# @raise [StandardError] re-raises the original exception if retry intervals are not configured
|
|
21
|
-
#
|
|
20
|
+
# @raise [StandardError] re-raises the original exception if retry intervals are not configured,
|
|
21
|
+
# if retry limit is exceeded, or if the exception is classified as non-retryable
|
|
22
22
|
def call(worker, _queue, sqs_msg, _body)
|
|
23
23
|
return yield unless worker.class.exponential_backoff?
|
|
24
24
|
|
|
@@ -30,7 +30,12 @@ module Shoryuken
|
|
|
30
30
|
started_at = Time.now
|
|
31
31
|
yield
|
|
32
32
|
rescue => e
|
|
33
|
-
|
|
33
|
+
worker_options = worker.class.get_shoryuken_options
|
|
34
|
+
retry_intervals = worker_options['retry_intervals']
|
|
35
|
+
|
|
36
|
+
# Non-retryable exceptions must not be backoff retried; re-raise so the
|
|
37
|
+
# NonRetryableException middleware can delete the message immediately.
|
|
38
|
+
raise if NonRetryableException.non_retryable?(e, worker_options['non_retryable_exceptions'])
|
|
34
39
|
|
|
35
40
|
if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
|
|
36
41
|
# Re-raise the exception if the job is not going to be exponential backoff retried.
|
|
@@ -28,6 +28,22 @@ module Shoryuken
|
|
|
28
28
|
class NonRetryableException
|
|
29
29
|
include Util
|
|
30
30
|
|
|
31
|
+
# Checks whether an exception is classified as non-retryable for a worker
|
|
32
|
+
#
|
|
33
|
+
# @param exception [Exception] the exception to classify
|
|
34
|
+
# @param non_retryable_exceptions [Array<Class>, #call, nil] the worker's
|
|
35
|
+
# non_retryable_exceptions option: exception classes or a callable
|
|
36
|
+
# @return [Boolean] true if the exception must not be retried
|
|
37
|
+
def self.non_retryable?(exception, non_retryable_exceptions)
|
|
38
|
+
return false unless non_retryable_exceptions
|
|
39
|
+
|
|
40
|
+
if non_retryable_exceptions.respond_to?(:call)
|
|
41
|
+
!!non_retryable_exceptions.call(exception)
|
|
42
|
+
else
|
|
43
|
+
Array(non_retryable_exceptions).any? { |klass| exception.is_a?(klass) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
31
47
|
# Processes a message and handles non-retryable exceptions
|
|
32
48
|
#
|
|
33
49
|
# @param worker [Object] the worker instance
|
|
@@ -41,14 +57,7 @@ module Shoryuken
|
|
|
41
57
|
rescue => e
|
|
42
58
|
non_retryable_exceptions = worker.class.get_shoryuken_options['non_retryable_exceptions']
|
|
43
59
|
|
|
44
|
-
return raise unless non_retryable_exceptions
|
|
45
|
-
|
|
46
|
-
if non_retryable_exceptions.respond_to?(:call)
|
|
47
|
-
return raise unless non_retryable_exceptions.call(e)
|
|
48
|
-
else
|
|
49
|
-
exception_classes = Array(non_retryable_exceptions)
|
|
50
|
-
return raise unless exception_classes.any? { |klass| e.is_a?(klass) }
|
|
51
|
-
end
|
|
60
|
+
return raise unless self.class.non_retryable?(e, non_retryable_exceptions)
|
|
52
61
|
|
|
53
62
|
# Handle batch messages
|
|
54
63
|
messages = sqs_msg.is_a?(Array) ? sqs_msg : [sqs_msg]
|
data/lib/shoryuken/options.rb
CHANGED
|
@@ -34,7 +34,8 @@ module Shoryuken
|
|
|
34
34
|
# @return [Class] the executor class for running workers
|
|
35
35
|
# @return [Shoryuken::WorkerRegistry] the registry for worker classes
|
|
36
36
|
# @return [Array<#call>] handlers for processing exceptions
|
|
37
|
-
attr_accessor :active_job_queue_name_prefixing, :
|
|
37
|
+
attr_accessor :active_job_queue_name_prefixing, :active_job_fifo_message_deduplication,
|
|
38
|
+
:cache_visibility_timeout,
|
|
38
39
|
:groups, :launcher_executor, :reloader, :enable_reloading,
|
|
39
40
|
:start_callback, :stop_callback, :worker_executor, :worker_registry,
|
|
40
41
|
:exception_handlers
|
|
@@ -53,6 +54,7 @@ module Shoryuken
|
|
|
53
54
|
self.worker_registry = DefaultWorkerRegistry.new
|
|
54
55
|
self.exception_handlers = [DefaultExceptionHandler]
|
|
55
56
|
self.active_job_queue_name_prefixing = false
|
|
57
|
+
self.active_job_fifo_message_deduplication = true
|
|
56
58
|
self.worker_executor = Worker::DefaultExecutor
|
|
57
59
|
self.cache_visibility_timeout = false
|
|
58
60
|
self.reloader = proc { |&block| block.call }
|
|
@@ -73,14 +75,21 @@ module Shoryuken
|
|
|
73
75
|
# @param group [String] the name of the group
|
|
74
76
|
# @param concurrency [Integer, nil] the number of concurrent workers for the group
|
|
75
77
|
# @param delay [Float, nil] the delay between polling cycles
|
|
78
|
+
# @param polling_strategy [Class, String, nil] the polling strategy class for the group
|
|
76
79
|
# @return [Hash] the group configuration
|
|
77
|
-
|
|
80
|
+
# @raise [Errors::InvalidPollingStrategyError] if polling_strategy is not a Class, String, or nil
|
|
81
|
+
def add_group(group, concurrency = nil, delay: nil, polling_strategy: nil)
|
|
82
|
+
unless polling_strategy.nil? || polling_strategy.is_a?(Class) || polling_strategy.is_a?(String)
|
|
83
|
+
raise Errors::InvalidPollingStrategyError, "#{polling_strategy} is not a valid polling_strategy"
|
|
84
|
+
end
|
|
85
|
+
|
|
78
86
|
concurrency ||= options[:concurrency]
|
|
79
87
|
delay ||= options[:delay]
|
|
80
88
|
|
|
81
89
|
groups[group] ||= {
|
|
82
90
|
concurrency: concurrency,
|
|
83
91
|
delay: delay,
|
|
92
|
+
polling_strategy: polling_strategy,
|
|
84
93
|
queues: []
|
|
85
94
|
}
|
|
86
95
|
end
|
|
@@ -109,7 +118,8 @@ module Shoryuken
|
|
|
109
118
|
# @param group [String] the name of the group
|
|
110
119
|
# @return [Class] the polling strategy class to use
|
|
111
120
|
def polling_strategy(group)
|
|
112
|
-
strategy =
|
|
121
|
+
strategy = groups[group].to_h[:polling_strategy] ||
|
|
122
|
+
(group == 'default' ? options : options[:groups].to_h[group]).to_h[:polling_strategy]
|
|
113
123
|
case strategy
|
|
114
124
|
when 'WeightedRoundRobin', nil # Default case
|
|
115
125
|
Polling::WeightedRoundRobin
|
|
@@ -294,6 +304,15 @@ module Shoryuken
|
|
|
294
304
|
@active_job_queue_name_prefixing
|
|
295
305
|
end
|
|
296
306
|
|
|
307
|
+
# Checks if the ActiveJob adapter should auto-generate a content-based
|
|
308
|
+
# message_deduplication_id for FIFO queues. When disabled, distinct enqueues
|
|
309
|
+
# of the same job class and arguments are no longer silently deduplicated.
|
|
310
|
+
#
|
|
311
|
+
# @return [Boolean] true if FIFO deduplication id generation is enabled
|
|
312
|
+
def active_job_fifo_message_deduplication?
|
|
313
|
+
@active_job_fifo_message_deduplication
|
|
314
|
+
end
|
|
315
|
+
|
|
297
316
|
private
|
|
298
317
|
|
|
299
318
|
# Creates the default server middleware chain
|
|
@@ -22,6 +22,10 @@ module Shoryuken
|
|
|
22
22
|
.each_with_object({}) { |queue, h| h[queue] = Time.at(0) }
|
|
23
23
|
|
|
24
24
|
@delay = delay
|
|
25
|
+
# next_queue/messages_found run on the dispatch thread while
|
|
26
|
+
# message_processed runs on processor-completion threads (for FIFO
|
|
27
|
+
# queues), so all access to the shared state is serialized.
|
|
28
|
+
@mutex = Mutex.new
|
|
25
29
|
# Start queues at 0
|
|
26
30
|
reset_next_queue
|
|
27
31
|
end
|
|
@@ -30,8 +34,10 @@ module Shoryuken
|
|
|
30
34
|
#
|
|
31
35
|
# @return [QueueConfiguration, nil] the next queue configuration or nil if all paused
|
|
32
36
|
def next_queue
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
next_queue = next_active_queue
|
|
39
|
+
next_queue.nil? ? nil : QueueConfiguration.new(next_queue, {})
|
|
40
|
+
end
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
# Handles the result of polling a queue
|
|
@@ -40,10 +46,12 @@ module Shoryuken
|
|
|
40
46
|
# @param messages_found [Integer] number of messages found
|
|
41
47
|
# @return [void]
|
|
42
48
|
def messages_found(queue, messages_found)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
if messages_found == 0
|
|
51
|
+
pause(queue)
|
|
52
|
+
else
|
|
53
|
+
reset_next_queue
|
|
54
|
+
end
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
@@ -51,11 +59,13 @@ module Shoryuken
|
|
|
51
59
|
#
|
|
52
60
|
# @return [Array<Array>] array of [queue_name, priority] pairs
|
|
53
61
|
def active_queues
|
|
54
|
-
@
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
@queues
|
|
64
|
+
.reverse
|
|
65
|
+
.map.with_index(1)
|
|
66
|
+
.reject { |q, _| queue_paused?(q) }
|
|
67
|
+
.reverse
|
|
68
|
+
end
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
# Called when a message from a queue has been processed
|
|
@@ -63,9 +73,11 @@ module Shoryuken
|
|
|
63
73
|
# @param queue [String] the queue name
|
|
64
74
|
# @return [void]
|
|
65
75
|
def message_processed(queue)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
if queue_paused?(queue)
|
|
78
|
+
logger.debug "Unpausing #{queue}"
|
|
79
|
+
@paused_until[queue] = Time.at(0)
|
|
80
|
+
end
|
|
69
81
|
end
|
|
70
82
|
end
|
|
71
83
|
|
|
@@ -15,18 +15,26 @@ module Shoryuken
|
|
|
15
15
|
@queues = queues.dup.uniq
|
|
16
16
|
@paused_queues = []
|
|
17
17
|
@delay = delay
|
|
18
|
+
# next_queue/messages_found run on the dispatch thread while
|
|
19
|
+
# message_processed runs on processor-completion threads (for FIFO
|
|
20
|
+
# queues), so all access to the shared state is serialized.
|
|
21
|
+
@mutex = Mutex.new
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# Returns the next queue to poll in round-robin order
|
|
21
25
|
#
|
|
22
26
|
# @return [QueueConfiguration, nil] the next queue configuration or nil if all paused
|
|
23
27
|
def next_queue
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
unpause_queues
|
|
30
|
+
queue = @queues.shift
|
|
31
|
+
if queue.nil?
|
|
32
|
+
nil
|
|
33
|
+
else
|
|
34
|
+
@queues << queue
|
|
35
|
+
QueueConfiguration.new(queue, {})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
# Handles the result of polling a queue, adjusting weight if messages were found
|
|
@@ -35,16 +43,17 @@ module Shoryuken
|
|
|
35
43
|
# @param messages_found [Integer] number of messages found
|
|
36
44
|
# @return [void]
|
|
37
45
|
def messages_found(queue, messages_found)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
@mutex.synchronize do
|
|
47
|
+
if messages_found == 0
|
|
48
|
+
pause(queue)
|
|
49
|
+
else
|
|
50
|
+
maximum_weight = maximum_queue_weight(queue)
|
|
51
|
+
current_weight = current_queue_weight(queue)
|
|
52
|
+
if maximum_weight > current_weight
|
|
53
|
+
logger.info { "Increasing #{queue} weight to #{current_weight + 1}, max: #{maximum_weight}" }
|
|
54
|
+
@queues << queue
|
|
55
|
+
end
|
|
56
|
+
end
|
|
48
57
|
end
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -52,7 +61,7 @@ module Shoryuken
|
|
|
52
61
|
#
|
|
53
62
|
# @return [Array<Array>] array of [queue_name, weight] pairs
|
|
54
63
|
def active_queues
|
|
55
|
-
unparse_queues(@queues)
|
|
64
|
+
@mutex.synchronize { unparse_queues(@queues) }
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
# Called when a message from a queue has been processed
|
|
@@ -60,10 +69,10 @@ module Shoryuken
|
|
|
60
69
|
# @param queue [String] the queue name
|
|
61
70
|
# @return [void]
|
|
62
71
|
def message_processed(queue)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
paused_queue = @paused_queues.find { |_time, name| name == queue }
|
|
74
|
+
paused_queue[0] = Time.at(0) if paused_queue
|
|
75
|
+
end
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
private
|
|
@@ -79,16 +88,21 @@ module Shoryuken
|
|
|
79
88
|
logger.debug "Paused #{queue}"
|
|
80
89
|
end
|
|
81
90
|
|
|
82
|
-
# Unpauses
|
|
91
|
+
# Unpauses the first queue whose delay has expired.
|
|
92
|
+
#
|
|
93
|
+
# Scans for any expired entry rather than only the head of the list:
|
|
94
|
+
# message_processed marks a queue ready by setting its time to the epoch,
|
|
95
|
+
# and that entry may sit behind an earlier-paused queue that is still
|
|
96
|
+
# paused - checking only the head would leave the ready queue stuck.
|
|
83
97
|
#
|
|
84
98
|
# @return [void]
|
|
85
99
|
def unpause_queues
|
|
86
|
-
|
|
87
|
-
return if
|
|
100
|
+
index = @paused_queues.index { |time, _name| time <= Time.now }
|
|
101
|
+
return if index.nil?
|
|
88
102
|
|
|
89
|
-
|
|
90
|
-
@queues <<
|
|
91
|
-
logger.debug "Unpaused #{
|
|
103
|
+
paused = @paused_queues.delete_at(index)
|
|
104
|
+
@queues << paused[1]
|
|
105
|
+
logger.debug "Unpaused #{paused[1]}"
|
|
92
106
|
end
|
|
93
107
|
|
|
94
108
|
# Returns the current weight of a queue in the active rotation
|
data/lib/shoryuken/queue.rb
CHANGED
|
@@ -52,11 +52,18 @@ module Shoryuken
|
|
|
52
52
|
failed_messages = client.delete_message_batch(
|
|
53
53
|
options.merge(queue_url: url)
|
|
54
54
|
).failed || []
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
# Log every failure (not just the first) and return a plain boolean.
|
|
57
|
+
# The previous `any? { logger.error ... }` short-circuited after the first
|
|
58
|
+
# failure - so the rest went unlogged - and only returned truthy because
|
|
59
|
+
# Logger#error happens to return true.
|
|
60
|
+
failed_messages.each do |failure|
|
|
56
61
|
logger.error do
|
|
57
62
|
"Could not delete #{failure.id}, code: '#{failure.code}', message: '#{failure.message}', sender_fault: #{failure.sender_fault}"
|
|
58
63
|
end
|
|
59
64
|
end
|
|
65
|
+
|
|
66
|
+
failed_messages.any?
|
|
60
67
|
end
|
|
61
68
|
|
|
62
69
|
# Sends a single message to the queue
|
data/lib/shoryuken/util.rb
CHANGED
|
@@ -20,7 +20,10 @@ module Shoryuken
|
|
|
20
20
|
def fire_event(event, reverse = false, event_options = {})
|
|
21
21
|
logger.debug { "Firing '#{event}' lifecycle event" }
|
|
22
22
|
arr = Shoryuken.options[:lifecycle_events][event]
|
|
23
|
-
|
|
23
|
+
# reverse (not reverse!) so the stored handler array keeps its original
|
|
24
|
+
# order: events fired more than once with reverse: true (e.g. :shutdown
|
|
25
|
+
# via stop then stop!) must not alternate their handler order.
|
|
26
|
+
arr = arr.reverse if reverse
|
|
24
27
|
arr.each do |block|
|
|
25
28
|
block.call(event_options)
|
|
26
29
|
rescue => e
|
data/lib/shoryuken/version.rb
CHANGED
data/lib/shoryuken/worker.rb
CHANGED
|
@@ -224,7 +224,11 @@ module Shoryuken
|
|
|
224
224
|
#
|
|
225
225
|
# @example Configuring exponential backoff
|
|
226
226
|
# shoryuken_options retry_intervals: [1, 5, 25, 125, 625]
|
|
227
|
-
# #
|
|
227
|
+
# # Retries after 1s, 5s, 25s, 125s, then 625s for every later attempt.
|
|
228
|
+
# # Shoryuken does not stop retrying on its own once the intervals are
|
|
229
|
+
# # exhausted - it keeps reusing the last interval. Configure an SQS
|
|
230
|
+
# # redrive policy (maxReceiveCount) to send exhausted messages to a
|
|
231
|
+
# # dead-letter queue.
|
|
228
232
|
#
|
|
229
233
|
# @see #shoryuken_options Documentation for configuring retry_intervals
|
|
230
234
|
def exponential_backoff?
|
data/lib/shoryuken.rb
CHANGED
|
@@ -58,6 +58,8 @@ module Shoryuken
|
|
|
58
58
|
:stop_callback=,
|
|
59
59
|
:active_job_queue_name_prefixing?,
|
|
60
60
|
:active_job_queue_name_prefixing=,
|
|
61
|
+
:active_job_fifo_message_deduplication?,
|
|
62
|
+
:active_job_fifo_message_deduplication=,
|
|
61
63
|
:sqs_client,
|
|
62
64
|
:sqs_client=,
|
|
63
65
|
:sqs_client_receive_message_opts,
|
data/renovate.json
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
"config:recommended"
|
|
5
5
|
],
|
|
6
6
|
"minimumReleaseAge": "7 days",
|
|
7
|
+
"includePaths": [
|
|
8
|
+
".ruby-version",
|
|
9
|
+
"Gemfile",
|
|
10
|
+
"Gemfile.lint",
|
|
11
|
+
"shoryuken.gemspec",
|
|
12
|
+
"spec/gemfiles/**",
|
|
13
|
+
"spec/integration/**/Gemfile",
|
|
14
|
+
".github/workflows/**",
|
|
15
|
+
"docker-compose.yml"
|
|
16
|
+
],
|
|
7
17
|
"github-actions": {
|
|
8
18
|
"enabled": true,
|
|
9
19
|
"pinDigests": true
|
|
@@ -46,7 +56,21 @@
|
|
|
46
56
|
"ruby/setup-ruby",
|
|
47
57
|
"ruby"
|
|
48
58
|
],
|
|
49
|
-
"groupName": "ruby setup"
|
|
59
|
+
"groupName": "ruby setup",
|
|
60
|
+
"internalChecksFilter": "strict"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"description": "Let setup-ruby pass age gate before ruby so it is ready when the group PR is created",
|
|
64
|
+
"matchPackageNames": [
|
|
65
|
+
"ruby/setup-ruby"
|
|
66
|
+
],
|
|
67
|
+
"minimumReleaseAge": "5 days"
|
|
50
68
|
}
|
|
51
|
-
]
|
|
69
|
+
],
|
|
70
|
+
"lockFileMaintenance": {
|
|
71
|
+
"enabled": true,
|
|
72
|
+
"schedule": [
|
|
73
|
+
"before 4am on the first day of the month"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
52
76
|
}
|