shoryuken 6.2.1 → 7.0.2

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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +36 -0
  3. data/.github/workflows/specs.yml +49 -44
  4. data/.github/workflows/verify-action-pins.yml +16 -0
  5. data/.gitignore +4 -1
  6. data/.rspec +3 -1
  7. data/.rubocop.yml +6 -1
  8. data/.ruby-version +1 -0
  9. data/.yard-lint.yml +279 -0
  10. data/CHANGELOG.md +308 -139
  11. data/Gemfile +1 -8
  12. data/Gemfile.lint +9 -0
  13. data/Gemfile.lint.lock +69 -0
  14. data/README.md +16 -33
  15. data/Rakefile +6 -10
  16. data/bin/clean_sqs +52 -0
  17. data/bin/cli/base.rb +22 -2
  18. data/bin/cli/sqs.rb +74 -7
  19. data/bin/integrations +275 -0
  20. data/bin/scenario +154 -0
  21. data/bin/shoryuken +3 -2
  22. data/docker-compose.yml +6 -0
  23. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +20 -6
  24. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  25. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  26. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  27. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  28. data/lib/shoryuken/body_parser.rb +11 -1
  29. data/lib/shoryuken/client.rb +16 -0
  30. data/lib/shoryuken/default_exception_handler.rb +11 -0
  31. data/lib/shoryuken/default_worker_registry.rb +39 -11
  32. data/lib/shoryuken/environment_loader.rb +85 -15
  33. data/lib/shoryuken/errors.rb +36 -0
  34. data/lib/shoryuken/fetcher.rb +41 -3
  35. data/lib/shoryuken/helpers/atomic_boolean.rb +58 -0
  36. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  37. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  38. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  39. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  40. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  41. data/lib/shoryuken/inline_message.rb +22 -0
  42. data/lib/shoryuken/launcher.rb +55 -0
  43. data/lib/shoryuken/logging/base.rb +26 -0
  44. data/lib/shoryuken/logging/pretty.rb +25 -0
  45. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  46. data/lib/shoryuken/logging.rb +43 -15
  47. data/lib/shoryuken/manager.rb +84 -5
  48. data/lib/shoryuken/message.rb +116 -1
  49. data/lib/shoryuken/middleware/chain.rb +141 -43
  50. data/lib/shoryuken/middleware/entry.rb +30 -0
  51. data/lib/shoryuken/middleware/server/active_record.rb +10 -0
  52. data/lib/shoryuken/middleware/server/auto_delete.rb +12 -0
  53. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +37 -11
  54. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +34 -3
  55. data/lib/shoryuken/middleware/server/non_retryable_exception.rb +95 -0
  56. data/lib/shoryuken/middleware/server/timing.rb +13 -0
  57. data/lib/shoryuken/options.rb +154 -13
  58. data/lib/shoryuken/polling/base_strategy.rb +127 -0
  59. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  60. data/lib/shoryuken/polling/strict_priority.rb +41 -0
  61. data/lib/shoryuken/polling/weighted_round_robin.rb +44 -0
  62. data/lib/shoryuken/processor.rb +37 -3
  63. data/lib/shoryuken/queue.rb +99 -8
  64. data/lib/shoryuken/runner.rb +54 -16
  65. data/lib/shoryuken/util.rb +32 -7
  66. data/lib/shoryuken/version.rb +4 -1
  67. data/lib/shoryuken/worker/default_executor.rb +23 -1
  68. data/lib/shoryuken/worker/inline_executor.rb +33 -2
  69. data/lib/shoryuken/worker.rb +224 -0
  70. data/lib/shoryuken/worker_registry.rb +35 -0
  71. data/lib/shoryuken.rb +27 -38
  72. data/renovate.json +62 -0
  73. data/shoryuken.gemspec +8 -4
  74. data/spec/integration/.rspec +1 -0
  75. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  76. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  77. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  78. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  79. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  80. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  81. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  82. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  83. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  84. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  85. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  86. data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +63 -0
  87. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  88. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  89. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  90. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  91. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  92. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  93. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  94. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  95. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  96. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  97. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  98. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  99. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  100. data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
  101. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  102. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  103. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  104. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  105. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  106. data/spec/integration/launcher/launcher_spec.rb +40 -0
  107. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  108. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  109. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  110. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  111. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  112. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  113. data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +149 -0
  114. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  115. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  116. data/spec/integration/rails/rails_72/Gemfile +6 -0
  117. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  118. data/spec/integration/rails/rails_80/Gemfile +6 -0
  119. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  120. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  121. data/spec/integration/rails/rails_81/Gemfile +6 -0
  122. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  123. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  124. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  125. data/spec/integration/spec_helper.rb +7 -0
  126. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  127. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  128. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  129. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  130. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  131. data/spec/integrations_helper.rb +243 -0
  132. data/spec/lib/active_job/extensions_spec.rb +225 -0
  133. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  134. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +5 -4
  135. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +6 -5
  136. data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +2 -4
  137. data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +1 -1
  138. data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +9 -10
  139. data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +1 -2
  140. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +10 -9
  141. data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +23 -26
  142. data/spec/lib/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  143. data/spec/lib/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  144. data/spec/lib/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  145. data/spec/lib/shoryuken/helpers/hash_utils_spec.rb +145 -0
  146. data/spec/lib/shoryuken/helpers/string_utils_spec.rb +124 -0
  147. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  148. data/spec/lib/shoryuken/helpers_integration_spec.rb +96 -0
  149. data/spec/lib/shoryuken/inline_message_spec.rb +196 -0
  150. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +23 -2
  151. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  152. data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +1 -2
  153. data/spec/lib/shoryuken/message_spec.rb +109 -0
  154. data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +1 -1
  155. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  156. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  157. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +1 -1
  158. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +51 -1
  159. data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  160. data/spec/lib/shoryuken/middleware/server/non_retryable_exception_spec.rb +214 -0
  161. data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +1 -1
  162. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +49 -6
  163. data/spec/lib/shoryuken/polling/base_strategy_spec.rb +280 -0
  164. data/spec/lib/shoryuken/polling/queue_configuration_spec.rb +195 -0
  165. data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +1 -1
  166. data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +1 -1
  167. data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +1 -1
  168. data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +2 -3
  169. data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +1 -3
  170. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +2 -2
  171. data/spec/lib/shoryuken/version_spec.rb +17 -0
  172. data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +1 -1
  173. data/spec/lib/shoryuken/worker/inline_executor_spec.rb +105 -0
  174. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  175. data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +15 -11
  176. data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +1 -1
  177. data/spec/shared_examples_for_active_job.rb +40 -15
  178. data/spec/spec_helper.rb +48 -2
  179. metadata +295 -101
  180. data/.codeclimate.yml +0 -20
  181. data/.devcontainer/Dockerfile +0 -17
  182. data/.devcontainer/base.Dockerfile +0 -43
  183. data/.devcontainer/devcontainer.json +0 -35
  184. data/.github/FUNDING.yml +0 -12
  185. data/.github/dependabot.yml +0 -6
  186. data/.github/workflows/stale.yml +0 -20
  187. data/.reek.yml +0 -5
  188. data/Appraisals +0 -42
  189. data/gemfiles/.gitignore +0 -1
  190. data/gemfiles/aws_sdk_core_2.gemfile +0 -21
  191. data/gemfiles/rails_4_2.gemfile +0 -20
  192. data/gemfiles/rails_5_2.gemfile +0 -21
  193. data/gemfiles/rails_6_0.gemfile +0 -21
  194. data/gemfiles/rails_6_1.gemfile +0 -21
  195. data/gemfiles/rails_7_0.gemfile +0 -22
  196. data/lib/shoryuken/core_ext.rb +0 -69
  197. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -103
  198. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  199. data/lib/shoryuken/polling/base.rb +0 -67
  200. data/shoryuken.jpg +0 -0
  201. data/spec/integration/launcher_spec.rb +0 -128
  202. data/spec/shoryuken/core_ext_spec.rb +0 -40
  203. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -7
  204. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -84
  205. data/spec/shoryuken/worker/inline_executor_spec.rb +0 -49
