shoryuken 7.0.0.alpha2 → 7.0.0

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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +2 -2
  3. data/.github/workflows/specs.yml +38 -43
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.gitignore +3 -0
  6. data/.rspec +1 -0
  7. data/.ruby-version +1 -1
  8. data/.yard-lint.yml +279 -0
  9. data/CHANGELOG.md +69 -1
  10. data/Gemfile +1 -1
  11. data/README.md +2 -7
  12. data/Rakefile +4 -10
  13. data/bin/clean_localstack +52 -0
  14. data/bin/cli/base.rb +21 -0
  15. data/bin/cli/sqs.rb +61 -2
  16. data/bin/integrations +275 -0
  17. data/bin/scenario +154 -0
  18. data/bin/shoryuken +1 -1
  19. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +15 -4
  20. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  21. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  22. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  23. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  24. data/lib/shoryuken/body_parser.rb +8 -0
  25. data/lib/shoryuken/client.rb +14 -0
  26. data/lib/shoryuken/default_exception_handler.rb +9 -0
  27. data/lib/shoryuken/default_worker_registry.rb +29 -1
  28. data/lib/shoryuken/environment_loader.rb +78 -8
  29. data/lib/shoryuken/errors.rb +33 -0
  30. data/lib/shoryuken/fetcher.rb +37 -1
  31. data/lib/shoryuken/helpers/atomic_boolean.rb +19 -5
  32. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  33. data/lib/shoryuken/launcher.rb +53 -0
  34. data/lib/shoryuken/logging/base.rb +26 -0
  35. data/lib/shoryuken/logging/pretty.rb +25 -0
  36. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  37. data/lib/shoryuken/logging.rb +39 -25
  38. data/lib/shoryuken/manager.rb +70 -1
  39. data/lib/shoryuken/message.rb +114 -1
  40. data/lib/shoryuken/middleware/chain.rb +139 -43
  41. data/lib/shoryuken/middleware/entry.rb +30 -0
  42. data/lib/shoryuken/middleware/server/active_record.rb +8 -0
  43. data/lib/shoryuken/middleware/server/auto_delete.rb +10 -0
  44. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +27 -1
  45. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +29 -0
  46. data/lib/shoryuken/middleware/server/timing.rb +11 -0
  47. data/lib/shoryuken/options.rb +129 -6
  48. data/lib/shoryuken/polling/base_strategy.rb +1 -0
  49. data/lib/shoryuken/polling/strict_priority.rb +39 -0
  50. data/lib/shoryuken/polling/weighted_round_robin.rb +42 -0
  51. data/lib/shoryuken/processor.rb +32 -1
  52. data/lib/shoryuken/queue.rb +93 -4
  53. data/lib/shoryuken/runner.rb +45 -4
  54. data/lib/shoryuken/util.rb +26 -1
  55. data/lib/shoryuken/version.rb +2 -1
  56. data/lib/shoryuken/worker/default_executor.rb +21 -1
  57. data/lib/shoryuken/worker/inline_executor.rb +24 -0
  58. data/lib/shoryuken/worker.rb +193 -0
  59. data/lib/shoryuken/worker_registry.rb +33 -0
  60. data/lib/shoryuken.rb +18 -6
  61. data/renovate.json +29 -2
  62. data/shoryuken.gemspec +2 -1
  63. data/spec/integration/.rspec +1 -0
  64. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  65. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  66. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  67. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  68. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  69. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  70. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  71. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  72. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  73. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  74. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  75. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  76. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  77. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  78. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  79. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  80. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  81. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  82. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  83. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  84. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  85. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  86. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  87. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  88. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  89. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  90. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  91. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  92. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  93. data/spec/integration/launcher/launcher_spec.rb +40 -0
  94. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  95. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  96. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  97. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  98. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  99. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  100. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  101. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  102. data/spec/integration/rails/rails_72/Gemfile +6 -0
  103. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  104. data/spec/integration/rails/rails_80/Gemfile +6 -0
  105. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  106. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  107. data/spec/integration/rails/rails_81/Gemfile +6 -0
  108. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  109. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  110. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  111. data/spec/integration/spec_helper.rb +7 -0
  112. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  113. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  114. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  115. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  116. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  117. data/spec/integrations_helper.rb +243 -0
  118. data/spec/lib/active_job/extensions_spec.rb +149 -0
  119. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  120. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +3 -3
  121. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +4 -4
  122. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +1 -1
  123. data/spec/{shoryuken → lib/shoryuken}/helpers/hash_utils_spec.rb +14 -14
  124. data/spec/{shoryuken → lib/shoryuken}/helpers/string_utils_spec.rb +3 -3
  125. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  126. data/spec/{shoryuken → lib/shoryuken}/helpers_integration_spec.rb +9 -9
  127. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +22 -0
  128. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  129. data/spec/lib/shoryuken/message_spec.rb +109 -0
  130. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  131. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  132. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +50 -0
  133. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +2 -2
  134. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +1 -1
  135. data/spec/lib/shoryuken/version_spec.rb +17 -0
  136. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  137. data/spec/shared_examples_for_active_job.rb +29 -9
  138. data/spec/spec_helper.rb +34 -3
  139. metadata +230 -91
  140. data/.devcontainer/Dockerfile +0 -17
  141. data/.devcontainer/base.Dockerfile +0 -43
  142. data/.devcontainer/devcontainer.json +0 -35
  143. data/Appraisals +0 -23
  144. data/gemfiles/.gitignore +0 -1
  145. data/gemfiles/rails_7_0.gemfile +0 -19
  146. data/gemfiles/rails_7_1.gemfile +0 -19
  147. data/gemfiles/rails_7_2.gemfile +0 -19
  148. data/gemfiles/rails_8_0.gemfile +0 -19
  149. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -110
  150. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  151. data/spec/integration/launcher_spec.rb +0 -127
  152. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -8
  153. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -85
  154. /data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +0 -0
  155. /data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +0 -0
  156. /data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +0 -0
  157. /data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +0 -0
  158. /data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +0 -0
  159. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_boolean_spec.rb +0 -0
  160. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_counter_spec.rb +0 -0
  161. /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_hash_spec.rb +0 -0
  162. /data/spec/{shoryuken → lib/shoryuken}/inline_message_spec.rb +0 -0
  163. /data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +0 -0
  164. /data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +0 -0
  165. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +0 -0
  166. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +0 -0
  167. /data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +0 -0
  168. /data/spec/{shoryuken → lib/shoryuken}/polling/base_strategy_spec.rb +0 -0
  169. /data/spec/{shoryuken → lib/shoryuken}/polling/queue_configuration_spec.rb +0 -0
  170. /data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +0 -0
  171. /data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +0 -0
  172. /data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +0 -0
  173. /data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +0 -0
  174. /data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +0 -0
  175. /data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +0 -0
  176. /data/spec/{shoryuken → lib/shoryuken}/worker/inline_executor_spec.rb +0 -0
  177. /data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +0 -0
  178. /data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +0 -0
