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
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveJob docs: http://edgeguides.rubyonrails.org/active_job_basics.html
4
+ # Example adapters ref: https://github.com/rails/rails/tree/master/activejob/lib/active_job/queue_adapters
5
+
6
+ require 'shoryuken'
7
+ require 'shoryuken/active_job/job_wrapper'
8
+
9
+ # Rails ActiveJob module providing background job processing
10
+ module ActiveJob
11
+ # Queue adapter implementations for various backends
12
+ module QueueAdapters
13
+ # Shoryuken adapter for Active Job.
14
+ # To use Shoryuken set the queue_adapter config to +:shoryuken+.
15
+ #
16
+ # @example Rails configuration
17
+ # Rails.application.config.active_job.queue_adapter = :shoryuken
18
+
19
+ # Determine the appropriate base class based on Rails version
20
+ # This prevents AbstractAdapter autoloading issues in Rails 7.0-7.1
21
+ base = if defined?(Rails) && defined?(Rails::VERSION)
22
+ (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR < 2 ? Object : AbstractAdapter)
23
+ else
24
+ Object
25
+ end
26
+
27
+ # Shoryuken queue adapter for ActiveJob integration.
28
+ # Provides methods for enqueueing jobs to SQS queues.
29
+ class ShoryukenAdapter < base
30
+ class << self
31
+ # Returns the singleton adapter instance
32
+ #
33
+ # @return [ShoryukenAdapter] the adapter instance
34
+ def instance
35
+ # https://github.com/ruby-shoryuken/shoryuken/pull/174#issuecomment-174555657
36
+ @instance ||= new
37
+ end
38
+
39
+ # Enqueues a job for immediate processing
40
+ #
41
+ # @param job [ActiveJob::Base] the job to enqueue
42
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
43
+ def enqueue(job)
44
+ instance.enqueue(job)
45
+ end
46
+
47
+ # Enqueues a job for delayed processing
48
+ #
49
+ # @param job [ActiveJob::Base] the job to enqueue
50
+ # @param timestamp [Float] Unix timestamp when the job should be processed
51
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
52
+ def enqueue_at(job, timestamp)
53
+ instance.enqueue_at(job, timestamp)
54
+ end
55
+ end
56
+
57
+ # Checks if jobs should be enqueued after transaction commit (Rails 7.2+)
58
+ #
59
+ # @return [Boolean] always returns true
60
+ def enqueue_after_transaction_commit?
61
+ true
62
+ end
63
+
64
+ # Indicates whether Shoryuken is in the process of shutting down.
65
+ #
66
+ # This method is required for ActiveJob Continuations support (Rails 8.1+).
67
+ # When true, it signals to jobs that they should checkpoint their progress
68
+ # and gracefully interrupt execution to allow for resumption after restart.
69
+ #
70
+ # @return [Boolean] true if Shoryuken is shutting down, false otherwise
71
+ # @see https://github.com/rails/rails/pull/55127 Rails ActiveJob Continuations
72
+ def stopping?
73
+ launcher = Shoryuken::Runner.instance.launcher
74
+ launcher&.stopping? || false
75
+ end
76
+
77
+ # Enqueues a job for immediate processing
78
+ #
79
+ # @param job [ActiveJob::Base] the job to enqueue
80
+ # @param options [Hash] SQS message configuration
81
+ # @option options [Integer] :delay_seconds delay before the message becomes visible
82
+ # @option options [String] :message_group_id FIFO queue group ID
83
+ # @option options [String] :message_deduplication_id FIFO queue deduplication ID
84
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
85
+ def enqueue(job, options = {}) # :nodoc:
86
+ register_worker!(job)
87
+
88
+ job.sqs_send_message_parameters.merge! options
89
+
90
+ queue = Shoryuken::Client.queues(job.queue_name)
91
+ send_message_params = message queue, job
92
+ job.sqs_send_message_parameters = send_message_params
93
+ queue.send_message send_message_params
94
+ end
95
+
96
+ # Enqueues a job for delayed processing
97
+ #
98
+ # @param job [ActiveJob::Base] the job to enqueue
99
+ # @param timestamp [Float] Unix timestamp when the job should be processed
100
+ # @return [Aws::SQS::Types::SendMessageResult] the send result
101
+ # @raise [ArgumentError] if delay is used with a FIFO queue
102
+ def enqueue_at(job, timestamp) # :nodoc:
103
+ delay = calculate_delay(timestamp)
104
+
105
+ # FIFO queues do not support per-message delays
106
+ # Check early to fail synchronously (before any async wrapping in subclasses)
107
+ # Note: negative delays (past timestamps) don't need handling here -
108
+ # SQS treats them as immediate delivery (delay_seconds=0)
109
+ # See https://github.com/ruby-shoryuken/shoryuken/issues/924
110
+ if delay.positive?
111
+ queue = Shoryuken::Client.queues(job.queue_name)
112
+ if queue.fifo?
113
+ raise ArgumentError,
114
+ "FIFO queue '#{queue.name}' does not support per-message delays. " \
115
+ 'When using ActiveJob retry_on with FIFO queues, set `wait: 0`.'
116
+ end
117
+ end
118
+
119
+ enqueue(job, delay_seconds: delay)
120
+ end
121
+
122
+ # Bulk enqueue multiple jobs efficiently using SQS batch API.
123
+ # Called by ActiveJob.perform_all_later (Rails 7.1+).
124
+ #
125
+ # @param jobs [Array<ActiveJob::Base>] array of ActiveJob instances to be enqueued
126
+ # @return [Integer] number of jobs successfully enqueued
127
+ def enqueue_all(jobs) # :nodoc:
128
+ jobs.group_by(&:queue_name).each do |queue_name, queue_jobs|
129
+ queue = Shoryuken::Client.queues(queue_name)
130
+
131
+ queue_jobs.each_slice(10) do |batch|
132
+ entries = batch.map.with_index do |job, idx|
133
+ register_worker!(job)
134
+ msg = message(queue, job)
135
+ job.sqs_send_message_parameters = msg
136
+ { id: idx.to_s }.merge(msg)
137
+ end
138
+
139
+ response = queue.send_messages(entries: entries)
140
+ successful_ids = response.successful.map { |r| r.id.to_i }.to_set
141
+ batch.each_with_index do |job, idx|
142
+ job.successfully_enqueued = successful_ids.include?(idx)
143
+ end
144
+ end
145
+ end
146
+
147
+ jobs.count(&:successfully_enqueued?)
148
+ end
149
+
150
+ private
151
+
152
+ # Calculates the delay in seconds from a timestamp
153
+ #
154
+ # @param timestamp [Float] Unix timestamp
155
+ # @return [Integer] delay in seconds
156
+ # @raise [RuntimeError] if delay exceeds 15 minutes
157
+ def calculate_delay(timestamp)
158
+ delay = (timestamp - Time.current.to_f).round
159
+ raise 'The maximum allowed delay is 15 minutes' if delay > 15.minutes
160
+
161
+ delay
162
+ end
163
+
164
+ # Builds the SQS message parameters for a job
165
+ #
166
+ # @param queue [Shoryuken::Queue] the queue to send to
167
+ # @param job [ActiveJob::Base] the job to serialize
168
+ # @return [Hash] the message parameters
169
+ def message(queue, job)
170
+ body = job.serialize
171
+ job_params = job.sqs_send_message_parameters
172
+
173
+ attributes = job_params[:message_attributes] || {}
174
+
175
+ msg = {
176
+ message_body: body,
177
+ message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
178
+ }
179
+
180
+ if queue.fifo?
181
+ # See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
182
+ # https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
183
+ msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
184
+ JSON.dump(body.except('job_id', 'enqueued_at'))
185
+ )
186
+ end
187
+
188
+ msg.merge(job_params.except(:message_attributes))
189
+ end
190
+
191
+ # Registers the JobWrapper as the worker for the job's queue
192
+ #
193
+ # @param job [ActiveJob::Base] the job being enqueued
194
+ # @return [void]
195
+ def register_worker!(job)
196
+ Shoryuken.register_worker(job.queue_name, Shoryuken::ActiveJob::JobWrapper)
197
+ end
198
+
199
+ # Default message attributes identifying the Shoryuken worker class
200
+ MESSAGE_ATTRIBUTES = {
201
+ 'shoryuken_class' => {
202
+ string_value: Shoryuken::ActiveJob::JobWrapper.to_s,
203
+ data_type: 'String'
204
+ }
205
+ }.freeze
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ActiveJob docs: http://edgeguides.rubyonrails.org/active_job_basics.html
4
+ # Example adapters ref: https://github.com/rails/rails/tree/master/activejob/lib/active_job/queue_adapters
5
+ require_relative 'shoryuken_adapter'
6
+
7
+ module ActiveJob
8
+ module QueueAdapters
9
+ # Shoryuken concurrent adapter for Active Job.
10
+ #
11
+ # This adapter sends messages asynchronously (ie non-blocking) and allows
12
+ # the caller to set up handlers for both success and failure.
13
+ #
14
+ # @example Setting up the adapter
15
+ # success_handler = ->(response, job, options) { StatsD.increment("#{job.class.name}.success") }
16
+ # error_handler = ->(err, job, options) { StatsD.increment("#{job.class.name}.failure") }
17
+ #
18
+ # adapter = ActiveJob::QueueAdapters::ShoryukenConcurrentSendAdapter.new(success_handler, error_handler)
19
+ #
20
+ # config.active_job.queue_adapter = adapter
21
+ class ShoryukenConcurrentSendAdapter < ShoryukenAdapter
22
+ # Initializes a new concurrent send adapter
23
+ #
24
+ # @param success_handler [Proc, nil] callback for successful enqueues
25
+ # @param error_handler [Proc, nil] callback for failed enqueues
26
+ def initialize(success_handler = nil, error_handler = nil)
27
+ super() if defined?(super)
28
+ @success_handler = success_handler
29
+ @error_handler = error_handler
30
+ end
31
+
32
+ # Enqueues a job asynchronously
33
+ #
34
+ # @param job [ActiveJob::Base] the job to enqueue
35
+ # @param options [Hash] SQS message configuration
36
+ # @option options [Integer] :delay_seconds delay before the message becomes visible
37
+ # @option options [String] :message_group_id FIFO queue group ID
38
+ # @option options [String] :message_deduplication_id FIFO queue deduplication ID
39
+ # @return [Concurrent::Promises::Future] the future representing the async operation
40
+ def enqueue(job, options = {})
41
+ send_concurrently(job, options) { |f_job, f_options| super(f_job, f_options) }
42
+ end
43
+
44
+ # Returns the success handler, using a default no-op if not set
45
+ #
46
+ # @return [Proc] the success handler
47
+ def success_handler
48
+ @success_handler ||= ->(_send_message_response, _job, _options) { nil }
49
+ end
50
+
51
+ # Returns the error handler, using a default logger if not set
52
+ #
53
+ # @return [Proc] the error handler
54
+ def error_handler
55
+ @error_handler ||= lambda { |error, job, _options|
56
+ Shoryuken.logger.warn("Failed to enqueue job: #{job.inspect} due to error: #{error}")
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ # Sends a message concurrently using futures
63
+ #
64
+ # @param job [ActiveJob::Base] the job to enqueue
65
+ # @param options [Hash] SQS message configuration passed to the enqueue operation
66
+ # @option options [Integer] :delay_seconds delay before the message becomes visible
67
+ # @option options [String] :message_group_id FIFO queue group ID
68
+ # @yield [job, options] the actual enqueue operation
69
+ # @return [Concurrent::Promises::Future] the future representing the async operation
70
+ def send_concurrently(job, options)
71
+ Concurrent::Promises
72
+ .future(job, options) { |f_job, f_options| [yield(f_job, f_options), f_job, f_options] }
73
+ .then { |send_message_response, f_job, f_options| success_handler.call(send_message_response, f_job, f_options) }
74
+ .rescue(job, options) { |err, f_job, f_options| error_handler.call(err, f_job, f_options) }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/current_attributes'
4
+ require 'active_job'
5
+
6
+ module Shoryuken
7
+ # ActiveJob integration module for Shoryuken
8
+ module ActiveJob
9
+ # Middleware to persist Rails CurrentAttributes across job execution.
10
+ #
11
+ # This ensures that request-scoped context (like current user, tenant, locale)
12
+ # automatically flows from the code that enqueues a job to the job's execution.
13
+ #
14
+ # Based on Sidekiq's approach to persisting current attributes.
15
+ #
16
+ # @example Setup in initializer
17
+ # require 'shoryuken/active_job/current_attributes'
18
+ # Shoryuken::ActiveJob::CurrentAttributes.persist('MyApp::Current')
19
+ #
20
+ # @example Multiple CurrentAttributes classes
21
+ # Shoryuken::ActiveJob::CurrentAttributes.persist('MyApp::Current', 'MyApp::RequestContext')
22
+ #
23
+ # @see https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
24
+ # @see https://github.com/sidekiq/sidekiq/blob/main/lib/sidekiq/middleware/current_attributes.rb
25
+ module CurrentAttributes
26
+ # Serializer for current attributes using ActiveJob::Arguments.
27
+ # Supports Symbols and GlobalID objects.
28
+ module Serializer
29
+ module_function
30
+
31
+ # Serializes attributes hash for SQS message storage
32
+ #
33
+ # @param attrs [Hash] the attributes to serialize
34
+ # @return [Object] the serialized attributes
35
+ def serialize(attrs)
36
+ ::ActiveJob::Arguments.serialize([attrs]).first
37
+ end
38
+
39
+ # Deserializes attributes hash from SQS message
40
+ #
41
+ # @param attrs [Object] the serialized attributes
42
+ # @return [Hash] the deserialized attributes
43
+ def deserialize(attrs)
44
+ ::ActiveJob::Arguments.deserialize([attrs]).first
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # @return [Hash{String => String}] serialization keys mapped to CurrentAttributes class names
50
+ attr_reader :cattrs
51
+
52
+ # Register CurrentAttributes classes to persist across job execution.
53
+ #
54
+ # @param klasses [Array<String, Class>] CurrentAttributes class names or classes
55
+ # @example
56
+ # Shoryuken::ActiveJob::CurrentAttributes.persist('Current')
57
+ # Shoryuken::ActiveJob::CurrentAttributes.persist(Current, RequestContext)
58
+ def persist(*klasses)
59
+ @cattrs ||= {}
60
+
61
+ klasses.flatten.each_with_index do |klass, idx|
62
+ key = @cattrs.empty? ? 'cattr' : "cattr_#{idx}"
63
+ @cattrs[key] = klass.to_s
64
+ end
65
+
66
+ # Prepend the persistence module to the adapter for serialization
67
+ unless ::ActiveJob::QueueAdapters::ShoryukenAdapter.ancestors.include?(Persistence)
68
+ ::ActiveJob::QueueAdapters::ShoryukenAdapter.prepend(Persistence)
69
+ end
70
+
71
+ # Prepend the loading module to JobWrapper for deserialization
72
+ unless Shoryuken::ActiveJob::JobWrapper.ancestors.include?(Loading)
73
+ Shoryuken::ActiveJob::JobWrapper.prepend(Loading)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Module prepended to ShoryukenAdapter to serialize CurrentAttributes on enqueue.
79
+ module Persistence
80
+ private
81
+
82
+ # Builds the SQS message with CurrentAttributes data
83
+ #
84
+ # @param queue [Shoryuken::Queue] the target queue
85
+ # @param job [ActiveJob::Base] the job being enqueued
86
+ # @return [Hash] the message parameters
87
+ def message(queue, job)
88
+ hash = super
89
+
90
+ CurrentAttributes.cattrs&.each do |key, klass_name|
91
+ next if hash[:message_body].key?(key)
92
+
93
+ klass = klass_name.constantize
94
+ attrs = klass.attributes
95
+ next if attrs.empty?
96
+
97
+ hash[:message_body][key] = Serializer.serialize(attrs)
98
+ end
99
+
100
+ hash
101
+ end
102
+ end
103
+
104
+ # Module prepended to JobWrapper to restore CurrentAttributes on execute.
105
+ module Loading
106
+ # Performs the job after restoring CurrentAttributes
107
+ #
108
+ # @param sqs_msg [Shoryuken::Message] the SQS message
109
+ # @param hash [Hash] the deserialized job data
110
+ # @return [void]
111
+ def perform(sqs_msg, hash)
112
+ klasses_to_reset = []
113
+
114
+ CurrentAttributes.cattrs&.each do |key, klass_name|
115
+ next unless hash.key?(key)
116
+
117
+ klass = klass_name.constantize
118
+ klasses_to_reset << klass
119
+
120
+ begin
121
+ attrs = Serializer.deserialize(hash[key])
122
+ attrs.each do |attr_name, value|
123
+ klass.public_send(:"#{attr_name}=", value) if klass.respond_to?(:"#{attr_name}=")
124
+ end
125
+ rescue => e
126
+ # Log but don't fail if attributes can't be restored
127
+ # (e.g., attribute removed between enqueue and execute)
128
+ Shoryuken.logger.warn("Failed to restore CurrentAttributes #{klass_name}: #{e.message}")
129
+ end
130
+ end
131
+
132
+ super
133
+ ensure
134
+ klasses_to_reset.each(&:reset)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_job'
4
+ require 'shoryuken/worker'
5
+
6
+ module Shoryuken
7
+ module ActiveJob
8
+ # Internal worker class that processes ActiveJob jobs.
9
+ # This class bridges ActiveJob's interface with Shoryuken's worker interface.
10
+ #
11
+ # @api private
12
+ class JobWrapper # :nodoc:
13
+ include Shoryuken::Worker
14
+
15
+ shoryuken_options body_parser: :json, auto_delete: true
16
+
17
+ # Processes an ActiveJob job from an SQS message.
18
+ #
19
+ # @param sqs_msg [Shoryuken::Message] The SQS message containing the job data
20
+ # @param hash [Hash] The parsed job data from the message body
21
+ def perform(sqs_msg, hash)
22
+ receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i
23
+ past_receives = receive_count - 1
24
+ ::ActiveJob::Base.execute hash.merge({ 'executions' => past_receives })
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Parses SQS message bodies according to worker configuration.
5
+ # Supports JSON parsing, text extraction, custom Procs, and
6
+ # any object that responds to parse or load methods.
4
7
  class BodyParser
5
8
  class << self
9
+ # Parses the body of an SQS message according to the worker's body_parser option
10
+ #
11
+ # @param worker_class [Class] the worker class with shoryuken options
12
+ # @param sqs_msg [Shoryuken::Message] the SQS message to parse
13
+ # @return [Object] the parsed message body
6
14
  def parse(worker_class, sqs_msg)
7
15
  body_parser = worker_class.get_shoryuken_options['body_parser']
8
16
 
@@ -1,18 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Client class for interacting with SQS queues.
5
+ # Provides a simple interface for accessing and managing queue instances.
4
6
  class Client
7
+ # @return [Hash{String => Shoryuken::Queue}] cached queue instances by name
5
8
  @@queues = {}
6
9
 
7
10
  class << self
11
+ # Returns a Queue instance for the given queue name
12
+ #
13
+ # @param name [String, Symbol] the name of the queue
14
+ # @return [Shoryuken::Queue] the queue instance
8
15
  def queues(name)
9
16
  @@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name)
