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
data/bin/cli/sqs.rb
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'date'
|
|
2
4
|
|
|
3
5
|
# rubocop:disable Metrics/BlockLength
|
|
4
6
|
module Shoryuken
|
|
5
7
|
module CLI
|
|
8
|
+
# SQS command line interface for queue management operations.
|
|
9
|
+
# Provides commands for listing, creating, deleting, moving, and dumping queue messages.
|
|
6
10
|
class SQS < Base
|
|
7
|
-
#
|
|
11
|
+
# Maximum batch size in bytes for SQS batch operations
|
|
12
|
+
# @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html
|
|
8
13
|
MAX_BATCH_SIZE = 1024 * 1024
|
|
9
14
|
|
|
10
15
|
namespace :sqs
|
|
11
16
|
class_option :endpoint, aliases: '-e', type: :string, default: ENV['SHORYUKEN_SQS_ENDPOINT'], desc: 'Endpoint URL'
|
|
12
17
|
|
|
13
18
|
no_commands do
|
|
19
|
+
# Normalizes a dump message for requeuing
|
|
20
|
+
#
|
|
21
|
+
# @param message [Hash] the dumped message hash
|
|
22
|
+
# @return [Hash] the normalized message
|
|
14
23
|
def normalize_dump_message(message)
|
|
15
24
|
# symbolize_keys is needed for keeping it compatible with `requeue`
|
|
16
|
-
attributes = message[:attributes].
|
|
25
|
+
attributes = message[:attributes].transform_keys(&:to_sym)
|
|
17
26
|
|
|
18
27
|
# See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
|
|
19
28
|
# The `string_list_values` and `binary_list_values` are not implemented. Reserved for future use.
|
|
@@ -30,6 +39,9 @@ module Shoryuken
|
|
|
30
39
|
}
|
|
31
40
|
end
|
|
32
41
|
|
|
42
|
+
# Returns the SQS client options based on CLI options
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash] the client options hash
|
|
33
45
|
def client_options
|
|
34
46
|
endpoint = options[:endpoint]
|
|
35
47
|
{}.tap do |hash|
|
|
@@ -37,16 +49,28 @@ module Shoryuken
|
|
|
37
49
|
end
|
|
38
50
|
end
|
|
39
51
|
|
|
52
|
+
# Returns the SQS client instance
|
|
53
|
+
#
|
|
54
|
+
# @return [Aws::SQS::Client] the SQS client
|
|
40
55
|
def sqs
|
|
41
56
|
@_sqs ||= Aws::SQS::Client.new(client_options)
|
|
42
57
|
end
|
|
43
58
|
|
|
59
|
+
# Finds the URL for a queue by name
|
|
60
|
+
#
|
|
61
|
+
# @param queue_name [String] the queue name
|
|
62
|
+
# @return [String] the queue URL
|
|
44
63
|
def find_queue_url(queue_name)
|
|
45
64
|
sqs.get_queue_url(queue_name: queue_name).queue_url
|
|
46
65
|
rescue Aws::SQS::Errors::NonExistentQueue
|
|
47
66
|
fail_task "The specified queue #{queue_name} does not exist"
|
|
48
67
|
end
|
|
49
68
|
|
|
69
|
+
# Batch deletes messages from a queue
|
|
70
|
+
#
|
|
71
|
+
# @param url [String] the queue URL
|
|
72
|
+
# @param messages [Array<Aws::SQS::Types::Message>] the messages to delete
|
|
73
|
+
# @return [void]
|
|
50
74
|
def batch_delete(url, messages)
|
|
51
75
|
messages.to_a.flatten.each_slice(10) do |batch|
|
|
52
76
|
sqs.delete_message_batch(
|
|
@@ -61,11 +85,23 @@ module Shoryuken
|
|
|
61
85
|
end
|
|
62
86
|
end
|
|
63
87
|
|
|
88
|
+
# Batch sends messages to a queue
|
|
89
|
+
#
|
|
90
|
+
# @param url [String] the queue URL
|
|
91
|
+
# @param messages [Array<Hash>] the messages to send
|
|
92
|
+
# @param max_batch_size [Integer] maximum messages per batch
|
|
93
|
+
# @return [void]
|
|
64
94
|
def batch_send(url, messages, max_batch_size = 10)
|
|
65
95
|
messages = messages.to_a.flatten.map(&method(:normalize_dump_message))
|
|
66
96
|
batch_send_normalized_messages url, messages, max_batch_size
|
|
67
97
|
end
|
|
68
98
|
|
|
99
|
+
# Batch sends normalized messages to a queue
|
|
100
|
+
#
|
|
101
|
+
# @param url [String] the queue URL
|
|
102
|
+
# @param messages [Array<Hash>] the normalized messages
|
|
103
|
+
# @param max_batch_size [Integer] maximum messages per batch
|
|
104
|
+
# @return [void]
|
|
69
105
|
def batch_send_normalized_messages(url, messages, max_batch_size)
|
|
70
106
|
# Repeatedly take the longest prefix of messages such that
|
|
71
107
|
# 1. The number of messages is less than or equal to max_batch_size
|
|
@@ -90,10 +126,18 @@ module Shoryuken
|
|
|
90
126
|
end
|
|
91
127
|
end
|
|
92
128
|
|
|
129
|
+
# Calculates the total payload size of a batch
|
|
130
|
+
#
|
|
131
|
+
# @param messages [Array<Hash>] the messages
|
|
132
|
+
# @return [Integer] total size in bytes
|
|
93
133
|
def batch_payload_size(messages)
|
|
94
134
|
messages.sum(&method(:message_size))
|
|
95
135
|
end
|
|
96
136
|
|
|
137
|
+
# Calculates the size of a single message
|
|
138
|
+
#
|
|
139
|
+
# @param message [Hash] the message
|
|
140
|
+
# @return [Integer] size in bytes
|
|
97
141
|
def message_size(message)
|
|
98
142
|
attribute_size = (message[:message_attributes] || []).sum do |name, value|
|
|
99
143
|
name_size = name.to_s.bytesize
|
|
@@ -111,6 +155,12 @@ module Shoryuken
|
|
|
111
155
|
attribute_size + body_size
|
|
112
156
|
end
|
|
113
157
|
|
|
158
|
+
# Receives all messages from a queue up to a limit
|
|
159
|
+
#
|
|
160
|
+
# @param url [String] the queue URL
|
|
161
|
+
# @param limit [Integer, Float] maximum messages to receive
|
|
162
|
+
# @yield [Aws::SQS::Types::Message] yields each received message
|
|
163
|
+
# @return [Integer] the number of messages received
|
|
114
164
|
def find_all(url, limit, &block)
|
|
115
165
|
count = 0
|
|
116
166
|
batch_size = limit > 10 ? 10 : limit
|
|
@@ -137,6 +187,10 @@ module Shoryuken
|
|
|
137
187
|
count
|
|
138
188
|
end
|
|
139
189
|
|
|
190
|
+
# Lists queues and prints their attributes as a table
|
|
191
|
+
#
|
|
192
|
+
# @param urls [Array<String>] the queue URLs
|
|
193
|
+
# @return [void]
|
|
140
194
|
def list_and_print_queues(urls)
|
|
141
195
|
attrs = %w[QueueArn ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible LastModifiedTimestamp]
|
|
142
196
|
|
|
@@ -154,6 +208,11 @@ module Shoryuken
|
|
|
154
208
|
print_table(entries)
|
|
155
209
|
end
|
|
156
210
|
|
|
211
|
+
# Generates the dump file path for a queue
|
|
212
|
+
#
|
|
213
|
+
# @param path [String] the directory path
|
|
214
|
+
# @param queue_name [String] the queue name
|
|
215
|
+
# @return [String] the full file path
|
|
157
216
|
def dump_file(path, queue_name)
|
|
158
217
|
File.join(path, "#{queue_name}-#{Date.today}.jsonl")
|
|
159
218
|
end
|
data/bin/integrations
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Shoryuken integration test runner
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bin/integrations # Run all integration tests
|
|
8
|
+
# bin/integrations fifo # Run tests with 'fifo' in path
|
|
9
|
+
# bin/integrations rails/rails_72 # Run Rails 7.2 tests
|
|
10
|
+
# bin/integrations batch retry # Run tests matching 'batch' OR 'retry'
|
|
11
|
+
# bin/integrations -v fifo # Run with verbose output
|
|
12
|
+
|
|
13
|
+
require 'bundler'
|
|
14
|
+
require 'fileutils'
|
|
15
|
+
require 'timeout'
|
|
16
|
+
|
|
17
|
+
TIMEOUT = 300 # 5 minutes per scenario
|
|
18
|
+
SPEC_DIR = File.expand_path('../spec/integration', __dir__)
|
|
19
|
+
ROOT_DIR = File.expand_path('..', __dir__)
|
|
20
|
+
|
|
21
|
+
class IntegrationRunner
|
|
22
|
+
def initialize(args)
|
|
23
|
+
@verbose = args.delete('-v') || args.delete('--verbose')
|
|
24
|
+
@filters = args.reject { |a| a.start_with?('-') }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
specs = find_specs
|
|
29
|
+
specs = filter_specs(specs) if @filters.any?
|
|
30
|
+
|
|
31
|
+
if specs.empty?
|
|
32
|
+
puts 'No specs found matching filters'
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "Running #{specs.size} integration specs..."
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
results = run_specs(specs)
|
|
40
|
+
report_results(results)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def find_specs
|
|
46
|
+
Dir.glob(File.join(SPEC_DIR, '**/*_spec.rb')).reject do |path|
|
|
47
|
+
# Exclude vendor and .bundle directories
|
|
48
|
+
path.include?('/vendor/') || path.include?('/.bundle/')
|
|
49
|
+
end.map do |path|
|
|
50
|
+
relative_path = path.sub("#{SPEC_DIR}/", '')
|
|
51
|
+
dir = File.dirname(path)
|
|
52
|
+
gemfile = File.exist?(File.join(dir, 'Gemfile')) ? File.join(dir, 'Gemfile') : File.join(ROOT_DIR, 'Gemfile')
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
name: relative_path.sub('_spec.rb', '').gsub('/', ' / '),
|
|
56
|
+
path: path,
|
|
57
|
+
relative_path: relative_path,
|
|
58
|
+
directory: dir,
|
|
59
|
+
gemfile: gemfile
|
|
60
|
+
}
|
|
61
|
+
end.sort_by { |s| s[:relative_path] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def filter_specs(specs)
|
|
65
|
+
specs.select do |spec|
|
|
66
|
+
@filters.any? { |filter| spec[:relative_path].include?(filter) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run_specs(specs)
|
|
71
|
+
results = []
|
|
72
|
+
|
|
73
|
+
specs.each do |spec|
|
|
74
|
+
result = run_spec(spec)
|
|
75
|
+
results << result
|
|
76
|
+
|
|
77
|
+
if result[:skipped]
|
|
78
|
+
print 'S'
|
|
79
|
+
elsif result[:success]
|
|
80
|
+
print '.'
|
|
81
|
+
else
|
|
82
|
+
print 'F'
|
|
83
|
+
end
|
|
84
|
+
$stdout.flush
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
puts
|
|
88
|
+
results
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_spec(spec)
|
|
92
|
+
# Start with a clean bundler environment to prevent pollution between tests
|
|
93
|
+
env = {
|
|
94
|
+
'BUNDLE_GEMFILE' => spec[:gemfile],
|
|
95
|
+
'RAILS_ENV' => 'test',
|
|
96
|
+
'RUBYOPT' => nil # Clear any -rbundler/setup from CI or previous tests
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Install dependencies if using a local Gemfile
|
|
100
|
+
uses_local_gemfile = spec[:gemfile] != File.join(ROOT_DIR, 'Gemfile')
|
|
101
|
+
if uses_local_gemfile
|
|
102
|
+
install_result = install_bundle(spec, env)
|
|
103
|
+
unless install_result[:success]
|
|
104
|
+
# Skip test if bundle install fails (e.g., gems not available in CI)
|
|
105
|
+
return {
|
|
106
|
+
spec: spec,
|
|
107
|
+
success: true,
|
|
108
|
+
skipped: true,
|
|
109
|
+
skip_reason: 'Bundle install failed (dependencies not available)',
|
|
110
|
+
output: install_result[:output]
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Use isolated bundle config to match install_bundle
|
|
115
|
+
bundle_path = File.join(spec[:directory], 'vendor', 'bundle')
|
|
116
|
+
bundle_config = File.join(spec[:directory], '.bundle')
|
|
117
|
+
env['BUNDLE_PATH'] = bundle_path
|
|
118
|
+
env['BUNDLE_FROZEN'] = 'false'
|
|
119
|
+
env['BUNDLE_APP_CONFIG'] = bundle_config
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Run the spec
|
|
123
|
+
# For local gemfiles, use standalone bundle setup which doesn't need bundler at runtime
|
|
124
|
+
# This avoids issues with bundle exec inheriting the wrong config
|
|
125
|
+
cmd = if uses_local_gemfile
|
|
126
|
+
standalone_setup = File.join(bundle_path, 'bundler', 'setup.rb')
|
|
127
|
+
['ruby', "-r#{standalone_setup}", File.join(ROOT_DIR, 'bin/scenario'), spec[:path]]
|
|
128
|
+
else
|
|
129
|
+
['bundle', 'exec', 'ruby', File.join(ROOT_DIR, 'bin/scenario'), spec[:path]]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
output = []
|
|
133
|
+
start_time = Time.now
|
|
134
|
+
|
|
135
|
+
begin
|
|
136
|
+
Timeout.timeout(TIMEOUT) do
|
|
137
|
+
# Use unbundled env to prevent pollution from previous test runs
|
|
138
|
+
# This is especially important after Rails integration tests that use
|
|
139
|
+
# bundle install --standalone with different gem versions
|
|
140
|
+
Bundler.with_unbundled_env do
|
|
141
|
+
IO.popen(env, cmd, chdir: spec[:directory], err: [:child, :out]) do |io|
|
|
142
|
+
io.each_line { |line| output << line }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
spec: spec,
|
|
149
|
+
success: $?.success?,
|
|
150
|
+
exit_code: $?.exitstatus,
|
|
151
|
+
duration: Time.now - start_time,
|
|
152
|
+
output: output.join
|
|
153
|
+
}
|
|
154
|
+
rescue Timeout::Error
|
|
155
|
+
{
|
|
156
|
+
spec: spec,
|
|
157
|
+
success: false,
|
|
158
|
+
exit_code: -1,
|
|
159
|
+
error: 'Timeout',
|
|
160
|
+
duration: Time.now - start_time,
|
|
161
|
+
output: output.join
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def install_bundle(spec, env)
|
|
167
|
+
return { success: true } if @bundle_installed&.include?(spec[:gemfile])
|
|
168
|
+
|
|
169
|
+
output = []
|
|
170
|
+
|
|
171
|
+
# Create isolated bundle environment to avoid CI cache interference
|
|
172
|
+
# Use a unique path per Gemfile to avoid conflicts
|
|
173
|
+
bundle_path = File.join(spec[:directory], 'vendor', 'bundle')
|
|
174
|
+
bundle_config = File.join(spec[:directory], '.bundle')
|
|
175
|
+
|
|
176
|
+
# Create local .bundle/config to override project-level config
|
|
177
|
+
FileUtils.mkdir_p(bundle_config)
|
|
178
|
+
File.write(File.join(bundle_config, 'config'), <<~CONFIG)
|
|
179
|
+
---
|
|
180
|
+
BUNDLE_PATH: "#{bundle_path}"
|
|
181
|
+
BUNDLE_FROZEN: "false"
|
|
182
|
+
CONFIG
|
|
183
|
+
|
|
184
|
+
clean_env = env.merge(
|
|
185
|
+
'BUNDLE_PATH' => bundle_path,
|
|
186
|
+
'BUNDLE_FROZEN' => 'false',
|
|
187
|
+
'BUNDLE_DEPLOYMENT' => nil,
|
|
188
|
+
'BUNDLE_WITHOUT' => nil,
|
|
189
|
+
'BUNDLE_CACHE_PATH' => nil,
|
|
190
|
+
'BUNDLE_BIN' => nil,
|
|
191
|
+
'BUNDLE_APP_CONFIG' => bundle_config,
|
|
192
|
+
'RUBYOPT' => nil # Clear any -rbundler/setup from CI
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Use --standalone to generate a setup.rb that doesn't need bundler at runtime
|
|
196
|
+
# Run in a completely unbundled environment using Bundler API
|
|
197
|
+
cmd_script = <<~RUBY
|
|
198
|
+
require 'bundler'
|
|
199
|
+
Bundler.with_unbundled_env do
|
|
200
|
+
system({'BUNDLE_GEMFILE' => '#{spec[:gemfile]}', 'BUNDLE_PATH' => '#{bundle_path}', 'BUNDLE_FROZEN' => 'false', 'BUNDLE_APP_CONFIG' => '#{bundle_config}'}, 'bundle', 'install', '--standalone')
|
|
201
|
+
end
|
|
202
|
+
exit($?.success? ? 0 : 1)
|
|
203
|
+
RUBY
|
|
204
|
+
|
|
205
|
+
IO.popen(['ruby', '-e', cmd_script], chdir: spec[:directory], err: [:child, :out]) do |io|
|
|
206
|
+
io.each_line { |line| output << line }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@bundle_installed ||= []
|
|
210
|
+
@bundle_installed << spec[:gemfile] if $?.success?
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
spec: spec,
|
|
214
|
+
success: $?.success?,
|
|
215
|
+
output: output.join,
|
|
216
|
+
error: $?.success? ? nil : 'Bundle install failed',
|
|
217
|
+
bundle_config: bundle_config
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def report_results(results)
|
|
222
|
+
skipped = results.select { |r| r[:skipped] }
|
|
223
|
+
failed = results.reject { |r| r[:success] || r[:skipped] }
|
|
224
|
+
passed = results.count { |r| r[:success] && !r[:skipped] }
|
|
225
|
+
total = results.size
|
|
226
|
+
|
|
227
|
+
puts
|
|
228
|
+
summary = "#{passed}/#{total} passed"
|
|
229
|
+
summary += ", #{skipped.size} skipped" if skipped.any?
|
|
230
|
+
puts summary
|
|
231
|
+
|
|
232
|
+
if skipped.any?
|
|
233
|
+
puts
|
|
234
|
+
puts 'Skipped:'
|
|
235
|
+
puts
|
|
236
|
+
skipped.each do |result|
|
|
237
|
+
puts " - #{result[:spec][:name]}"
|
|
238
|
+
puts " Reason: #{result[:skip_reason]}" if result[:skip_reason]
|
|
239
|
+
if result[:output] && !result[:output].strip.empty?
|
|
240
|
+
lines = result[:output].lines.last(15)
|
|
241
|
+
lines.each { |line| puts " #{line}" }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if failed.any?
|
|
247
|
+
puts
|
|
248
|
+
puts 'Failures:'
|
|
249
|
+
puts
|
|
250
|
+
|
|
251
|
+
failed.each_with_index do |result, idx|
|
|
252
|
+
puts " #{idx + 1}) #{result[:spec][:name]}"
|
|
253
|
+
if result[:error]
|
|
254
|
+
puts " Error: #{result[:error]}"
|
|
255
|
+
end
|
|
256
|
+
if result[:output] && !result[:output].strip.empty?
|
|
257
|
+
# Show last 30 lines of output for context
|
|
258
|
+
lines = result[:output].lines
|
|
259
|
+
if lines.size > 30
|
|
260
|
+
puts " ... (#{lines.size - 30} lines truncated)"
|
|
261
|
+
lines = lines.last(30)
|
|
262
|
+
end
|
|
263
|
+
lines.each { |line| puts " #{line}" }
|
|
264
|
+
end
|
|
265
|
+
puts
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
exit(failed.empty? ? 0 : 1)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if __FILE__ == $0
|
|
274
|
+
IntegrationRunner.new(ARGV.dup).run
|
|
275
|
+
end
|
data/bin/scenario
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Individual scenario runner for integration testing
|
|
5
|
+
# This script runs a single integration test file in complete isolation
|
|
6
|
+
|
|
7
|
+
require 'bundler/setup'
|
|
8
|
+
|
|
9
|
+
# Exit codes
|
|
10
|
+
EXIT_SUCCESS = 0
|
|
11
|
+
EXIT_FAILURE = 1
|
|
12
|
+
EXIT_TIMEOUT = 2
|
|
13
|
+
EXIT_SETUP_ERROR = 3
|
|
14
|
+
|
|
15
|
+
class ScenarioRunner
|
|
16
|
+
attr_reader :test_file
|
|
17
|
+
|
|
18
|
+
def initialize(test_file)
|
|
19
|
+
@test_file = test_file
|
|
20
|
+
@exit_code = EXIT_SUCCESS
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run
|
|
24
|
+
puts "Running: #{File.basename(test_file)}" if ENV['VERBOSE']
|
|
25
|
+
|
|
26
|
+
# Set up the scenario-specific environment
|
|
27
|
+
setup_scenario
|
|
28
|
+
|
|
29
|
+
# Load and run the test file
|
|
30
|
+
load_and_run_test
|
|
31
|
+
|
|
32
|
+
exit EXIT_SUCCESS
|
|
33
|
+
rescue => e
|
|
34
|
+
puts "FAILED: #{File.basename(test_file)} - #{e.message}" if ENV['VERBOSE']
|
|
35
|
+
puts e.backtrace.first(5).join("\n") if ENV['VERBOSE']
|
|
36
|
+
exit EXIT_FAILURE
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def setup_scenario
|
|
42
|
+
# Each test handles its own specific requirements
|
|
43
|
+
require 'bundler/setup'
|
|
44
|
+
|
|
45
|
+
puts "Setting up isolated test environment" if ENV['VERBOSE']
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_and_run_test
|
|
50
|
+
# Test file might be relative to current directory
|
|
51
|
+
if File.exist?(test_file)
|
|
52
|
+
absolute_test_path = File.expand_path(test_file)
|
|
53
|
+
else
|
|
54
|
+
# Fallback to project root resolution
|
|
55
|
+
project_root = File.expand_path('..', __dir__)
|
|
56
|
+
absolute_test_path = File.join(project_root, test_file)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts "Current directory: #{Dir.pwd}" if ENV['VERBOSE']
|
|
60
|
+
puts "Loading test file: #{absolute_test_path}" if ENV['VERBOSE']
|
|
61
|
+
|
|
62
|
+
unless File.exist?(absolute_test_path)
|
|
63
|
+
raise "Test file not found: #{absolute_test_path}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if this is an RSpec file (contains RSpec.describe or describe)
|
|
67
|
+
file_content = File.read(absolute_test_path, encoding: 'UTF-8')
|
|
68
|
+
if file_content.match?(/\b(?:RSpec\.describe|describe)\b/)
|
|
69
|
+
puts "Running as RSpec test" if ENV['VERBOSE']
|
|
70
|
+
run_rspec_test(absolute_test_path)
|
|
71
|
+
else
|
|
72
|
+
puts "Running as plain Ruby test" if ENV['VERBOSE']
|
|
73
|
+
# Load integrations_helper for plain Ruby integration tests
|
|
74
|
+
if absolute_test_path.include?('spec/integration')
|
|
75
|
+
project_root = File.expand_path('..', __dir__)
|
|
76
|
+
integrations_helper = File.join(project_root, 'spec', 'integrations_helper.rb')
|
|
77
|
+
require integrations_helper
|
|
78
|
+
end
|
|
79
|
+
# Load as plain Ruby test
|
|
80
|
+
load absolute_test_path
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def run_rspec_test(test_file_path)
|
|
85
|
+
# Change to project root for RSpec to find spec_helper
|
|
86
|
+
project_root = File.expand_path('..', __dir__)
|
|
87
|
+
Dir.chdir(project_root) do
|
|
88
|
+
# Disable SimpleCov for integration tests to avoid coverage failures
|
|
89
|
+
ENV['SIMPLECOV_DISABLED'] = 'true'
|
|
90
|
+
|
|
91
|
+
# Make the test file path relative to project root for RSpec
|
|
92
|
+
relative_test_path = test_file_path.sub("#{project_root}/", '')
|
|
93
|
+
|
|
94
|
+
puts "Running RSpec with file: #{relative_test_path}" if ENV['VERBOSE']
|
|
95
|
+
puts "Working directory: #{Dir.pwd}" if ENV['VERBOSE']
|
|
96
|
+
|
|
97
|
+
# Check if this test requires Rails but Rails is not available
|
|
98
|
+
if requires_rails?(test_file_path) && !rails_available?
|
|
99
|
+
puts "Skipping #{File.basename(test_file_path)} - Rails not available"
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Run RSpec with the specific test file
|
|
104
|
+
require 'rspec/core'
|
|
105
|
+
|
|
106
|
+
# Load integration spec_helper for integration tests
|
|
107
|
+
if relative_test_path.include?('spec/integration/')
|
|
108
|
+
require_relative '../spec/integration/spec_helper'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
result = RSpec::Core::Runner.run([relative_test_path], $stderr, $stdout)
|
|
112
|
+
|
|
113
|
+
if result != 0
|
|
114
|
+
raise "RSpec failed with exit code #{result}"
|
|
115
|
+
end
|
|
116
|
+
ensure
|
|
117
|
+
# Clean up environment
|
|
118
|
+
ENV.delete('SIMPLECOV_DISABLED')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def requires_rails?(test_file_path)
|
|
123
|
+
# Check if the test file mentions Rails dependencies
|
|
124
|
+
content = File.read(test_file_path)
|
|
125
|
+
content.match?(/require.*rails|Rails::|ActiveJob::|ActionController::/)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def rails_available?
|
|
129
|
+
begin
|
|
130
|
+
require 'rails'
|
|
131
|
+
true
|
|
132
|
+
rescue LoadError
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validate arguments
|
|
139
|
+
if ARGV.empty?
|
|
140
|
+
puts "Usage: bin/scenario <test_file>"
|
|
141
|
+
puts "Example: bin/scenario spec/integration/rails_integration_spec.rb"
|
|
142
|
+
exit EXIT_SETUP_ERROR
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
test_file = ARGV[0]
|
|
146
|
+
|
|
147
|
+
unless File.exist?(test_file)
|
|
148
|
+
puts "Test file not found: #{test_file}"
|
|
149
|
+
exit EXIT_SETUP_ERROR
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Run the scenario
|
|
154
|
+
ScenarioRunner.new(test_file).run
|
data/bin/shoryuken
CHANGED
|
@@ -29,7 +29,7 @@ module Shoryuken
|
|
|
29
29
|
method_option :delay, aliases: '-D', type: :numeric,
|
|
30
30
|
desc: 'Number of seconds to pause fetching from an empty queue'
|
|
31
31
|
def start
|
|
32
|
-
opts = options.to_h.
|
|
32
|
+
opts = options.to_h.transform_keys(&:to_sym)
|
|
33
33
|
|
|
34
34
|
say '[DEPRECATED] Please use --config instead of --config-file', :yellow if opts[:config_file]
|
|
35
35
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
|
-
module
|
|
4
|
+
module ActiveJob
|
|
5
5
|
# Adds an accessor for SQS SendMessage parameters on ActiveJob jobs
|
|
6
6
|
# (instances of ActiveJob::Base). Shoryuken ActiveJob queue adapters use
|
|
7
7
|
# these parameters when enqueueing jobs; other adapters can ignore them.
|
|
@@ -9,6 +9,7 @@ module Shoryuken
|
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
|
+
# @return [Hash] the SQS send message parameters
|
|
12
13
|
attr_accessor :sqs_send_message_parameters
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -17,12 +18,22 @@ module Shoryuken
|
|
|
17
18
|
# to the empty hash, and populates it whenever `#enqueue` is called, such
|
|
18
19
|
# as when using ActiveJob::Base.set.
|
|
19
20
|
module SQSSendMessageParametersSupport
|
|
21
|
+
# Initializes a new ActiveJob instance with empty SQS parameters
|
|
22
|
+
#
|
|
23
|
+
# @param arguments [Array] the job arguments
|
|
20
24
|
def initialize(*arguments)
|
|
21
25
|
super(*arguments)
|
|
22
26
|
self.sqs_send_message_parameters = {}
|
|
23
27
|
end
|
|
24
|
-
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
|
|
25
28
|
|
|
29
|
+
# Enqueues the job with optional SQS-specific parameters
|
|
30
|
+
#
|
|
31
|
+
# @param options [Hash] enqueue options
|
|
32
|
+
# @option options [Hash] :message_attributes custom SQS message attributes
|
|
33
|
+
# @option options [Hash] :message_system_attributes system attributes
|
|
34
|
+
# @option options [String] :message_deduplication_id FIFO deduplication ID
|
|
35
|
+
# @option options [String] :message_group_id FIFO message group ID
|
|
36
|
+
# @return [Object] the enqueue result
|
|
26
37
|
def enqueue(options = {})
|
|
27
38
|
sqs_options = options.extract! :message_attributes,
|
|
28
39
|
:message_system_attributes,
|
|
@@ -36,5 +47,5 @@ module Shoryuken
|
|
|
36
47
|
end
|
|
37
48
|
end
|
|
38
49
|
|
|
39
|
-
ActiveJob::Base.include Shoryuken::
|
|
40
|
-
ActiveJob::Base.prepend Shoryuken::
|
|
50
|
+
ActiveJob::Base.include Shoryuken::ActiveJob::SQSSendMessageParametersAccessor
|
|
51
|
+
ActiveJob::Base.prepend Shoryuken::ActiveJob::SQSSendMessageParametersSupport
|