@@ -1,30 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Loads and configures the Shoryuken environment from configuration files
5
+ # and command line options. Handles Rails integration and queue setup.
4
6
  class EnvironmentLoader
7
+ # @return [Hash] the configuration options
5
8
  attr_reader :options
6
9
 
10
+ # Sets up a new EnvironmentLoader with the given options
11
+ #
12
+ # @param options [Hash] configuration options
13
+ # @option options [String] :config_file path to the configuration file
14
+ # @option options [Boolean] :rails whether to initialize Rails
15
+ # @option options [String] :logfile path to the log file
16
+ # @option options [Boolean] :verbose whether to enable verbose logging
17
+ # @option options [String] :require path to require workers from
18
+ # @option options [Integer] :concurrency number of concurrent workers
19
+ # @return [Shoryuken::EnvironmentLoader] the configured instance
7
20
  def self.setup_options(options)
8
21
  instance = new(options)
9
22
  instance.setup_options
10
23
  instance
11
24
  end
12
25
 
26
+ # Loads the environment for Rails console usage
27
+ #
28
+ # @return [void]
13
29
  def self.load_for_rails_console
14
30
  instance = setup_options(config_file: (Rails.root + 'config' + 'shoryuken.yml'))
15
31
  instance.load
16
32
  end
17
33
 
34
+ # Initializes a new EnvironmentLoader with the given options
35
+ #
36
+ # @param options [Hash] configuration options
37
+ # @option options [String] :config_file path to the configuration file
38
+ # @option options [Boolean] :rails whether to initialize Rails
39
+ # @option options [String] :logfile path to the log file
40
+ # @option options [Boolean] :verbose whether to enable verbose logging
41
+ # @option options [String] :require path to require workers from
42
+ # @option options [Integer] :concurrency number of concurrent workers
18
43
  def initialize(options)