@@ -1,27 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
4
  require 'logger'
5
+ require_relative 'logging/base'
6
+ require_relative 'logging/pretty'
7
+ require_relative 'logging/without_timestamp'
3
8
 
4
9
  module Shoryuken
10
+ # Provides logging functionality for Shoryuken.
11
+ # Manages the global logger instance and fiber-local context.
5
12
  module Logging
6
- class Pretty < Logger::Formatter
7
- # Provide a call() method that returns the formatted message.
8
- def call(severity, time, _program_name, message)
9
- "#{time.utc.iso8601} #{Process.pid} TID-#{Thread.current.object_id.to_s(36)}#{context} #{severity}: #{message}\n"
10
- end
11
-
12
- def context
13
- c = Thread.current[:shoryuken_context]
14
- c ? " #{c}" : ''
15
- end
16
- end
17
-
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
18
19
  def self.with_context(msg)
19
- Thread.current[:shoryuken_context] = msg
20
+ previous = context_storage[:shoryuken_context]
21
+ context_storage[:shoryuken_context] = msg
20
22
  yield
21
23
  ensure
22
- Thread.current[:shoryuken_context] = nil
24
+ context_storage[:shoryuken_context] = previous
25
+ end
26
+
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]
32
+ end
33
+
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
23
40
  end
