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
@@ -2,40 +2,47 @@
2
2
 
3
3
  require 'time'
4
4
  require 'logger'
5
+ require_relative 'logging/base'
6
+ require_relative 'logging/pretty'
7
+ require_relative 'logging/without_timestamp'
5
8
 
6
9
  module Shoryuken
10
+ # Provides logging functionality for Shoryuken.
11
+ # Manages the global logger instance and fiber-local context.
7
12
  module Logging
8
- class Base < ::Logger::Formatter
9
- def tid
10
- Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
11
- end
12
-
13
- def context
14
- c = Thread.current[:shoryuken_context]
15
- c ? " #{c}" : ''
16
- end
17
- end
18
-
19
- class Pretty < Base
20
- # Provide a call() method that returns the formatted message.
21
- def call(severity, time, _program_name, message)
22
- "#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
23
- end
13
+ # Executes a block with a fiber-local logging context.
14
+ # Uses Fiber storage (Ruby 3.2+) for proper isolation in async environments.
15
+ #
16
+ # @param msg [String] the context message to set
17
+ # @yield the block to execute within the context
18
+ # @return [Object] the result of the block
19
+ def self.with_context(msg)
20
+ previous = context_storage[:shoryuken_context]
21
+ context_storage[:shoryuken_context] = msg
22
+ yield
23
+ ensure
24
+ context_storage[:shoryuken_context] = previous
24
25
  end
25
26
 
26
- class WithoutTimestamp < Base
27
- def call(severity, _time, _program_name, message)
28
- "pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
29
- end
27
+ # Returns the current logging context value
28
+ #
29
+ # @return [String, nil] the current context or nil if not set
30
+ def self.current_context
31
+ context_storage[:shoryuken_context]
30
32
  end
31
33
 
32
- def self.with_context(msg)
33
- Thread.current[:shoryuken_context] = msg
34
- yield
35
- ensure
36
- Thread.current[:shoryuken_context] = nil
34
+ # Returns the Fiber class for fiber-local context storage.
35
+ # Uses Fiber[] and Fiber[]= (Ruby 3.2+) for proper isolation in async environments.
36
+ #
37
+ # @return [Class] the Fiber class
38
+ def self.context_storage
39
+ Fiber
37
40
  end
38
41
 
42
+ # Initializes a new logger instance
43
+ #
44
+ # @param log_target [IO, String] the logging target (file path or IO object)
45
+ # @return [Logger] the initialized logger
39
46
  def self.initialize_logger(log_target = STDOUT)
40
47
  @logger = Logger.new(log_target)
41
48
  @logger.level = Logger::INFO
@@ -43,10 +50,17 @@ module Shoryuken
43
50
  @logger
44
51
  end
45
52
 
53
+ # Returns the current logger instance, initializing if needed
54
+ #
55
+ # @return [Logger] the logger instance
46
56
  def self.logger
47
57
  @logger ||= initialize_logger
48
58
  end
49
59
 
60
+ # Sets the logger instance
61
+ #
62
+ # @param log [Logger, nil] the logger to use, or nil for null logger
63
+ # @return [Logger] the logger instance
50
64
  def self.logger=(log)
51
65
  @logger = log || Logger.new('/dev/null')
52
66
  end
@@ -1,15 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Manages message dispatching and processing for a single processing group.
5
+ # Coordinates between the fetcher, polling strategy, and processor.
4
6
  class Manager
5
7
  include Util
6
8
 
9
+ # Maximum number of messages to fetch in a single batch request
7
10
  BATCH_LIMIT = 10
8
- # See https://github.com/ruby-shoryuken/shoryuken/issues/348#issuecomment-292847028
11
+
12
+ # Minimum interval between dispatch cycles
13
+ # @see https://github.com/ruby-shoryuken/shoryuken/issues/348#issuecomment-292847028
9
14
  MIN_DISPATCH_INTERVAL = 0.1
10
15
 
16
+ # @return [String] the processing group name
11
17
  attr_reader :group
12
18
 
19
+ # Initializes a new Manager for a processing group
20
+ #
21
+ # @param group [String] the processing group name
22
+ # @param fetcher [Shoryuken::Fetcher] the message fetcher
23
+ # @param polling_strategy [Shoryuken::Polling::BaseStrategy] the polling strategy
24
+ # @param concurrency [Integer] the maximum number of concurrent processors
25
+ # @param executor [Concurrent::ExecutorService] the executor for async operations
13
26
  def initialize(group, fetcher, polling_strategy, concurrency, executor)