19
44
  @options = options
20
45
  end
21
46
 
47
+ # Sets up configuration options from file and initializes components
48
+ #
49
+ # @return [void]
22
50
  def setup_options
23
51
  initialize_rails if load_rails?
24
52
  initialize_options
25
53
  initialize_logger
26
54
  end
27
55
 
56
+ # Loads the environment including queues and workers
57
+ #
58
+ # @return [void]
28
59
  def load
29
60
  prefix_active_job_queue_names
30
61
  parse_queues
@@ -35,15 +66,21 @@ module Shoryuken
35
66
 
36
67
  private
37
68
 
69
+ # Merges configuration file options with runtime options
70
+ #
71
+ # @return [void]
38
72
  def initialize_options
39
73
  Shoryuken.options.merge!(config_file_options)
40
74
  Shoryuken.options.merge!(options)
41
75
  end
42
76
 
77
+ # Reads and parses the configuration file
78
+ #
79
+ # @return [Hash] the parsed configuration options
43
80
  def config_file_options
44
81
  return {} unless (path = options[:config_file])
45
82
 
46
- fail ArgumentError, "The supplied config file #{path} does not exist" unless File.exist?(path)
83
+ raise Errors::InvalidConfigurationError, "The supplied config file #{path} does not exist" unless File.exist?(path)
47
84
 
48
85
  if (result = YAML.load(ERB.new(IO.read(path)).result))
49
86
  Shoryuken::Helpers::HashUtils.deep_symbolize_keys(result)
@@ -52,11 +89,17 @@ module Shoryuken
52
89
  end
53
90
  end
54
91
 
92
+ # Initializes the logger with file output and verbosity settings
93
+ #
94
+ # @return [void]
55
95
  def initialize_logger
56
96
  Shoryuken::Logging.initialize_logger(Shoryuken.options[:logfile]) if Shoryuken.options[:logfile]
57
97
  Shoryuken.logger.level = Logger::DEBUG if Shoryuken.options[:verbose]
58
98
  end
59
99
 
100
+ # Initializes the Rails environment
101
+ #
102
+ # @return [void]
60
103
  def initialize_rails
61
104
  # Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
62
105
 
@@ -80,18 +123,26 @@ module Shoryuken
80
123
  end
81
124
  end
82
125
  if Shoryuken.active_job?
83
- require 'shoryuken/extensions/active_job_extensions'
84
- require 'shoryuken/extensions/active_job_adapter'
85
- require 'shoryuken/extensions/active_job_concurrent_send_adapter'
126
+ require 'active_job/extensions'
127
+ require 'active_job/queue_adapters/shoryuken_adapter'
128
+ require 'active_job/queue_adapters/shoryuken_concurrent_send_adapter'
86
129
  end
87
130
  require File.expand_path('config/environment.rb')
88
131
  end
89
132
  end
90
133
 
134
+ # Checks if Rails should be loaded
135
+ #
136
+ # @return [Boolean] true if Rails should be initialized
91
137
  def load_rails?
92
138
  options[:rails]
93
139
  end
94
140
 
141
+ # Prefixes a queue name with ActiveJob queue name prefix
142
+ #
143
+ # @param queue_name [String] the queue name to prefix
144
+ # @param weight [Integer] the queue weight
145
+ # @return [Array<String, Integer>] the prefixed queue name and weight
95
146
  def prefix_active_job_queue_name(queue_name, weight)
96
147
  return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')
97
148
 
@@ -104,6 +155,9 @@ module Shoryuken
104
155
  [prefixed_queue_name, weight]
105
156
  end
106
157
 
158
+ # Prefixes all queue names with ActiveJob prefix if enabled
159
+ #
160
+ # @return [void]
107
161
  def prefix_active_job_queue_names
108
162
  return unless Shoryuken.active_job?
109
163
  return unless Shoryuken.active_job_queue_name_prefixing?
@@ -123,10 +177,19 @@ module Shoryuken
123
177
  end
124
178
  end
125
179
 
