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
|
@@ -13,7 +13,7 @@ module ActiveJob
|
|
|
13
13
|
# Shoryuken adapter for Active Job.
|
|
14
14
|
# To use Shoryuken set the queue_adapter config to +:shoryuken+.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
16
|
+
# Example Rails configuration:
|
|
17
17
|
# Rails.application.config.active_job.queue_adapter = :shoryuken
|
|
18
18
|
|
|
19
19
|
# Determine the appropriate base class based on Rails version
|
|
@@ -63,9 +63,8 @@ module ActiveJob
|
|
|
63
63
|
|
|
64
64
|
# Indicates whether Shoryuken is in the process of shutting down.
|
|
65
65
|
#
|
|
66
|
-
# This method is required for ActiveJob Continuations support (Rails 8.1+).
|
|
67
|
-
#
|
|
68
|
-
# and gracefully interrupt execution to allow for resumption after restart.
|
|
66
|
+
# This method is required for ActiveJob Continuations support (Rails 8.1+). When true, it signals to jobs that they
|
|
67
|
+
# should checkpoint their progress and gracefully interrupt execution to allow for resumption after restart.
|
|
69
68
|
#
|
|
70
69
|
# @return [Boolean] true if Shoryuken is shutting down, false otherwise
|
|
71
70
|
# @see https://github.com/rails/rails/pull/55127 Rails ActiveJob Continuations
|
|
@@ -177,9 +176,11 @@ module ActiveJob
|
|
|
177
176
|
message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
if queue.fifo?
|
|
179
|
+
if queue.fifo? && Shoryuken.active_job_fifo_message_deduplication?
|
|
181
180
|
# See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
|
|
182
181
|
# https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
|
|
182
|
+
# Disable via Shoryuken.active_job_fifo_message_deduplication = false when distinct
|
|
183
|
+
# enqueues of the same job class and arguments must not be silently deduplicated.
|
|
183
184
|
msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
|
|
184
185
|
JSON.dump(body.except('job_id', 'enqueued_at'))
|
|
185
186
|
)
|
|
@@ -11,7 +11,10 @@ module Shoryuken
|
|
|
11
11
|
# This ensures that request-scoped context (like current user, tenant, locale)
|
|
12
12
|
# automatically flows from the code that enqueues a job to the job's execution.
|
|
13
13
|
#
|
|
14
|
-
# Based on Sidekiq's approach to persisting current attributes
|
|
14
|
+
# Based on Sidekiq's approach to persisting current attributes, with one deliberate difference in cleanup: Sidekiq only
|
|
15
|
+
# touches the classes carried by a job and leaves the general reset to the Rails executor it runs jobs inside. Shoryuken
|
|
16
|
+
# does not run jobs inside that executor by default, so the loader resets every registered class after each job itself -
|
|
17
|
+
# see the note on {Loading#perform}.
|
|
15
18
|
#
|
|
16
19
|
# @example Setup in initializer
|
|
17
20
|
# require 'shoryuken/active_job/current_attributes'
|
|
@@ -58,8 +61,12 @@ module Shoryuken
|
|
|
58
61
|
def persist(*klasses)
|
|
59
62
|
@cattrs ||= {}
|
|
60
63
|
|
|
61
|
-
klasses.flatten.
|
|
62
|
-
|
|
64
|
+
klasses.flatten.each do |klass|
|
|
65
|
+
# Key off the running registry size, not the per-call index, so that
|
|
66
|
+
# registering classes across separate persist calls still produces
|
|
67
|
+
# distinct keys (a per-call index restarts at 0 each call and would
|
|
68
|
+
# overwrite earlier registrations).
|
|
69
|
+
key = @cattrs.empty? ? 'cattr' : "cattr_#{@cattrs.size}"
|
|
63
70
|
@cattrs[key] = klass.to_s
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -109,13 +116,10 @@ module Shoryuken
|
|
|
109
116
|
# @param hash [Hash] the deserialized job data
|
|
110
117
|
# @return [void]
|
|
111
118
|
def perform(sqs_msg, hash)
|
|
112
|
-
klasses_to_reset = []
|
|
113
|
-
|
|
114
119
|
CurrentAttributes.cattrs&.each do |key, klass_name|
|
|
115
120
|
next unless hash.key?(key)
|
|
116
121
|
|
|
117
122
|
klass = klass_name.constantize
|
|
118
|
-
klasses_to_reset << klass
|
|
119
123
|
|
|
120
124
|
begin
|
|
121
125
|
attrs = Serializer.deserialize(hash[key])
|
|
@@ -131,7 +135,29 @@ module Shoryuken
|
|
|
131
135
|
|
|
132
136
|
super
|
|
133
137
|
ensure
|
|
134
|
-
|
|
138
|
+
# Reset every registered CurrentAttributes class after the job - not
|
|
139
|
+
# only the ones whose key was in this message.
|
|
140
|
+
#
|
|
141
|
+
# Why unconditional (and why this differs from Sidekiq): Sidekiq's
|
|
142
|
+
# loader only touches classes present in the job and relies on the
|
|
143
|
+
# Rails executor - which it runs every job inside - to reset all
|
|
144
|
+
# CurrentAttributes between units of work. Shoryuken has no such safety
|
|
145
|
+
# net: it wraps a job in the reloader/executor only when
|
|
146
|
+
# `enable_reloading` is set, which is off by default, so nothing else
|
|
147
|
+
# clears CurrentAttributes between jobs.
|
|
148
|
+
#
|
|
149
|
+
# A blanket reset here is therefore the only thing guaranteeing a clean
|
|
150
|
+
# thread. Resetting only the present keys leaks whenever a value ends up
|
|
151
|
+
# set during a job whose message carried no cattr key - e.g. the worker
|
|
152
|
+
# (or code it calls) writes to Current, on a keyless message (empty
|
|
153
|
+
# context at enqueue, a different producer, or persist configured after
|
|
154
|
+
# the message was queued). CurrentAttributes are thread-local and the
|
|
155
|
+
# pool reuses threads, so that value would surface in the next job.
|
|
156
|
+
CurrentAttributes.cattrs&.each_value do |klass_name|
|
|
157
|
+
klass_name.constantize.reset
|
|
158
|
+
rescue => e
|
|
159
|
+
Shoryuken.logger.warn("Failed to reset CurrentAttributes #{klass_name}: #{e.message}")
|
|
160
|
+
end
|
|
135
161
|
end
|
|
136
162
|
end
|
|
137
163
|
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Parses SQS message bodies according to worker configuration.
|
|
5
|
-
# Supports JSON parsing, text extraction, custom Procs, and
|
|
4
|
+
# Parses SQS message bodies according to worker configuration. Supports JSON parsing, text extraction, custom Procs, and
|
|
6
5
|
# any object that responds to parse or load methods.
|
|
7
6
|
class BodyParser
|
|
8
7
|
class << self
|
data/lib/shoryuken/client.rb
CHANGED
|
@@ -4,16 +4,22 @@ module Shoryuken
|
|
|
4
4
|
# Client class for interacting with SQS queues.
|
|
5
5
|
# Provides a simple interface for accessing and managing queue instances.
|
|
6
6
|
class Client
|
|
7
|
-
#
|
|
7
|
+
# Cached queue instances by name (Hash{String => Shoryuken::Queue}).
|
|
8
8
|
@@queues = {}
|
|
9
9
|
|
|
10
|
+
# Guards the queue cache. queues is called concurrently from the dispatch
|
|
11
|
+
# thread, processor-completion threads and worker threads, and building a
|
|
12
|
+
# Shoryuken::Queue makes SQS API calls, so an unsynchronized `||=` would let
|
|
13
|
+
# several callers build the same queue (and corrupt the hash on JRuby).
|
|
14
|
+
@@queues_mutex = Mutex.new
|
|
15
|
+
|
|
10
16
|
class << self
|
|
11
17
|
# Returns a Queue instance for the given queue name
|
|
12
18
|
#
|
|
13
19
|
# @param name [String, Symbol] the name of the queue
|
|
14
20
|
# @return [Shoryuken::Queue] the queue instance
|
|
15
21
|
def queues(name)
|
|
16
|
-
@@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name)
|
|
22
|
+
@@queues_mutex.synchronize { @@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name) }
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
# Returns the current SQS client
|
|
@@ -29,10 +35,13 @@ module Shoryuken
|
|
|
29
35
|
# @return [Aws::SQS::Client] the SQS client
|
|
30
36
|
def sqs=(sqs)
|
|
31
37
|
# Since the @@queues values (Shoryuken::Queue objects) are built referencing @@sqs, if it changes, we need to
|
|
32
|
-
# re-build them on subsequent calls to `.queues(name)`.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
# re-build them on subsequent calls to `.queues(name)`. Swap the client and reset the cache inside the same
|
|
39
|
+
# critical section: otherwise a concurrent `queues` call could slip between the reset and the assignment,
|
|
40
|
+
# read the old client, and cache a queue built with it that survives the replacement.
|
|
41
|
+
@@queues_mutex.synchronize do
|
|
42
|
+
Shoryuken.sqs_client = sqs
|
|
43
|
+
@@queues = {}
|
|
44
|
+
end
|
|
36
45
|
end
|
|
37
46
|
end
|
|
38
47
|
end
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Default exception handler that logs errors during message processing.
|
|
5
|
-
#
|
|
6
|
-
# message and backtrace to the configured logger.
|
|
4
|
+
# Default exception handler that logs errors during message processing. Implements a simple error logging strategy that
|
|
5
|
+
# outputs the exception message and backtrace to the configured logger.
|
|
7
6
|
class DefaultExceptionHandler
|
|
8
7
|
extend Util
|
|
9
8
|
|
data/lib/shoryuken/errors.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Namespace for all Shoryuken-specific errors.
|
|
5
|
-
# These provide more meaningful error types than generic Ruby exceptions,
|
|
4
|
+
# Namespace for all Shoryuken-specific errors. These provide more meaningful error types than generic Ruby exceptions,
|
|
6
5
|
# making it easier to rescue and handle specific failure cases.
|
|
7
6
|
module Errors
|
|
8
7
|
# Base class for all Shoryuken errors
|
|
@@ -14,8 +13,7 @@ module Shoryuken
|
|
|
14
13
|
# Raised when a specified SQS queue does not exist or cannot be accessed
|
|
15
14
|
QueueNotFoundError = Class.new(BaseError)
|
|
16
15
|
|
|
17
|
-
# Raised when worker registration fails due to conflicts
|
|
18
|
-
# (e.g., registering multiple workers for a batch queue)
|
|
16
|
+
# Raised when worker registration fails due to conflicts (e.g., registering multiple workers for a batch queue)
|
|
19
17
|
InvalidWorkerRegistrationError = Class.new(BaseError)
|
|
20
18
|
|
|
21
19
|
# Raised when an invalid polling strategy is specified
|
data/lib/shoryuken/fetcher.rb
CHANGED
|
@@ -72,12 +72,18 @@ module Shoryuken
|
|
|
72
72
|
|
|
73
73
|
shoryuken_queue = Shoryuken::Client.queues(queue.name)
|
|
74
74
|
|
|
75
|
-
options[:max_number_of_messages] = max_number_of_messages(shoryuken_queue, limit, options)
|
|
76
75
|
options[:message_attribute_names] = %w[All]
|
|
77
76
|
options[:attribute_names] = %w[All]
|
|
78
77
|
|
|
78
|
+
# Merge per-queue options BEFORE computing the cap so the FIFO
|
|
79
|
+
# one-at-a-time guard (and FETCH_LIMIT) always win. Computing the cap last
|
|
80
|
+
# means a queue option of max_number_of_messages can only lower the count,
|
|
81
|
+
# never raise a non-batch FIFO queue above 1 - which would let SQS return
|
|
82
|
+
# several messages from the same group and break ordering.
|
|
79
83
|
options.merge!(queue.options)
|
|
80
84
|
|
|
85
|
+
options[:max_number_of_messages] = max_number_of_messages(shoryuken_queue, limit, options)
|
|
86
|
+
|
|
81
87
|
shoryuken_queue.receive_messages(options)
|
|
82
88
|
end
|
|
83
89
|
|
|
@@ -4,13 +4,11 @@ module Shoryuken
|
|
|
4
4
|
module Helpers
|
|
5
5
|
# A thread-safe counter implementation using Ruby's Mutex.
|
|
6
6
|
#
|
|
7
|
-
# This class provides atomic operations for incrementing, decrementing, and reading
|
|
8
|
-
#
|
|
9
|
-
# Concurrent::AtomicFixnum without requiring external dependencies.
|
|
7
|
+
# This class provides atomic operations for incrementing, decrementing, and reading integer values in a thread-safe
|
|
8
|
+
# manner. It serves as a drop-in replacement for Concurrent::AtomicFixnum without requiring external dependencies.
|
|
10
9
|
#
|
|
11
|
-
# The implementation uses a Mutex to ensure thread safety across all Ruby
|
|
12
|
-
#
|
|
13
|
-
# critical for data integrity.
|
|
10
|
+
# The implementation uses a Mutex to ensure thread safety across all Ruby implementations including JRuby, where true
|
|
11
|
+
# parallelism makes atomic operations critical for data integrity.
|
|
14
12
|
#
|
|
15
13
|
# @note This class is optimized for scenarios with frequent atomic updates
|
|
16
14
|
# and occasional reads, such as tracking active worker counts.
|
|
@@ -82,9 +80,8 @@ module Shoryuken
|
|
|
82
80
|
|
|
83
81
|
# Atomically decrements the counter by 1 and returns the new value.
|
|
84
82
|
#
|
|
85
|
-
# This operation is thread-safe and can be called concurrently from
|
|
86
|
-
#
|
|
87
|
-
# The counter can go negative if decremented below zero.
|
|
83
|
+
# This operation is thread-safe and can be called concurrently from multiple threads without risk of data corruption or
|
|
84
|
+
# lost updates. The counter can go negative if decremented below zero.
|
|
88
85
|
#
|
|
89
86
|
# @return [Integer] The new counter value after decrementing
|
|
90
87
|
#
|
|
@@ -4,14 +4,12 @@ module Shoryuken
|
|
|
4
4
|
module Helpers
|
|
5
5
|
# A thread-safe hash implementation using Ruby's Mutex for all operations.
|
|
6
6
|
#
|
|
7
|
-
# This class provides a hash-like interface with thread-safe operations, serving as a
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# ensuring complete thread safety across all Ruby implementations including JRuby.
|
|
7
|
+
# This class provides a hash-like interface with thread-safe operations, serving as a drop-in replacement for
|
|
8
|
+
# Concurrent::Hash without requiring external dependencies. The implementation uses a single mutex to protect both read
|
|
9
|
+
# and write operations, ensuring complete thread safety across all Ruby implementations including JRuby.
|
|
11
10
|
#
|
|
12
|
-
# Since hash operations (lookup, assignment) are very fast, the mutex overhead is
|
|
13
|
-
#
|
|
14
|
-
# the complexity of copy-on-write while maintaining excellent performance for
|
|
11
|
+
# Since hash operations (lookup, assignment) are very fast, the mutex overhead is minimal while providing guaranteed
|
|
12
|
+
# safety and simplicity. This approach avoids the complexity of copy-on-write while maintaining excellent performance for
|
|
15
13
|
# typical usage patterns.
|
|
16
14
|
#
|
|
17
15
|
# @note This implementation uses mutex synchronization for all operations,
|
|
@@ -52,8 +50,7 @@ module Shoryuken
|
|
|
52
50
|
class AtomicHash
|
|
53
51
|
# Creates a new empty atomic hash.
|
|
54
52
|
#
|
|
55
|
-
# The hash starts empty and ready to accept key-value pairs through
|
|
56
|
-
# thread-safe operations.
|
|
53
|
+
# The hash starts empty and ready to accept key-value pairs through thread-safe operations.
|
|
57
54
|
#
|
|
58
55
|
# @return [AtomicHash] A new empty atomic hash instance
|
|
59
56
|
#
|
|
@@ -67,8 +64,7 @@ module Shoryuken
|
|
|
67
64
|
|
|
68
65
|
# Returns the value associated with the given key.
|
|
69
66
|
#
|
|
70
|
-
# This operation is thread-safe and will return a consistent value
|
|
71
|
-
# even when called concurrently with write operations.
|
|
67
|
+
# This operation is thread-safe and will return a consistent value even when called concurrently with write operations.
|
|
72
68
|
#
|
|
73
69
|
# @param key [Object] The key to look up
|
|
74
70
|
# @return [Object, nil] The value associated with the key, or nil if not found
|
|
@@ -115,8 +111,7 @@ module Shoryuken
|
|
|
115
111
|
|
|
116
112
|
# Removes all key-value pairs from the hash.
|
|
117
113
|
#
|
|
118
|
-
# This is a thread-safe write operation that ensures atomicity
|
|
119
|
-
# when called concurrently with other operations.
|
|
114
|
+
# This is a thread-safe write operation that ensures atomicity when called concurrently with other operations.
|
|
120
115
|
#
|
|
121
116
|
# @return [Hash] An empty hash (for compatibility with standard Hash#clear)
|
|
122
117
|
#
|
|
@@ -154,8 +149,7 @@ module Shoryuken
|
|
|
154
149
|
|
|
155
150
|
# Returns the value for the given key, or a default value if the key is not found.
|
|
156
151
|
#
|
|
157
|
-
# This operation is thread-safe and will return a consistent value
|
|
158
|
-
# even when called concurrently with write operations.
|
|
152
|
+
# This operation is thread-safe and will return a consistent value even when called concurrently with write operations.
|
|
159
153
|
#
|
|
160
154
|
# @param key [Object] The key to look up
|
|
161
155
|
# @param default [Object] The value to return if the key is not found
|
|
@@ -4,10 +4,9 @@ module Shoryuken
|
|
|
4
4
|
module Helpers
|
|
5
5
|
# Utility methods for hash manipulation.
|
|
6
6
|
#
|
|
7
|
-
# This module provides helper methods for common hash operations that were
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# the same functionality.
|
|
7
|
+
# This module provides helper methods for common hash operations that were previously implemented as core class
|
|
8
|
+
# extensions. By using a dedicated helper module, we avoid polluting the global namespace while maintaining the same
|
|
9
|
+
# functionality.
|
|
11
10
|
#
|
|
12
11
|
# @example Basic usage
|
|
13
12
|
# hash = { 'key1' => 'value1', 'key2' => { 'nested' => 'value2' } }
|
|
@@ -17,9 +16,8 @@ module Shoryuken
|
|
|
17
16
|
class << self
|
|
18
17
|
# Recursively converts hash keys to symbols.
|
|
19
18
|
#
|
|
20
|
-
# This method traverses a hash structure and converts all string keys
|
|
21
|
-
#
|
|
22
|
-
# This is useful for normalizing configuration data loaded from YAML files.
|
|
19
|
+
# This method traverses a hash structure and converts all string keys to symbols, including nested hashes. Non-hash
|
|
20
|
+
# values are left unchanged. This is useful for normalizing configuration data loaded from YAML files.
|
|
23
21
|
#
|
|
24
22
|
# @param hash [Hash, Object] The hash to convert, or any other object
|
|
25
23
|
# @return [Hash, Object] Hash with symbolized keys, or the original object if not a hash
|
|
@@ -4,10 +4,9 @@ module Shoryuken
|
|
|
4
4
|
module Helpers
|
|
5
5
|
# Utility methods for string manipulation.
|
|
6
6
|
#
|
|
7
|
-
# This module provides helper methods for common string operations that were
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# the same functionality.
|
|
7
|
+
# This module provides helper methods for common string operations that were previously implemented as core class
|
|
8
|
+
# extensions. By using a dedicated helper module, we avoid polluting the global namespace while maintaining the same
|
|
9
|
+
# functionality.
|
|
11
10
|
#
|
|
12
11
|
# @example Basic usage
|
|
13
12
|
# klass = Shoryuken::Helpers::StringUtils.constantize('MyWorker')
|
|
@@ -16,10 +15,9 @@ module Shoryuken
|
|
|
16
15
|
class << self
|
|
17
16
|
# Converts a string to a constant.
|
|
18
17
|
#
|
|
19
|
-
# This method takes a string representation of a constant name and returns
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# dynamically loading worker classes from configuration.
|
|
18
|
+
# This method takes a string representation of a constant name and returns the actual constant. It handles nested
|
|
19
|
+
# constants (e.g., 'Foo::Bar') and leading double colons (e.g., '::Object'). This is commonly used for dynamically
|
|
20
|
+
# loading worker classes from configuration.
|
|
23
21
|
#
|
|
24
22
|
# @param string [String] The string to convert to a constant
|
|
25
23
|
# @return [Class, Module] The constant represented by the string
|
|
@@ -42,14 +42,21 @@ module Shoryuken
|
|
|
42
42
|
#
|
|
43
43
|
# @return [Boolean] true if killed, false if already killed
|
|
44
44
|
def kill
|
|
45
|
+
thread_to_kill = nil
|
|
46
|
+
|
|
45
47
|
@mutex.synchronize do
|
|
46
48
|
return false if @killed
|
|
47
49
|
|
|
48
50
|
@killed = true
|
|
49
51
|
@running = false
|
|
50
|
-
|
|
51
|
-
@thread.kill if @thread&.alive?
|
|
52
|
+
thread_to_kill = @thread
|
|
52
53
|
end
|
|
54
|
+
|
|
55
|
+
# Kill the thread AFTER releasing the mutex. The timer loop's ensure
|
|
56
|
+
# block calls @mutex.synchronize to clear @running; killing the thread
|
|
57
|
+
# while holding that mutex deadlocks on Ruby 3.2, where Thread#kill
|
|
58
|
+
# yields the GVL to the killed thread for cleanup before returning.
|
|
59
|
+
thread_to_kill&.kill if thread_to_kill&.alive?
|
|
53
60
|
true
|
|
54
61
|
end
|
|
55
62
|
|
|
@@ -59,6 +66,16 @@ module Shoryuken
|
|
|
59
66
|
#
|
|
60
67
|
# @return [void]
|
|
61
68
|
def run_timer_loop
|
|
69
|
+
# The timer thread inherits the priority of the thread that called
|
|
70
|
+
# #execute. Shoryuken runs workers at a lowered priority
|
|
71
|
+
# (Shoryuken.thread_priority, default -1) and starts the
|
|
72
|
+
# auto-visibility-extension timer from inside that worker thread, so the
|
|
73
|
+
# timer would otherwise inherit -1. A latency-sensitive timer must not
|
|
74
|
+
# run below normal priority: under CPU contention a delayed extension can
|
|
75
|
+
# miss the visibility timeout and let the message be redelivered (double
|
|
76
|
+
# processed). Reset to normal priority.
|
|
77
|
+
Thread.current.priority = 0
|
|
78
|
+
|
|
62
79
|
until @killed
|
|
63
80
|
sleep(@execution_interval)
|
|
64
81
|
break if @killed
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
module Shoryuken
|
|
4
4
|
# A high-performance alternative to OpenStruct for representing SQS messages.
|
|
5
5
|
#
|
|
6
|
-
# InlineMessage is a Struct-based implementation that provides the same interface
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# needed to represent an Amazon SQS message within the Shoryuken framework.
|
|
6
|
+
# InlineMessage is a Struct-based implementation that provides the same interface as the previous OpenStruct-based message
|
|
7
|
+
# representation but with significantly better performance characteristics. It contains all the essential attributes needed
|
|
8
|
+
# to represent an Amazon SQS message within the Shoryuken framework.
|
|
10
9
|
InlineMessage = Struct.new(
|
|
11
10
|
:body,
|
|
12
11
|
:attributes,
|
data/lib/shoryuken/launcher.rb
CHANGED
|
@@ -14,9 +14,8 @@ module Shoryuken
|
|
|
14
14
|
|
|
15
15
|
# Indicates whether the launcher is in the process of stopping.
|
|
16
16
|
#
|
|
17
|
-
# This flag is set to true when either {#stop} or {#stop!} is called,
|
|
18
|
-
#
|
|
19
|
-
# checkpoint and prepare for graceful shutdown.
|
|
17
|
+
# This flag is set to true when either {#stop} or {#stop!} is called, and is used by ActiveJob adapters to signal jobs
|
|
18
|
+
# that they should checkpoint and prepare for graceful shutdown.
|
|
20
19
|
#
|
|
21
20
|
# @return [Boolean] true if stopping, false otherwise
|
|
22
21
|
def stopping?
|
|
@@ -43,8 +42,7 @@ module Shoryuken
|
|
|
43
42
|
# Don't await here so the timeout below is not delayed
|
|
44
43
|
stop_new_dispatching
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
|
|
45
|
+
shutdown_executor
|
|
48
46
|
|
|
49
47
|
fire_event(:stopped)
|
|
50
48
|
end
|
|
@@ -61,8 +59,7 @@ module Shoryuken
|
|
|
61
59
|
stop_new_dispatching
|
|
62
60
|
await_dispatching_in_progress
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
executor.wait_for_termination
|
|
62
|
+
shutdown_executor
|
|
66
63
|
|
|
67
64
|
fire_event(:stopped)
|
|
68
65
|
end
|
|
@@ -93,11 +90,25 @@ module Shoryuken
|
|
|
93
90
|
@managers.each(&:await_dispatching_in_progress)
|
|
94
91
|
end
|
|
95
92
|
|
|
93
|
+
# Shuts the executor down, giving in-flight workers up to the configured timeout to finish before force-killing them so
|
|
94
|
+
# the process can exit. Used by both the graceful ({#stop}) and immediate ({#stop!}) shutdowns: a graceful stop still
|
|
95
|
+
# waits for workers, but must not block forever on a hung one.
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
def shutdown_executor
|
|
99
|
+
executor.shutdown
|
|
100
|
+
executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
|
|
101
|
+
end
|
|
102
|
+
|
|
96
103
|
# Returns the executor for running async operations
|
|
97
104
|
#
|
|
105
|
+
# Owns a dedicated executor rather than borrowing Concurrent.global_io_executor: {#stop} and {#stop!} shut down and kill
|
|
106
|
+
# this executor, and destroying the process-global pool would break anything else relying on it (including Shoryuken's
|
|
107
|
+
# own ShoryukenConcurrentSendAdapter) and prevent a fresh launcher from starting in the same process.
|
|
108
|
+
#
|
|
98
109
|
# @return [Concurrent::ExecutorService] the executor service
|
|
99
110
|
def executor
|
|
100
|
-
@_executor ||= Shoryuken.launcher_executor || Concurrent.
|
|
111
|
+
@_executor ||= Shoryuken.launcher_executor || Concurrent::CachedThreadPool.new(auto_terminate: true)
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
# Starts all managers in parallel futures
|
data/lib/shoryuken/manager.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Shoryuken
|
|
|
32
32
|
@executor = executor
|
|
33
33
|
@running = Shoryuken::Helpers::AtomicBoolean.new(true)
|
|
34
34
|
@stop_new_dispatching = Shoryuken::Helpers::AtomicBoolean.new(false)
|
|
35
|
+
@dispatch_started = Shoryuken::Helpers::AtomicBoolean.new(false)
|
|
35
36
|
@dispatching_release_signal = ::Queue.new
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -48,6 +49,14 @@ module Shoryuken
|
|
|
48
49
|
# @return [void]
|
|
49
50
|
def stop_new_dispatching
|
|
50
51
|
@stop_new_dispatching.make_true
|
|
52
|
+
|
|
53
|
+
# If the dispatch loop never ran (e.g. a stop arrives before the start
|
|
54
|
+
# Future is scheduled, or in an embedded host whose executor is
|
|
55
|
+
# saturated), there is no loop to observe the flag and close the release
|
|
56
|
+
# signal, so await_dispatching_in_progress would block forever. Close it
|
|
57
|
+
# here in that case. Queue#close is idempotent, so a dispatch_loop that
|
|
58
|
+
# does start later and closes it again is harmless.
|
|
59
|
+
@dispatching_release_signal.close unless @dispatch_started.true?
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
# Waits for any in-progress dispatching to complete
|
|
@@ -57,6 +66,9 @@ module Shoryuken
|
|
|
57
66
|
# There might still be a dispatching on-going, as the response from SQS could take some time
|
|
58
67
|
# We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
|
|
59
68
|
# We use a queue, as the dispatch_loop is running on another thread, and this is a efficient way of communicating between threads.
|
|
69
|
+
# The dispatch loop closes the queue when it observes the stop flag; pop on a closed queue
|
|
70
|
+
# returns immediately, so this stays safe when stop is requested more than once
|
|
71
|
+
# (e.g. TSTP followed by USR1, which both trigger a graceful stop).
|
|
60
72
|
@dispatching_release_signal.pop
|
|
61
73
|
end
|
|
62
74
|
|
|
@@ -73,12 +85,29 @@ module Shoryuken
|
|
|
73
85
|
#
|
|
74
86
|
# @return [void]
|
|
75
87
|
def dispatch_loop
|
|
88
|
+
# Mark that the loop has run at least once so stop_new_dispatching knows
|
|
89
|
+
# it can rely on the loop to close the release signal (and otherwise
|
|
90
|
+
# closes it itself to avoid a deadlock).
|
|
91
|
+
@dispatch_started.make_true
|
|
92
|
+
|
|
76
93
|
if @stop_new_dispatching.true? || !running?
|
|
77
|
-
|
|
94
|
+
# Close (instead of push) so every pending and future
|
|
95
|
+
# await_dispatching_in_progress call returns, not just the first one
|
|
96
|
+
@dispatching_release_signal.close
|
|
78
97
|
return
|
|
79
98
|
end
|
|
80
99
|
|
|
81
100
|
@executor.post { dispatch }
|
|
101
|
+
rescue Concurrent::RejectedExecutionError
|
|
102
|
+
# The executor was shut down between the running? check and the post
|
|
103
|
+
# (e.g. a hard stop racing the dispatch loop); release any waiters as
|
|
104
|
+
# the dispatch chain ends here
|
|
105
|
+
@dispatching_release_signal.close
|
|
106
|
+
rescue StandardError
|
|
107
|
+
# Unexpected error from post (e.g. ThreadError if the OS thread limit is
|
|
108
|
+
# hit); the dispatch chain ends here so release waiters before re-raising
|
|
109
|
+
@dispatching_release_signal.close
|
|
110
|
+
raise
|
|
82
111
|
end
|
|
83
112
|
|
|
84
113
|
# Dispatches messages from a queue
|
|
@@ -129,13 +158,20 @@ module Shoryuken
|
|
|
129
158
|
return unless @polling_strategy.respond_to?(:message_processed)
|
|
130
159
|
|
|
131
160
|
@polling_strategy.message_processed(queue)
|
|
161
|
+
rescue => e
|
|
162
|
+
# Swallow (but log) failures from the SQS lookups or the strategy callback:
|
|
163
|
+
# the busy counter was already decremented above and the caller's ensure
|
|
164
|
+
# must not run completion twice
|
|
165
|
+
logger.error { "Processor completion failed for #{queue}: #{e.message}" }
|
|
166
|
+
logger.debug { e.backtrace.join("\n") } unless e.backtrace.nil?
|
|
132
167
|
end
|
|
133
168
|
|
|
134
169
|
# Assigns a message to a processor
|
|
135
170
|
#
|
|
136
171
|
# @param queue_name [String] the queue name
|
|
137
172
|
# @param sqs_msg [Aws::SQS::Types::Message, Array<Aws::SQS::Types::Message>] the message or batch
|
|
138
|
-
# @return [Concurrent::Promise, nil] the processing promise or nil if
|
|
173
|
+
# @return [Concurrent::Promise, nil] the processing promise, or nil if the manager
|
|
174
|
+
# is not running or the executor rejects the worker post
|
|
139
175
|
def assign(queue_name, sqs_msg)
|
|
140
176
|
return unless running?
|
|
141
177
|
|
|
@@ -144,6 +180,11 @@ module Shoryuken
|
|
|
144
180
|
@busy_processors.increment
|
|
145
181
|
fire_utilization_update_event
|
|
146
182
|
|
|
183
|
+
# Completion runs in an ensure so it executes exactly once whether
|
|
184
|
+
# processing succeeds or raises. The previous `.then { processor_done }
|
|
185
|
+
# .rescue { processor_done }` chain ran completion twice when
|
|
186
|
+
# processor_done itself raised, double decrementing the busy counter
|
|
187
|
+
# and silently breaking the concurrency limit
|
|
147
188
|
Concurrent::Promise
|
|
148
189
|
.execute(executor: @executor) do
|
|
149
190
|
original_priority = Thread.current.priority
|
|
@@ -152,10 +193,20 @@ module Shoryuken
|
|
|
152
193
|
Processor.process(queue_name, sqs_msg)
|
|
153
194
|
ensure
|
|
154
195
|
Thread.current.priority = original_priority
|
|
196
|
+
processor_done(queue_name)
|
|
155
197
|
end
|
|
156
198
|
end
|
|
157
|
-
|
|
158
|
-
|
|
199
|
+
rescue Concurrent::RejectedExecutionError
|
|
200
|
+
# The executor was shut down (or a bounded custom launcher_executor is
|
|
201
|
+
# saturated) between the running? check above and the post. The promise
|
|
202
|
+
# body - and therefore processor_done - never ran, so roll back the
|
|
203
|
+
# increment here. Leaking it would permanently shrink `ready`
|
|
204
|
+
# (@max_processors - busy) until dispatch stalls and the group stops
|
|
205
|
+
# processing. The message was never processed, so we must not run the
|
|
206
|
+
# FIFO message_processed callback - decrement directly instead.
|
|
207
|
+
@busy_processors.decrement
|
|
208
|
+
fire_utilization_update_event
|
|
209
|
+
nil
|
|
159
210
|
end
|
|
160
211
|
|
|
161
212
|
# Dispatches a batch of messages from a queue
|
|
@@ -208,9 +259,17 @@ module Shoryuken
|
|
|
208
259
|
logger.error { "Manager failed: #{ex.message}" }
|
|
209
260
|
logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
|
|
210
261
|
|
|
211
|
-
|
|
212
|
-
|
|
262
|
+
# Stop this manager first so Launcher#healthy? surfaces the failure to
|
|
263
|
+
# whoever is supervising us (the CLI Runner, or an embedding application).
|
|
213
264
|
@running.make_false
|
|
265
|
+
|
|
266
|
+
# In server (CLI) mode the Runner traps USR1 and turns it into a graceful
|
|
267
|
+
# shutdown of the whole process, so a process supervisor can restart us.
|
|
268
|
+
# When embedded (no Runner), USR1 keeps its default disposition and would
|
|
269
|
+
# terminate the host process - and any in-flight workers - so we must not
|
|
270
|
+
# send it. The stopped manager above is enough for Launcher#healthy? to
|
|
271
|
+
# report the failure to the embedding application.
|
|
272
|
+
Process.kill('USR1', Process.pid) if Shoryuken.server?
|
|
214
273
|
end
|
|
215
274
|
|
|
216
275
|
# Fires a utilization update event
|
data/lib/shoryuken/message.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Represents an SQS message received by a Shoryuken worker.
|
|
5
|
-
#
|
|
6
|
-
# for interacting with the message, including deletion and visibility timeout management.
|
|
4
|
+
# Represents an SQS message received by a Shoryuken worker. This class wraps the raw AWS SQS message data and provides
|
|
5
|
+
# convenient methods for interacting with the message, including deletion and visibility timeout management.
|
|
7
6
|
#
|
|
8
7
|
# Message instances are automatically created by Shoryuken and passed to your
|
|
9
8
|
# worker's `perform` method as the first argument.
|