shoryuken 7.0.1 → 7.0.3
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 +3 -3
- data/.github/workflows/specs.yml +37 -10
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.gitignore +0 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +120 -0
- data/README.md +1 -1
- data/bin/{clean_localstack → clean_sqs} +4 -4
- data/bin/cli/sqs.rb +7 -0
- data/bin/integrations +52 -34
- data/docker-compose.yml +4 -20
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +3 -1
- data/lib/shoryuken/active_job/current_attributes.rb +35 -7
- data/lib/shoryuken/environment_loader.rb +1 -1
- data/lib/shoryuken/fetcher.rb +7 -1
- data/lib/shoryuken/helpers/timer_task.rb +19 -2
- data/lib/shoryuken/launcher.rb +21 -5
- data/lib/shoryuken/manager.rb +38 -5
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +32 -2
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +8 -3
- data/lib/shoryuken/middleware/server/non_retryable_exception.rb +17 -8
- data/lib/shoryuken/options.rb +22 -3
- data/lib/shoryuken/polling/strict_priority.rb +26 -14
- data/lib/shoryuken/polling/weighted_round_robin.rb +41 -27
- data/lib/shoryuken/queue.rb +8 -1
- data/lib/shoryuken/util.rb +4 -1
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker.rb +5 -1
- data/lib/shoryuken.rb +2 -0
- data/renovate.json +26 -2
- data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/complex_types_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/cross_job_reset_spec.rb +47 -0
- data/spec/integration/active_job/current_attributes/empty_context_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/full_context_spec.rb +1 -1
- data/spec/integration/active_job/current_attributes/incremental_persist_spec.rb +76 -0
- data/spec/integration/active_job/current_attributes/partial_context_spec.rb +1 -1
- data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +1 -1
- data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +1 -1
- data/spec/integration/active_job/fifo_dedup_opt_out/fifo_dedup_opt_out_spec.rb +67 -0
- data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +1 -1
- data/spec/integration/active_job/retry/discard_on_spec.rb +1 -1
- data/spec/integration/active_job/retry/retry_on_spec.rb +1 -1
- data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +2 -2
- data/spec/integration/active_job/scheduled/scheduled_spec.rb +1 -1
- data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +1 -1
- data/spec/integration/auto_delete/auto_delete_spec.rb +1 -1
- data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +1 -1
- data/spec/integration/auto_extend_visibility/short_visibility_timeout_spec.rb +52 -0
- data/spec/integration/aws_config/aws_config_spec.rb +3 -3
- data/spec/integration/batch_processing/batch_processing_spec.rb +1 -1
- data/spec/integration/body_parser/json_parser_spec.rb +1 -1
- data/spec/integration/body_parser/proc_parser_spec.rb +1 -1
- data/spec/integration/body_parser/text_parser_spec.rb +1 -1
- data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +1 -1
- data/spec/integration/concurrent_processing/processor_accounting_spec.rb +94 -0
- data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
- data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +1 -1
- data/spec/integration/exception_handlers/exception_handlers_spec.rb +1 -1
- data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +1 -1
- data/spec/integration/fifo_ordering/fifo_max_messages_cap_spec.rb +96 -0
- data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +1 -1
- data/spec/integration/large_payloads/large_payloads_spec.rb +1 -1
- data/spec/integration/launcher/double_graceful_stop_spec.rb +71 -0
- data/spec/integration/launcher/embedded_dispatch_error_spec.rb +85 -0
- data/spec/integration/launcher/global_executor_preserved_spec.rb +76 -0
- data/spec/integration/launcher/graceful_stop_timeout_spec.rb +74 -0
- data/spec/integration/launcher/launcher_spec.rb +1 -1
- data/spec/integration/message_attributes/message_attributes_spec.rb +1 -1
- data/spec/integration/message_operations/message_operations_spec.rb +1 -1
- data/spec/integration/message_operations/partial_batch_delete_spec.rb +67 -0
- data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +2 -2
- data/spec/integration/non_retryable_exception/with_retry_intervals_spec.rb +115 -0
- data/spec/integration/polling_strategies/polling_strategies_spec.rb +1 -1
- data/spec/integration/queue_operations/queue_operations_spec.rb +1 -1
- data/spec/integration/retry_behavior/retry_behavior_spec.rb +1 -1
- data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +1 -1
- data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +1 -1
- data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +1 -1
- data/spec/integration/worker_groups/worker_groups_spec.rb +1 -1
- data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +1 -1
- data/spec/integrations_helper.rb +13 -12
- data/spec/lib/shoryuken/fetcher_spec.rb +13 -0
- data/spec/lib/shoryuken/helpers/timer_task_spec.rb +24 -0
- data/spec/lib/shoryuken/launcher_spec.rb +38 -0
- data/spec/lib/shoryuken/manager_spec.rb +99 -0
- data/spec/lib/shoryuken/middleware/server/auto_extend_visibility_spec.rb +35 -0
- data/spec/lib/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +56 -0
- data/spec/lib/shoryuken/options_spec.rb +43 -0
- data/spec/lib/shoryuken/polling/strict_priority_spec.rb +25 -0
- data/spec/lib/shoryuken/polling/weighted_round_robin_spec.rb +50 -0
- data/spec/lib/shoryuken/queue_spec.rb +37 -0
- data/spec/lib/shoryuken/util_spec.rb +26 -0
- data/spec/shared_examples_for_active_job.rb +18 -0
- data/spec/spec_helper.rb +26 -7
- metadata +29 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A message without a CurrentAttributes key must not inherit a previous job's
|
|
4
|
+
# context. Every registered class is reset after each job - even keyless ones -
|
|
5
|
+
# so values set during one job don't leak into the next on a reused worker
|
|
6
|
+
# thread.
|
|
7
|
+
|
|
8
|
+
setup_sqs
|
|
9
|
+
setup_active_job
|
|
10
|
+
|
|
11
|
+
require 'active_support/current_attributes'
|
|
12
|
+
require 'shoryuken/active_job/current_attributes'
|
|
13
|
+
|
|
14
|
+
queue_name = DT.queue
|
|
15
|
+
create_test_queue(queue_name)
|
|
16
|
+
|
|
17
|
+
class LeakCurrent < ActiveSupport::CurrentAttributes
|
|
18
|
+
attribute :user_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(LeakCurrent)
|
|
22
|
+
|
|
23
|
+
class CrossJobResetTestJob < ActiveJob::Base
|
|
24
|
+
def perform
|
|
25
|
+
# Record what the worker thread sees at the start of the job, then dirty
|
|
26
|
+
# Current. Without a reset after a keyless job, the second execution on the
|
|
27
|
+
# same thread would observe 'leaked' instead of nil.
|
|
28
|
+
DT[:observed] << LeakCurrent.user_id
|
|
29
|
+
LeakCurrent.user_id = 'leaked'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
CrossJobResetTestJob.queue_as(queue_name)
|
|
34
|
+
|
|
35
|
+
# Concurrency 1 so the two jobs run sequentially on the same worker thread.
|
|
36
|
+
Shoryuken.add_group('default', 1)
|
|
37
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
38
|
+
Shoryuken.register_worker(queue_name, Shoryuken::ActiveJob::JobWrapper)
|
|
39
|
+
|
|
40
|
+
# Enqueued with an empty context, so both messages carry no cattr key.
|
|
41
|
+
CrossJobResetTestJob.perform_later
|
|
42
|
+
CrossJobResetTestJob.perform_later
|
|
43
|
+
|
|
44
|
+
poll_queues_until(timeout: 30) { DT[:observed].size >= 2 }
|
|
45
|
+
|
|
46
|
+
assert_equal([nil, nil], DT[:observed].first(2),
|
|
47
|
+
"CurrentAttributes leaked across jobs: #{DT[:observed].inspect}")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CurrentAttributes registered one class per persist call are all preserved.
|
|
4
|
+
#
|
|
5
|
+
# persist derived each storage key from the per-call index, so calling it once
|
|
6
|
+
# per class (as an app might across several initializers) made the third call
|
|
7
|
+
# reuse "cattr_0" and silently overwrite the second class - its attributes were
|
|
8
|
+
# then never serialized or restored.
|
|
9
|
+
|
|
10
|
+
setup_sqs
|
|
11
|
+
setup_active_job
|
|
12
|
+
|
|
13
|
+
require 'active_support/current_attributes'
|
|
14
|
+
require 'shoryuken/active_job/current_attributes'
|
|
15
|
+
|
|
16
|
+
queue_name = DT.queue
|
|
17
|
+
create_test_queue(queue_name)
|
|
18
|
+
|
|
19
|
+
class CurrentA < ActiveSupport::CurrentAttributes
|
|
20
|
+
attribute :a_val
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class CurrentB < ActiveSupport::CurrentAttributes
|
|
24
|
+
attribute :b_val
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CurrentC < ActiveSupport::CurrentAttributes
|
|
28
|
+
attribute :c_val
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Registered incrementally, one class per call.
|
|
32
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentA)
|
|
33
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentB)
|
|
34
|
+
Shoryuken::ActiveJob::CurrentAttributes.persist(CurrentC)
|
|
35
|
+
|
|
36
|
+
# Every class must be registered, each under a distinct storage key.
|
|
37
|
+
registered = Shoryuken::ActiveJob::CurrentAttributes.cattrs
|
|
38
|
+
assert_equal(
|
|
39
|
+
%w[CurrentA CurrentB CurrentC],
|
|
40
|
+
registered.values.sort,
|
|
41
|
+
'every persisted CurrentAttributes class must be registered (none silently dropped)'
|
|
42
|
+
)
|
|
43
|
+
assert_equal(3, registered.keys.uniq.size, 'each class must use a distinct storage key')
|
|
44
|
+
|
|
45
|
+
class IncrementalCurrentJob < ActiveJob::Base
|
|
46
|
+
def perform
|
|
47
|
+
DT[:executions] << {
|
|
48
|
+
a_val: CurrentA.a_val,
|
|
49
|
+
b_val: CurrentB.b_val,
|
|
50
|
+
c_val: CurrentC.c_val
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
IncrementalCurrentJob.queue_as(queue_name)
|
|
56
|
+
|
|
57
|
+
Shoryuken.add_group('default', 1)
|
|
58
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
59
|
+
Shoryuken.register_worker(queue_name, Shoryuken::ActiveJob::JobWrapper)
|
|
60
|
+
|
|
61
|
+
CurrentA.a_val = 'a-value'
|
|
62
|
+
CurrentB.b_val = 'b-value'
|
|
63
|
+
CurrentC.c_val = 'c-value'
|
|
64
|
+
|
|
65
|
+
IncrementalCurrentJob.perform_later
|
|
66
|
+
|
|
67
|
+
CurrentA.reset
|
|
68
|
+
CurrentB.reset
|
|
69
|
+
CurrentC.reset
|
|
70
|
+
|
|
71
|
+
poll_queues_until(timeout: 30) { DT[:executions].size >= 1 }
|
|
72
|
+
|
|
73
|
+
result = DT[:executions].first
|
|
74
|
+
assert_equal('a-value', result[:a_val])
|
|
75
|
+
assert_equal('b-value', result[:b_val], 'the second incrementally-persisted class must not be dropped')
|
|
76
|
+
assert_equal('c-value', result[:c_val])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
# This spec tests opting out of the automatic FIFO message_deduplication_id that
|
|
6
|
+
# the ActiveJob adapter generates.
|
|
7
|
+
#
|
|
8
|
+
# For FIFO queues the adapter derives a content-based message_deduplication_id
|
|
9
|
+
# from the serialized job minus job_id/enqueued_at (see issues #457 / #750). That
|
|
10
|
+
# means two *distinct* enqueues of the same job class + args within SQS's 5-minute
|
|
11
|
+
# window collapse into one - the second is silently dropped. That is intentional,
|
|
12
|
+
# but it is a "skipped message" trap for users who legitimately enqueue the same
|
|
13
|
+
# work twice.
|
|
14
|
+
#
|
|
15
|
+
# Expected behavior: setting Shoryuken.active_job_fifo_message_deduplication = false
|
|
16
|
+
# stops the adapter from generating the id, so those enqueues are no longer
|
|
17
|
+
# silently deduplicated. The default (true) preserves the existing behavior.
|
|
18
|
+
#
|
|
19
|
+
# Regression: there was no way to disable the auto-generated deduplication id.
|
|
20
|
+
|
|
21
|
+
setup_active_job
|
|
22
|
+
|
|
23
|
+
class FifoOptOutJob < ActiveJob::Base
|
|
24
|
+
queue_as :fifo_opt_out
|
|
25
|
+
|
|
26
|
+
def perform(_arg); end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
fifo_queue_mock = Object.new
|
|
30
|
+
fifo_queue_mock.define_singleton_method(:fifo?) { true }
|
|
31
|
+
fifo_queue_mock.define_singleton_method(:name) { 'fifo_opt_out.fifo' }
|
|
32
|
+
|
|
33
|
+
captured = nil
|
|
34
|
+
fifo_queue_mock.define_singleton_method(:send_message) { |params| captured = params }
|
|
35
|
+
|
|
36
|
+
Shoryuken::Client.define_singleton_method(:queues) { |_queue_name = nil| fifo_queue_mock }
|
|
37
|
+
Shoryuken.define_singleton_method(:register_worker) { |*| nil }
|
|
38
|
+
|
|
39
|
+
# Default behavior: the adapter sets a content-based dedup id (excluding the
|
|
40
|
+
# per-enqueue job_id/enqueued_at), so two identical jobs would collapse to one.
|
|
41
|
+
FifoOptOutJob.perform_later('same-args')
|
|
42
|
+
|
|
43
|
+
assert(
|
|
44
|
+
captured.key?(:message_deduplication_id),
|
|
45
|
+
'by default a FIFO ActiveJob send gets an auto-generated message_deduplication_id'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
body = captured[:message_body]
|
|
49
|
+
expected = Digest::SHA256.hexdigest(JSON.dump(body.except('job_id', 'enqueued_at')))
|
|
50
|
+
assert_equal(expected, captured[:message_deduplication_id])
|
|
51
|
+
|
|
52
|
+
# Opt-out: with deduplication disabled the adapter no longer sets a dedup id, so
|
|
53
|
+
# distinct enqueues of identical jobs are not silently dropped.
|
|
54
|
+
Shoryuken.active_job_fifo_message_deduplication = false
|
|
55
|
+
captured = nil
|
|
56
|
+
FifoOptOutJob.perform_later('same-args')
|
|
57
|
+
|
|
58
|
+
refute(
|
|
59
|
+
captured.key?(:message_deduplication_id),
|
|
60
|
+
'with deduplication disabled the adapter must not set message_deduplication_id'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# An explicit deduplication id must still be honored even when auto-generation is off.
|
|
64
|
+
captured = nil
|
|
65
|
+
FifoOptOutJob.set(message_deduplication_id: 'explicit-id').perform_later('same-args')
|
|
66
|
+
|
|
67
|
+
assert_equal('explicit-id', captured[:message_deduplication_id])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Full round-trip ActiveJob integration test
|
|
4
|
-
# Enqueues a job via ActiveJob → sends to
|
|
4
|
+
# Enqueues a job via ActiveJob → sends to ElasticMQ SQS → processes via Shoryuken → verifies execution
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
setup_sqs
|
|
7
7
|
setup_active_job
|
|
8
8
|
|
|
9
9
|
queue_name = DT.queue
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This spec tests that auto_visibility_timeout still processes messages when the
|
|
4
|
+
# queue's visibility timeout is short (<= EXTEND_UPFRONT_SECONDS).
|
|
5
|
+
#
|
|
6
|
+
# AutoExtendVisibility schedules a TimerTask at execution_interval =
|
|
7
|
+
# visibility_timeout - EXTEND_UPFRONT_SECONDS (5s). When the queue's visibility
|
|
8
|
+
# timeout is <= 5s that interval is <= 0, and TimerTask#initialize raises
|
|
9
|
+
# ArgumentError *before* the worker runs - so every receive fails and the
|
|
10
|
+
# message is reprocessed (and re-fails) until it hits a DLQ.
|
|
11
|
+
#
|
|
12
|
+
# Expected behavior: a short visibility timeout no longer breaks processing; the
|
|
13
|
+
# extension interval is clamped to a positive value and the worker runs normally.
|
|
14
|
+
#
|
|
15
|
+
# Regression: auto_visibility_timeout + a <= 5s queue visibility timeout broke
|
|
16
|
+
# processing entirely.
|
|
17
|
+
|
|
18
|
+
require 'timeout'
|
|
19
|
+
|
|
20
|
+
setup_sqs
|
|
21
|
+
|
|
22
|
+
DT.clear
|
|
23
|
+
|
|
24
|
+
queue_name = DT.queue
|
|
25
|
+
# 5s visibility timeout -> interval would be 5 - 5 = 0 -> TimerTask raised pre-fix.
|
|
26
|
+
create_test_queue(queue_name, attributes: { 'VisibilityTimeout' => '5' })
|
|
27
|
+
Shoryuken.add_group('default', 1)
|
|
28
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
29
|
+
|
|
30
|
+
short_vt_worker = Class.new do
|
|
31
|
+
include Shoryuken::Worker
|
|
32
|
+
|
|
33
|
+
shoryuken_options auto_delete: true, auto_visibility_timeout: true
|
|
34
|
+
|
|
35
|
+
def perform(_sqs_msg, body)
|
|
36
|
+
DT[:processed] << body
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
short_vt_worker.get_shoryuken_options['queue'] = queue_name
|
|
41
|
+
Shoryuken.register_worker(queue_name, short_vt_worker)
|
|
42
|
+
|
|
43
|
+
queue_url = Shoryuken::Client.sqs.get_queue_url(queue_name: queue_name).queue_url
|
|
44
|
+
Shoryuken::Client.sqs.send_message(queue_url: queue_url, message_body: 'hello')
|
|
45
|
+
|
|
46
|
+
poll_queues_until(timeout: 20) { DT[:processed].size >= 1 }
|
|
47
|
+
|
|
48
|
+
assert_equal(
|
|
49
|
+
['hello'],
|
|
50
|
+
DT[:processed].to_a,
|
|
51
|
+
'auto_visibility_timeout must not break processing on a queue with a short visibility timeout'
|
|
52
|
+
)
|
|
@@ -10,7 +10,7 @@ Shoryuken.sqs_client = nil
|
|
|
10
10
|
# Configure AWS options programmatically (simulating PORO setup with config file)
|
|
11
11
|
Shoryuken.options[:aws] = {
|
|
12
12
|
region: 'us-east-1',
|
|
13
|
-
endpoint: 'http://localhost:
|
|
13
|
+
endpoint: 'http://localhost:9324',
|
|
14
14
|
access_key_id: 'test-key-from-config',
|
|
15
15
|
secret_access_key: 'test-secret-from-config'
|
|
16
16
|
}
|
|
@@ -22,7 +22,7 @@ client = Shoryuken.sqs_client
|
|
|
22
22
|
config = client.config
|
|
23
23
|
|
|
24
24
|
assert_equal('us-east-1', config.region, "Region should be set from options[:aws]")
|
|
25
|
-
assert_equal('http://localhost:
|
|
25
|
+
assert_equal('http://localhost:9324', config.endpoint.to_s, "Endpoint should be set from options[:aws]")
|
|
26
26
|
|
|
27
27
|
# Verify the client actually works by creating a queue
|
|
28
28
|
queue_name = "aws-config-test-#{SecureRandom.hex(6)}"
|
|
@@ -49,7 +49,7 @@ assert_equal('us-west-2', client2.config.region, "New client should use updated
|
|
|
49
49
|
Shoryuken.sqs_client = nil
|
|
50
50
|
Shoryuken.options[:aws] = {
|
|
51
51
|
region: 'us-east-1',
|
|
52
|
-
endpoint: 'http://localhost:
|
|
52
|
+
endpoint: 'http://localhost:9324',
|
|
53
53
|
access_key_id: 'another-key',
|
|
54
54
|
secret_access_key: 'another-secret'
|
|
55
55
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This spec tests that the manager's busy-processor accounting stays correct
|
|
4
|
+
# when processor completion handling itself raises.
|
|
5
|
+
#
|
|
6
|
+
# Manager#processor_done makes SQS calls (Shoryuken::Client.queues, #fifo?)
|
|
7
|
+
# and invokes the polling strategy's message_processed callback (a documented
|
|
8
|
+
# extension point) - any of these can raise, e.g. on a transient network
|
|
9
|
+
# error or a bug in a custom strategy.
|
|
10
|
+
#
|
|
11
|
+
# Expected behavior: processor_done runs exactly once per processed message
|
|
12
|
+
# and busy_processors never goes negative, even when processor_done raises.
|
|
13
|
+
#
|
|
14
|
+
# Regression: Manager#assign chained `.then { processor_done }` with
|
|
15
|
+
# `.rescue { processor_done }`, so an exception inside processor_done
|
|
16
|
+
# rejected the then-promise and ran processor_done a SECOND time. The busy
|
|
17
|
+
# counter was decremented twice for one message and drifted negative,
|
|
18
|
+
# inflating `ready` and silently breaking the concurrency limit for the
|
|
19
|
+
# life of the process.
|
|
20
|
+
|
|
21
|
+
require 'timeout'
|
|
22
|
+
|
|
23
|
+
setup_sqs
|
|
24
|
+
|
|
25
|
+
DT.clear
|
|
26
|
+
|
|
27
|
+
queue_name = "#{DT.uuid}.fifo"
|
|
28
|
+
create_fifo_queue(queue_name)
|
|
29
|
+
|
|
30
|
+
# A custom polling strategy whose message_processed callback raises, like a
|
|
31
|
+
# buggy user strategy or one that performs I/O and hits a transient error.
|
|
32
|
+
# Manager#processor_done invokes it for FIFO queues.
|
|
33
|
+
class FlakyCallbackStrategy < Shoryuken::Polling::WeightedRoundRobin
|
|
34
|
+
def message_processed(queue)
|
|
35
|
+
DT[:message_processed_calls] << { queue: queue, time: Time.now }
|
|
36
|
+
raise 'flaky message_processed callback'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Shoryuken.add_group('default', 2, polling_strategy: FlakyCallbackStrategy)
|
|
41
|
+
Shoryuken.add_queue(queue_name, 1, 'default')
|
|
42
|
+
|
|
43
|
+
accounting_worker = Class.new do
|
|
44
|
+
include Shoryuken::Worker
|
|
45
|
+
|
|
46
|
+
shoryuken_options auto_delete: true
|
|
47
|
+
|
|
48
|
+
def perform(_sqs_msg, body)
|
|
49
|
+
DT[:processed] << body
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
accounting_worker.get_shoryuken_options['queue'] = queue_name
|
|
54
|
+
Shoryuken.register_worker(queue_name, accounting_worker)
|
|
55
|
+
|
|
56
|
+
# busy_processors is exposed through the public utilization_update event
|
|
57
|
+
Shoryuken.on(:utilization_update) do |opts|
|
|
58
|
+
DT[:utilization] << opts.dup
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
launcher = Shoryuken::Launcher.new
|
|
62
|
+
launcher.start
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
Shoryuken::Client.queues(queue_name).send_message(message_body: 'hello')
|
|
66
|
+
|
|
67
|
+
# Wait for the message to be processed and the completion path to run
|
|
68
|
+
Timeout.timeout(15) { sleep 0.5 until DT[:message_processed_calls].size >= 1 }
|
|
69
|
+
|
|
70
|
+
# Give the (buggy) rescue path time to run processor_done a second time
|
|
71
|
+
sleep 3
|
|
72
|
+
|
|
73
|
+
assert_equal(['hello'], DT[:processed].to_a, 'Message should have been processed exactly once')
|
|
74
|
+
|
|
75
|
+
assert_equal(
|
|
76
|
+
1,
|
|
77
|
+
DT[:message_processed_calls].size,
|
|
78
|
+
'processor_done must run exactly once per message, even when it raises ' \
|
|
79
|
+
"(message_processed was called #{DT[:message_processed_calls].size} times)"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
negative = DT[:utilization].select { |u| u[:busy_processors].negative? }
|
|
83
|
+
assert(
|
|
84
|
+
negative.empty?,
|
|
85
|
+
'busy_processors must never go negative; a double decrement inflates ready ' \
|
|
86
|
+
"and breaks the concurrency limit (saw: #{negative.first.inspect})"
|
|
87
|
+
)
|
|
88
|
+
ensure
|
|
89
|
+
begin
|
|
90
|
+
Timeout.timeout(10) { launcher.stop }
|
|
91
|
+
rescue Timeout::Error
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This spec tests that a custom polling strategy can be configured per-group
|
|
4
|
+
# via the add_group API.
|
|
5
|
+
#
|
|
6
|
+
# Bug #925: Cannot configure custom polling_strategy from YAML config
|
|
7
|
+
# The add_group method does not accept a polling_strategy parameter,
|
|
8
|
+
# making it impossible to set a per-group polling strategy programmatically.
|
|
9
|
+
# Additionally, polling_strategy() reads from options[:groups] (raw config)
|
|
10
|
+
# rather than from groups (the processed hash populated by add_group).
|
|
11
|
+
|
|
12
|
+
# Define a custom polling strategy
|
|
13
|
+
class CustomRoundRobin < Shoryuken::Polling::BaseStrategy
|
|
14
|
+
def initialize(queues, delay = nil)
|
|
15
|
+
@queues = queues.dup.uniq
|
|
16
|
+
@delay = delay
|
|
17
|
+
@index = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def next_queue
|
|
21
|
+
return nil if @queues.empty?
|
|
22
|
+
|
|
23
|
+
queue = @queues[@index % @queues.length]
|
|
24
|
+
@index += 1
|
|
25
|
+
Shoryuken::Polling::QueueConfiguration.new(queue, {})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def messages_found(_queue, _count)
|
|
29
|
+
# noop
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def active_queues
|
|
33
|
+
@queues.map { |q| [q, 1] }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ---- Part 1: API assertions (no SQS needed) ----
|
|
38
|
+
# add_group accepts polling_strategy: keyword argument
|
|
39
|
+
Shoryuken.add_group('custom_group', 1, polling_strategy: CustomRoundRobin)
|
|
40
|
+
|
|
41
|
+
# polling_strategy() returns our custom strategy for the group
|
|
42
|
+
strategy = Shoryuken.polling_strategy('custom_group')
|
|
43
|
+
assert_equal(
|
|
44
|
+
CustomRoundRobin,
|
|
45
|
+
strategy,
|
|
46
|
+
"Expected polling_strategy('custom_group') to return CustomRoundRobin, got #{strategy.inspect}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# add_group raises InvalidPollingStrategyError for invalid types
|
|
50
|
+
begin
|
|
51
|
+
Shoryuken.add_group('bad_group', 1, polling_strategy: 123)
|
|
52
|
+
raise 'Expected InvalidPollingStrategyError to be raised'
|
|
53
|
+
rescue Shoryuken::Errors::InvalidPollingStrategyError
|
|
54
|
+
# expected
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ---- Part 2: End-to-end with SQS ----
|
|
58
|
+
setup_sqs
|
|
59
|
+
|
|
60
|
+
queue_name = DT.queues[0]
|
|
61
|
+
create_test_queue(queue_name)
|
|
62
|
+
|
|
63
|
+
Shoryuken.groups.clear
|
|
64
|
+
Shoryuken.add_group('custom_group', 1, polling_strategy: CustomRoundRobin)
|
|
65
|
+
Shoryuken.add_queue(queue_name, 1, 'custom_group')
|
|
66
|
+
|
|
67
|
+
worker_class = Class.new do
|
|
68
|
+
include Shoryuken::Worker
|
|
69
|
+
|
|
70
|
+
shoryuken_options auto_delete: true, batch: false
|
|
71
|
+
|
|
72
|
+
def perform(_sqs_msg, body)
|
|
73
|
+
DT[:processed] << body
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
worker_class.get_shoryuken_options['queue'] = queue_name
|
|
78
|
+
Shoryuken.register_worker(queue_name, worker_class)
|
|
79
|
+
|
|
80
|
+
Shoryuken::Client.queues(queue_name).send_message(message_body: 'custom-strategy-msg')
|
|
81
|
+
|
|
82
|
+
sleep 1
|
|
83
|
+
|
|
84
|
+
poll_queues_until { DT[:processed].size >= 1 }
|
|
85
|
+
|
|
86
|
+
assert_equal(1, DT[:processed].size)
|
|
87
|
+
assert_equal('custom-strategy-msg', DT[:processed].first)
|