180
+ # Parses a single queue and adds it to a group
181
+ #
182
+ # @param queue [String] the queue name
183
+ # @param weight [Integer] the queue weight
184
+ # @param group [String] the group name
185
+ # @return [void]
126
186
  def parse_queue(queue, weight, group)
127
187
  Shoryuken.add_queue(queue, [weight.to_i, 1].max, group)
128
188
  end
129
189
 
190
+ # Parses all queues from configuration and adds them to groups
191
+ #
192
+ # @return [void]
130
193
  def parse_queues
131
194
  if Shoryuken.options[:queues].to_a.any?
132
195
  Shoryuken.add_group('default', Shoryuken.options[:concurrency])
@@ -145,6 +208,9 @@ module Shoryuken
145
208
  end
146
209
  end
147
210
 
211
+ # Requires worker files from the configured path
212
+ #
213
+ # @return [void]
148
214
  def require_workers
149
215
  required = Shoryuken.options[:require]
150
216
 
@@ -157,6 +223,10 @@ module Shoryuken
157
223
  end
158
224
  end
159
225
 
226
+ # Validates that all configured queues exist in SQS
227
+ #
228
+ # @return [void]
229
+ # @raise [ArgumentError] if any queues do not exist
160
230
  def validate_queues
161
231
  return Shoryuken.logger.warn { 'No queues supplied' } if Shoryuken.ungrouped_queues.empty?
162
232
 
@@ -178,12 +248,12 @@ module Shoryuken
178
248
  It's also possible that you don't have permission to access the specified queues.
179
249
  MSG
180
250
 
181
- fail(
182
- ArgumentError,
183
- error_msg
184
- )
251
+ raise Errors::QueueNotFoundError, error_msg
185
252
  end
186
253
 
254
+ # Validates that all queues have registered workers
255
+ #
256
+ # @return [void]
187
257
  def validate_workers
188
258
  return if Shoryuken.active_job?
189
259
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ # Namespace for all Shoryuken-specific errors.
5
+ # These provide more meaningful error types than generic Ruby exceptions,
6
+ # making it easier to rescue and handle specific failure cases.
7
+ module Errors
8
+ # Base class for all Shoryuken errors
9
+ BaseError = Class.new(StandardError)
10
+
11
+ # Raised when there is a configuration validation failure
12
+ InvalidConfigurationError = Class.new(BaseError)
13
+
14
+ # Raised when a specified SQS queue does not exist or cannot be accessed
15
+ QueueNotFoundError = Class.new(BaseError)
16
+
17
+ # Raised when worker registration fails due to conflicts
18
+ # (e.g., registering multiple workers for a batch queue)
19
+ InvalidWorkerRegistrationError = Class.new(BaseError)
20
+
21
+ # Raised when an invalid polling strategy is specified
22
+ InvalidPollingStrategyError = Class.new(BaseError)
23
+
24
+ # Raised when an invalid lifecycle event name is used
25
+ InvalidEventError = Class.new(BaseError)
26
+
27
+ # Raised when a delay exceeds the maximum allowed by SQS (15 minutes)
28
+ InvalidDelayError = Class.new(BaseError)
29
+
30
+ # Raised when an ARN format is invalid
31
+ InvalidArnError = Class.new(BaseError)
32
+ end
33
+ end
@@ -1,15 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Fetches messages from SQS queues.
5
+ # Handles message retrieval with automatic retry on connectivity errors.
4
6
  class Fetcher
5
7
  include Util
6
8
 
9
+ # Maximum number of messages that can be fetched in a single SQS request
7
10
  FETCH_LIMIT = 10
8
11
 
12
+ # Initializes a new Fetcher for a processing group
13
+ #
14
+ # @param group [String] the processing group name
9
15
  def initialize(group)
10
16
  @group = group
11
17
  end
12
18
 
19
+ # Fetches messages from a queue with automatic retry
20
+ #
21
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
22
+ # @param limit [Integer] the maximum number of messages to fetch
23
+ # @return [Array<Aws::SQS::Types::Message>] the fetched messages
13
24
  def fetch(queue, limit)
14
25
  fetch_with_auto_retry(3) do
15
26
  started_at = Time.now
@@ -27,6 +38,11 @@ module Shoryuken
27
38
 
28
39
  private
29
40
 
41
+ # Fetches with automatic retry on errors
42
+ #
43
+ # @param max_attempts [Integer] maximum number of retry attempts
44
+ # @yield the fetch operation to retry
45
+ # @return [Object] the result of the block
30
46
  def fetch_with_auto_retry(max_attempts)
