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
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
+
# Loads and configures the Shoryuken environment from configuration files
|
|
5
|
+
# and command line options. Handles Rails integration and queue setup.
|
|
4
6
|
class EnvironmentLoader
|
|
7
|
+
# @return [Hash] the configuration options
|
|
5
8
|
attr_reader :options
|
|
6
9
|
|
|
10
|
+
# Sets up a new EnvironmentLoader with the given options
|
|
11
|
+
#
|
|
12
|
+
# @param options [Hash] configuration options
|
|
13
|
+
# @option options [String] :config_file path to the configuration file
|
|
14
|
+
# @option options [Boolean] :rails whether to initialize Rails
|
|
15
|
+
# @option options [String] :logfile path to the log file
|
|
16
|
+
# @option options [Boolean] :verbose whether to enable verbose logging
|
|
17
|
+
# @option options [String] :require path to require workers from
|
|
18
|
+
# @option options [Integer] :concurrency number of concurrent workers
|
|
19
|
+
# @return [Shoryuken::EnvironmentLoader] the configured instance
|
|
7
20
|
def self.setup_options(options)
|
|
8
21
|
instance = new(options)
|
|
9
22
|
instance.setup_options
|
|
10
23
|
instance
|
|
11
24
|
end
|
|
12
25
|
|
|
26
|
+
# Loads the environment for Rails console usage
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
13
29
|
def self.load_for_rails_console
|
|
14
30
|
instance = setup_options(config_file: (Rails.root + 'config' + 'shoryuken.yml'))
|
|
15
31
|
instance.load
|
|
16
32
|
end
|
|
17
33
|
|
|
34
|
+
# Initializes a new EnvironmentLoader with the given options
|
|
35
|
+
#
|
|
36
|
+
# @param options [Hash] configuration options
|
|
37
|
+
# @option options [String] :config_file path to the configuration file
|
|
38
|
+
# @option options [Boolean] :rails whether to initialize Rails
|
|
39
|
+
# @option options [String] :logfile path to the log file
|
|
40
|
+
# @option options [Boolean] :verbose whether to enable verbose logging
|
|
41
|
+
# @option options [String] :require path to require workers from
|
|
42
|
+
# @option options [Integer] :concurrency number of concurrent workers
|
|
18
43
|
def initialize(options)
|
|
19
44
|
@options = options
|
|
20
45
|
end
|
|
21
46
|
|
|
47
|
+
# Sets up configuration options from file and initializes components
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
22
50
|
def setup_options
|
|
23
51
|
initialize_rails if load_rails?
|
|
24
52
|
initialize_options
|
|
25
53
|
initialize_logger
|
|
26
54
|
end
|
|
27
55
|
|
|
56
|
+
# Loads the environment including queues and workers
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
28
59
|
def load
|
|
29
60
|
prefix_active_job_queue_names
|
|
30
61
|
parse_queues
|
|
@@ -35,15 +66,21 @@ module Shoryuken
|
|
|
35
66
|
|
|
36
67
|
private
|
|
37
68
|
|
|
69
|
+
# Merges configuration file options with runtime options
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
38
72
|
def initialize_options
|
|
39
73
|
Shoryuken.options.merge!(config_file_options)
|
|
40
74
|
Shoryuken.options.merge!(options)
|
|
41
75
|
end
|
|
42
76
|
|
|
77
|
+
# Reads and parses the configuration file
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash] the parsed configuration options
|
|
43
80
|
def config_file_options
|
|
44
81
|
return {} unless (path = options[:config_file])
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
raise Errors::InvalidConfigurationError, "The supplied config file #{path} does not exist" unless File.exist?(path)
|
|
47
84
|
|
|
48
85
|
if (result = YAML.load(ERB.new(IO.read(path)).result))
|
|
49
86
|
Shoryuken::Helpers::HashUtils.deep_symbolize_keys(result)
|
|
@@ -52,11 +89,17 @@ module Shoryuken
|
|
|
52
89
|
end
|
|
53
90
|
end
|
|
54
91
|
|
|
92
|
+
# Initializes the logger with file output and verbosity settings
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
55
95
|
def initialize_logger
|
|
56
96
|
Shoryuken::Logging.initialize_logger(Shoryuken.options[:logfile]) if Shoryuken.options[:logfile]
|
|
57
97
|
Shoryuken.logger.level = Logger::DEBUG if Shoryuken.options[:verbose]
|
|
58
98
|
end
|
|
59
99
|
|
|
100
|
+
# Initializes the Rails environment
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
60
103
|
def initialize_rails
|
|
61
104
|
# Adapted from: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/cli.rb
|
|
62
105
|
|
|
@@ -80,18 +123,26 @@ module Shoryuken
|
|
|
80
123
|
end
|
|
81
124
|
end
|
|
82
125
|
if Shoryuken.active_job?
|
|
83
|
-
require '
|
|
84
|
-
require '
|
|
85
|
-
require '
|
|
126
|
+
require 'active_job/extensions'
|
|
127
|
+
require 'active_job/queue_adapters/shoryuken_adapter'
|
|
128
|
+
require 'active_job/queue_adapters/shoryuken_concurrent_send_adapter'
|
|
86
129
|
end
|
|
87
130
|
require File.expand_path('config/environment.rb')
|
|
88
131
|
end
|
|
89
132
|
end
|
|
90
133
|
|
|
134
|
+
# Checks if Rails should be loaded
|
|
135
|
+
#
|
|
136
|
+
# @return [Boolean] true if Rails should be initialized
|
|
91
137
|
def load_rails?
|
|
92
138
|
options[:rails]
|
|
93
139
|
end
|
|
94
140
|
|
|
141
|
+
# Prefixes a queue name with ActiveJob queue name prefix
|
|
142
|
+
#
|
|
143
|
+
# @param queue_name [String] the queue name to prefix
|
|
144
|
+
# @param weight [Integer] the queue weight
|
|
145
|
+
# @return [Array<String, Integer>] the prefixed queue name and weight
|
|
95
146
|
def prefix_active_job_queue_name(queue_name, weight)
|
|
96
147
|
return [queue_name, weight] if queue_name.start_with?('https://', 'arn:')
|
|
97
148
|
|
|
@@ -104,6 +155,9 @@ module Shoryuken
|
|
|
104
155
|
[prefixed_queue_name, weight]
|
|
105
156
|
end
|
|
106
157
|
|
|
158
|
+
# Prefixes all queue names with ActiveJob prefix if enabled
|
|
159
|
+
#
|
|
160
|
+
# @return [void]
|
|
107
161
|
def prefix_active_job_queue_names
|
|
108
162
|
return unless Shoryuken.active_job?
|
|
109
163
|
return unless Shoryuken.active_job_queue_name_prefixing?
|
|
@@ -123,10 +177,19 @@ module Shoryuken
|
|
|
123
177
|
end
|
|
124
178
|
end
|
|
125
179
|
|
|
180
|
+
# Parses a single queue and adds it to a group
|
|
181
|
+
#
|
|
182
|
+
# @param queue [String] the queue name
|
|
183
|
+
# @param weight [Integer] the queue weight
|
|
184
|
+
# @param group [String] the group name
|
|
185
|
+
# @return [void]
|
|
126
186
|
def parse_queue(queue, weight, group)
|
|
127
187
|
Shoryuken.add_queue(queue, [weight.to_i, 1].max, group)
|
|
128
188
|
end
|
|
129
189
|
|
|
190
|
+
# Parses all queues from configuration and adds them to groups
|
|
191
|
+
#
|
|
192
|
+
# @return [void]
|
|
130
193
|
def parse_queues
|
|
131
194
|
if Shoryuken.options[:queues].to_a.any?
|
|
132
195
|
Shoryuken.add_group('default', Shoryuken.options[:concurrency])
|
|
@@ -145,6 +208,9 @@ module Shoryuken
|
|
|
145
208
|
end
|
|
146
209
|
end
|
|
147
210
|
|
|
211
|
+
# Requires worker files from the configured path
|
|
212
|
+
#
|
|
213
|
+
# @return [void]
|
|
148
214
|
def require_workers
|
|
149
215
|
required = Shoryuken.options[:require]
|
|
150
216
|
|
|
@@ -157,6 +223,10 @@ module Shoryuken
|
|
|
157
223
|
end
|
|
158
224
|
end
|
|
159
225
|
|
|
226
|
+
# Validates that all configured queues exist in SQS
|
|
227
|
+
#
|
|
228
|
+
# @return [void]
|
|
229
|
+
# @raise [ArgumentError] if any queues do not exist
|
|
160
230
|
def validate_queues
|
|
161
231
|
return Shoryuken.logger.warn { 'No queues supplied' } if Shoryuken.ungrouped_queues.empty?
|
|
162
232
|
|
|
@@ -178,12 +248,12 @@ module Shoryuken
|
|
|
178
248
|
It's also possible that you don't have permission to access the specified queues.
|
|
179
249
|
MSG
|
|
180
250
|
|
|
181
|
-
|
|
182
|
-
ArgumentError,
|
|
183
|
-
error_msg
|
|
184
|
-
)
|
|
251
|
+
raise Errors::QueueNotFoundError, error_msg
|
|
185
252
|
end
|
|
186
253
|
|
|
254
|
+
# Validates that all queues have registered workers
|
|
255
|
+
#
|
|
256
|
+
# @return [void]
|
|
187
257
|
def validate_workers
|
|
188
258
|
return if Shoryuken.active_job?
|
|
189
259
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
# Namespace for all Shoryuken-specific errors.
|
|
5
|
+
# These provide more meaningful error types than generic Ruby exceptions,
|
|
6
|
+
# making it easier to rescue and handle specific failure cases.
|
|
7
|
+
module Errors
|
|
8
|
+
# Base class for all Shoryuken errors
|
|
9
|
+
BaseError = Class.new(StandardError)
|
|
10
|
+
|
|
11
|
+
# Raised when there is a configuration validation failure
|
|
12
|
+
InvalidConfigurationError = Class.new(BaseError)
|
|
13
|
+
|
|
14
|
+
# Raised when a specified SQS queue does not exist or cannot be accessed
|
|
15
|
+
QueueNotFoundError = Class.new(BaseError)
|
|
16
|
+
|
|
17
|
+
# Raised when worker registration fails due to conflicts
|
|
18
|
+
# (e.g., registering multiple workers for a batch queue)
|
|
19
|
+
InvalidWorkerRegistrationError = Class.new(BaseError)
|
|
20
|
+
|
|
21
|
+
# Raised when an invalid polling strategy is specified
|
|
22
|
+
InvalidPollingStrategyError = Class.new(BaseError)
|
|
23
|
+
|
|
24
|
+
# Raised when an invalid lifecycle event name is used
|
|
25
|
+
InvalidEventError = Class.new(BaseError)
|
|
26
|
+
|
|
27
|
+
# Raised when a delay exceeds the maximum allowed by SQS (15 minutes)
|
|
28
|
+
InvalidDelayError = Class.new(BaseError)
|
|
29
|
+
|
|
30
|
+
# Raised when an ARN format is invalid
|
|
31
|
+
InvalidArnError = Class.new(BaseError)
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/shoryuken/fetcher.rb
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
+
# Fetches messages from SQS queues.
|
|
5
|
+
# Handles message retrieval with automatic retry on connectivity errors.
|
|
4
6
|
class Fetcher
|
|
5
7
|
include Util
|
|
6
8
|
|
|
9
|
+
# Maximum number of messages that can be fetched in a single SQS request
|
|
7
10
|
FETCH_LIMIT = 10
|
|
8
11
|
|
|
12
|
+
# Initializes a new Fetcher for a processing group
|
|
13
|
+
#
|
|
14
|
+
# @param group [String] the processing group name
|
|
9
15
|
def initialize(group)
|
|
10
16
|
@group = group
|
|
11
17
|
end
|
|
12
18
|
|
|
19
|
+
# Fetches messages from a queue with automatic retry
|
|
20
|
+
#
|
|
21
|
+
# @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
|
|
22
|
+
# @param limit [Integer] the maximum number of messages to fetch
|
|
23
|
+
# @return [Array<Aws::SQS::Types::Message>] the fetched messages
|
|
13
24
|
def fetch(queue, limit)
|
|
14
25
|
fetch_with_auto_retry(3) do
|
|
15
26
|
started_at = Time.now
|
|
@@ -27,6 +38,11 @@ module Shoryuken
|
|
|
27
38
|
|
|
28
39
|
private
|
|
29
40
|
|
|
41
|
+
# Fetches with automatic retry on errors
|
|
42
|
+
#
|
|
43
|
+
# @param max_attempts [Integer] maximum number of retry attempts
|
|
44
|
+
# @yield the fetch operation to retry
|
|
45
|
+
# @return [Object] the result of the block
|
|
30
46
|
def fetch_with_auto_retry(max_attempts)
|
|
31
47
|
attempts = 0
|
|
32
48
|
|
|
@@ -46,6 +62,11 @@ module Shoryuken
|
|
|
46
62
|
end
|
|
47
63
|
end
|
|
48
64
|
|
|
65
|
+
# Receives messages from an SQS queue
|
|
66
|
+
#
|
|
67
|
+
# @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
|
|
68
|
+
# @param limit [Integer] the maximum number of messages to receive
|
|
69
|
+
# @return [Array<Aws::SQS::Types::Message>, nil] the received messages
|
|
49
70
|
def receive_messages(queue, limit)
|
|
50
71
|
options = receive_options(queue)
|
|
51
72
|
|
|
@@ -60,6 +81,13 @@ module Shoryuken
|
|
|
60
81
|
shoryuken_queue.receive_messages(options)
|
|
61
82
|
end
|
|
62
83
|
|
|
84
|
+
# Determines the maximum number of messages to fetch
|
|
85
|
+
#
|
|
86
|
+
# @param shoryuken_queue [Shoryuken::Queue] the queue instance
|
|
87
|
+
# @param limit [Integer] the requested limit
|
|
88
|
+
# @param options [Hash] receive options that may contain max_number_of_messages
|
|
89
|
+
# @option options [Integer] :max_number_of_messages optional override for max messages
|
|
90
|
+
# @return [Integer] the maximum number of messages to fetch
|
|
63
91
|
def max_number_of_messages(shoryuken_queue, limit, options)
|
|
64
92
|
# For FIFO queues we want to make sure we process one message per group at a time
|
|
65
93
|
# if we set max_number_of_messages greater than 1,
|
|
@@ -75,6 +103,10 @@ module Shoryuken
|
|
|
75
103
|
[limit, FETCH_LIMIT, options[:max_number_of_messages]].compact.min
|
|
76
104
|
end
|
|
77
105
|
|
|
106
|
+
# Returns the receive options for a queue
|
|
107
|
+
#
|
|
108
|
+
# @param queue [Shoryuken::Polling::QueueConfiguration] the queue configuration
|
|
109
|
+
# @return [Hash] the receive options hash
|
|
78
110
|
def receive_options(queue)
|
|
79
111
|
options = Shoryuken.sqs_client_receive_message_opts[queue.name]
|
|
80
112
|
options ||= Shoryuken.sqs_client_receive_message_opts[@group]
|
|
@@ -82,8 +114,12 @@ module Shoryuken
|
|
|
82
114
|
options.to_h.dup
|
|
83
115
|
end
|
|
84
116
|
|
|
117
|
+
# Checks if a queue uses batch message processing
|
|
118
|
+
#
|
|
119
|
+
# @param queue [Shoryuken::Queue] the queue to check
|
|
120
|
+
# @return [Boolean] true if the queue is configured for batch processing
|
|
85
121
|
def batched_queue?(queue)
|
|
86
122
|
Shoryuken.worker_registry.batch_receive_messages?(queue.name)
|
|
87
123
|
end
|
|
88
124
|
end
|
|
89
|
-
end
|
|
125
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
+
# Helper classes providing thread-safe utilities and data structures
|
|
4
5
|
module Helpers
|
|
5
6
|
# A thread-safe boolean implementation using AtomicCounter as base.
|
|
6
7
|
# Drop-in replacement for Concurrent::AtomicBoolean without external dependencies.
|
|
@@ -9,33 +10,46 @@ module Shoryuken
|
|
|
9
10
|
# Prevent misuse of counter operations on a boolean
|
|
10
11
|
undef_method :increment, :decrement
|
|
11
12
|
|
|
13
|
+
# Initializes a new AtomicBoolean
|
|
14
|
+
#
|
|
15
|
+
# @param initial_value [Boolean] the initial value
|
|
12
16
|
def initialize(initial_value = false)
|
|
13
17
|
super(initial_value ? 1 : 0)
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
#
|
|
20
|
+
# Gets the current value as a boolean
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean] the current value
|
|
17
23
|
def value
|
|
18
24
|
super != 0
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
#
|
|
27
|
+
# Sets the value to true
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean] true
|
|
22
30
|
def make_true
|
|
23
31
|
@mutex.synchronize { @value = 1 }
|
|
24
32
|
true
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
#
|
|
35
|
+
# Sets the value to false
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean] false
|
|
28
38
|
def make_false
|
|
29
39
|
@mutex.synchronize { @value = 0 }
|
|
30
40
|
false
|
|
31
41
|
end
|
|
32
42
|
|
|
33
|
-
#
|
|
43
|
+
# Checks if the value is true
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true if the value is true
|
|
34
46
|
def true?
|
|
35
47
|
@mutex.synchronize { @value != 0 }
|
|
36
48
|
end
|
|
37
49
|
|
|
38
|
-
#
|
|
50
|
+
# Checks if the value is false
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean] true if the value is false
|
|
39
53
|
def false?
|
|
40
54
|
@mutex.synchronize { @value == 0 }
|
|
41
55
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Helpers
|
|
5
|
+
# A thread-safe timer task implementation.
|
|
6
|
+
# Drop-in replacement for Concurrent::TimerTask without external dependencies.
|
|
7
|
+
class TimerTask
|
|
8
|
+
# Initializes a new TimerTask
|
|
9
|
+
#
|
|
10
|
+
# @param execution_interval [Float] interval in seconds between task executions
|
|
11
|
+
# @param task [Proc] the task to execute on each interval (provided as a block)
|
|
12
|
+
# @return [TimerTask] a new TimerTask instance
|
|
13
|
+
# @raise [ArgumentError] if no block is provided or interval is not positive
|
|
14
|
+
# @yield the task to execute on each interval
|
|
15
|
+
def initialize(execution_interval:, &task)
|
|
16
|
+
raise ArgumentError, 'A block must be provided' unless block_given?
|
|
17
|
+
|
|
18
|
+
@execution_interval = Float(execution_interval)
|
|
19
|
+
raise ArgumentError, 'execution_interval must be positive' if @execution_interval <= 0
|
|
20
|
+
|
|
21
|
+
@task = task
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
@thread = nil
|
|
24
|
+
@running = false
|
|
25
|
+
@killed = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Starts the timer task execution
|
|
29
|
+
#
|
|
30
|
+
# @return [TimerTask] self for method chaining
|
|
31
|
+
def execute
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
return self if @running || @killed
|
|
34
|
+
|
|
35
|
+
@running = true
|
|
36
|
+
@thread = Thread.new { run_timer_loop }
|
|
37
|
+
end
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Stops and kills the timer task
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] true if killed, false if already killed
|
|
44
|
+
def kill
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
return false if @killed
|
|
47
|
+
|
|
48
|
+
@killed = true
|
|
49
|
+
@running = false
|
|
50
|
+
|
|
51
|
+
@thread.kill if @thread&.alive?
|
|
52
|
+
end
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Runs the timer loop in a separate thread
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
def run_timer_loop
|
|
62
|
+
until @killed
|
|
63
|
+
sleep(@execution_interval)
|
|
64
|
+
break if @killed
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
@task.call
|
|
68
|
+
rescue => e
|
|
69
|
+
# Log the error but continue running
|
|
70
|
+
# This matches the behavior of Concurrent::TimerTask
|
|
71
|
+
warn "TimerTask execution error: #{e.message}"
|
|
72
|
+
warn e.backtrace.join("\n") if e.backtrace
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
ensure
|
|
76
|
+
@mutex.synchronize { @running = false }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/shoryuken/launcher.rb
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
+
# Launches and coordinates Shoryuken's message processing managers.
|
|
5
|
+
# Handles the lifecycle of the processing system including startup, shutdown, and health checks.
|
|
4
6
|
class Launcher
|
|
5
7
|
include Util
|
|
6
8
|
|
|
9
|
+
# Initializes a new Launcher with managers for each processing group
|
|
7
10
|
def initialize
|
|
8
11
|
@managers = create_managers
|
|
12
|
+
@stopping = false
|
|
9
13
|
end
|
|
10
14
|
|
|
15
|
+
# Indicates whether the launcher is in the process of stopping.
|
|
16
|
+
#
|
|
17
|
+
# This flag is set to true when either {#stop} or {#stop!} is called,
|
|
18
|
+
# and is used by ActiveJob adapters to signal jobs that they should
|
|
19
|
+
# checkpoint and prepare for graceful shutdown.
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true if stopping, false otherwise
|
|
22
|
+
def stopping?
|
|
23
|
+
@stopping
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Starts the message processing system
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
11
29
|
def start
|
|
12
30
|
logger.info { 'Starting' }
|
|
13
31
|
|
|
@@ -15,7 +33,11 @@ module Shoryuken
|
|
|
15
33
|
start_managers
|
|
16
34
|
end
|
|
17
35
|
|
|
36
|
+
# Forces an immediate stop of all processing
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
18
39
|
def stop!
|
|
40
|
+
@stopping = true
|
|
19
41
|
initiate_stop
|
|
20
42
|
|
|
21
43
|
# Don't await here so the timeout below is not delayed
|
|
@@ -27,7 +49,11 @@ module Shoryuken
|
|
|
27
49
|
fire_event(:stopped)
|
|
28
50
|
end
|
|
29
51
|
|
|
52
|
+
# Gracefully stops all processing, waiting for in-flight messages
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
30
55
|
def stop
|
|
56
|
+
@stopping = true
|
|
31
57
|
fire_event(:quiet, true)
|
|
32
58
|
|
|
33
59
|
initiate_stop
|
|
@@ -41,6 +67,9 @@ module Shoryuken
|
|
|
41
67
|
fire_event(:stopped)
|
|
42
68
|
end
|
|
43
69
|
|
|
70
|
+
# Checks if all processing groups are healthy
|
|
71
|
+
#
|
|
72
|
+
# @return [Boolean] true if all groups are running normally
|
|
44
73
|
def healthy?
|
|
45
74
|
Shoryuken.groups.keys.all? do |group|
|
|
46
75
|
manager = @managers.find { |m| m.group == group }
|
|
@@ -50,30 +79,48 @@ module Shoryuken
|
|
|
50
79
|
|
|
51
80
|
private
|
|
52
81
|
|
|
82
|
+
# Stops all managers from dispatching new messages
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
53
85
|
def stop_new_dispatching
|
|
54
86
|
@managers.each(&:stop_new_dispatching)
|
|
55
87
|
end
|
|
56
88
|
|
|
89
|
+
# Waits for any in-progress dispatching to complete
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
57
92
|
def await_dispatching_in_progress
|
|
58
93
|
@managers.each(&:await_dispatching_in_progress)
|
|
59
94
|
end
|
|
60
95
|
|
|
96
|
+
# Returns the executor for running async operations
|
|
97
|
+
#
|
|
98
|
+
# @return [Concurrent::ExecutorService] the executor service
|
|
61
99
|
def executor
|
|
62
100
|
@_executor ||= Shoryuken.launcher_executor || Concurrent.global_io_executor
|
|
63
101
|
end
|
|
64
102
|
|
|
103
|
+
# Starts all managers in parallel futures
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
65
106
|
def start_managers
|
|
66
107
|
@managers.each do |manager|
|
|
67
108
|
Concurrent::Future.execute { manager.start }
|
|
68
109
|
end
|
|
69
110
|
end
|
|
70
111
|
|
|
112
|
+
# Initiates the stop sequence
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
71
115
|
def initiate_stop
|
|
72
116
|
logger.info { 'Shutting down' }
|
|
73
117
|
|
|
74
118
|
stop_callback
|
|
75
119
|
end
|
|
76
120
|
|
|
121
|
+
# Executes the start callback and fires startup event
|
|
122
|
+
#
|
|
123
|
+
# @return [void]
|
|
77
124
|
def start_callback
|
|
78
125
|
if (callback = Shoryuken.start_callback)
|
|
79
126
|
logger.debug { 'Calling start_callback' }
|
|
@@ -83,6 +130,9 @@ module Shoryuken
|
|
|
83
130
|
fire_event(:startup)
|
|
84
131
|
end
|
|
85
132
|
|
|
133
|
+
# Executes the stop callback and fires shutdown event
|
|
134
|
+
#
|
|
135
|
+
# @return [void]
|
|
86
136
|
def stop_callback
|
|
87
137
|
if (callback = Shoryuken.stop_callback)
|
|
88
138
|
logger.debug { 'Calling stop_callback' }
|
|
@@ -92,6 +142,9 @@ module Shoryuken
|
|
|
92
142
|
fire_event(:shutdown, true)
|
|
93
143
|
end
|
|
94
144
|
|
|
145
|
+
# Creates managers for each configured processing group
|
|
146
|
+
#
|
|
147
|
+
# @return [Array<Shoryuken::Manager>] the created managers
|
|
95
148
|
def create_managers
|
|
96
149
|
Shoryuken.groups.map do |group, options|
|
|
97
150
|
Shoryuken::Manager.new(
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Logging
|
|
5
|
+
# Base formatter class that provides common functionality for Shoryuken log formatters.
|
|
6
|
+
# Provides thread ID generation and context management.
|
|
7
|
+
class Base < ::Logger::Formatter
|
|
8
|
+
# Generates a thread ID for the current thread.
|
|
9
|
+
# Uses a combination of thread object_id and process ID to create a unique identifier.
|
|
10
|
+
#
|
|
11
|
+
# @return [String] A base36-encoded thread identifier
|
|
12
|
+
def tid
|
|
13
|
+
Thread.current['shoryuken_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the current logging context as a formatted string.
|
|
17
|
+
# Context is set using {Shoryuken::Logging.with_context}.
|
|
18
|
+
#
|
|
19
|
+
# @return [String] Formatted context string or empty string if no context
|
|
20
|
+
def context
|
|
21
|
+
c = Shoryuken::Logging.current_context
|
|
22
|
+
c ? " #{c}" : ''
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Logging
|
|
5
|
+
# A pretty log formatter that includes timestamps, process ID, thread ID,
|
|
6
|
+
# context information, and severity in a human-readable format.
|
|
7
|
+
#
|
|
8
|
+
# Output format: "TIMESTAMP PID TID-THREAD_ID CONTEXT SEVERITY: MESSAGE"
|
|
9
|
+
#
|
|
10
|
+
# @example Sample output
|
|
11
|
+
# # 2023-08-15T10:30:45Z 12345 TID-abc123 MyWorker/queue1/msg-456 INFO: Processing message
|
|
12
|
+
class Pretty < Base
|
|
13
|
+
# Formats a log message with timestamp and full context information.
|
|
14
|
+
#
|
|
15
|
+
# @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
|
|
16
|
+
# @param time [Time] Timestamp when the log entry was created
|
|
17
|
+
# @param _program_name [String] Program name (unused)
|
|
18
|
+
# @param message [String] The log message
|
|
19
|
+
# @return [String] Formatted log entry with newline
|
|
20
|
+
def call(severity, time, _program_name, message)
|
|
21
|
+
"#{time.utc.iso8601} #{Process.pid} TID-#{tid}#{context} #{severity}: #{message}\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Logging
|
|
5
|
+
# A log formatter that excludes timestamps from output.
|
|
6
|
+
# Useful for environments where timestamps are added by external logging systems.
|
|
7
|
+
#
|
|
8
|
+
# Output format: "pid=PID tid=THREAD_ID CONTEXT SEVERITY: MESSAGE"
|
|
9
|
+
#
|
|
10
|
+
# @example Sample output
|
|
11
|
+
# # pid=12345 tid=abc123 MyWorker/queue1/msg-456 INFO: Processing message
|
|
12
|
+
class WithoutTimestamp < Base
|
|
13
|
+
# Formats a log message without timestamp information.
|
|
14
|
+
#
|
|
15
|
+
# @param severity [String] Log severity level (DEBUG, INFO, WARN, ERROR, FATAL)
|
|
16
|
+
# @param _time [Time] Timestamp (unused)
|
|
17
|
+
# @param _program_name [String] Program name (unused)
|
|
18
|
+
# @param message [String] The log message
|
|
19
|
+
# @return [String] Formatted log entry with newline
|
|
20
|
+
def call(severity, _time, _program_name, message)
|
|
21
|
+
"pid=#{Process.pid} tid=#{tid}#{context} #{severity}: #{message}\n"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|