14
27
  @group = group
15
28
  @fetcher = fetcher
@@ -22,15 +35,24 @@ module Shoryuken
22
35
  @dispatching_release_signal = ::Queue.new
23
36
  end
24
37
 
38
+ # Starts the dispatch loop
39
+ #
40
+ # @return [void]
25
41
  def start
26
42
  fire_utilization_update_event
27
43
  dispatch_loop
28
44
  end
29
45
 
46
+ # Signals the manager to stop dispatching new messages
47
+ #
48
+ # @return [void]
30
49
  def stop_new_dispatching
31
50
  @stop_new_dispatching.make_true
32
51
  end
33
52
 
53
+ # Waits for any in-progress dispatching to complete
54
+ #
55
+ # @return [void]
34
56
  def await_dispatching_in_progress
35
57
  # There might still be a dispatching on-going, as the response from SQS could take some time
36
58
  # We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
@@ -38,12 +60,18 @@ module Shoryuken
38
60
  @dispatching_release_signal.pop
39
61
  end
40
62
 
63
+ # Checks if the manager is still running
64
+ #
65
+ # @return [Boolean] true if the manager is running
41
66
  def running?
42
67
  @running.true? && @executor.running?
43
68
  end
44
69
 
45
70
  private
46
71
 
72
+ # The main dispatch loop
73
+ #
74
+ # @return [void]
47
75
  def dispatch_loop
48
76
  if @stop_new_dispatching.true? || !running?
49
77
  @dispatching_release_signal << 1
@@ -53,6 +81,9 @@ module Shoryuken
53
81
  @executor.post { dispatch }
54
82
  end
55
83
 
84
+ # Dispatches messages from a queue
85
+ #
86
+ # @return [void]
56
87
  def dispatch
57
88
  return unless running?
58
89
 
@@ -71,14 +102,24 @@ module Shoryuken
71
102
  dispatch_loop
72
103
  end
73
104
 
105
+ # Returns the count of busy processors
106
+ #
107
+ # @return [Integer] the number of busy processors
74
108
  def busy
75
109
  @busy_processors.value
76
110
  end
77
111
 
112
+ # Returns the count of ready processors
113
+ #
114
+ # @return [Integer] the number of available processors
78
115
  def ready
79
116
  @max_processors - busy
80
117
  end
81
118
 
119
+ # Handles completion of processor work
120
+ #
121
+ # @param queue [String] the queue name
122
+ # @return [void]
82
123
  def processor_done(queue)
83
124
  @busy_processors.decrement
84
125
  fire_utilization_update_event
@@ -90,6 +131,11 @@ module Shoryuken
90
131
  @polling_strategy.message_processed(queue)
91
132
  end
92
133
 
134
+ # Assigns a message to a processor
135
+ #
136
+ # @param queue_name [String] the queue name
137
+ # @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
93
139
  def assign(queue_name, sqs_msg)
94
140
  return unless running?
95
141
 
@@ -112,22 +158,38 @@ module Shoryuken
112
158
  .rescue { processor_done(queue_name) }
113
159
  end
114
160
 
161
+ # Dispatches a batch of messages from a queue
162
+ #
163
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
164
+ # @return [void]
115
165
  def dispatch_batch(queue)
116
166
  batch = @fetcher.fetch(queue, BATCH_LIMIT)
117
167
  @polling_strategy.messages_found(queue.name, batch.size)
118
168
  assign(queue.name, patch_batch!(batch)) if batch.any?
119
169
  end
120
170
 
171
+ # Dispatches individual messages from a queue
172
+ #
173
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
174
+ # @return [void]
121
175
  def dispatch_single_messages(queue)
122
176
  messages = @fetcher.fetch(queue, ready)
123
177
  @polling_strategy.messages_found(queue.name, messages.size)
124
178
  messages.each { |message| assign(queue.name, message) }
125
179
  end
126
180
 
181
+ # Checks if a queue uses batch message processing
182
+ #
183
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
184
+ # @return [Boolean] true if the queue is configured for batch processing
127
185
  def batched_queue?(queue)
128
186
  Shoryuken.worker_registry.batch_receive_messages?(queue.name)
129
187
  end
130
188
 