31
47
  attempts = 0
32
48
 
@@ -46,6 +62,11 @@ module Shoryuken
46
62
  end
47
63
  end
48
64
 
65
+ # Receives messages from an SQS queue
66
+ #
67
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
68
+ # @param limit [Integer] the maximum number of messages to receive
69
+ # @return [Array<Aws::SQS::Types::Message>, nil] the received messages
49
70
  def receive_messages(queue, limit)
50
71
  options = receive_options(queue)
51
72
 
@@ -60,6 +81,13 @@ module Shoryuken
60
81
  shoryuken_queue.receive_messages(options)
61
82
  end
62
83
 
84
+ # Determines the maximum number of messages to fetch
85
+ #
86
+ # @param shoryuken_queue [Shoryuken::Queue] the queue instance
87
+ # @param limit [Integer] the requested limit
88
+ # @param options [Hash] receive options that may contain max_number_of_messages
89
+ # @option options [Integer] :max_number_of_messages optional override for max messages
90
+ # @return [Integer] the maximum number of messages to fetch
63
91
  def max_number_of_messages(shoryuken_queue, limit, options)
64
92
  # For FIFO queues we want to make sure we process one message per group at a time
65
93
  # if we set max_number_of_messages greater than 1,
@@ -75,6 +103,10 @@ module Shoryuken
75
103
  [limit, FETCH_LIMIT, options[:max_number_of_messages]].compact.min
76
104
  end
77
105
 
106
+ # Returns the receive options for a queue
107
+ #
108
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
109
+ # @return [Hash] the receive options hash
78
110
  def receive_options(queue)
79
111
  options = Shoryuken.sqs_client_receive_message_opts[queue.name]
80
112
  options ||= Shoryuken.sqs_client_receive_message_opts[@group]
@@ -82,8 +114,12 @@ module Shoryuken
82
114
  options.to_h.dup
83
115
  end
84
116
 
117
+ # Checks if a queue uses batch message processing
118
+ #
119
+ # @param queue [Shoryuken::Queue] the queue to check
120
+ # @return [Boolean] true if the queue is configured for batch processing
85
121
  def batched_queue?(queue)
86
122
  Shoryuken.worker_registry.batch_receive_messages?(queue.name)
87
123
  end
88
124
  end
89
- end
125
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Helper classes providing thread-safe utilities and data structures
4
5
  module Helpers
5
6
  # A thread-safe boolean implementation using AtomicCounter as base.
6
7
  # Drop-in replacement for Concurrent::AtomicBoolean without external dependencies.
@@ -9,33 +10,46 @@ module Shoryuken
9
10
  # Prevent misuse of counter operations on a boolean
10
11
  undef_method :increment, :decrement
11
12
 
13
+ # Initializes a new AtomicBoolean
14
+ #
15
+ # @param initial_value [Boolean] the initial value
12
16
  def initialize(initial_value = false)
13
17
  super(initial_value ? 1 : 0)
14
18
  end
15
19
 
16
- # Get the current value as boolean
20
+ # Gets the current value as a boolean
21
+ #
22
+ # @return [Boolean] the current value
17
23
  def value
18
24
  super != 0
19
25
  end
20
26
 
21
- # Set the value to true
27
+ # Sets the value to true
28
+ #
29
+ # @return [Boolean] true
22
30
  def make_true
23
31
  @mutex.synchronize { @value = 1 }
24
32
  true
25
33
  end
26
34
 
27
- # Set the value to false
35
+ # Sets the value to false
36
+ #
37
+ # @return [Boolean] false
28
38
  def make_false
29
39
  @mutex.synchronize { @value = 0 }
30
40
  false
31
41
  end
32
42
 
33
- # Check if the value is true
43
+ # Checks if the value is true
44
+ #
45
+ # @return [Boolean] true if the value is true
34
46
  def true?
35
47
  @mutex.synchronize { @value != 0 }
36
48
  end
37
49
 
38
- # Check if the value is false
50
+ # Checks if the value is false
51
+ #
52
+ # @return [Boolean] true if the value is false
39
53
  def false?
40
54
  @mutex.synchronize { @value == 0 }