10
17
  end
11
18
 
19
+ # Returns the current SQS client
20
+ #
21
+ # @return [Aws::SQS::Client] the SQS client
12
22
  def sqs
13
23
  Shoryuken.sqs_client
14
24
  end
15
25
 
26
+ # Sets a new SQS client and clears the queue cache
27
+ #
28
+ # @param sqs [Aws::SQS::Client] the new SQS client
29
+ # @return [Aws::SQS::Client] the SQS client
16
30
  def sqs=(sqs)
17
31
  # Since the @@queues values (Shoryuken::Queue objects) are built referencing @@sqs, if it changes, we need to
18
32
  # re-build them on subsequent calls to `.queues(name)`.
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Default exception handler that logs errors during message processing.
5
+ # Implements a simple error logging strategy that outputs the exception
6
+ # message and backtrace to the configured logger.
4
7
  class DefaultExceptionHandler
5
8
  extend Util
6
9
 
10
+ # Handles an exception that occurred during message processing
11
+ #
12
+ # @param exception [Exception] the exception that was raised
13
+ # @param _queue [String] the queue name where the error occurred (unused)
14
+ # @param _sqs_msg [Shoryuken::Message] the message being processed (unused)
15
+ # @return [void]
7
16
  def self.call(exception, _queue, _sqs_msg)