24
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
25
46
  def self.initialize_logger(log_target = STDOUT)
26
47
  @logger = Logger.new(log_target)
27
48
  @logger.level = Logger::INFO
@@ -29,12 +50,19 @@ module Shoryuken
29
50
  @logger
30
51
  end
31
52
 
53
+ # Returns the current logger instance, initializing if needed
54
+ #
55
+ # @return [Logger] the logger instance
32
56
  def self.logger
33
57
  @logger ||= initialize_logger
34
58
  end
35
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
36
64
  def self.logger=(log)
37
- @logger = (log || Logger.new('/dev/null'))
65
+ @logger = log || Logger.new('/dev/null')
38
66
  end
39
67
  end
40
68
  end
@@ -1,34 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
4
+ # Manages message dispatching and processing for a single processing group.
5
+ # Coordinates between the fetcher, polling strategy, and processor.
2
6
  class Manager
3
7
  include Util
4
8
 
9
+ # Maximum number of messages to fetch in a single batch request
5
10
  BATCH_LIMIT = 10
6
- # See https://github.com/phstc/shoryuken/issues/348#issuecomment-292847028
11
+
12
+ # Minimum interval between dispatch cycles
13
+ # @see https://github.com/ruby-shoryuken/shoryuken/issues/348#issuecomment-292847028
7
14
  MIN_DISPATCH_INTERVAL = 0.1
8
15
 
16
+ # @return [String] the processing group name
9
17
  attr_reader :group
10
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
11
26
  def initialize(group, fetcher, polling_strategy, concurrency, executor)
12
27
  @group = group
13
28
  @fetcher = fetcher
14
29
  @polling_strategy = polling_strategy
15
30
  @max_processors = concurrency
16
- @busy_processors = Concurrent::AtomicFixnum.new(0)
31
+ @busy_processors = Shoryuken::Helpers::AtomicCounter.new(0)
17
32
  @executor = executor
18
- @running = Concurrent::AtomicBoolean.new(true)
19
- @stop_new_dispatching = Concurrent::AtomicBoolean.new(false)
33
+ @running = Shoryuken::Helpers::AtomicBoolean.new(true)
34
+ @stop_new_dispatching = Shoryuken::Helpers::AtomicBoolean.new(false)
20
35
  @dispatching_release_signal = ::Queue.new
21
36
  end
22
37
 
38
+ # Starts the dispatch loop
39
+ #
40
+ # @return [void]
23
41
  def start
24
42
  fire_utilization_update_event
25
43
  dispatch_loop
26
44
  end
27
45
 
46
+ # Signals the manager to stop dispatching new messages
47
+ #
48
+ # @return [void]
28
49
  def stop_new_dispatching
29
50
  @stop_new_dispatching.make_true
30
51
  end
31
52
 
53
+ # Waits for any in-progress dispatching to complete
54
+ #
55
+ # @return [void]
32
56
  def await_dispatching_in_progress
33
57
  # There might still be a dispatching on-going, as the response from SQS could take some time
34
58
  # We don't want to stop the process before processing incoming messages, as they would stay "in-flight" for some time on SQS
@@ -36,12 +60,18 @@ module Shoryuken
36
60
  @dispatching_release_signal.pop
37
61
  end
38
62
 
63
+ # Checks if the manager is still running
64
+ #
65
+ # @return [Boolean] true if the manager is running
39
66
  def running?
40
67
  @running.true? && @executor.running?
41
68
  end
42
69
 
43
70
  private
44
71
 
72
+ # The main dispatch loop
73
+ #
74
+ # @return [void]
45
75
  def dispatch_loop
46
76
  if @stop_new_dispatching.true? || !running?
47
77
  @dispatching_release_signal << 1
@@ -51,6 +81,9 @@ module Shoryuken
51
81
  @executor.post { dispatch }
52
82
  end
53
83
 
84
+ # Dispatches messages from a queue
85
+ #
86
+ # @return [void]
54
87
  def dispatch
55
88
  return unless running?
56
89
 
@@ -69,14 +102,24 @@ module Shoryuken
69
102
  dispatch_loop
70
103
  end
71
104
 