41
55
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Helpers
5
+ # A thread-safe timer task implementation.
6
+ # Drop-in replacement for Concurrent::TimerTask without external dependencies.
7
+ class TimerTask
8
+ # Initializes a new TimerTask
9
+ #
10
+ # @param execution_interval [Float] interval in seconds between task executions
11
+ # @param task [Proc] the task to execute on each interval (provided as a block)
12
+ # @return [TimerTask] a new TimerTask instance
13
+ # @raise [ArgumentError] if no block is provided or interval is not positive
14
+ # @yield the task to execute on each interval
15
+ def initialize(execution_interval:, &task)
16
+ raise ArgumentError, 'A block must be provided' unless block_given?
17
+
18
+ @execution_interval = Float(execution_interval)
19
+ raise ArgumentError, 'execution_interval must be positive' if @execution_interval <= 0
20
+
21
+ @task = task
22
+ @mutex = Mutex.new
23
+ @thread = nil
24
+ @running = false
25
+ @killed = false
26
+ end
27
+
28
+ # Starts the timer task execution
29
+ #
30
+ # @return [TimerTask] self for method chaining
31
+ def execute
32
+ @mutex.synchronize do
33
+ return self if @running || @killed
34
+
35
+ @running = true
36
+ @thread = Thread.new { run_timer_loop }
37
+ end
38
+ self
39
+ end
40
+
41
+ # Stops and kills the timer task
42
+ #
43
+ # @return [Boolean] true if killed, false if already killed
44
+ def kill
45
+ @mutex.synchronize do
46
+ return false if @killed
47
+
48
+ @killed = true
49
+ @running = false
50
+
51
+ @thread.kill if @thread&.alive?
52
+ end
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ # Runs the timer loop in a separate thread
59
+ #
60
+ # @return [void]
61
+ def run_timer_loop
62
+ until @killed
63
+ sleep(@execution_interval)
64
+ break if @killed
65
+
66
+ begin
67
+ @task.call
68
+ rescue => e
69
+ # Log the error but continue running
70
+ # This matches the behavior of Concurrent::TimerTask
71
+ warn "TimerTask execution error: #{e.message}"
72
+ warn e.backtrace.join("\n") if e.backtrace
73
+ end
74
+ end
75
+ ensure
76
+ @mutex.synchronize { @running = false }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,13 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Launches and coordinates Shoryuken's message processing managers.
5
+ # Handles the lifecycle of the processing system including startup, shutdown, and health checks.
4
6
  class Launcher
5
7
  include Util
6
8
 
9
+ # Initializes a new Launcher with managers for each processing group
7
10
  def initialize
8
11
  @managers = create_managers
12
+ @stopping = false
9
13
  end
10
14
 
15
+ # Indicates whether the launcher is in the process of stopping.
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.
20
+ #
21
+ # @return [Boolean] true if stopping, false otherwise
22
+ def stopping?
23
+ @stopping
24
+ end
25
+
26
+ # Starts the message processing system
27
+ #
28
+ # @return [void]
11
29
  def start
12
30
  logger.info { 'Starting' }
13
31
 
@@ -15,7 +33,11 @@ module Shoryuken
15
33
  start_managers
16
34
  end
17
35
 
36
+ # Forces an immediate stop of all processing
37
+ #
38
+ # @return [void]
18
39
  def stop!
40
+ @stopping = true
19
41
  initiate_stop
20
42
 
21
43
  # Don't await here so the timeout below is not delayed
@@ -27,7 +49,11 @@ module Shoryuken
27
49
  fire_event(:stopped)
28
50
  end
29
51
 
52
+ # Gracefully stops all processing, waiting for in-flight messages
53
+ #
54
+ # @return [void]
30
55
  def stop
56
+ @stopping = true
31
57
  fire_event(:quiet, true)
32
58
 
33
59
  initiate_stop
@@ -41,6 +67,9 @@ module Shoryuken
41
67
  fire_event(:stopped)
42
68
  end
43
69
 
70
+ # Checks if all processing groups are healthy
71
+ #
72
+ # @return [Boolean] true if all groups are running normally
44
73
  def healthy?
45
74
  Shoryuken.groups.keys.all? do |group|
46
75
  manager = @managers.find { |m| m.group == group }
@@ -50,30 +79,48 @@ module Shoryuken
50
79
 
51
80
  private
52
81
 
