shoryuken 7.0.2 → 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 +32 -5
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +107 -0
- data/bin/cli/sqs.rb +7 -0
- data/bin/integrations +52 -34
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +3 -1
- data/lib/shoryuken/active_job/current_attributes.rb +35 -7
- 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 +12 -1
- 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 +16 -2
- data/spec/integration/active_job/current_attributes/cross_job_reset_spec.rb +47 -0
- data/spec/integration/active_job/current_attributes/incremental_persist_spec.rb +76 -0
- data/spec/integration/active_job/fifo_dedup_opt_out/fifo_dedup_opt_out_spec.rb +67 -0
- data/spec/integration/auto_extend_visibility/short_visibility_timeout_spec.rb +52 -0
- data/spec/integration/concurrent_processing/processor_accounting_spec.rb +94 -0
- data/spec/integration/fifo_ordering/fifo_max_messages_cap_spec.rb +96 -0
- 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/message_operations/partial_batch_delete_spec.rb +67 -0
- data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +1 -1
- data/spec/integration/non_retryable_exception/with_retry_intervals_spec.rb +115 -0
- data/spec/integrations_helper.rb +10 -9
- 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/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 +26 -2
|
@@ -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 }
|
|
@@ -302,6 +304,15 @@ module Shoryuken
|
|
|
302
304
|
@active_job_queue_name_prefixing
|
|
303
305
|
end
|
|
304
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
|
+
|
|
305
316
|
private
|
|
306
317
|
|
|
307
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
|
@@ -56,7 +56,21 @@
|
|
|
56
56
|
"ruby/setup-ruby",
|
|
57
57
|
"ruby"
|
|
58
58
|
],
|
|
59
|
-
"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"
|
|
60
68
|
}
|
|
61
|
-
]
|
|
69
|
+
],
|
|
70
|
+
"lockFileMaintenance": {
|
|
71
|
+
"enabled": true,
|
|
72
|
+
"schedule": [
|
|
73
|
+
"before 4am on the first day of the month"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
62
76
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A message without a CurrentAttributes key must not inherit a previous job's
|
|
4
|
+
# context. Every registered class is reset after each job - even keyless ones -
|
|
5
|
+
# so values set during one job don't leak into the next on a reused worker
|
|
6
|
+
# thread.
|
|
7
|
+
|
|
8
|
+
setup_sqs
|
|
9
|
+
setup_active_job
|
|
10
|
+
|
|
11
|
+
require 'active_support/current_attributes'
|
|
12
|
+
require 'shoryuken/active_job/current_attributes'
|
|
13
|
+
|
|
14
|
+
queue_name = DT.queue
|
|
15
|
+
create_test_queue(queue_name)
|
|
16
|
+
|
|
17
|
+
class LeakCurrent < ActiveSupport::CurrentAttributes
|
|
18
|
+
attribute :user_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(LeakCurrent)
|
|
22
|
+
|
|
23
|
+
class CrossJobResetTestJob < ActiveJob::Base
|
|
24
|
+
def perform
|
|
25
|
+
# Record what the worker thread sees at the start of the job, then dirty
|
|
26
|
+
# Current. Without a reset after a keyless job, the second execution on the
|
|
27
|
+
# same thread would observe 'leaked' instead of nil.
|
|
28
|
+
DT[:observed] << LeakCurrent.user_id
|
|
29
|
+
LeakCurrent.user_id = 'leaked'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
CrossJobResetTestJob.queue_as(queue_name)
|
|
34
|
+
|
|
35
|
+
# Concurrency 1 so the two jobs run sequentially on the same worker thread.
|
|
36
|
+
Shoryuken.add_group('default', 1)
|
|
37
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
38
|
+
Shoryuken.register_worker(queue_name, Shoryuken::ActiveJob::JobWrapper)
|
|
39
|
+
|
|
40
|
+
# Enqueued with an empty context, so both messages carry no cattr key.
|
|
41
|
+
CrossJobResetTestJob.perform_later
|
|
42
|
+
CrossJobResetTestJob.perform_later
|
|
43
|
+
|
|
44
|
+
poll_queues_until(timeout: 30) { DT[:observed].size >= 2 }
|
|
45
|
+
|
|
46
|
+
assert_equal([nil, nil], DT[:observed].first(2),
|
|
47
|
+
"CurrentAttributes leaked across jobs: #{DT[:observed].inspect}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CurrentAttributes registered one class per persist call are all preserved.
|
|
4
|
+
#
|
|
5
|
+
# persist derived each storage key from the per-call index, so calling it once
|
|
6
|
+
# per class (as an app might across several initializers) made the third call
|
|
7
|
+
# reuse "cattr_0" and silently overwrite the second class - its attributes were
|
|
8
|
+
# then never serialized or restored.
|
|
9
|
+
|
|
10
|
+
setup_sqs
|
|
11
|
+
setup_active_job
|
|
12
|
+
|
|
13
|
+
require 'active_support/current_attributes'
|
|
14
|
+
require 'shoryuken/active_job/current_attributes'
|
|
15
|
+
|
|
16
|
+
queue_name = DT.queue
|
|
17
|
+
create_test_queue(queue_name)
|
|
18
|
+
|
|
19
|
+
class CurrentA < ActiveSupport::CurrentAttributes
|
|
20
|
+
attribute :a_val
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class CurrentB < ActiveSupport::CurrentAttributes
|
|
24
|
+
attribute :b_val
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CurrentC < ActiveSupport::CurrentAttributes
|
|
28
|
+
attribute :c_val
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Registered incrementally, one class per call.
|
|
32
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentA)
|
|
33
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentB)
|
|
34
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentC)
|
|
35
|
+
|
|
36
|
+
# Every class must be registered, each under a distinct storage key.
|
|
37
|
+
registered = Shoryuken::ActiveJob::CurrentAttributes.cattrs
|
|
38
|
+
assert_equal(
|
|
39
|
+
%w[CurrentA CurrentB CurrentC],
|
|
40
|
+
registered.values.sort,
|
|
41
|
+
'every persisted CurrentAttributes class must be registered (none silently dropped)'
|
|
42
|
+
)
|
|
43
|
+
assert_equal(3, registered.keys.uniq.size, 'each class must use a distinct storage key')
|
|
44
|
+
|
|
45
|
+
class IncrementalCurrentJob < ActiveJob::Base
|
|
46
|
+
def perform
|
|
47
|
+
DT[:executions] << {
|
|
48
|
+
a_val: CurrentA.a_val,
|
|
49
|
+
b_val: CurrentB.b_val,
|
|
50
|
+
c_val: CurrentC.c_val
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
IncrementalCurrentJob.queue_as(queue_name)
|
|
56
|
+
|
|
57
|
+
Shoryuken.add_group('default', 1)
|
|
58
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
59
|
+
Shoryuken.register_worker(queue_name, Shoryuken::ActiveJob::JobWrapper)
|
|
60
|
+
|
|
61
|
+
CurrentA.a_val = 'a-value'
|
|
62
|
+
CurrentB.b_val = 'b-value'
|
|
63
|
+
CurrentC.c_val = 'c-value'
|
|
64
|
+
|
|
65
|
+
IncrementalCurrentJob.perform_later
|
|
66
|
+
|
|
67
|
+
CurrentA.reset
|
|
68
|
+
CurrentB.reset
|
|
69
|
+
CurrentC.reset
|
|
70
|
+
|
|
71
|
+
poll_queues_until(timeout: 30) { DT[:executions].size >= 1 }
|
|
72
|
+
|
|
73
|
+
result = DT[:executions].first
|
|
74
|
+
assert_equal('a-value', result[:a_val])
|
|
75
|
+
assert_equal('b-value', result[:b_val], 'the second incrementally-persisted class must not be dropped')
|
|
76
|
+
assert_equal('c-value', result[:c_val])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
# This spec tests opting out of the automatic FIFO message_deduplication_id that
|
|
6
|
+
# the ActiveJob adapter generates.
|
|
7
|
+
#
|
|
8
|
+
# For FIFO queues the adapter derives a content-based message_deduplication_id
|
|
9
|
+
# from the serialized job minus job_id/enqueued_at (see issues #457 / #750). That
|
|
10
|
+
# means two *distinct* enqueues of the same job class + args within SQS's 5-minute
|
|
11
|
+
# window collapse into one - the second is silently dropped. That is intentional,
|
|
12
|
+
# but it is a "skipped message" trap for users who legitimately enqueue the same
|
|
13
|
+
# work twice.
|
|
14
|
+
#
|
|
15
|
+
# Expected behavior: setting Shoryuken.active_job_fifo_message_deduplication = false
|
|
16
|
+
# stops the adapter from generating the id, so those enqueues are no longer
|
|
17
|
+
# silently deduplicated. The default (true) preserves the existing behavior.
|
|
18
|
+
#
|
|
19
|
+
# Regression: there was no way to disable the auto-generated deduplication id.
|
|
20
|
+
|
|
21
|
+
setup_active_job
|
|
22
|
+
|
|
23
|
+
class FifoOptOutJob < ActiveJob::Base
|
|
24
|
+
queue_as :fifo_opt_out
|
|
25
|
+
|
|
26
|
+
def perform(_arg); end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
fifo_queue_mock = Object.new
|
|
30
|
+
fifo_queue_mock.define_singleton_method(:fifo?) { true }
|
|
31
|
+
fifo_queue_mock.define_singleton_method(:name) { 'fifo_opt_out.fifo' }
|
|
32
|
+
|
|
33
|
+
captured = nil
|
|
34
|
+
fifo_queue_mock.define_singleton_method(:send_message) { |params| captured = params }
|
|
35
|
+
|
|
36
|
+
Shoryuken::Client.define_singleton_method(:queues) { |_queue_name = nil| fifo_queue_mock }
|
|
37
|
+
Shoryuken.define_singleton_method(:register_worker) { |*| nil }
|
|
38
|
+
|
|
39
|
+
# Default behavior: the adapter sets a content-based dedup id (excluding the
|
|
40
|
+
# per-enqueue job_id/enqueued_at), so two identical jobs would collapse to one.
|
|
41
|
+
FifoOptOutJob.perform_later('same-args')
|
|
42
|
+
|
|
43
|
+
assert(
|
|
44
|
+
captured.key?(:message_deduplication_id),
|
|
45
|
+
'by default a FIFO ActiveJob send gets an auto-generated message_deduplication_id'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
body = captured[:message_body]
|
|
49
|
+
expected = Digest::SHA256.hexdigest(JSON.dump(body.except('job_id', 'enqueued_at')))
|
|
50
|
+
assert_equal(expected, captured[:message_deduplication_id])
|
|
51
|
+
|
|
52
|
+
# Opt-out: with deduplication disabled the adapter no longer sets a dedup id, so
|
|
53
|
+
# distinct enqueues of identical jobs are not silently dropped.
|
|
54
|
+
Shoryuken.active_job_fifo_message_deduplication = false
|
|
55
|
+
captured = nil
|
|
56
|
+
FifoOptOutJob.perform_later('same-args')
|
|
57
|
+
|
|
58
|
+
refute(
|
|
59
|
+
captured.key?(:message_deduplication_id),
|
|
60
|
+
'with deduplication disabled the adapter must not set message_deduplication_id'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# An explicit deduplication id must still be honored even when auto-generation is off.
|
|
64
|
+
captured = nil
|
|
65
|
+
FifoOptOutJob.set(message_deduplication_id: 'explicit-id').perform_later('same-args')
|
|
66
|
+
|
|
67
|
+
assert_equal('explicit-id', captured[:message_deduplication_id])
|