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.
- checksums.yaml +4 -4
- data/.github/workflows/push.yml +2 -2
- data/.github/workflows/specs.yml +38 -43
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -1
- data/.yard-lint.yml +279 -0
- data/CHANGELOG.md +69 -1
- data/Gemfile +1 -1
- data/README.md +2 -7
- data/Rakefile +4 -10
- data/bin/clean_localstack +52 -0
- data/bin/cli/base.rb +21 -0
- data/bin/cli/sqs.rb +61 -2
- data/bin/integrations +275 -0
- data/bin/scenario +154 -0
- data/bin/shoryuken +1 -1
- data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +15 -4
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
- data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
- data/lib/shoryuken/active_job/current_attributes.rb +139 -0
- data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
- data/lib/shoryuken/body_parser.rb +8 -0
- data/lib/shoryuken/client.rb +14 -0
- data/lib/shoryuken/default_exception_handler.rb +9 -0
- data/lib/shoryuken/default_worker_registry.rb +29 -1
- data/lib/shoryuken/environment_loader.rb +78 -8
- data/lib/shoryuken/errors.rb +33 -0
- data/lib/shoryuken/fetcher.rb +37 -1
- data/lib/shoryuken/helpers/atomic_boolean.rb +19 -5
- data/lib/shoryuken/helpers/timer_task.rb +80 -0
- data/lib/shoryuken/launcher.rb +53 -0
- data/lib/shoryuken/logging/base.rb +26 -0
- data/lib/shoryuken/logging/pretty.rb +25 -0
- data/lib/shoryuken/logging/without_timestamp.rb +25 -0
- data/lib/shoryuken/logging.rb +39 -25
- data/lib/shoryuken/manager.rb +70 -1
- data/lib/shoryuken/message.rb +114 -1
- data/lib/shoryuken/middleware/chain.rb +139 -43
- data/lib/shoryuken/middleware/entry.rb +30 -0
- data/lib/shoryuken/middleware/server/active_record.rb +8 -0
- data/lib/shoryuken/middleware/server/auto_delete.rb +10 -0
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +27 -1
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +29 -0
- data/lib/shoryuken/middleware/server/timing.rb +11 -0
- data/lib/shoryuken/options.rb +129 -6
- data/lib/shoryuken/polling/base_strategy.rb +1 -0
- data/lib/shoryuken/polling/strict_priority.rb +39 -0
- data/lib/shoryuken/polling/weighted_round_robin.rb +42 -0
- data/lib/shoryuken/processor.rb +32 -1
- data/lib/shoryuken/queue.rb +93 -4
- data/lib/shoryuken/runner.rb +45 -4
- data/lib/shoryuken/util.rb +26 -1
- data/lib/shoryuken/version.rb +2 -1
- data/lib/shoryuken/worker/default_executor.rb +21 -1
- data/lib/shoryuken/worker/inline_executor.rb +24 -0
- data/lib/shoryuken/worker.rb +193 -0
- data/lib/shoryuken/worker_registry.rb +33 -0
- data/lib/shoryuken.rb +18 -6
- data/renovate.json +29 -2
- data/shoryuken.gemspec +2 -1
- data/spec/integration/.rspec +1 -0
- data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
- data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
- data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
- data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
- data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
- data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
- data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
- data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
- data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
- data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
- data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
- data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
- data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
- data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
- data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
- data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
- data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
- data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
- data/spec/integration/aws_config/aws_config_spec.rb +59 -0
- data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
- data/spec/integration/body_parser/json_parser_spec.rb +45 -0
- data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
- data/spec/integration/body_parser/text_parser_spec.rb +43 -0
- data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
- data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
- data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
- data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
- data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
- data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
- data/spec/integration/launcher/launcher_spec.rb +40 -0
- data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
- data/spec/integration/message_operations/message_operations_spec.rb +59 -0
- data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
- data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
- data/spec/integration/middleware_chain/removal_spec.rb +31 -0
- data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
- data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
- data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
- data/spec/integration/rails/rails_72/Gemfile +6 -0
- data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
- data/spec/integration/rails/rails_80/Gemfile +6 -0
- data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
- data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
- data/spec/integration/rails/rails_81/Gemfile +6 -0
- data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
- data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
- data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
- data/spec/integration/spec_helper.rb +7 -0
- data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
- data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
- data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
- data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
- data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
- data/spec/integrations_helper.rb +243 -0
- data/spec/lib/active_job/extensions_spec.rb +149 -0
- data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
- data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +3 -3
- data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +4 -4
- data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +1 -1
- data/spec/{shoryuken → lib/shoryuken}/helpers/hash_utils_spec.rb +14 -14
- data/spec/{shoryuken → lib/shoryuken}/helpers/string_utils_spec.rb +3 -3
- data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
- data/spec/{shoryuken → lib/shoryuken}/helpers_integration_spec.rb +9 -9
- data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +22 -0
- data/spec/lib/shoryuken/logging_spec.rb +242 -0
- data/spec/lib/shoryuken/message_spec.rb +109 -0
- data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
- data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
- data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +50 -0
- data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +2 -2
- data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +1 -1
- data/spec/lib/shoryuken/version_spec.rb +17 -0
- data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
- data/spec/shared_examples_for_active_job.rb +29 -9
- data/spec/spec_helper.rb +34 -3
- metadata +230 -91
- data/.devcontainer/Dockerfile +0 -17
- data/.devcontainer/base.Dockerfile +0 -43
- data/.devcontainer/devcontainer.json +0 -35
- data/Appraisals +0 -23
- data/gemfiles/.gitignore +0 -1
- data/gemfiles/rails_7_0.gemfile +0 -19
- data/gemfiles/rails_7_1.gemfile +0 -19
- data/gemfiles/rails_7_2.gemfile +0 -19
- data/gemfiles/rails_8_0.gemfile +0 -19
- data/lib/shoryuken/extensions/active_job_adapter.rb +0 -110
- data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
- data/spec/integration/launcher_spec.rb +0 -127
- data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -8
- data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -85
- /data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_boolean_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_counter_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/helpers/atomic_hash_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/inline_message_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/polling/base_strategy_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/polling/queue_configuration_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/worker/inline_executor_spec.rb +0 -0
- /data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +0 -0
- /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
|
|
data/lib/shoryuken/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|