8
17
  logger.error { "Processor failed: #{exception.message}" }
9
18
  logger.error { exception.backtrace.join("\n") } if exception.backtrace
@@ -1,19 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shoryuken
4
+ # Default implementation of the worker registry.
5
+ # Stores and retrieves worker classes mapped to queue names.
4
6
  class DefaultWorkerRegistry < WorkerRegistry
7
+ # Initializes a new DefaultWorkerRegistry with an empty workers hash
5
8
  def initialize
6
9
  @workers = Shoryuken::Helpers::AtomicHash.new
7
10
  end
8
11
 
12
+ # Checks if a queue is configured for batch message receiving
13
+ #
14
+ # @param queue [String] the queue name
15
+ # @return [Boolean] true if the queue's worker has batch mode enabled
9
16
  def batch_receive_messages?(queue)
10
17
  !!(@workers[queue] && @workers[queue].get_shoryuken_options['batch'])
11
18
  end
12
19
 
20
+ # Clears all registered workers
21
+ #
22
+ # @return [void]
13
23
  def clear
14
24
  @workers.clear
15
25
  end
16
26
 
27
+ # Fetches a worker instance for processing a message
28
+ #
29
+ # @param queue [String] the queue name
30
+ # @param message [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
31
+ # @return [Object, nil] a new worker instance or nil if not found
17
32
  def fetch_worker(queue, message)
