shoryuken 7.0.2 → 7.0.4
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/.yard-lint.yml +29 -1
- data/CHANGELOG.md +171 -0
- data/Gemfile.lint.lock +8 -7
- data/bin/cli/sqs.rb +67 -3
- data/bin/integrations +52 -34
- data/lib/active_job/extensions.rb +5 -8
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +6 -5
- data/lib/shoryuken/active_job/current_attributes.rb +33 -7
- data/lib/shoryuken/body_parser.rb +1 -2
- data/lib/shoryuken/client.rb +15 -6
- data/lib/shoryuken/default_exception_handler.rb +2 -3
- data/lib/shoryuken/errors.rb +2 -4
- data/lib/shoryuken/fetcher.rb +7 -1
- data/lib/shoryuken/helpers/atomic_counter.rb +6 -9
- data/lib/shoryuken/helpers/atomic_hash.rb +9 -15
- data/lib/shoryuken/helpers/hash_utils.rb +5 -7
- data/lib/shoryuken/helpers/string_utils.rb +6 -8
- data/lib/shoryuken/helpers/timer_task.rb +19 -2
- data/lib/shoryuken/inline_message.rb +3 -4
- data/lib/shoryuken/launcher.rb +19 -8
- data/lib/shoryuken/manager.rb +65 -6
- data/lib/shoryuken/message.rb +2 -3
- data/lib/shoryuken/middleware/chain.rb +7 -13
- data/lib/shoryuken/middleware/entry.rb +1 -2
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +30 -2
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +40 -18
- data/lib/shoryuken/middleware/server/non_retryable_exception.rb +17 -8
- data/lib/shoryuken/middleware/server/timing.rb +2 -3
- data/lib/shoryuken/options.rb +37 -4
- data/lib/shoryuken/polling/base_strategy.rb +16 -23
- data/lib/shoryuken/polling/queue_configuration.rb +8 -12
- data/lib/shoryuken/polling/strict_priority.rb +26 -14
- data/lib/shoryuken/polling/weighted_round_robin.rb +40 -27
- data/lib/shoryuken/queue.rb +41 -4
- data/lib/shoryuken/util.rb +4 -1
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker/default_executor.rb +11 -5
- data/lib/shoryuken/worker/inline_executor.rb +10 -5
- data/lib/shoryuken/worker.rb +15 -13
- data/lib/shoryuken.rb +6 -3
- data/renovate.json +16 -2
- data/shoryuken.gemspec +9 -0
- 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/cli/find_all_spec.rb +120 -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/client_spec.rb +62 -0
- 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 +147 -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 +88 -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 +123 -0
- data/spec/lib/shoryuken/util_spec.rb +26 -0
- data/spec/lib/shoryuken/worker/default_executor_spec.rb +13 -0
- data/spec/lib/shoryuken/worker/inline_executor_spec.rb +12 -0
- data/spec/shared_examples_for_active_job.rb +18 -0
- data/spec/spec_helper.rb +40 -20
- metadata +35 -3
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Middleware provides a way to wrap message processing with custom logic,
|
|
5
|
-
#
|
|
6
|
-
# side and can perform setup, teardown, error handling, and monitoring around
|
|
7
|
-
# job execution.
|
|
4
|
+
# Middleware provides a way to wrap message processing with custom logic, similar to Rack middleware in web applications.
|
|
5
|
+
# Middleware runs on the server side and can perform setup, teardown, error handling, and monitoring around job execution.
|
|
8
6
|
#
|
|
9
7
|
# Middleware classes must implement a `call` method that accepts the worker instance,
|
|
10
8
|
# queue name, and SQS message, and must yield to continue the middleware chain.
|
|
@@ -84,9 +82,8 @@ module Shoryuken
|
|
|
84
82
|
# @see Shoryuken::Middleware::Chain Middleware chain management
|
|
85
83
|
# @see https://github.com/ruby-shoryuken/shoryuken/wiki/Middleware Comprehensive middleware guide
|
|
86
84
|
module Middleware
|
|
87
|
-
# Manages a chain of middleware classes that will be instantiated and invoked
|
|
88
|
-
#
|
|
89
|
-
# and reordering middleware.
|
|
85
|
+
# Manages a chain of middleware classes that will be instantiated and invoked in sequence around message processing.
|
|
86
|
+
# Provides methods for adding, removing, and reordering middleware.
|
|
90
87
|
class Chain
|
|
91
88
|
# @return [Array<Entry>] The ordered list of middleware entries
|
|
92
89
|
attr_reader :entries
|
|
@@ -121,8 +118,7 @@ module Shoryuken
|
|
|
121
118
|
entries.delete_if { |entry| entry.klass == klass }
|
|
122
119
|
end
|
|
123
120
|
|
|
124
|
-
# Adds middleware to the end of the chain. Does nothing if the middleware
|
|
125
|
-
# class is already present in the chain.
|
|
121
|
+
# Adds middleware to the end of the chain. Does nothing if the middleware class is already present in the chain.
|
|
126
122
|
#
|
|
127
123
|
# @param klass [Class] The middleware class to add
|
|
128
124
|
# @param args [Array] Arguments to pass to the middleware constructor
|
|
@@ -132,8 +128,7 @@ module Shoryuken
|
|
|
132
128
|
entries << Entry.new(klass, *args) unless exists?(klass)
|
|
133
129
|
end
|
|
134
130
|
|
|
135
|
-
# Adds middleware to the beginning of the chain. Does nothing if the middleware
|
|
136
|
-
# class is already present in the chain.
|
|
131
|
+
# Adds middleware to the beginning of the chain. Does nothing if the middleware class is already present in the chain.
|
|
137
132
|
#
|
|
138
133
|
# @param klass [Class] The middleware class to prepend
|
|
139
134
|
# @param args [Array] Arguments to pass to the middleware constructor
|
|
@@ -195,8 +190,7 @@ module Shoryuken
|
|
|
195
190
|
entries.clear
|
|
196
191
|
end
|
|
197
192
|
|
|
198
|
-
# Invokes the middleware chain with the given arguments.
|
|
199
|
-
# Each middleware's call method will be invoked in sequence,
|
|
193
|
+
# Invokes the middleware chain with the given arguments. Each middleware's call method will be invoked in sequence,
|
|
200
194
|
# with control passed through yielding.
|
|
201
195
|
#
|
|
202
196
|
# @param args [Array] arguments to pass to each middleware
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
4
|
module Middleware
|
|
5
|
-
# Represents an entry in a middleware chain, storing the middleware class
|
|
6
|
-
# and any arguments needed for its instantiation.
|
|
5
|
+
# Represents an entry in a middleware chain, storing the middleware class and any arguments needed for its instantiation.
|
|
7
6
|
#
|
|
8
7
|
# @api private
|
|
9
8
|
class Entry
|
|
@@ -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,23 @@ 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 visibility before it expires, or nil when the
|
|
77
|
+
# visibility timeout is too short to schedule one. Normally this is EXTEND_UPFRONT_SECONDS before expiry, but for
|
|
78
|
+
# short timeouts (<= EXTEND_UPFRONT_SECONDS) it falls back to half the timeout so the timer still fires in time
|
|
79
|
+
# instead of TimerTask raising on a non-positive interval.
|
|
80
|
+
#
|
|
81
|
+
# @param visibility_timeout [Integer] the queue's visibility timeout in seconds
|
|
82
|
+
# @return [Float, Integer, nil] the execution interval, or nil if too short
|
|
83
|
+
def extension_interval(visibility_timeout)
|
|
84
|
+
return nil if visibility_timeout <= 0
|
|
85
|
+
|
|
86
|
+
interval = visibility_timeout - EXTEND_UPFRONT_SECONDS
|
|
87
|
+
interval = visibility_timeout / 2.0 if interval <= 0
|
|
88
|
+
interval
|
|
89
|
+
end
|
|
64
90
|
end
|
|
65
91
|
|
|
66
92
|
# Creates and starts a visibility extension timer
|
|
@@ -71,7 +97,9 @@ module Shoryuken
|
|
|
71
97
|
# @param body [Object] the parsed message body
|
|
72
98
|
# @return [Shoryuken::Helpers::TimerTask] the started timer
|
|
73
99
|
def auto_visibility_timer(worker, queue, sqs_msg, body)
|
|
74
|
-
MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body)
|
|
100
|
+
timer = MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body)
|
|
101
|
+
timer&.execute
|
|
102
|
+
timer
|
|
75
103
|
end
|
|
76
104
|
end
|
|
77
105
|
end
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
module Shoryuken
|
|
4
4
|
module Middleware
|
|
5
5
|
module Server
|
|
6
|
-
# Middleware that implements exponential backoff retry for failed messages.
|
|
7
|
-
#
|
|
8
|
-
# configured retry intervals.
|
|
6
|
+
# Middleware that implements exponential backoff retry for failed messages. When a job fails, the message visibility
|
|
7
|
+
# timeout is adjusted based on configured retry intervals.
|
|
9
8
|
class ExponentialBackoffRetry
|
|
10
9
|
include Util
|
|
11
10
|
|
|
@@ -17,8 +16,8 @@ module Shoryuken
|
|
|
17
16
|
# @param _body [Object] the parsed message body (unused)
|
|
18
17
|
# @yield continues to the next middleware in the chain
|
|
19
18
|
# @return [void]
|
|
20
|
-
# @raise [StandardError] re-raises the original exception if retry intervals are not configured
|
|
21
|
-
#
|
|
19
|
+
# @raise [StandardError] re-raises the original exception if retry intervals are not configured,
|
|
20
|
+
# if retry limit is exceeded, or if the exception is classified as non-retryable
|
|
22
21
|
def call(worker, _queue, sqs_msg, _body)
|
|
23
22
|
return yield unless worker.class.exponential_backoff?
|
|
24
23
|
|
|
@@ -28,19 +27,31 @@ module Shoryuken
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
started_at = Time.now
|
|
31
|
-
yield
|
|
32
|
-
rescue => e
|
|
33
|
-
retry_intervals = worker.class.get_shoryuken_options['retry_intervals']
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
# Scope the rescue to the single-message yield only. A method-level
|
|
32
|
+
# rescue would also catch the batch `return yield` above, then route the
|
|
33
|
+
# Array into handle_failure (which calls #attributes/#message_id on it),
|
|
34
|
+
# raising a NoMethodError that masks the original worker error.
|
|
35
|
+
begin
|
|
36
|
+
yield
|
|
37
|
+
rescue => e
|
|
38
|
+
worker_options = worker.class.get_shoryuken_options
|
|
39
|
+
retry_intervals = worker_options['retry_intervals']
|
|
40
|
+
|
|
41
|
+
# Non-retryable exceptions must not be backoff retried; re-raise so the
|
|
42
|
+
# NonRetryableException middleware can delete the message immediately.
|
|
43
|
+
raise if NonRetryableException.non_retryable?(e, worker_options['non_retryable_exceptions'])
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
|
|
46
|
+
# Re-raise the exception if the job is not going to be exponential backoff retried.
|
|
47
|
+
# This allows custom middleware (like exception notifiers) to be aware of the unhandled failure.
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
logger.warn { "Message #{sqs_msg.message_id} will attempt retry due to error: #{e.message}" }
|
|
52
|
+
# since we didn't raise, lets log the backtrace for debugging purposes.
|
|
53
|
+
logger.debug { e.backtrace.join("\n") } unless e.backtrace.nil?
|
|
54
|
+
end
|
|
44
55
|
end
|
|
45
56
|
|
|
46
57
|
private
|
|
@@ -64,11 +75,14 @@ module Shoryuken
|
|
|
64
75
|
#
|
|
65
76
|
# @param interval [Integer] the desired interval
|
|
66
77
|
# @param started_at [Time] when processing started
|
|
67
|
-
# @return [Integer] the capped visibility timeout
|
|
78
|
+
# @return [Integer] the capped visibility timeout (never negative)
|
|
68
79
|
def next_visibility_timeout(interval, started_at)
|
|
69
80
|
max_timeout = 43_200 - (Time.now - started_at).ceil - 1
|
|
70
81
|
interval = max_timeout if interval > max_timeout
|
|
71
|
-
|
|
82
|
+
# Never go negative: a job that ran longer than the 12h SQS ceiling
|
|
83
|
+
# drives max_timeout below zero, and change_message_visibility rejects
|
|
84
|
+
# a negative timeout. Clamp to 0 (retry as soon as possible) instead.
|
|
85
|
+
[interval.to_i, 0].max
|
|
72
86
|
end
|
|
73
87
|
|
|
74
88
|
# Handles a message failure by adjusting visibility timeout
|
|
@@ -87,6 +101,14 @@ module Shoryuken
|
|
|
87
101
|
logger.info { "Message #{sqs_msg.message_id} failed, will be retried in #{interval} seconds" }
|
|
88
102
|
|
|
89
103
|
true
|
|
104
|
+
rescue => e
|
|
105
|
+
# Rescheduling itself failed (e.g. an expired receipt handle, or a
|
|
106
|
+
# transient SQS error). Don't let that exception escape and mask the
|
|
107
|
+
# original worker failure - log it and report "not retried" so the
|
|
108
|
+
# caller re-raises the original error and the message falls back to the
|
|
109
|
+
# queue's default visibility timeout.
|
|
110
|
+
logger.warn { "Failed to set backoff visibility timeout for #{sqs_msg.message_id}: #{e.message}" }
|
|
111
|
+
false
|
|
90
112
|
end
|
|
91
113
|
end
|
|
92
114
|
end
|
|
@@ -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]
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
module Shoryuken
|
|
4
4
|
module Middleware
|
|
5
5
|
module Server
|
|
6
|
-
# Middleware that logs timing information for message processing.
|
|
7
|
-
#
|
|
8
|
-
# exceeds the queue's visibility timeout.
|
|
6
|
+
# Middleware that logs timing information for message processing. Records start time, completion time, and warns if
|
|
7
|
+
# processing exceeds the queue's visibility timeout.
|
|
9
8
|
class Timing
|
|
10
9
|
include Util
|
|
11
10
|
|
data/lib/shoryuken/options.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Stores and manages all Shoryuken configuration options.
|
|
5
|
-
#
|
|
6
|
-
# middleware, and other runtime configurations.
|
|
4
|
+
# Stores and manages all Shoryuken configuration options. This class is used internally to hold settings for workers,
|
|
5
|
+
# queues, middleware, and other runtime configurations.
|
|
7
6
|
class Options
|
|
8
7
|
# Default configuration values for Shoryuken
|
|
9
8
|
DEFAULTS = {
|
|
@@ -34,7 +33,9 @@ module Shoryuken
|
|
|
34
33
|
# @return [Class] the executor class for running workers
|
|
35
34
|
# @return [Shoryuken::WorkerRegistry] the registry for worker classes
|
|
36
35
|
# @return [Array<#call>] handlers for processing exceptions
|
|
37
|
-
attr_accessor :active_job_queue_name_prefixing, :
|
|
36
|
+
attr_accessor :active_job_queue_name_prefixing, :active_job_fifo_message_deduplication,
|
|
37
|
+
: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,8 @@ 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
|
|
58
|
+
self.fifo_message_deduplication = true
|
|
56
59
|
self.worker_executor = Worker::DefaultExecutor
|
|
57
60
|
self.cache_visibility_timeout = false
|
|
58
61
|
self.reloader = proc { |&block| block.call }
|
|
@@ -302,6 +305,36 @@ module Shoryuken
|
|
|
302
305
|
@active_job_queue_name_prefixing
|
|
303
306
|
end
|
|
304
307
|
|
|
308
|
+
# Checks if the ActiveJob adapter should auto-generate a content-based message_deduplication_id for FIFO queues. When
|
|
309
|
+
# disabled, distinct enqueues 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
|
+
|
|
316
|
+
# Checks whether Shoryuken auto-generates a content-based
|
|
317
|
+
# message_deduplication_id for raw FIFO sends (Worker.perform_async,
|
|
318
|
+
# Queue#send_message / #send_messages). When disabled, distinct sends of an
|
|
319
|
+
# identical body are no longer silently deduplicated by SQS - you must then
|
|
320
|
+
# provide a message_deduplication_id yourself or enable
|
|
321
|
+
# ContentBasedDeduplication on the queue.
|
|
322
|
+
#
|
|
323
|
+
# ActiveJob enqueues are primarily controlled by
|
|
324
|
+
# #active_job_fifo_message_deduplication?. When that is enabled the adapter
|
|
325
|
+
# sets a content-based message_deduplication_id itself (from the body minus
|
|
326
|
+
# job_id/enqueued_at) before the raw send, so this flag is a no-op for them.
|
|
327
|
+
# When it is disabled the adapter omits the id and this flag governs the
|
|
328
|
+
# fallback: left enabled (the default) each enqueue still gets a hash of the
|
|
329
|
+
# full serialized body - which includes the per-enqueue job_id, so distinct
|
|
330
|
+
# enqueues are NOT deduplicated - while disabling it drops the id entirely
|
|
331
|
+
# (then requiring an explicit id or the queue's ContentBasedDeduplication).
|
|
332
|
+
#
|
|
333
|
+
# @return [Boolean] true if FIFO deduplication id generation is enabled
|
|
334
|
+
def fifo_message_deduplication?
|
|
335
|
+
@fifo_message_deduplication
|
|
336
|
+
end
|
|
337
|
+
|
|
305
338
|
private
|
|
306
339
|
|
|
307
340
|
# Creates the default server middleware chain
|
|
@@ -5,10 +5,9 @@ module Shoryuken
|
|
|
5
5
|
module Polling
|
|
6
6
|
# Abstract base class for queue polling strategies.
|
|
7
7
|
#
|
|
8
|
-
# This class defines the interface that all polling strategies must implement
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# how to handle scenarios where queues have no messages available.
|
|
8
|
+
# This class defines the interface that all polling strategies must implement to manage queue selection and message flow
|
|
9
|
+
# control in Shoryuken workers. Polling strategies determine which queue to fetch messages from next and how to handle
|
|
10
|
+
# scenarios where queues have no messages available.
|
|
12
11
|
#
|
|
13
12
|
# @abstract Subclass and override {#next_queue}, {#messages_found}, and {#active_queues}
|
|
14
13
|
# to implement a custom polling strategy.
|
|
@@ -42,9 +41,8 @@ module Shoryuken
|
|
|
42
41
|
|
|
43
42
|
# Returns the next queue to poll for messages.
|
|
44
43
|
#
|
|
45
|
-
# This method should return a QueueConfiguration object representing
|
|
46
|
-
#
|
|
47
|
-
# queues are currently available for polling.
|
|
44
|
+
# This method should return a QueueConfiguration object representing the next queue that should be polled for messages,
|
|
45
|
+
# or nil if no queues are currently available for polling.
|
|
48
46
|
#
|
|
49
47
|
# @abstract Subclasses must implement this method
|
|
50
48
|
# @return [QueueConfiguration, nil] Next queue to poll, or nil if none available
|
|
@@ -55,10 +53,9 @@ module Shoryuken
|
|
|
55
53
|
|
|
56
54
|
# Called when messages are found (or not found) in a queue.
|
|
57
55
|
#
|
|
58
|
-
# This method is invoked after polling a queue to inform the strategy
|
|
59
|
-
# about
|
|
60
|
-
#
|
|
61
|
-
# such as pausing empty queues or adjusting queue weights.
|
|
56
|
+
# This method is invoked after polling a queue to inform the strategy about the number of messages that were retrieved.
|
|
57
|
+
# Strategies can use this information to make decisions about future polling behavior, such as pausing empty queues or
|
|
58
|
+
# adjusting queue weights.
|
|
62
59
|
#
|
|
63
60
|
# @abstract Subclasses must implement this method
|
|
64
61
|
# @param _queue [String] The name of the queue that was polled
|
|
@@ -70,9 +67,8 @@ module Shoryuken
|
|
|
70
67
|
|
|
71
68
|
# Called when a message from a queue has been processed.
|
|
72
69
|
#
|
|
73
|
-
# This optional callback is invoked after a message has been successfully
|
|
74
|
-
#
|
|
75
|
-
# or to adjust their polling behavior.
|
|
70
|
+
# This optional callback is invoked after a message has been successfully processed by a worker. Strategies can use
|
|
71
|
+
# this information for cleanup or to adjust their polling behavior.
|
|
76
72
|
#
|
|
77
73
|
# @param _queue [String] The name of the queue whose message was processed
|
|
78
74
|
# @return [void]
|
|
@@ -80,9 +76,8 @@ module Shoryuken
|
|
|
80
76
|
|
|
81
77
|
# Returns the list of currently active queues.
|
|
82
78
|
#
|
|
83
|
-
# This method should return an array representing the queues that are
|
|
84
|
-
#
|
|
85
|
-
# strategy implementation.
|
|
79
|
+
# This method should return an array representing the queues that are currently active and available for polling. The
|
|
80
|
+
# format may vary by strategy implementation.
|
|
86
81
|
#
|
|
87
82
|
# @abstract Subclasses must implement this method
|
|
88
83
|
# @return [Array] List of active queues
|
|
@@ -93,9 +88,8 @@ module Shoryuken
|
|
|
93
88
|
|
|
94
89
|
# Compares this strategy with another object for equality.
|
|
95
90
|
#
|
|
96
|
-
# Two strategies are considered equal if they have the same active queues.
|
|
97
|
-
#
|
|
98
|
-
# compatibility.
|
|
91
|
+
# Two strategies are considered equal if they have the same active queues. This method also supports comparison with
|
|
92
|
+
# Array objects for backward compatibility.
|
|
99
93
|
#
|
|
100
94
|
# @param other [Object] Object to compare with
|
|
101
95
|
# @return [Boolean] true if strategies are equivalent
|
|
@@ -114,9 +108,8 @@ module Shoryuken
|
|
|
114
108
|
|
|
115
109
|
# Returns the delay time for pausing empty queues.
|
|
116
110
|
#
|
|
117
|
-
# This method returns the amount of time (in seconds) that empty queues
|
|
118
|
-
#
|
|
119
|
-
# the strategy level or falls back to the global Shoryuken delay setting.
|
|
111
|
+
# This method returns the amount of time (in seconds) that empty queues should be paused before being polled again. The
|
|
112
|
+
# delay can be set at the strategy level or falls back to the global Shoryuken delay setting.
|
|
120
113
|
#
|
|
121
114
|
# @return [Float] Delay time in seconds
|
|
122
115
|
def delay
|
|
@@ -4,9 +4,8 @@ module Shoryuken
|
|
|
4
4
|
module Polling
|
|
5
5
|
# Configuration object representing a queue and its associated options.
|
|
6
6
|
#
|
|
7
|
-
# This class encapsulates a queue name along with any polling-specific
|
|
8
|
-
#
|
|
9
|
-
# information between polling strategies and the message fetching system.
|
|
7
|
+
# This class encapsulates a queue name along with any polling-specific options or metadata. It provides a structured way
|
|
8
|
+
# to pass queue information between polling strategies and the message fetching system.
|
|
10
9
|
#
|
|
11
10
|
# The class extends Struct to provide attribute accessors for name and options
|
|
12
11
|
# while adding custom behavior for equality comparison and string representation.
|
|
@@ -32,9 +31,8 @@ module Shoryuken
|
|
|
32
31
|
QueueConfiguration = Struct.new(:name, :options) do
|
|
33
32
|
# Generates a hash value based on the queue name.
|
|
34
33
|
#
|
|
35
|
-
# This method ensures that QueueConfiguration objects can be used
|
|
36
|
-
#
|
|
37
|
-
# will have the same hash value regardless of their options.
|
|
34
|
+
# This method ensures that QueueConfiguration objects can be used as hash keys and that configurations with the same
|
|
35
|
+
# queue name will have the same hash value regardless of their options.
|
|
38
36
|
#
|
|
39
37
|
# @return [Integer] Hash value based on the queue name
|
|
40
38
|
def hash
|
|
@@ -43,9 +41,8 @@ module Shoryuken
|
|
|
43
41
|
|
|
44
42
|
# Compares this configuration with another object for equality.
|
|
45
43
|
#
|
|
46
|
-
# Two QueueConfiguration objects are equal if they have the same name
|
|
47
|
-
#
|
|
48
|
-
# also be compared directly with a string queue name.
|
|
44
|
+
# Two QueueConfiguration objects are equal if they have the same name and options. For convenience, a configuration
|
|
45
|
+
# with empty options can also be compared directly with a string queue name.
|
|
49
46
|
#
|
|
50
47
|
# @param other [Object] The object to compare with
|
|
51
48
|
# @return [Boolean] true if the objects are considered equal
|
|
@@ -78,9 +75,8 @@ module Shoryuken
|
|
|
78
75
|
|
|
79
76
|
# Returns a string representation of the queue configuration.
|
|
80
77
|
#
|
|
81
|
-
# For configurations with empty options, returns just the queue name.
|
|
82
|
-
#
|
|
83
|
-
# showing both the name and the options hash.
|
|
78
|
+
# For configurations with empty options, returns just the queue name. For configurations with options, returns a
|
|
79
|
+
# detailed representation showing both the name and the options hash.
|
|
84
80
|
#
|
|
85
81
|
# @return [String] String representation of the configuration
|
|
86
82
|
#
|
|
@@ -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,20 @@ 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: message_processed marks a queue ready by setting
|
|
94
|
+
# its time to the epoch, and that entry may sit behind an earlier-paused queue that is still paused - checking only the
|
|
95
|
+
# head would leave the ready queue stuck.
|
|
83
96
|
#
|
|
84
97
|
# @return [void]
|
|
85
98
|
def unpause_queues
|
|
86
|
-
|
|
87
|
-
return if
|
|
99
|
+
index = @paused_queues.index { |time, _name| time <= Time.now }
|
|
100
|
+
return if index.nil?
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
@queues <<
|
|
91
|
-
logger.debug "Unpaused #{
|
|
102
|
+
paused = @paused_queues.delete_at(index)
|
|
103
|
+
@queues << paused[1]
|
|
104
|
+
logger.debug "Unpaused #{paused[1]}"
|
|
92
105
|
end
|
|
93
106
|
|
|
94
107
|
# Returns the current weight of a queue in the active rotation
|