shoryuken 7.0.0.alpha2 → 7.0.0.rc1
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/specs.yml +22 -9
- data/Appraisals +6 -12
- data/CHANGELOG.md +14 -0
- data/bin/cli/sqs.rb +1 -1
- data/bin/shoryuken +1 -1
- data/gemfiles/rails_7_2.gemfile +10 -10
- data/gemfiles/rails_8_0.gemfile +10 -10
- data/gemfiles/rails_8_1.gemfile +19 -0
- data/lib/shoryuken/extensions/active_job_adapter.rb +13 -0
- data/lib/shoryuken/helpers/timer_task.rb +66 -0
- data/lib/shoryuken/launcher.rb +14 -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 +3 -23
- data/lib/shoryuken/message.rb +114 -1
- data/lib/shoryuken/middleware/chain.rb +138 -43
- data/lib/shoryuken/middleware/entry.rb +30 -0
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +1 -1
- data/lib/shoryuken/runner.rb +3 -0
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker.rb +171 -0
- data/spec/integration/active_job_continuation_spec.rb +145 -0
- data/spec/shoryuken/extensions/active_job_continuation_spec.rb +110 -0
- data/spec/shoryuken/helpers/timer_task_spec.rb +298 -0
- data/spec/shoryuken/launcher_spec.rb +22 -0
- metadata +13 -3
- data/gemfiles/rails_7_0.gemfile +0 -19
- data/gemfiles/rails_7_1.gemfile +0 -19
|
@@ -1,72 +1,156 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
# Middleware
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
4
|
+
# Middleware provides a way to wrap message processing with custom logic,
|
|
5
|
+
# similar to Rack middleware in web applications. Middleware runs on the server
|
|
6
|
+
# side and can perform setup, teardown, error handling, and monitoring around
|
|
7
|
+
# job execution.
|
|
8
|
+
#
|
|
9
|
+
# Middleware classes must implement a `call` method that accepts the worker instance,
|
|
10
|
+
# queue name, and SQS message, and must yield to continue the middleware chain.
|
|
11
|
+
#
|
|
12
|
+
# ## Global Middleware Configuration
|
|
13
|
+
#
|
|
14
|
+
# Configure middleware globally for all workers:
|
|
15
|
+
#
|
|
16
|
+
# Shoryuken.configure_server do |config|
|
|
17
|
+
# config.server_middleware do |chain|
|
|
18
|
+
# chain.add MyServerHook
|
|
19
|
+
# chain.remove Shoryuken::Middleware::Server::ActiveRecord
|
|
20
|
+
# end
|
|
16
21
|
# end
|
|
17
|
-
# end
|
|
18
22
|
#
|
|
19
|
-
#
|
|
23
|
+
# ## Per-Worker Middleware Configuration
|
|
24
|
+
#
|
|
25
|
+
# Configure middleware for specific workers:
|
|
20
26
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
27
|
+
# class MyWorker
|
|
28
|
+
# include Shoryuken::Worker
|
|
29
|
+
#
|
|
30
|
+
# server_middleware do |chain|
|
|
31
|
+
# chain.add MyWorkerSpecificMiddleware
|
|
32
|
+
# end
|
|
24
33
|
# end
|
|
25
|
-
# end
|
|
26
34
|
#
|
|
27
|
-
#
|
|
35
|
+
# ## Middleware Ordering
|
|
36
|
+
#
|
|
37
|
+
# Insert middleware at specific positions in the chain:
|
|
38
|
+
#
|
|
39
|
+
# # Insert before existing middleware
|
|
40
|
+
# chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, MyDatabaseSetup
|
|
41
|
+
#
|
|
42
|
+
# # Insert after existing middleware
|
|
43
|
+
# chain.insert_after Shoryuken::Middleware::Server::Timing, MyMetricsCollector
|
|
28
44
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
45
|
+
# # Add to beginning of chain
|
|
46
|
+
# chain.prepend MyFirstMiddleware
|
|
47
|
+
#
|
|
48
|
+
# ## Example Middleware Implementations
|
|
49
|
+
#
|
|
50
|
+
# # Basic logging middleware
|
|
51
|
+
# class LoggingMiddleware
|
|
52
|
+
# def call(worker_instance, queue, sqs_msg, body)
|
|
53
|
+
# puts "Processing #{sqs_msg.message_id} on #{queue}"
|
|
54
|
+
# start_time = Time.now
|
|
55
|
+
# yield
|
|
56
|
+
# puts "Completed in #{Time.now - start_time}s"
|
|
57
|
+
# end
|
|
32
58
|
# end
|
|
33
|
-
# end
|
|
34
59
|
#
|
|
35
|
-
#
|
|
60
|
+
# # Error reporting middleware
|
|
61
|
+
# class ErrorReportingMiddleware
|
|
62
|
+
# def call(worker_instance, queue, sqs_msg, body)
|
|
63
|
+
# yield
|
|
64
|
+
# rescue => error
|
|
65
|
+
# ErrorReporter.notify(error, {
|
|
66
|
+
# worker: worker_instance.class.name,
|
|
67
|
+
# queue: queue,
|
|
68
|
+
# message_id: sqs_msg.message_id
|
|
69
|
+
# })
|
|
70
|
+
# raise
|
|
71
|
+
# end
|
|
72
|
+
# end
|
|
36
73
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
74
|
+
# # Performance monitoring middleware
|
|
75
|
+
# class MetricsMiddleware
|
|
76
|
+
# def call(worker_instance, queue, sqs_msg, body)
|
|
77
|
+
# start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
78
|
+
# yield
|
|
79
|
+
# duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
80
|
+
# StatsD.timing("shoryuken.#{worker_instance.class.name.underscore}.duration", duration)
|
|
81
|
+
# end
|
|
42
82
|
# end
|
|
43
|
-
# end
|
|
44
83
|
#
|
|
84
|
+
# @see Shoryuken::Middleware::Chain Middleware chain management
|
|
85
|
+
# @see https://github.com/ruby-shoryuken/shoryuken/wiki/Middleware Comprehensive middleware guide
|
|
45
86
|
module Middleware
|
|
87
|
+
# Manages a chain of middleware classes that will be instantiated and invoked
|
|
88
|
+
# in sequence around message processing. Provides methods for adding, removing,
|
|
89
|
+
# and reordering middleware.
|
|
46
90
|
class Chain
|
|
91
|
+
# @return [Array<Entry>] The ordered list of middleware entries
|
|
47
92
|
attr_reader :entries
|
|
48
93
|
|
|
94
|
+
# Creates a new middleware chain.
|
|
95
|
+
#
|
|
96
|
+
# @yield [Chain] The chain instance for configuration
|
|
97
|
+
# @example Creating and configuring a chain
|
|
98
|
+
# chain = Shoryuken::Middleware::Chain.new do |c|
|
|
99
|
+
# c.add MyMiddleware
|
|
100
|
+
# c.add AnotherMiddleware, option: 'value'
|
|
101
|
+
# end
|
|
49
102
|
def initialize
|
|
50
103
|
@entries = []
|
|
51
104
|
yield self if block_given?
|
|
52
105
|
end
|
|
53
106
|
|
|
107
|
+
# Creates a copy of this middleware chain.
|
|
108
|
+
#
|
|
109
|
+
# @return [Chain] A new chain with the same middleware entries
|
|
54
110
|
def dup
|
|
55
111
|
self.class.new.tap { |new_chain| new_chain.entries.replace(entries) }
|
|
56
112
|
end
|
|
57
113
|
|
|
114
|
+
# Removes all instances of the specified middleware class from the chain.
|
|
115
|
+
#
|
|
116
|
+
# @param klass [Class] The middleware class to remove
|
|
117
|
+
# @return [Array<Entry>] The removed entries
|
|
118
|
+
# @example Removing ActiveRecord middleware
|
|
119
|
+
# chain.remove Shoryuken::Middleware::Server::ActiveRecord
|
|
58
120
|
def remove(klass)
|
|
59
121
|
entries.delete_if { |entry| entry.klass == klass }
|
|
60
122
|
end
|
|
61
123
|
|
|
124
|
+
# Adds middleware to the end of the chain. Does nothing if the middleware
|
|
125
|
+
# class is already present in the chain.
|
|
126
|
+
#
|
|
127
|
+
# @param klass [Class] The middleware class to add
|
|
128
|
+
# @param args [Array] Arguments to pass to the middleware constructor
|
|
129
|
+
# @example Adding middleware with arguments
|
|
130
|
+
# chain.add MyMiddleware, timeout: 30, retries: 3
|
|
62
131
|
def add(klass, *args)
|
|
63
132
|
entries << Entry.new(klass, *args) unless exists?(klass)
|
|
64
133
|
end
|
|
65
134
|
|
|
135
|
+
# Adds middleware to the beginning of the chain. Does nothing if the middleware
|
|
136
|
+
# class is already present in the chain.
|
|
137
|
+
#
|
|
138
|
+
# @param klass [Class] The middleware class to prepend
|
|
139
|
+
# @param args [Array] Arguments to pass to the middleware constructor
|
|
140
|
+
# @example Adding middleware to run first
|
|
141
|
+
# chain.prepend AuthenticationMiddleware
|
|
66
142
|
def prepend(klass, *args)
|
|
67
143
|
entries.insert(0, Entry.new(klass, *args)) unless exists?(klass)
|
|
68
144
|
end
|
|
69
145
|
|
|
146
|
+
# Inserts middleware immediately before another middleware class.
|
|
147
|
+
# If the new middleware already exists, it's moved to the new position.
|
|
148
|
+
#
|
|
149
|
+
# @param oldklass [Class] The existing middleware to insert before
|
|
150
|
+
# @param newklass [Class] The middleware class to insert
|
|
151
|
+
# @param args [Array] Arguments to pass to the middleware constructor
|
|
152
|
+
# @example Insert database setup before ActiveRecord middleware
|
|
153
|
+
# chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, DatabaseSetup
|
|
70
154
|
def insert_before(oldklass, newklass, *args)
|
|
71
155
|
i = entries.index { |entry| entry.klass == newklass }
|
|
72
156
|
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
|
@@ -74,6 +158,14 @@ module Shoryuken
|
|
|
74
158
|
entries.insert(i, new_entry)
|
|
75
159
|
end
|
|
76
160
|
|
|
161
|
+
# Inserts middleware immediately after another middleware class.
|
|
162
|
+
# If the new middleware already exists, it's moved to the new position.
|
|
163
|
+
#
|
|
164
|
+
# @param oldklass [Class] The existing middleware to insert after
|
|
165
|
+
# @param newklass [Class] The middleware class to insert
|
|
166
|
+
# @param args [Array] Arguments to pass to the middleware constructor
|
|
167
|
+
# @example Insert metrics collection after timing middleware
|
|
168
|
+
# chain.insert_after Shoryuken::Middleware::Server::Timing, MetricsCollector
|
|
77
169
|
def insert_after(oldklass, newklass, *args)
|
|
78
170
|
i = entries.index { |entry| entry.klass == newklass }
|
|
79
171
|
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
|
@@ -81,18 +173,34 @@ module Shoryuken
|
|
|
81
173
|
entries.insert(i + 1, new_entry)
|
|
82
174
|
end
|
|
83
175
|
|
|
176
|
+
# Checks if a middleware class is already in the chain.
|
|
177
|
+
#
|
|
178
|
+
# @param klass [Class] The middleware class to check for
|
|
179
|
+
# @return [Boolean] True if the middleware is in the chain
|
|
84
180
|
def exists?(klass)
|
|
85
181
|
entries.any? { |entry| entry.klass == klass }
|
|
86
182
|
end
|
|
87
183
|
|
|
184
|
+
# Creates instances of all middleware classes in the chain.
|
|
185
|
+
#
|
|
186
|
+
# @return [Array] Array of middleware instances
|
|
88
187
|
def retrieve
|
|
89
188
|
entries.map(&:make_new)
|
|
90
189
|
end
|
|
91
190
|
|
|
191
|
+
# Removes all middleware from the chain.
|
|
192
|
+
#
|
|
193
|
+
# @return [Array] Empty array
|
|
92
194
|
def clear
|
|
93
195
|
entries.clear
|
|
94
196
|
end
|
|
95
197
|
|
|
198
|
+
# Invokes the middleware chain with the given arguments.
|
|
199
|
+
# Each middleware's call method will be invoked in sequence,
|
|
200
|
+
# with control passed through yielding.
|
|
201
|
+
#
|
|
202
|
+
# @param args [Array] Arguments to pass to each middleware
|
|
203
|
+
# @yield The final action to perform after all middleware
|
|
96
204
|
def invoke(*args, &final_action)
|
|
97
205
|
chain = retrieve.dup
|
|
98
206
|
traverse_chain = lambda do
|
|
@@ -105,18 +213,5 @@ module Shoryuken
|
|
|
105
213
|
traverse_chain.call
|
|
106
214
|
end
|
|
107
215
|
end
|
|
108
|
-
|
|
109
|
-
class Entry
|
|
110
|
-
attr_reader :klass
|
|
111
|
-
|
|
112
|
-
def initialize(klass, *args)
|
|
113
|
-
@klass = klass
|
|
114
|
-
@args = args
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def make_new
|
|
118
|
-
@klass.new(*@args)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
216
|
end
|
|
122
217
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Middleware
|
|
5
|
+
# Represents an entry in a middleware chain, storing the middleware class
|
|
6
|
+
# and any arguments needed for its instantiation.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Entry
|
|
10
|
+
# @return [Class] The middleware class this entry represents
|
|
11
|
+
attr_reader :klass
|
|
12
|
+
|
|
13
|
+
# Creates a new middleware entry.
|
|
14
|
+
#
|
|
15
|
+
# @param klass [Class] The middleware class
|
|
16
|
+
# @param args [Array] Arguments to pass to the middleware constructor
|
|
17
|
+
def initialize(klass, *args)
|
|
18
|
+
@klass = klass
|
|
19
|
+
@args = args
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Creates a new instance of the middleware class with the stored arguments.
|
|
23
|
+
#
|
|
24
|
+
# @return [Object] A new instance of the middleware class
|
|
25
|
+
def make_new
|
|
26
|
+
@klass.new(*@args)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -30,7 +30,7 @@ module Shoryuken
|
|
|
30
30
|
def auto_extend(_worker, queue, sqs_msg, _body)
|
|
31
31
|
queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Shoryuken::Helpers::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
|
|
34
34
|
logger.debug do
|
|
35
35
|
"Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
|
|
36
36
|
end
|
data/lib/shoryuken/runner.rb
CHANGED
data/lib/shoryuken/version.rb
CHANGED
data/lib/shoryuken/worker.rb
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
+
# Worker module provides the core functionality for creating Shoryuken workers
|
|
5
|
+
# that process messages from Amazon SQS queues.
|
|
6
|
+
#
|
|
7
|
+
# Including this module in a class provides methods for configuring queue processing,
|
|
8
|
+
# enqueueing jobs, and setting up middleware. Workers can be configured for different
|
|
9
|
+
# processing patterns including single message processing, batch processing, and
|
|
10
|
+
# various retry and visibility timeout strategies.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic worker implementation
|
|
13
|
+
# class EmailWorker
|
|
14
|
+
# include Shoryuken::Worker
|
|
15
|
+
# shoryuken_options queue: 'emails'
|
|
16
|
+
#
|
|
17
|
+
# def perform(sqs_msg, body)
|
|
18
|
+
# send_email(body['recipient'], body['subject'], body['content'])
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Advanced worker with all options
|
|
23
|
+
# class AdvancedWorker
|
|
24
|
+
# include Shoryuken::Worker
|
|
25
|
+
#
|
|
26
|
+
# shoryuken_options queue: 'advanced_queue',
|
|
27
|
+
# batch: false,
|
|
28
|
+
# auto_delete: true,
|
|
29
|
+
# auto_visibility_timeout: true,
|
|
30
|
+
# retry_intervals: [1, 5, 25, 125, 625]
|
|
31
|
+
#
|
|
32
|
+
# server_middleware do |chain|
|
|
33
|
+
# chain.add MyCustomMiddleware
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def perform(sqs_msg, body)
|
|
37
|
+
# # Worker implementation
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# @see ClassMethods#shoryuken_options Primary configuration method
|
|
42
|
+
# @see ClassMethods#perform_async For enqueueing jobs
|
|
43
|
+
# @see https://github.com/ruby-shoryuken/shoryuken/wiki/Workers Comprehensive worker documentation
|
|
4
44
|
module Worker
|
|
5
45
|
def self.included(base)
|
|
6
46
|
base.extend(ClassMethods)
|
|
@@ -8,35 +48,166 @@ module Shoryuken
|
|
|
8
48
|
end
|
|
9
49
|
|
|
10
50
|
module ClassMethods
|
|
51
|
+
# Enqueues a job to be processed asynchronously by a Shoryuken worker.
|
|
52
|
+
#
|
|
53
|
+
# @param body [Object] The job payload that will be passed to the worker's perform method
|
|
54
|
+
# @param options [Hash] Additional options for job enqueueing
|
|
55
|
+
# @option options [String] :message_group_id FIFO queue group ID for message ordering
|
|
56
|
+
# @option options [String] :message_deduplication_id FIFO queue deduplication ID
|
|
57
|
+
# @option options [Hash] :message_attributes Custom SQS message attributes
|
|
58
|
+
# @return [String] The message ID of the enqueued job
|
|
59
|
+
#
|
|
60
|
+
# @example Basic job enqueueing
|
|
61
|
+
# MyWorker.perform_async({ user_id: 123, action: 'send_email' })
|
|
62
|
+
#
|
|
63
|
+
# @example FIFO queue with ordering
|
|
64
|
+
# MyWorker.perform_async(data, message_group_id: 'user_123')
|
|
11
65
|
def perform_async(body, options = {})
|
|
12
66
|
Shoryuken.worker_executor.perform_async(self, body, options)
|
|
13
67
|
end
|
|
14
68
|
|
|
69
|
+
# Enqueues a job to be processed after a specified time interval.
|
|
70
|
+
#
|
|
71
|
+
# @param interval [Integer, ActiveSupport::Duration] Delay in seconds, or duration object
|
|
72
|
+
# @param body [Object] The job payload that will be passed to the worker's perform method
|
|
73
|
+
# @param options [Hash] Additional options for job enqueueing (see {#perform_async})
|
|
74
|
+
# @return [String] The message ID of the enqueued job
|
|
75
|
+
#
|
|
76
|
+
# @example Delay job by 5 minutes
|
|
77
|
+
# MyWorker.perform_in(5.minutes, { user_id: 123 })
|
|
78
|
+
#
|
|
79
|
+
# @example Delay job by specific number of seconds
|
|
80
|
+
# MyWorker.perform_in(300, { user_id: 123 })
|
|
15
81
|
def perform_in(interval, body, options = {})
|
|
16
82
|
Shoryuken.worker_executor.perform_in(self, interval, body, options)
|
|
17
83
|
end
|
|
18
84
|
|
|
19
85
|
alias_method :perform_at, :perform_in
|
|
20
86
|
|
|
87
|
+
# Configures server-side middleware chain for this worker class.
|
|
88
|
+
# Middleware runs before and after job processing, similar to Rack middleware.
|
|
89
|
+
#
|
|
90
|
+
# @yield [Shoryuken::Middleware::Chain] The middleware chain for configuration
|
|
91
|
+
# @return [Shoryuken::Middleware::Chain] The configured middleware chain
|
|
92
|
+
#
|
|
93
|
+
# @example Adding custom middleware
|
|
94
|
+
# class MyWorker
|
|
95
|
+
# include Shoryuken::Worker
|
|
96
|
+
#
|
|
97
|
+
# server_middleware do |chain|
|
|
98
|
+
# chain.add MyCustomMiddleware
|
|
99
|
+
# chain.remove Shoryuken::Middleware::Server::ActiveRecord
|
|
100
|
+
# end
|
|
101
|
+
# end
|
|
21
102
|
def server_middleware
|
|
22
103
|
@_server_chain ||= Shoryuken.server_middleware.dup
|
|
23
104
|
yield @_server_chain if block_given?
|
|
24
105
|
@_server_chain
|
|
25
106
|
end
|
|
26
107
|
|
|
108
|
+
# Configures worker options including queue assignment, processing behavior,
|
|
109
|
+
# and SQS-specific settings. This is the main configuration method for workers.
|
|
110
|
+
#
|
|
111
|
+
# @param opts [Hash] Configuration options for the worker
|
|
112
|
+
# @option opts [String, Array<String>] :queue Queue name(s) this worker processes
|
|
113
|
+
# @option opts [Boolean] :batch (false) Process messages in batches of up to 10
|
|
114
|
+
# @option opts [Boolean] :auto_delete (false) Automatically delete messages after processing
|
|
115
|
+
# @option opts [Boolean] :auto_visibility_timeout (false) Automatically extend message visibility
|
|
116
|
+
# @option opts [Array<Integer>] :retry_intervals Exponential backoff retry intervals in seconds
|
|
117
|
+
# @option opts [Hash] :sqs Additional SQS client options
|
|
118
|
+
#
|
|
119
|
+
# @example Basic worker configuration
|
|
120
|
+
# class MyWorker
|
|
121
|
+
# include Shoryuken::Worker
|
|
122
|
+
# shoryuken_options queue: 'my_queue'
|
|
123
|
+
#
|
|
124
|
+
# def perform(sqs_msg, body)
|
|
125
|
+
# # Process the message
|
|
126
|
+
# end
|
|
127
|
+
# end
|
|
128
|
+
#
|
|
129
|
+
# @example Worker with auto-delete and retries
|
|
130
|
+
# class ReliableWorker
|
|
131
|
+
# include Shoryuken::Worker
|
|
132
|
+
# shoryuken_options queue: 'important_queue',
|
|
133
|
+
# auto_delete: true,
|
|
134
|
+
# retry_intervals: [1, 5, 25, 125]
|
|
135
|
+
# end
|
|
136
|
+
#
|
|
137
|
+
# @example Batch processing worker
|
|
138
|
+
# class BatchWorker
|
|
139
|
+
# include Shoryuken::Worker
|
|
140
|
+
# shoryuken_options queue: 'batch_queue', batch: true
|
|
141
|
+
#
|
|
142
|
+
# def perform(sqs_msgs, bodies)
|
|
143
|
+
# # Process array of up to 10 messages
|
|
144
|
+
# bodies.each { |body| process_item(body) }
|
|
145
|
+
# end
|
|
146
|
+
# end
|
|
147
|
+
#
|
|
148
|
+
# @example Multiple queues with priorities
|
|
149
|
+
# class MultiQueueWorker
|
|
150
|
+
# include Shoryuken::Worker
|
|
151
|
+
# shoryuken_options queue: ['high_priority', 'low_priority']
|
|
152
|
+
# end
|
|
153
|
+
#
|
|
154
|
+
# @example Auto-extending visibility timeout for long-running jobs
|
|
155
|
+
# class LongRunningWorker
|
|
156
|
+
# include Shoryuken::Worker
|
|
157
|
+
# shoryuken_options queue: 'slow_queue',
|
|
158
|
+
# auto_visibility_timeout: true
|
|
159
|
+
#
|
|
160
|
+
# def perform(sqs_msg, body)
|
|
161
|
+
# # Long processing that might exceed visibility timeout
|
|
162
|
+
# complex_processing(body)
|
|
163
|
+
# end
|
|
164
|
+
# end
|
|
27
165
|
def shoryuken_options(opts = {})
|
|
28
166
|
self.shoryuken_options_hash = get_shoryuken_options.merge(stringify_keys(opts || {}))
|
|
29
167
|
normalize_worker_queue!
|
|
30
168
|
end
|
|
31
169
|
|
|
170
|
+
# Checks if automatic visibility timeout extension is enabled for this worker.
|
|
171
|
+
# When enabled, Shoryuken automatically extends the message visibility timeout
|
|
172
|
+
# during processing to prevent the message from becoming visible to other consumers.
|
|
173
|
+
#
|
|
174
|
+
# @return [Boolean] true if auto visibility timeout is enabled
|
|
175
|
+
#
|
|
176
|
+
# @see #shoryuken_options Documentation for enabling auto_visibility_timeout
|
|
32
177
|
def auto_visibility_timeout?
|
|
33
178
|
!!get_shoryuken_options['auto_visibility_timeout']
|
|
34
179
|
end
|
|
35
180
|
|
|
181
|
+
# Checks if exponential backoff retry is configured for this worker.
|
|
182
|
+
# When retry intervals are specified, failed jobs will be retried with
|
|
183
|
+
# increasing delays between attempts.
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] true if retry intervals are configured
|
|
186
|
+
#
|
|
187
|
+
# @example Configuring exponential backoff
|
|
188
|
+
# shoryuken_options retry_intervals: [1, 5, 25, 125, 625]
|
|
189
|
+
# # Will retry after 1s, 5s, 25s, 125s, then 625s before giving up
|
|
190
|
+
#
|
|
191
|
+
# @see #shoryuken_options Documentation for configuring retry_intervals
|
|
36
192
|
def exponential_backoff?
|
|
37
193
|
!!get_shoryuken_options['retry_intervals']
|
|
38
194
|
end
|
|
39
195
|
|
|
196
|
+
# Checks if automatic message deletion is enabled for this worker.
|
|
197
|
+
# When enabled, successfully processed messages are automatically deleted
|
|
198
|
+
# from the SQS queue. When disabled, you must manually delete messages
|
|
199
|
+
# or they will become visible again after the visibility timeout.
|
|
200
|
+
#
|
|
201
|
+
# @return [Boolean] true if auto delete is enabled
|
|
202
|
+
#
|
|
203
|
+
# @example Manual message deletion when auto_delete is false
|
|
204
|
+
# def perform(sqs_msg, body)
|
|
205
|
+
# process_message(body)
|
|
206
|
+
# # Manually delete the message after successful processing
|
|
207
|
+
# sqs_msg.delete
|
|
208
|
+
# end
|
|
209
|
+
#
|
|
210
|
+
# @see #shoryuken_options Documentation for enabling auto_delete
|
|
40
211
|
def auto_delete?
|
|
41
212
|
!!(get_shoryuken_options['delete'] || get_shoryuken_options['auto_delete'])
|
|
42
213
|
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'active_job'
|
|
5
|
+
require 'shoryuken/extensions/active_job_adapter'
|
|
6
|
+
require 'shoryuken/extensions/active_job_extensions'
|
|
7
|
+
|
|
8
|
+
RSpec.describe 'ActiveJob Continuations Integration' do
|
|
9
|
+
# Skip all tests in this suite if ActiveJob::Continuable is not available (Rails < 8.0)
|
|
10
|
+
before(:all) do
|
|
11
|
+
skip 'ActiveJob::Continuable not available (Rails < 8.0)' unless defined?(ActiveJob::Continuable)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Test job that uses ActiveJob Continuations
|
|
15
|
+
class ContinuableTestJob < ActiveJob::Base
|
|
16
|
+
include ActiveJob::Continuable if defined?(ActiveJob::Continuable)
|
|
17
|
+
|
|
18
|
+
queue_as :default
|
|
19
|
+
|
|
20
|
+
class_attribute :executions_log, default: []
|
|
21
|
+
class_attribute :checkpoints_reached, default: []
|
|
22
|
+
|
|
23
|
+
def perform(max_iterations: 10)
|
|
24
|
+
self.class.executions_log << { execution: executions, started_at: Time.current }
|
|
25
|
+
|
|
26
|
+
step :initialize_work do
|
|
27
|
+
self.class.checkpoints_reached << "initialize_work_#{executions}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
step :process_items, start: cursor || 0 do
|
|
31
|
+
(cursor..max_iterations).each do |i|
|
|
32
|
+
self.class.checkpoints_reached << "processing_item_#{i}"
|
|
33
|
+
|
|
34
|
+
# Check if we should stop (checkpoint)
|
|
35
|
+
checkpoint
|
|
36
|
+
|
|
37
|
+
# Simulate some work
|
|
38
|
+
sleep 0.01
|
|
39
|
+
|
|
40
|
+
# Advance cursor
|
|
41
|
+
cursor.advance!
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
step :finalize_work do
|
|
46
|
+
self.class.checkpoints_reached << 'finalize_work'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
self.class.executions_log.last[:completed] = true
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe 'stopping? method (unit tests)' do
|
|
54
|
+
it 'returns false when launcher is not initialized' do
|
|
55
|
+
adapter = ActiveJob::QueueAdapters::ShoryukenAdapter.new
|
|
56
|
+
expect(adapter.stopping?).to be false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'returns true when launcher is stopping' do
|
|
60
|
+
launcher = Shoryuken::Launcher.new
|
|
61
|
+
runner = Shoryuken::Runner.instance
|
|
62
|
+
runner.instance_variable_set(:@launcher, launcher)
|
|
63
|
+
|
|
64
|
+
adapter = ActiveJob::QueueAdapters::ShoryukenAdapter.new
|
|
65
|
+
expect(adapter.stopping?).to be false
|
|
66
|
+
|
|
67
|
+
launcher.instance_variable_set(:@stopping, true)
|
|
68
|
+
expect(adapter.stopping?).to be true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe 'timestamp handling for continuation retries' do
|
|
73
|
+
it 'handles past timestamps for continuation retries' do
|
|
74
|
+
adapter = ActiveJob::QueueAdapters::ShoryukenAdapter.new
|
|
75
|
+
job = ContinuableTestJob.new
|
|
76
|
+
job.sqs_send_message_parameters = {}
|
|
77
|
+
|
|
78
|
+
# Mock the queue
|
|
79
|
+
queue = instance_double(Shoryuken::Queue, fifo?: false)
|
|
80
|
+
allow(Shoryuken::Client).to receive(:queues).and_return(queue)
|
|
81
|
+
allow(Shoryuken).to receive(:register_worker)
|
|
82
|
+
allow(queue).to receive(:send_message) do |params|
|
|
83
|
+
# Verify past timestamp results in immediate delivery (delay_seconds <= 0)
|
|
84
|
+
expect(params[:delay_seconds]).to be <= 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Enqueue with past timestamp (simulating continuation retry)
|
|
88
|
+
past_timestamp = Time.current.to_f - 60
|
|
89
|
+
adapter.enqueue_at(job, past_timestamp)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe 'enqueue_at with continuation timestamps (unit tests)' do
|
|
94
|
+
let(:adapter) { ActiveJob::QueueAdapters::ShoryukenAdapter.new }
|
|
95
|
+
let(:job) do
|
|
96
|
+
job = ContinuableTestJob.new
|
|
97
|
+
job.sqs_send_message_parameters = {}
|
|
98
|
+
job
|
|
99
|
+
end
|
|
100
|
+
let(:queue) { instance_double(Shoryuken::Queue, fifo?: false) }
|
|
101
|
+
|
|
102
|
+
before do
|
|
103
|
+
allow(Shoryuken::Client).to receive(:queues).and_return(queue)
|
|
104
|
+
allow(Shoryuken).to receive(:register_worker)
|
|
105
|
+
@sent_messages = []
|
|
106
|
+
allow(queue).to receive(:send_message) do |params|
|
|
107
|
+
@sent_messages << params
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'accepts past timestamps without error' do
|
|
112
|
+
past_timestamp = Time.current.to_f - 30
|
|
113
|
+
|
|
114
|
+
expect {
|
|
115
|
+
adapter.enqueue_at(job, past_timestamp)
|
|
116
|
+
}.not_to raise_error
|
|
117
|
+
|
|
118
|
+
expect(@sent_messages.size).to eq(1)
|
|
119
|
+
expect(@sent_messages.first[:delay_seconds]).to be <= 0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'accepts current timestamp' do
|
|
123
|
+
current_timestamp = Time.current.to_f
|
|
124
|
+
|
|
125
|
+
expect {
|
|
126
|
+
adapter.enqueue_at(job, current_timestamp)
|
|
127
|
+
}.not_to raise_error
|
|
128
|
+
|
|
129
|
+
expect(@sent_messages.size).to eq(1)
|
|
130
|
+
expect(@sent_messages.first[:delay_seconds]).to be_between(-1, 1)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'accepts future timestamp' do
|
|
134
|
+
future_timestamp = Time.current.to_f + 30
|
|
135
|
+
|
|
136
|
+
expect {
|
|
137
|
+
adapter.enqueue_at(job, future_timestamp)
|
|
138
|
+
}.not_to raise_error
|
|
139
|
+
|
|
140
|
+
expect(@sent_messages.size).to eq(1)
|
|
141
|
+
expect(@sent_messages.first[:delay_seconds]).to be > 0
|
|
142
|
+
expect(@sent_messages.first[:delay_seconds]).to be <= 30
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|