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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +3 -3
  3. data/.github/workflows/specs.yml +32 -5
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.ruby-version +1 -1
  6. data/.yard-lint.yml +29 -1
  7. data/CHANGELOG.md +171 -0
  8. data/Gemfile.lint.lock +8 -7
  9. data/bin/cli/sqs.rb +67 -3
  10. data/bin/integrations +52 -34
  11. data/lib/active_job/extensions.rb +5 -8
  12. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +6 -5
  13. data/lib/shoryuken/active_job/current_attributes.rb +33 -7
  14. data/lib/shoryuken/body_parser.rb +1 -2
  15. data/lib/shoryuken/client.rb +15 -6
  16. data/lib/shoryuken/default_exception_handler.rb +2 -3
  17. data/lib/shoryuken/errors.rb +2 -4
  18. data/lib/shoryuken/fetcher.rb +7 -1
  19. data/lib/shoryuken/helpers/atomic_counter.rb +6 -9
  20. data/lib/shoryuken/helpers/atomic_hash.rb +9 -15
  21. data/lib/shoryuken/helpers/hash_utils.rb +5 -7
  22. data/lib/shoryuken/helpers/string_utils.rb +6 -8
  23. data/lib/shoryuken/helpers/timer_task.rb +19 -2
  24. data/lib/shoryuken/inline_message.rb +3 -4
  25. data/lib/shoryuken/launcher.rb +19 -8
  26. data/lib/shoryuken/manager.rb +65 -6
  27. data/lib/shoryuken/message.rb +2 -3
  28. data/lib/shoryuken/middleware/chain.rb +7 -13
  29. data/lib/shoryuken/middleware/entry.rb +1 -2
  30. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +30 -2
  31. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +40 -18
  32. data/lib/shoryuken/middleware/server/non_retryable_exception.rb +17 -8
  33. data/lib/shoryuken/middleware/server/timing.rb +2 -3
  34. data/lib/shoryuken/options.rb +37 -4
  35. data/lib/shoryuken/polling/base_strategy.rb +16 -23
  36. data/lib/shoryuken/polling/queue_configuration.rb +8 -12
  37. data/lib/shoryuken/polling/strict_priority.rb +26 -14
  38. data/lib/shoryuken/polling/weighted_round_robin.rb +40 -27
  39. data/lib/shoryuken/queue.rb +41 -4
  40. data/lib/shoryuken/util.rb +4 -1
  41. data/lib/shoryuken/version.rb +1 -1
  42. data/lib/shoryuken/worker/default_executor.rb +11 -5
  43. data/lib/shoryuken/worker/inline_executor.rb +10 -5
  44. data/lib/shoryuken/worker.rb +15 -13
  45. data/lib/shoryuken.rb +6 -3
  46. data/renovate.json +16 -2
  47. data/shoryuken.gemspec +9 -0
  48. data/spec/integration/active_job/current_attributes/cross_job_reset_spec.rb +47 -0
  49. data/spec/integration/active_job/current_attributes/incremental_persist_spec.rb +76 -0
  50. data/spec/integration/active_job/fifo_dedup_opt_out/fifo_dedup_opt_out_spec.rb +67 -0
  51. data/spec/integration/auto_extend_visibility/short_visibility_timeout_spec.rb +52 -0
  52. data/spec/integration/cli/find_all_spec.rb +120 -0
  53. data/spec/integration/concurrent_processing/processor_accounting_spec.rb +94 -0
  54. data/spec/integration/fifo_ordering/fifo_max_messages_cap_spec.rb +96 -0
  55. data/spec/integration/launcher/double_graceful_stop_spec.rb +71 -0
  56. data/spec/integration/launcher/embedded_dispatch_error_spec.rb +85 -0
  57. data/spec/integration/launcher/global_executor_preserved_spec.rb +76 -0
  58. data/spec/integration/launcher/graceful_stop_timeout_spec.rb +74 -0
  59. data/spec/integration/message_operations/partial_batch_delete_spec.rb +67 -0
  60. data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +1 -1
  61. data/spec/integration/non_retryable_exception/with_retry_intervals_spec.rb +115 -0
  62. data/spec/integrations_helper.rb +10 -9
  63. data/spec/lib/shoryuken/client_spec.rb +62 -0
  64. data/spec/lib/shoryuken/fetcher_spec.rb +13 -0
  65. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +24 -0
  66. data/spec/lib/shoryuken/launcher_spec.rb +38 -0
  67. data/spec/lib/shoryuken/manager_spec.rb +147 -0
  68. data/spec/lib/shoryuken/middleware/server/auto_extend_visibility_spec.rb +35 -0
  69. data/spec/lib/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +88 -0
  70. data/spec/lib/shoryuken/polling/strict_priority_spec.rb +25 -0
  71. data/spec/lib/shoryuken/polling/weighted_round_robin_spec.rb +50 -0
  72. data/spec/lib/shoryuken/queue_spec.rb +123 -0
  73. data/spec/lib/shoryuken/util_spec.rb +26 -0
  74. data/spec/lib/shoryuken/worker/default_executor_spec.rb +13 -0
  75. data/spec/lib/shoryuken/worker/inline_executor_spec.rb +12 -0
  76. data/spec/shared_examples_for_active_job.rb +18 -0
  77. data/spec/spec_helper.rb +40 -20
  78. 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
- # @example Rails configuration
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
- # When true, it signals to jobs that they should checkpoint their progress
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.each_with_index do |klass, idx|
62
- key = @cattrs.empty? ? 'cattr' : "cattr_#{idx}"
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
- klasses_to_reset.each(&:reset)
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
@@ -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
- # @return [Hash{String => Shoryuken::Queue}] cached queue instances by name
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
- @@queues = {}
34
-
35
- Shoryuken.sqs_client = sqs
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
- # Implements a simple error logging strategy that outputs the exception
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
 
@@ -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
@@ -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
- # integer values in a thread-safe manner. It serves as a drop-in replacement for
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
- # implementations including JRuby, where true parallelism makes atomic operations
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
- # multiple threads without risk of data corruption or lost updates.
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
- # drop-in replacement for Concurrent::Hash without requiring external dependencies.
9
- # The implementation uses a single mutex to protect both read and write operations,
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
- # minimal while providing guaranteed safety and simplicity. This approach avoids
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
- # previously implemented as core class extensions. By using a dedicated
9
- # helper module, we avoid polluting the global namespace while maintaining
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
- # to symbols, including nested hashes. Non-hash values are left unchanged.
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
- # previously implemented as core class extensions. By using a dedicated
9
- # helper module, we avoid polluting the global namespace while maintaining
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
- # the actual constant. It handles nested constants (e.g., 'Foo::Bar') and
21
- # leading double colons (e.g., '::Object'). This is commonly used for
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
- # as the previous OpenStruct-based message representation but with significantly
8
- # better performance characteristics. It contains all the essential attributes
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,
@@ -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
- # and is used by ActiveJob adapters to signal jobs that they should
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
- executor.shutdown
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
- executor.shutdown
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.global_io_executor
111
+ @_executor ||= Shoryuken.launcher_executor || Concurrent::CachedThreadPool.new(auto_terminate: true)
101
112
  end
102
113
 
103
114
  # Starts all managers in parallel futures
@@ -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
- @dispatching_release_signal << 1
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 not running
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
- .then { processor_done(queue_name) }
158
- .rescue { processor_done(queue_name) }
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
- Process.kill('USR1', Process.pid)
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
@@ -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
- # This class wraps the raw AWS SQS message data and provides convenient methods
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.