82
+ # Stops all managers from dispatching new messages
83
+ #
84
+ # @return [void]
53
85
  def stop_new_dispatching
54
86
  @managers.each(&:stop_new_dispatching)
55
87
  end
56
88
 
89
+ # Waits for any in-progress dispatching to complete
90
+ #
91
+ # @return [void]
57
92
  def await_dispatching_in_progress
58
93
  @managers.each(&:await_dispatching_in_progress)
59
94
  end
60
95
 
96
+ # Returns the executor for running async operations
97
+ #
98
+ # @return [Concurrent::ExecutorService] the executor service
61
99
  def executor
62
100
  @_executor ||= Shoryuken.launcher_executor || Concurrent.global_io_executor
63
101
  end
64
102
 
103
+ # Starts all managers in parallel futures
104
+ #
105
+ # @return [void]
65
106
  def start_managers
66
107
  @managers.each do |manager|
67
108
  Concurrent::Future.execute { manager.start }
68
109
  end
69
110
  end
70
111
 
112
+ # Initiates the stop sequence
113
+ #
114
+ # @return [void]
71
115
  def initiate_stop
72
116
  logger.info { 'Shutting down' }
73
117
 
74
118
  stop_callback
75
119
  end
76
120
 
121
+ # Executes the start callback and fires startup event
122
+ #
123
+ # @return [void]
77
124
  def start_callback
78
125
  if (callback = Shoryuken.start_callback)
79
126
  logger.debug { 'Calling start_callback' }
@@ -83,6 +130,9 @@ module Shoryuken
83
130
  fire_event(:startup)
84
131
  end
85
132
 
133
+ # Executes the stop callback and fires shutdown event
134
+ #
135
+ # @return [void]
86
136
  def stop_callback
87
137
  if (callback = Shoryuken.stop_callback)
88
138
  logger.debug { 'Calling stop_callback' }
@@ -92,6 +142,9 @@ module Shoryuken
92
142
  fire_event(:shutdown, true)
93
143
  end
94
144
 
145
+ # Creates managers for each configured processing group
146
+ #
147
+ # @return [Array<Shoryuken::Manager>] the created managers
95
148
  def create_managers
96
149
  Shoryuken.groups.map do |group, options|
97
150
  Shoryuken::Manager.new(
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # Base formatter class that provides common functionality for Shoryuken log formatters.
6
+ # Provides thread ID generation and context management.
7
+ class Base < ::Logger::Formatter
8
+ # Generates a thread ID for the current thread.
9
+ # Uses a combination of thread object_id and process ID to create a unique identifier.
10
+ #
11
+ # @return [String] A base36-encoded thread identifier
12
+ def tid
13
+ Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
14
+ end
15
+
16
+ # Returns the current logging context as a formatted string.
17
+ # Context is set using {Shoryuken::Logging.with_context}.
18
+ #
19
+ # @return [String] Formatted context string or empty string if no context
20
+ def context
21
+ c = Shoryuken::Logging.current_context
22
+ c ? " #{c}" : ''
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A pretty log formatter that includes timestamps, process ID, thread ID,
6
+ # context information, and severity in a human-readable format.
7
+ #
8
+ # Output format: "TIMESTAMP PID TID-THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Sample output
11
+ # # 2023-08-15T10:30:45Z 12345 TID-abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class Pretty < Base
13
+ # Formats a log message with timestamp and full context information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param time [Time] Timestamp when the log entry was created
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, time, _program_name, message)
21
+ "#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shoryuken
4
+ module Logging
5
+ # A log formatter that excludes timestamps from output.
6
+ # Useful for environments where timestamps are added by external logging systems.
7
+ #
8
+ # Output format: "pid=PID tid=THREAD_ID CONTEXT SEVERITY: MESSAGE"
9
+ #
10
+ # @example Sample output
11
+ # # pid=12345 tid=abc123 MyWorker/queue1/msg-456 INFO: Processing message
12
+ class WithoutTimestamp < Base
13
+ # Formats a log message without timestamp information.
14
+ #
15
+ # @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
16
+ # @param _time [Time] Timestamp (unused)
17
+ # @param _program_name [String] Program name (unused)
18
+ # @param message [String] The log message
19
+ # @return [String] Formatted log entry with newline
20
+ def call(severity, _time, _program_name, message)
21
+ "pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
22
+ end
23
+ end
24
+ end
25
+ end