18
33
  worker_class = !message.is_a?(Array) &&
19
34
  message.message_attributes &&
@@ -29,13 +44,22 @@ module Shoryuken
29
44
  worker_class.new if worker_class
30
45
  end
31
46
 
47
+ # Returns all registered queue names
48
+ #
49
+ # @return [Array<String>] the queue names with registered workers
32
50
  def queues
33
51
  @workers.keys
34
52
  end
35
53
 
54
+ # Registers a worker class for a queue
55
+ #
56
+ # @param queue [String] the queue name
57
+ # @param clazz [Class] the worker class to register
58
+ # @return [Class] the registered worker class
59
+ # @raise [Errors::InvalidWorkerRegistrationError] if a batchable worker is already registered for the queue
36
60
  def register_worker(queue, clazz)
37
61
  if (worker_class = @workers[queue]) && (worker_class.get_shoryuken_options['batch'] == true || clazz.get_shoryuken_options['batch'] == true)
38
- fail ArgumentError, "Could not register #{clazz} for #{queue}, "\
62
+ raise Errors::InvalidWorkerRegistrationError, "Could not register #{clazz} for #{queue}, "\
39
63
  "because #{worker_class} is already registered for this queue, "\
40
64
  "and Shoryuken doesn't support a batchable worker for a queue with multiple workers"
41
65
  end
@@ -43,6 +67,10 @@ module Shoryuken
43
67
  @workers[queue] = clazz
44
68
  end
45
69
 
70
+ # Returns all worker classes for a queue
71
+ #
72
+ # @param queue [String] the queue name
73
+ # @return [Array<Class>] the registered worker classes
46
74
  def workers(queue)
47
75
  [@workers.fetch(queue, [])].flatten
48
76
  end