105
+ # Returns the count of busy processors
106
+ #
107
+ # @return [Integer] the number of busy processors
72
108
  def busy
73
109
  @busy_processors.value
74
110
  end
75
111
 
112
+ # Returns the count of ready processors
113
+ #
114
+ # @return [Integer] the number of available processors
76
115
  def ready
77
116
  @max_processors - busy
78
117
  end
79
118
 
119
+ # Handles completion of processor work
120
+ #
121
+ # @param queue [String] the queue name
122
+ # @return [void]
80
123
  def processor_done(queue)
81
124
  @busy_processors.decrement
82
125
  fire_utilization_update_event
@@ -88,6 +131,11 @@ module Shoryuken
88
131
  @polling_strategy.message_processed(queue)
89
132
  end
90
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
91
139
  def assign(queue_name, sqs_msg)
92
140
  return unless running?
93
141
 
@@ -97,27 +145,51 @@ module Shoryuken
97
145
  fire_utilization_update_event
98
146
 
99
147
  Concurrent::Promise
100
- .execute(executor: @executor) { Processor.process(queue_name, sqs_msg) }
148
+ .execute(executor: @executor) do
149
+ original_priority = Thread.current.priority
150
+ begin
151
+ Thread.current.priority = Shoryuken.thread_priority
152
+ Processor.process(queue_name, sqs_msg)
153
+ ensure
154
+ Thread.current.priority = original_priority
155
+ end
156
+ end
101
157
  .then { processor_done(queue_name) }
102
158
  .rescue { processor_done(queue_name) }
103
159
  end
104
160
 
161
+ # Dispatches a batch of messages from a queue
162
+ #
163
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
164
+ # @return [void]
105
165
  def dispatch_batch(queue)
106
166
  batch = @fetcher.fetch(queue, BATCH_LIMIT)
107
167
  @polling_strategy.messages_found(queue.name, batch.size)
108
168
  assign(queue.name, patch_batch!(batch)) if batch.any?
109
169
  end
110
170
 
171
+ # Dispatches individual messages from a queue
172
+ #
173
+ # @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
174
+ # @return [void]
111
175
  def dispatch_single_messages(queue)
112
176
  messages = @fetcher.fetch(queue, ready)
113
177
  @polling_strategy.messages_found(queue.name, messages.size)
114
178
  messages.each { |message| assign(queue.name, message) }
115
179
  end
116
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
117
185
  def batched_queue?(queue)
118
186
  Shoryuken.worker_registry.batch_receive_messages?(queue.name)
119
187
  end
120
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
121
193
  def patch_batch!(sqs_msgs)
122
194
  sqs_msgs.instance_eval do
123
195
  def message_id
@@ -128,6 +200,10 @@ module Shoryuken
128
200
  sqs_msgs
129
201
  end
130
202
 
203
+ # Handles errors during dispatch
204
+ #
205
+ # @param ex [Exception] the exception that occurred
206
+ # @return [void]
131
207
  def handle_dispatch_error(ex)
132
208
  logger.error { "Manager failed: #{ex.message}" }
133
209
  logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
@@ -137,6 +213,9 @@ module Shoryuken
137
213
  @running.make_false
138
214
  end
139
215
 
216
+ # Fires a utilization update event
217
+ #
218
+ # @return [void]
140
219
  def fire_utilization_update_event