189
+ # Patches a batch array with a message_id method
190
+ #
191
+ # @param sqs_msgs [Array<Aws::SQS::Types::Message>] the batch of messages
192
+ # @return [Array<Aws::SQS::Types::Message>] the patched batch
131
193
  def patch_batch!(sqs_msgs)
132
194
  sqs_msgs.instance_eval do
133
195
  def message_id
@@ -138,6 +200,10 @@ module Shoryuken
138
200
  sqs_msgs
139
201
  end
140
202
 
203
+ # Handles errors during dispatch
204
+ #
205
+ # @param ex [Exception] the exception that occurred
206
+ # @return [void]
141
207
  def handle_dispatch_error(ex)
142
208
  logger.error { "Manager failed: #{ex.message}" }
143
209
  logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
@@ -147,6 +213,9 @@ module Shoryuken
147
213
  @running.make_false
148
214
  end
149
215
 
216
+ # Fires a utilization update event
217
+ #
218
+ # @return [void]
150
219
  def fire_utilization_update_event
151
220
  fire_event :utilization_update, false, {
152
221
  group: @group,
@@ -1,9 +1,70 @@
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.
7
+ #
8
+ # Message instances are automatically created by Shoryuken and passed to your
9
+ # worker's `perform` method as the first argument.
10
+ #
11
+ # @example Basic worker with message handling
12
+ # class MyWorker
13
+ # include Shoryuken::Worker
14
+ # shoryuken_options queue: 'my_queue'
15
+ #
16
+ # def perform(sqs_msg, body)
17
+ # puts "Processing message #{sqs_msg.message_id}"
18
+ # puts "Message body: #{body}"
19
+ # puts "Queue: #{sqs_msg.queue_name}"
20
+ #
21
+ # # Process the message...
22
+ #
23
+ # # Delete the message when done (if auto_delete is false)
24
+ # sqs_msg.delete unless auto_delete?
25
+ # end
26
+ # end
27
+ #
28
+ # @example Working with message attributes
29
+ # def perform(sqs_msg, body)
30
+ # # Access standard SQS attributes
31
+ # sender_id = sqs_msg.attributes['SenderId']
32
+ # sent_timestamp = sqs_msg.attributes['SentTimestamp']
33
+ #
34
+ # # Access custom message attributes
35
+ # priority = sqs_msg.message_attributes['Priority']&.[]('StringValue')
36
+ # user_id = sqs_msg.message_attributes['UserId']&.[]('StringValue')
37
+ # end
4
38
  class Message
5
39
  extend Forwardable
6
40
 
41
+ # @!method message_id
42
+ # Returns the unique SQS message ID.
43
+ # @return [String] The message ID assigned by SQS
44
+ #
45
+ # @!method receipt_handle
46
+ # Returns the receipt handle needed for deleting or modifying the message.
47
+ # @return [String] The receipt handle for this message
48
+ #
49
+ # @!method md5_of_body
50
+ # Returns the MD5 hash of the message body.
51
+ # @return [String] MD5 hash of the message body
52
+ #
53
+ # @!method body
54
+ # Returns the raw message body as received from SQS.
55
+ # @return [String] The raw message body
56
+ #
57
+ # @!method attributes
58
+ # Returns the SQS message attributes (system attributes).
59
+ # @return [Hash] System attributes like SenderId, SentTimestamp, etc.
60
+ #
61
+ # @!method md5_of_message_attributes
62
+ # Returns the MD5 hash of the message attributes.
63
+ # @return [String] MD5 hash of message attributes
64
+ #
65
+ # @!method message_attributes
66
+ # Returns custom message attributes set by the sender.
67
+ # @return [Hash] Custom message attributes with typed values
7
68
  def_delegators(:data,
8
69
  :message_id,
9
70
  :receipt_handle,
@@ -13,8 +74,24 @@ module Shoryuken
13
74
  :md5_of_message_attributes,
14
75
  :message_attributes)
15
76
 
16
- attr_accessor :client, :queue_url, :queue_name, :data
77
+ # @return [Aws::SQS::Client] The SQS client used for message operations
78
+ attr_accessor :client
17
79
 
80
+ # @return [String] The URL of the SQS queue this message came from
81
+ attr_accessor :queue_url
82
+
83
+ # @return [String] The name of the queue this message came from
84
+ attr_accessor :queue_name
85
+
86
+ # @return [Aws::SQS::Types::Message] The raw SQS message data
87
+ attr_accessor :data
88
+
89
+ # Creates a new Message instance wrapping SQS message data.
90
+ #
91
+ # @param client [Aws::SQS::Client] The SQS client for message operations
92
+ # @param queue [Shoryuken::Queue] The queue this message came from
93
+ # @param data [Aws::SQS::Types::Message] The raw SQS message data
94
+ # @api private
18
95
  def initialize(client, queue, data)
19
96
  self.client = client
20
97
  self.data = data
@@ -22,6 +99,12 @@ module Shoryuken
22
99
  self.queue_name = queue.name
23
100
  end
24
101
 
102
+ # Deletes this message from the SQS queue.
103
+ # Once deleted, the message will not be redelivered and cannot be retrieved again.
104
+ # This is typically called after successful message processing when auto_delete is disabled.
105
+ #
106
+ # @return [Aws::SQS::Types::DeleteMessageResult] The deletion result
107
+ # @raise [Aws::SQS::Errors::ServiceError] If the deletion fails
25
108
  def delete
26
109
  client.delete_message(
27
110
  queue_url: queue_url,
@@ -29,12 +112,42 @@ module Shoryuken
29
112
  )
30
113
  end
31
114
 
115
+ # Changes the visibility timeout of this message with additional options.
116
+ # This allows you to hide the message from other consumers for a longer or shorter period.
117
+ #
118
+ # @param options [Hash] Options to pass to change_message_visibility
119
+ # @option options [Integer] :visibility_timeout New visibility timeout in seconds
120
+ # @return [Aws::SQS::Types::ChangeMessageVisibilityResult] The change result
121
+ # @raise [Aws::SQS::Errors::ServiceError] If the change fails
122
+ #
123
+ # @example Extending visibility with additional options
124
+ # sqs_msg.change_visibility(visibility_timeout: 300)
125
+ #
126
+ # @see #visibility_timeout= For a simpler interface
32
127
  def change_visibility(options)
33
128
  client.change_message_visibility(
34
129
  options.merge(queue_url: queue_url, receipt_handle: data.receipt_handle)
35
130
  )
36
131
  end
37
132
 
133
+ # Sets the visibility timeout for this message.
134
+ # This is a convenience method for changing only the visibility timeout.
135
+ #
136
+ # @param timeout [Integer] New visibility timeout in seconds (0-43200)
137
+ # @return [Aws::SQS::Types::ChangeMessageVisibilityResult] The change result
138
+ # @raise [Aws::SQS::Errors::ServiceError] If the change fails
139
+ #
140
+ # @example Extending processing time
141
+ # def perform(sqs_msg, body)
142
+ # if complex_processing_needed?(body)
143
+ # sqs_msg.visibility_timeout = 1800 # 30 minutes
144
+ # end
145
+ #
146
+ # process_message(body)
147
+ # end
148
+ #
149
+ # @example Making message immediately visible again
150
+ # sqs_msg.visibility_timeout = 0 # Make visible immediately
38
151
  def visibility_timeout=(timeout)
39
152
  client.change_message_visibility(
40
153
  queue_url: queue_url,
@@ -1,72 +1,156 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
- # Middleware is code configured to run before/after
5
- # a message is processed. It is patterned after Rack
6
- # middleware. Middleware exists for the server
7
- # side (when jobs are actually processed).
8
- #
9
- # To modify middleware for the server, just call
10
- # with another block:
11
- #
12
- # Shoryuken.configure_server do |config|
13
- # config.server_middleware do |chain|
14
- # chain.add MyServerHook
15
- # chain.remove ActiveRecord
4
+ # Middleware provides a way to wrap message processing with custom logic,
5
+ # similar to Rack middleware in web applications. Middleware runs on the server
6
+ # side and can perform setup, teardown, error handling, and monitoring around
7
+ # job execution.
8
+ #
9
+ # Middleware classes must implement a `call` method that accepts the worker instance,
10
+ # queue name, and SQS message, and must yield to continue the middleware chain.
11
+ #
12
+ # ## Global Middleware Configuration
13
+ #
14
+ # Configure middleware globally for all workers:
15
+ #
16
+ # Shoryuken.configure_server do |config|
17
+ # config.server_middleware do |chain|
18
+ # chain.add MyServerHook
19
+ # chain.remove Shoryuken::Middleware::Server::ActiveRecord
20
+ # end
16
21
  # end
17
- # end
18
22
  #
19
- # To insert immediately preceding another entry:
23
+ # ## Per-Worker Middleware Configuration
24
+ #
25
+ # Configure middleware for specific workers:
20
26
  #
21
- # Shoryuken.configure_server do |config|
22
- # config.server_middleware do |chain|
23
- # chain.insert_before ActiveRecord, MyServerHook
27
+ # class MyWorker
28
+ # include Shoryuken::Worker
29
+ #
30
+ # server_middleware do |chain|
31
+ # chain.add MyWorkerSpecificMiddleware
32
+ # end
24
33
  # end
25
- # end
26
34
  #
27
- # To insert immediately after another entry:
35
+ # ## Middleware Ordering
36
+ #
37
+ # Insert middleware at specific positions in the chain:
38
+ #
39
+ # # Insert before existing middleware
40
+ # chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, MyDatabaseSetup
41
+ #
42
+ # # Insert after existing middleware
43
+ # chain.insert_after Shoryuken::Middleware::Server::Timing, MyMetricsCollector
28
44
  #
29
- # Shoryuken.configure_server do |config|
30
- # config.server_middleware do |chain|
31
- # chain.insert_after ActiveRecord, MyServerHook
45
+ # # Add to beginning of chain
46
+ # chain.prepend MyFirstMiddleware
47
+ #
48
+ # ## Example Middleware Implementations
49
+ #
50
+ # # Basic logging middleware
51
+ # class LoggingMiddleware
52
+ # def call(worker_instance, queue, sqs_msg, body)
53
+ # puts "Processing #{sqs_msg.message_id} on #{queue}"
54
+ # start_time = Time.now
55
+ # yield
56
+ # puts "Completed in #{Time.now - start_time}s"
57
+ # end
32
58
  # end
33
- # end
34
59
  #
35
- # This is an example of a minimal server middleware:
60
+ # # Error reporting middleware
61
+ # class ErrorReportingMiddleware
62
+ # def call(worker_instance, queue, sqs_msg, body)
63
+ # yield
64
+ # rescue => error
65
+ # ErrorReporter.notify(error, {
66
+ # worker: worker_instance.class.name,
67
+ # queue: queue,
68
+ # message_id: sqs_msg.message_id
69
+ # })
70
+ # raise
71
+ # end
72
+ # end
36
73
  #
37
- # class MyServerHook
38
- # def call(worker_instance, queue, sqs_msg)
39
- # puts 'Before work'
40
- # yield
41
- # puts 'After work'
74
+ # # Performance monitoring middleware
75
+ # class MetricsMiddleware
76
+ # def call(worker_instance, queue, sqs_msg, body)
77
+ # start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
78
+ # yield
79
+ # duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
80
+ # StatsD.timing("shoryuken.#{worker_instance.class.name.underscore}.duration", duration)
81
+ # end
42
82
  # end
43
- # end
44
83
  #
84
+ # @see Shoryuken::Middleware::Chain Middleware chain management
85
+ # @see https://github.com/ruby-shoryuken/shoryuken/wiki/Middleware Comprehensive middleware guide
45
86
  module Middleware
87
+ # Manages a chain of middleware classes that will be instantiated and invoked
88
+ # in sequence around message processing. Provides methods for adding, removing,
89
+ # and reordering middleware.
46
90
  class Chain
91
+ # @return [Array<Entry>] The ordered list of middleware entries
47
92
  attr_reader :entries
48
93
 
94
+ # Creates a new middleware chain.
95
+ #
96
+ # @yield [Chain] The chain instance for configuration
97
+ # @example Creating and configuring a chain
98
+ # chain = Shoryuken::Middleware::Chain.new do |c|
99
+ # c.add MyMiddleware
100
+ # c.add AnotherMiddleware, option: 'value'
101
+ # end
49
102
  def initialize
50
103
  @entries = []
51
104
  yield self if block_given?
52
105
  end
53
106
 
107
+ # Creates a copy of this middleware chain.
108
+ #
109
+ # @return [Chain] A new chain with the same middleware entries
54
110
  def dup
55
111
  self.class.new.tap { |new_chain| new_chain.entries.replace(entries) }
56
112
  end
57
113
 
114
+ # Removes all instances of the specified middleware class from the chain.
115
+ #
116
+ # @param klass [Class] The middleware class to remove
117
+ # @return [Array<Entry>] The removed entries
118
+ # @example Removing ActiveRecord middleware
119
+ # chain.remove Shoryuken::Middleware::Server::ActiveRecord
58
120
  def remove(klass)
59
121
  entries.delete_if { |entry| entry.klass == klass }
60
122
  end
61
123
 
124
+ # Adds middleware to the end of the chain. Does nothing if the middleware
125
+ # class is already present in the chain.
126
+ #
127
+ # @param klass [Class] The middleware class to add
128
+ # @param args [Array] Arguments to pass to the middleware constructor
129
+ # @example Adding middleware with arguments
130
+ # chain.add MyMiddleware, timeout: 30, retries: 3
62
131
  def add(klass, *args)
63
132
  entries << Entry.new(klass, *args) unless exists?(klass)
64
133
  end
65
134
 
135
+ # Adds middleware to the beginning of the chain. Does nothing if the middleware
136
+ # class is already present in the chain.
137
+ #
138
+ # @param klass [Class] The middleware class to prepend
139
+ # @param args [Array] Arguments to pass to the middleware constructor
140
+ # @example Adding middleware to run first
141
+ # chain.prepend AuthenticationMiddleware
66
142
  def prepend(klass, *args)
67
143
  entries.insert(0, Entry.new(klass, *args)) unless exists?(klass)
68
144
  end
69
145
 
146
+ # Inserts middleware immediately before another middleware class.
147
+ # If the new middleware already exists, it's moved to the new position.
148
+ #
149
+ # @param oldklass [Class] The existing middleware to insert before
150
+ # @param newklass [Class] The middleware class to insert
151
+ # @param args [Array] Arguments to pass to the middleware constructor
152
+ # @example Insert database setup before ActiveRecord middleware
153
+ # chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, DatabaseSetup
70
154
  def insert_before(oldklass, newklass, *args)
71
155
  i = entries.index { |entry| entry.klass == newklass }
72
156
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
@@ -74,6 +158,14 @@ module Shoryuken
74
158
  entries.insert(i, new_entry)
75
159
  end
76
160
 
161
+ # Inserts middleware immediately after another middleware class.
162
+ # If the new middleware already exists, it's moved to the new position.
163
+ #
164
+ # @param oldklass [Class] The existing middleware to insert after
165
+ # @param newklass [Class] The middleware class to insert
166
+ # @param args [Array] Arguments to pass to the middleware constructor
167
+ # @example Insert metrics collection after timing middleware
168
+ # chain.insert_after Shoryuken::Middleware::Server::Timing, MetricsCollector
77
169
  def insert_after(oldklass, newklass, *args)
78
170
  i = entries.index { |entry| entry.klass == newklass }
79
171
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
@@ -81,18 +173,35 @@ module Shoryuken
81
173
  entries.insert(i + 1, new_entry)
82
174
  end
83
175
 
176
+ # Checks if a middleware class is already in the chain.
177
+ #
178
+ # @param klass [Class] The middleware class to check for
179
+ # @return [Boolean] True if the middleware is in the chain
84
180
  def exists?(klass)
85
181
  entries.any? { |entry| entry.klass == klass }
86
182
  end
87
183
 
184
+ # Creates instances of all middleware classes in the chain.
185
+ #
186
+ # @return [Array] Array of middleware instances
88
187
  def retrieve
89
188
  entries.map(&:make_new)
90
189
  end
91
190
 
191
+ # Removes all middleware from the chain.
192
+ #
193
+ # @return [Array] Empty array
92
194
  def clear
93
195
  entries.clear
94
196
  end
95
197
 
198
+ # Invokes the middleware chain with the given arguments.
199
+ # Each middleware's call method will be invoked in sequence,
200
+ # with control passed through yielding.
201
+ #
202
+ # @param args [Array] arguments to pass to each middleware
203
+ # @param final_action [Proc] the final action to perform after all middleware
204
+ # @return [void]
96
205
  def invoke(*args, &final_action)
97
206
  chain = retrieve.dup
98
207
  traverse_chain = lambda do
@@ -105,18 +214,5 @@ module Shoryuken
105
214
  traverse_chain.call
106
215
  end
107
216
  end
108
-
109
- class Entry
110
- attr_reader :klass
111
-
112
- def initialize(klass, *args)
113
- @klass = klass
114
- @args = args
115
- end
116
-
117
- def make_new
118
- @klass.new(*@args)
119
- end
120
- end
121
217
  end
122
218
  end