141
220
  fire_event :utilization_update, false, {
142
221
  group: @group,
@@ -1,7 +1,70 @@
1
+ # frozen_string_literal: true
2
+
1
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
2
38
  class Message
3
39
  extend Forwardable
4
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
5
68
  def_delegators(:data,
6
69
  :message_id,
7
70
  :receipt_handle,
@@ -11,8 +74,24 @@ module Shoryuken
11
74
  :md5_of_message_attributes,
12
75
  :message_attributes)
13
76
 
14
- attr_accessor :client, :queue_url, :queue_name, :data
77
+ # @return [Aws::SQS::Client] The SQS client used for message operations
78
+ attr_accessor :client
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
15
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
16
95
  def initialize(client, queue, data)
17
96
  self.client = client
18
97
  self.data = data
@@ -20,6 +99,12 @@ module Shoryuken
20
99
  self.queue_name = queue.name
21
100
  end
22
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
23
108
  def delete
24
109
  client.delete_message(
25
110
  queue_url: queue_url,
@@ -27,12 +112,42 @@ module Shoryuken
27
112
  )
28
113
  end
29
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
30
127
  def change_visibility(options)
31
128
  client.change_message_visibility(
32
129
  options.merge(queue_url: queue_url, receipt_handle: data.receipt_handle)
33
130
  )
34
131
  end
35
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
36
151
  def visibility_timeout=(timeout)
37
152
  client.change_message_visibility(
38
153
  queue_url: queue_url,
@@ -1,70 +1,156 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
- # Middleware is code configured to run before/after
3
- # a message is processed. It is patterned after Rack
4
- # middleware. Middleware exists for the server
5
- # side (when jobs are actually processed).
6
- #
7
- # To modify middleware for the server, just call
8
- # with another block:
9
- #
10
- # Shoryuken.configure_server do |config|
11
- # config.server_middleware do |chain|
12
- # chain.add MyServerHook
13
- # 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
14
21
  # end
15
- # end
16
22
  #
17
- # To insert immediately preceding another entry:
23
+ # ## Per-Worker Middleware Configuration
18
24
  #
19
- # Shoryuken.configure_server do |config|
20
- # config.server_middleware do |chain|
21
- # chain.insert_before ActiveRecord, MyServerHook
25
+ # Configure middleware for specific workers:
26
+ #
27
+ # class MyWorker
28
+ # include Shoryuken::Worker
29
+ #
30
+ # server_middleware do |chain|
31
+ # chain.add MyWorkerSpecificMiddleware
32
+ # end
22
33
  # end
23
- # end
24
34
  #
25
- # 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
26
44
  #
27
- # Shoryuken.configure_server do |config|
28
- # config.server_middleware do |chain|
29
- # 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
30
58
  # end
31
- # end
32
59
  #
33
- # 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
34
73
  #
35
- # class MyServerHook
36
- # def call(worker_instance, queue, sqs_msg)
37
- # puts 'Before work'
38
- # yield
39
- # 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
40
82
  # end
41
- # end
42
83
  #
84
+ # @see Shoryuken::Middleware::Chain Middleware chain management
85
+ # @see https://github.com/ruby-shoryuken/shoryuken/wiki/Middleware Comprehensive middleware guide
43
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.
44
90
  class Chain
91
+ # @return [Array<Entry>] The ordered list of middleware entries
45
92
  attr_reader :entries
46
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
47
102
  def initialize
48
103
  @entries = []
49
104
  yield self if block_given?
50
105
  end
51
106
 
107
+ # Creates a copy of this middleware chain.
108
+ #
109
+ # @return [Chain] A new chain with the same middleware entries
52
110
  def dup
53
111
  self.class.new.tap { |new_chain| new_chain.entries.replace(entries) }
54
112
  end
55
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
56
120
  def remove(klass)
57
121
  entries.delete_if { |entry| entry.klass == klass }
58
122
  end
59
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
60
131
  def add(klass, *args)
61
132
  entries << Entry.new(klass, *args) unless exists?(klass)
62
133
  end
63
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
64
142
  def prepend(klass, *args)
65
143
  entries.insert(0, Entry.new(klass, *args)) unless exists?(klass)
66
144
  end
67
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
68
154
  def insert_before(oldklass, newklass, *args)
69
155
  i = entries.index { |entry| entry.klass == newklass }
70
156
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
@@ -72,6 +158,14 @@ module Shoryuken
72
158
  entries.insert(i, new_entry)
73
159
  end
74
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
75
169
  def insert_after(oldklass, newklass, *args)
76
170
  i = entries.index { |entry| entry.klass == newklass }
77
171
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
@@ -79,18 +173,35 @@ module Shoryuken
79
173
  entries.insert(i + 1, new_entry)
80
174
  end
81
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
82
180
  def exists?(klass)
83
181
  entries.any? { |entry| entry.klass == klass }
84
182
  end
85
183
 
184
+ # Creates instances of all middleware classes in the chain.
185
+ #
186
+ # @return [Array] Array of middleware instances
86
187
  def retrieve
87
188
  entries.map(&:make_new)
88
189
  end
89
190
 
191
+ # Removes all middleware from the chain.
192
+ #
193
+ # @return [Array] Empty array
90
194
  def clear
91
195
  entries.clear
92
196
  end
93
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]
94
205
  def invoke(*args, &final_action)
95
206
  chain = retrieve.dup
96
207
  traverse_chain = lambda do
@@ -103,18 +214,5 @@ module Shoryuken
103
214
  traverse_chain.call
104
215
  end
105
216
  end
106
-
107
- class Entry
108
- attr_reader :klass
109
-
110
- def initialize(klass, *args)
111
- @klass = klass
112
- @args = args
113
- end
114
-
115
- def make_new
116
- @klass.new(*@args)
117
- end
118
- end
119
217
  end
120
218
  end