shoryuken 7.0.2 → 7.0.4
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 +32 -5
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.ruby-version +1 -1
- data/.yard-lint.yml +29 -1
- data/CHANGELOG.md +171 -0
- data/Gemfile.lint.lock +8 -7
- data/bin/cli/sqs.rb +67 -3
- data/bin/integrations +52 -34
- data/lib/active_job/extensions.rb +5 -8
- data/lib/active_job/queue_adapters/shoryuken_adapter.rb +6 -5
- data/lib/shoryuken/active_job/current_attributes.rb +33 -7
- data/lib/shoryuken/body_parser.rb +1 -2
- data/lib/shoryuken/client.rb +15 -6
- data/lib/shoryuken/default_exception_handler.rb +2 -3
- data/lib/shoryuken/errors.rb +2 -4
- data/lib/shoryuken/fetcher.rb +7 -1
- data/lib/shoryuken/helpers/atomic_counter.rb +6 -9
- data/lib/shoryuken/helpers/atomic_hash.rb +9 -15
- data/lib/shoryuken/helpers/hash_utils.rb +5 -7
- data/lib/shoryuken/helpers/string_utils.rb +6 -8
- data/lib/shoryuken/helpers/timer_task.rb +19 -2
- data/lib/shoryuken/inline_message.rb +3 -4
- data/lib/shoryuken/launcher.rb +19 -8
- data/lib/shoryuken/manager.rb +65 -6
- data/lib/shoryuken/message.rb +2 -3
- data/lib/shoryuken/middleware/chain.rb +7 -13
- data/lib/shoryuken/middleware/entry.rb +1 -2
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +30 -2
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +40 -18
- data/lib/shoryuken/middleware/server/non_retryable_exception.rb +17 -8
- data/lib/shoryuken/middleware/server/timing.rb +2 -3
- data/lib/shoryuken/options.rb +37 -4
- data/lib/shoryuken/polling/base_strategy.rb +16 -23
- data/lib/shoryuken/polling/queue_configuration.rb +8 -12
- data/lib/shoryuken/polling/strict_priority.rb +26 -14
- data/lib/shoryuken/polling/weighted_round_robin.rb +40 -27
- data/lib/shoryuken/queue.rb +41 -4
- data/lib/shoryuken/util.rb +4 -1
- data/lib/shoryuken/version.rb +1 -1
- data/lib/shoryuken/worker/default_executor.rb +11 -5
- data/lib/shoryuken/worker/inline_executor.rb +10 -5
- data/lib/shoryuken/worker.rb +15 -13
- data/lib/shoryuken.rb +6 -3
- data/renovate.json +16 -2
- data/shoryuken.gemspec +9 -0
- data/spec/integration/active_job/current_attributes/cross_job_reset_spec.rb +47 -0
- data/spec/integration/active_job/current_attributes/incremental_persist_spec.rb +76 -0
- data/spec/integration/active_job/fifo_dedup_opt_out/fifo_dedup_opt_out_spec.rb +67 -0
- data/spec/integration/auto_extend_visibility/short_visibility_timeout_spec.rb +52 -0
- data/spec/integration/cli/find_all_spec.rb +120 -0
- data/spec/integration/concurrent_processing/processor_accounting_spec.rb +94 -0
- data/spec/integration/fifo_ordering/fifo_max_messages_cap_spec.rb +96 -0
- 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/message_operations/partial_batch_delete_spec.rb +67 -0
- data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +1 -1
- data/spec/integration/non_retryable_exception/with_retry_intervals_spec.rb +115 -0
- data/spec/integrations_helper.rb +10 -9
- data/spec/lib/shoryuken/client_spec.rb +62 -0
- 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 +147 -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 +88 -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 +123 -0
- data/spec/lib/shoryuken/util_spec.rb +26 -0
- data/spec/lib/shoryuken/worker/default_executor_spec.rb +13 -0
- data/spec/lib/shoryuken/worker/inline_executor_spec.rb +12 -0
- data/spec/shared_examples_for_active_job.rb +18 -0
- data/spec/spec_helper.rb +40 -20
- metadata +35 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b332f880ab52b8380e13758f98bbca5d35a5a21f693753d536564c2dc760eadb
|
|
4
|
+
data.tar.gz: 9660f222a93c1fc517cc7f65bf029ee6c31e9a6b5526d67af917a91e64768ca4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce8200034dfe12e010bbb928f73d7ae35f2abfda4d85235cc7af189d26306d254bfd84e6956dace838ba0a222768628cf2c4785f657f8c23cd07ddc47219064a
|
|
7
|
+
data.tar.gz: c7a947da8aa8dadd9f79bedd89d02a67cfd715e715294d5e38a396cbb7070e5a06b4aa4055e347493eaa9373242593a5eacaed27ebb7528f16b21d8007a44825
|
data/.github/workflows/push.yml
CHANGED
|
@@ -19,12 +19,12 @@ jobs:
|
|
|
19
19
|
id-token: write
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
23
23
|
with:
|
|
24
24
|
fetch-depth: 0
|
|
25
25
|
|
|
26
26
|
- name: Set up Ruby
|
|
27
|
-
uses: ruby/setup-ruby@
|
|
27
|
+
uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
|
|
28
28
|
with:
|
|
29
29
|
bundler-cache: false
|
|
30
30
|
|
|
@@ -33,4 +33,4 @@ jobs:
|
|
|
33
33
|
bundle install --jobs 4 --retry 3
|
|
34
34
|
|
|
35
35
|
# Release
|
|
36
|
-
- uses: rubygems/release-gem@
|
|
36
|
+
- uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1
|
data/.github/workflows/specs.yml
CHANGED
|
@@ -11,17 +11,41 @@ jobs:
|
|
|
11
11
|
matrix:
|
|
12
12
|
ruby: ['3.2', '3.3', '3.4', '4.0']
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
|
+
# Unit specs finish in under a minute; fail fast instead of letting a
|
|
15
|
+
# wedged runner occupy the job slot for the 6-hour default
|
|
16
|
+
timeout-minutes: 10
|
|
14
17
|
steps:
|
|
15
18
|
- name: Checkout code
|
|
16
|
-
uses: actions/checkout@
|
|
19
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
17
20
|
|
|
18
|
-
- uses: ruby/setup-ruby@
|
|
21
|
+
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
|
|
19
22
|
with:
|
|
20
23
|
ruby-version: ${{ matrix.ruby }}
|
|
21
24
|
bundler-cache: true
|
|
22
25
|
|
|
23
26
|
- name: Run specs
|
|
24
|
-
run:
|
|
27
|
+
run: |
|
|
28
|
+
# Run rspec directly (not via rake) so $SPEC_PID is the ruby process
|
|
29
|
+
# itself — the USR1 watchdog below sends the signal to that pid, and
|
|
30
|
+
# spec_helper registers a USR1 handler that prints a thread dump.
|
|
31
|
+
# Running via 'bundle exec rake spec' would send the signal to rake,
|
|
32
|
+
# which has no USR1 handler and would crash before the dump appears.
|
|
33
|
+
bundle exec rspec --format documentation &
|
|
34
|
+
SPEC_PID=$!
|
|
35
|
+
# Send USR1 after 8 minutes so the thread-dump handler in spec_helper
|
|
36
|
+
# prints a backtrace before the 10-minute job timeout kills everything.
|
|
37
|
+
( sleep 480
|
|
38
|
+
if kill -0 $SPEC_PID 2>/dev/null; then
|
|
39
|
+
echo "=== specs still running after 8 minutes — requesting thread dump ==="
|
|
40
|
+
kill -USR1 $SPEC_PID 2>/dev/null || true
|
|
41
|
+
sleep 5
|
|
42
|
+
fi
|
|
43
|
+
) &
|
|
44
|
+
WATCHDOG_PID=$!
|
|
45
|
+
wait $SPEC_PID
|
|
46
|
+
EXIT_CODE=$?
|
|
47
|
+
kill $WATCHDOG_PID 2>/dev/null || true
|
|
48
|
+
exit $EXIT_CODE
|
|
25
49
|
|
|
26
50
|
integrations:
|
|
27
51
|
name: Integrations
|
|
@@ -29,9 +53,11 @@ jobs:
|
|
|
29
53
|
matrix:
|
|
30
54
|
ruby: ['3.2', '3.3', '3.4', '4.0']
|
|
31
55
|
runs-on: ubuntu-latest
|
|
56
|
+
# Integration specs normally take ~4 minutes
|
|
57
|
+
timeout-minutes: 15
|
|
32
58
|
steps:
|
|
33
59
|
- name: Checkout code
|
|
34
|
-
uses: actions/checkout@
|
|
60
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
35
61
|
|
|
36
62
|
- name: Start ElasticMQ
|
|
37
63
|
run: docker compose up -d
|
|
@@ -45,7 +71,7 @@ jobs:
|
|
|
45
71
|
done
|
|
46
72
|
'
|
|
47
73
|
|
|
48
|
-
- uses: ruby/setup-ruby@
|
|
74
|
+
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
|
|
49
75
|
with:
|
|
50
76
|
ruby-version: ${{ matrix.ruby }}
|
|
51
77
|
bundler-cache: true
|
|
@@ -56,6 +82,7 @@ jobs:
|
|
|
56
82
|
ci-success:
|
|
57
83
|
name: CI Success
|
|
58
84
|
runs-on: ubuntu-latest
|
|
85
|
+
timeout-minutes: 5
|
|
59
86
|
if: always()
|
|
60
87
|
needs:
|
|
61
88
|
- specs
|
|
@@ -7,7 +7,7 @@ jobs:
|
|
|
7
7
|
verify:
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
steps:
|
|
10
|
-
- uses: actions/checkout@
|
|
10
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
11
11
|
- name: Check SHA pins
|
|
12
12
|
run: |
|
|
13
13
|
if grep -E -r "uses: .*/.*@(v[0-9]+|main|master)($|[[:space:]]|$)" --include="*.yml" --include="*.yaml" .github/workflows/ | grep -v "#"; then
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.7
|
data/.yard-lint.yml
CHANGED
|
@@ -66,6 +66,34 @@ Documentation/BlankLineBeforeDefinition:
|
|
|
66
66
|
SingleBlankLine: true
|
|
67
67
|
OrphanedDocs: true
|
|
68
68
|
|
|
69
|
+
Documentation/DuplicateNamespaceComment:
|
|
70
|
+
Description: Detects namespaces documented with a YARD comment in more than one file.
|
|
71
|
+
Enabled: true
|
|
72
|
+
Severity: error
|
|
73
|
+
|
|
74
|
+
Documentation/UnderfilledLines:
|
|
75
|
+
Description: Detects documentation prose that wraps too early and wastes horizontal space.
|
|
76
|
+
Enabled: true
|
|
77
|
+
Severity: error
|
|
78
|
+
# Aligned with RuboCop's Layout/LineLength so documentation prose wraps to the same
|
|
79
|
+
# width as code.
|
|
80
|
+
MaxLength: 125
|
|
81
|
+
|
|
82
|
+
Documentation/LineLength:
|
|
83
|
+
Description: Detects documentation lines that exceed the maximum length.
|
|
84
|
+
Enabled: true
|
|
85
|
+
Severity: error
|
|
86
|
+
# Aligned with RuboCop's Layout/LineLength.
|
|
87
|
+
MaxLength: 125
|
|
88
|
+
|
|
89
|
+
Documentation/TextSubstitution:
|
|
90
|
+
Description: Detects em/en-dashes in documentation and replaces them with hyphens.
|
|
91
|
+
Enabled: true
|
|
92
|
+
Severity: error
|
|
93
|
+
Substitutions:
|
|
94
|
+
"—": "-" # em-dash (U+2014)
|
|
95
|
+
"–": "-" # en-dash (U+2013)
|
|
96
|
+
|
|
69
97
|
# Tags validators
|
|
70
98
|
Tags/Order:
|
|
71
99
|
Description: Enforces consistent ordering of YARD tags.
|
|
@@ -146,7 +174,7 @@ Tags/OptionTags:
|
|
|
146
174
|
Tags/ExampleSyntax:
|
|
147
175
|
Description: Validates Ruby syntax in @example tags.
|
|
148
176
|
Enabled: true
|
|
149
|
-
Severity:
|
|
177
|
+
Severity: error
|
|
150
178
|
|
|
151
179
|
Tags/RedundantParamDescription:
|
|
152
180
|
Description: Detects meaningless parameter descriptions that add no value.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
+
## [7.0.4] - 2026-09-23
|
|
2
|
+
|
|
3
|
+
- Fix: `shoryuken sqs dump`/`mv` no longer stop early on real SQS (mensfeld)
|
|
4
|
+
- `find_all` used short polling and broke on the first empty `receive_message` response. Real
|
|
5
|
+
(distributed) SQS routinely returns an empty batch while the queue still has messages, so dump/mv
|
|
6
|
+
quietly processed only a fraction of the queue (invisible on single-node ElasticMQ/LocalStack)
|
|
7
|
+
- It now long-polls and only stops after several consecutive empty batches, so the queue is actually drained
|
|
8
|
+
- Because a longer drain can outlast the queue's visibility timeout (dump/mv delete only after `find_all`
|
|
9
|
+
returns), re-read messages are now deduplicated by message id, so they are not dumped/moved or counted
|
|
10
|
+
twice, and the newest receipt handle is kept so the deferred delete still succeeds
|
|
11
|
+
- Deletion stays deferred (a failed dump/mv deletes nothing), so a queue larger than SQS's in-flight limit
|
|
12
|
+
(~120k standard / ~20k FIFO) drains over multiple runs rather than one - no message is lost
|
|
13
|
+
|
|
14
|
+
- Fix: Graceful stop no longer deadlocks when a manager's dispatch loop never started (mensfeld)
|
|
15
|
+
- `await_dispatching_in_progress` blocks on a `Queue` that is only closed from inside `dispatch_loop` when it
|
|
16
|
+
observes the stop flag. If the loop never runs - a graceful stop arriving before the start `Future` is
|
|
17
|
+
scheduled, or an embedded host whose executor pool is saturated so `manager.start` never runs - the signal
|
|
18
|
+
was never closed and `Launcher#stop` deadlocked
|
|
19
|
+
- `stop_new_dispatching` now closes the release signal itself when the loop hasn't started (tracked via a new
|
|
20
|
+
flag); `Queue#close` is idempotent, so a loop that does start later and closes it again is harmless
|
|
21
|
+
|
|
22
|
+
- Feature: `Shoryuken.fifo_message_deduplication` to opt out of content-based dedup id generation for raw sends (mensfeld)
|
|
23
|
+
- `Queue#add_fifo_attributes!` always set `message_deduplication_id` to a SHA256 of the body when none was
|
|
24
|
+
given, so two raw sends of an identical body within SQS's 5-minute window (`Worker.perform_async`,
|
|
25
|
+
`Queue#send_message`/`#send_messages`) were silently deduplicated - the second dropped. ActiveJob got an
|
|
26
|
+
opt-out in #1017 but the raw path had none
|
|
27
|
+
- Set `Shoryuken.fifo_message_deduplication = false` to stop auto-generating that id, so identical bodies are
|
|
28
|
+
no longer silently dropped (provide a `message_deduplication_id` yourself or enable
|
|
29
|
+
`ContentBasedDeduplication` on the queue)
|
|
30
|
+
- Defaults to `true`, preserving the existing behavior; an explicit `message_deduplication_id` is still honored
|
|
31
|
+
|
|
32
|
+
- Fix: Harden `ExponentialBackoffRetry` visibility handling (mensfeld)
|
|
33
|
+
- `next_visibility_timeout` could return a negative value when a job ran past the 12h SQS ceiling (its
|
|
34
|
+
`max_timeout` goes below zero), and `change_message_visibility` rejects a negative timeout; it now clamps
|
|
35
|
+
to 0 so the message is retried as soon as possible
|
|
36
|
+
- `handle_failure` called `change_visibility` unguarded, so a failure there (e.g. an expired receipt handle)
|
|
37
|
+
escaped the `rescue` in `#call` and masked the original worker error from exception handlers/notifiers; it
|
|
38
|
+
now rescues, logs, and reports "not retried" so the original error is re-raised and the message falls back
|
|
39
|
+
to the queue's default visibility timeout
|
|
40
|
+
- The failure `rescue` in `#call` is now scoped to the single-message `yield` only, so a batch worker's error
|
|
41
|
+
no longer routes the message Array into `handle_failure` (raising a `NoMethodError` that masked the original)
|
|
42
|
+
|
|
43
|
+
- Fix: `perform_async` no longer mutates the caller-supplied options hash (mensfeld)
|
|
44
|
+
- `DefaultExecutor#perform_async` and `InlineExecutor#perform_async` deleted `:queue`, injected
|
|
45
|
+
`:message_body`, and wrote `shoryuken_class` into the nested `:message_attributes` in place, so a caller
|
|
46
|
+
reusing one options hash across enqueues had `:queue` stripped after the first call - silently routing later
|
|
47
|
+
jobs to the worker's default queue
|
|
48
|
+
- Both now operate on a `dup` and rebuild `:message_attributes` with `merge`, leaving the caller's hash untouched
|
|
49
|
+
|
|
50
|
+
- Fix: `Shoryuken::Client.queues` no longer builds the same queue more than once under concurrency (mensfeld)
|
|
51
|
+
- The cache used an unsynchronized `@@queues[name] ||= Shoryuken::Queue.new(...)`. Building a queue makes
|
|
52
|
+
SQS API calls, and that I/O releases the GVL, so concurrent first-access (dispatch, processor-completion
|
|
53
|
+
and worker threads all call it) built the queue multiple times - redundant API calls, and a corrupt cache
|
|
54
|
+
on JRuby/TruffleRuby
|
|
55
|
+
- Access to the cache is now guarded by a mutex
|
|
56
|
+
|
|
57
|
+
- Fix: Busy-processor counter no longer leaks when the executor rejects a worker post (mensfeld)
|
|
58
|
+
- `Manager#assign` increments `@busy_processors` before posting the worker `Concurrent::Promise`, but the
|
|
59
|
+
matching decrement (`processor_done`) runs inside the promise body. When the post is rejected with
|
|
60
|
+
`Concurrent::RejectedExecutionError` - a hard stop racing the `running?` check, or a saturated bounded custom
|
|
61
|
+
`launcher_executor` - the body never runs and the counter leaks
|
|
62
|
+
- With a bounded executor the leak is permanent: `ready` (`@max_processors - busy`) keeps shrinking until
|
|
63
|
+
dispatch stalls and the group silently stops processing
|
|
64
|
+
- The increment is now rolled back on `RejectedExecutionError` by decrementing directly (the message was never
|
|
65
|
+
processed, so the FIFO `message_processed` callback must not run) (#1029)
|
|
66
|
+
|
|
67
|
+
## [7.0.3] - 2026-07-10
|
|
68
|
+
|
|
69
|
+
- Feature: `Shoryuken.active_job_fifo_message_deduplication` to opt out of FIFO dedup id generation (mensfeld)
|
|
70
|
+
- For FIFO queues the ActiveJob adapter derives a content-based `message_deduplication_id` from the
|
|
71
|
+
serialized job minus `job_id`/`enqueued_at` (#457 / #750), so two distinct enqueues of the same job
|
|
72
|
+
class and arguments within SQS's 5-minute window silently collapse into one - a "skipped message" trap
|
|
73
|
+
- Set `Shoryuken.active_job_fifo_message_deduplication = false` to stop generating that id, so identical
|
|
74
|
+
jobs are no longer silently dropped (rely on the queue's content-based deduplication or explicit ids)
|
|
75
|
+
- Defaults to `true`, preserving the existing behavior; an explicit `message_deduplication_id` is still honored
|
|
76
|
+
|
|
77
|
+
- Fix: Polling strategies are now thread-safe, and WeightedRoundRobin unpauses processed queues reliably (mensfeld)
|
|
78
|
+
- `message_processed` runs on processor-completion threads (for FIFO queues) while `next_queue`/`messages_found`
|
|
79
|
+
run on the dispatch thread; they mutate the same state with no synchronization, which is benign on MRI (GVL)
|
|
80
|
+
but corrupts state on JRuby/TruffleRuby. Both `WeightedRoundRobin` and `StrictPriority` now serialize access
|
|
81
|
+
with a mutex
|
|
82
|
+
- `WeightedRoundRobin#unpause_queues` only checked the head of the paused list, so a queue marked ready by
|
|
83
|
+
`message_processed` could stay stuck behind an earlier-paused queue; it now unpauses the first expired entry
|
|
84
|
+
anywhere in the list
|
|
85
|
+
|
|
86
|
+
- Fix: `TimerTask#kill` no longer deadlocks on Ruby 3.2 under concurrent callers (mensfeld)
|
|
87
|
+
- `kill` called `@thread.kill` while holding `@mutex`; the timer loop's `ensure` block calls
|
|
88
|
+
`@mutex.synchronize` to clear `@running`, so on Ruby 3.2 (where `Thread#kill` yields the GVL
|
|
89
|
+
to the killed thread for cleanup before returning) both threads waited on each other forever
|
|
90
|
+
- The thread is now killed after the mutex is released, so the ensure block can always acquire it
|
|
91
|
+
|
|
92
|
+
- Fix: Stopping the launcher no longer destroys the process-global IO executor (mensfeld)
|
|
93
|
+
- With no `launcher_executor` configured, `Launcher#executor` fell back to `Concurrent.global_io_executor`,
|
|
94
|
+
and `Launcher#stop`/`#stop!` call `shutdown` (and `kill`) on it
|
|
95
|
+
- Shutting down that process-wide pool broke anything else relying on concurrent-ruby's `:io` pool
|
|
96
|
+
(including Shoryuken's own `ShoryukenConcurrentSendAdapter`) and prevented starting a fresh launcher
|
|
97
|
+
in the same process
|
|
98
|
+
- The launcher now owns a dedicated `Concurrent::CachedThreadPool`, so stopping it leaves the global
|
|
99
|
+
IO executor untouched
|
|
100
|
+
|
|
101
|
+
- Fix: `Queue#delete_messages` now logs every batch-delete failure and returns a robust boolean (mensfeld)
|
|
102
|
+
- It used `failed.any? { |f| logger.error ... }`, which short-circuited after the first failure - so when
|
|
103
|
+
a batch had multiple failures only the first was logged - and only returned truthy because `Logger#error`
|
|
104
|
+
happens to return true (a custom logger returning falsey would have hidden the failure from callers)
|
|
105
|
+
- It now logs each failure and returns `failed.any?`, so `NonRetryableException`/`AutoDelete` reliably see
|
|
106
|
+
that some messages may remain and need reprocessing
|
|
107
|
+
|
|
108
|
+
- Fix: Lifecycle events fired in reverse no longer alternate handler order (mensfeld)
|
|
109
|
+
- `Util#fire_event` reversed the stored handler array in place with `reverse!`, so an event fired more
|
|
110
|
+
than once with `reverse: true` (e.g. `:shutdown` via `stop` then `stop!`) flipped order each time
|
|
111
|
+
- It now reverses a copy, leaving the stored handler order untouched
|
|
112
|
+
|
|
113
|
+
- Fix: A fatal dispatch error no longer hard-kills an embedded host process (mensfeld)
|
|
114
|
+
- `Manager#handle_dispatch_error` sent `Process.kill('USR1', Process.pid)` unconditionally after a
|
|
115
|
+
dispatch error (e.g. SQS still failing once the fetcher exhausted its retries)
|
|
116
|
+
- The CLI Runner traps USR1 and turns it into a graceful shutdown, but a host embedding
|
|
117
|
+
`Shoryuken::Launcher` directly has USR1's default disposition, so the whole process - and its
|
|
118
|
+
in-flight workers - was terminated
|
|
119
|
+
- When embedded (no CLI Runner), the failing manager now just stops itself and `Launcher#healthy?`
|
|
120
|
+
reports the failure; the USR1 signal is only sent in server (CLI) mode, preserving the
|
|
121
|
+
supervisor-restart behavior there
|
|
122
|
+
|
|
123
|
+
- Fix: Graceful stop is now bounded by the configured timeout (mensfeld)
|
|
124
|
+
- `Launcher#stop` (the soft shutdown behind USR1/TSTP) called `executor.wait_for_termination` with no
|
|
125
|
+
argument - an unbounded wait - so a single hung worker blocked shutdown forever
|
|
126
|
+
- Both `Launcher#stop` and `Launcher#stop!` now share a `shutdown_executor` helper that waits up to
|
|
127
|
+
`Shoryuken.options[:timeout]` seconds for in-flight workers, then force-kills the executor so the
|
|
128
|
+
process can exit; the graceful stop still waits for workers, just no longer indefinitely
|
|
129
|
+
|
|
130
|
+
- Docs: Correct the `retry_intervals` exponential backoff documentation (mensfeld)
|
|
131
|
+
- The `exponential_backoff?` docstring claimed retries stop ("before giving up") after the last configured
|
|
132
|
+
interval. They do not: once the intervals are exhausted, the last interval is reused for every later
|
|
133
|
+
attempt, and SQS's redrive policy (maxReceiveCount) is what ultimately moves a message to a dead-letter queue
|
|
134
|
+
- Added a spec pinning that far-later attempts keep reusing the last interval
|
|
135
|
+
|
|
136
|
+
- Fix: `CurrentAttributes.persist` no longer drops a class when called once per class (mensfeld)
|
|
137
|
+
- The storage key was derived from the per-call index, so registering classes across separate `persist`
|
|
138
|
+
calls made the third call reuse `cattr_0` and silently overwrite the second class - its attributes were
|
|
139
|
+
then never serialized or restored
|
|
140
|
+
- The key now uses the running registry size, so incremental and single-call registration both yield
|
|
141
|
+
distinct, stable keys (single-call `persist(A, B, C)` keys are unchanged)
|
|
142
|
+
|
|
143
|
+
- Fix: Busy-processor accounting no longer breaks when processor completion raises (mensfeld)
|
|
144
|
+
- `Manager#assign` chained `.then { processor_done }.rescue { processor_done }`, so an exception inside
|
|
145
|
+
`processor_done` (SQS lookups or a polling strategy's `message_processed` callback) ran completion twice
|
|
146
|
+
- The busy counter was decremented twice for one message and drifted negative, inflating `ready` and
|
|
147
|
+
silently breaking the configured concurrency limit for the life of the process
|
|
148
|
+
- Completion now runs in an `ensure` around processing (exactly once), and `processor_done` logs
|
|
149
|
+
instead of leaking exceptions from the FIFO bookkeeping
|
|
150
|
+
|
|
151
|
+
- Fix: Repeated graceful stop no longer deadlocks the process (mensfeld)
|
|
152
|
+
- `Manager#await_dispatching_in_progress` popped a signal queue that received exactly one token,
|
|
153
|
+
so a second `Launcher#stop` blocked forever on an empty queue
|
|
154
|
+
- Operationally this was the TSTP -> USR1 sequence: both signals trigger a graceful stop, leaving
|
|
155
|
+
the process stuck in the signal loop where even TERM/INT were no longer handled (only SIGKILL worked)
|
|
156
|
+
- The dispatch loop now closes the signal queue instead of pushing a token, releasing every pending
|
|
157
|
+
and future waiter; the dispatch chain also releases waiters if the executor rejects a post during
|
|
158
|
+
a racing hard shutdown
|
|
159
|
+
|
|
160
|
+
- Fix: `non_retryable_exceptions` is no longer ignored when `retry_intervals` is also configured (mensfeld)
|
|
161
|
+
- `ExponentialBackoffRetry` swallowed every exception after scheduling a retry, so `NonRetryableException`
|
|
162
|
+
(which sits outside it in the default middleware chain) never saw non-retryable errors - poison messages
|
|
163
|
+
were retried indefinitely (with the last interval repeated) instead of being deleted immediately
|
|
164
|
+
- `ExponentialBackoffRetry` now re-raises exceptions classified as non-retryable so the message gets deleted
|
|
165
|
+
- Exception classification is extracted to `NonRetryableException.non_retryable?` and shared by both middlewares
|
|
166
|
+
|
|
167
|
+
- Enhancement: Use dynamic Ruby warning category opt-in in test helpers (mensfeld)
|
|
168
|
+
- Replace version-gated `Warning[:performance]` with `Warning.categories`-based auto-enablement
|
|
169
|
+
- Automatically enables all non-deprecated, non-experimental warning categories for forward compatibility
|
|
170
|
+
- Applied to both `spec/spec_helper.rb` and `spec/integrations_helper.rb`
|
|
171
|
+
|
|
1
172
|
## [7.0.2] - 2026-04-16
|
|
2
173
|
|
|
3
174
|
- Enhancement: Replace LocalStack with ElasticMQ for SQS integration tests (mensfeld)
|
data/Gemfile.lint.lock
CHANGED
|
@@ -31,11 +31,11 @@ GEM
|
|
|
31
31
|
unicode-display_width (3.2.0)
|
|
32
32
|
unicode-emoji (~> 4.1)
|
|
33
33
|
unicode-emoji (4.2.0)
|
|
34
|
-
yard (0.9.
|
|
35
|
-
yard-lint (1.
|
|
34
|
+
yard (0.9.45)
|
|
35
|
+
yard-lint (1.10.2)
|
|
36
36
|
yard (~> 0.9)
|
|
37
37
|
zeitwerk (~> 2.6)
|
|
38
|
-
zeitwerk (2.
|
|
38
|
+
zeitwerk (2.8.2)
|
|
39
39
|
|
|
40
40
|
PLATFORMS
|
|
41
41
|
ruby
|
|
@@ -47,6 +47,7 @@ DEPENDENCIES
|
|
|
47
47
|
|
|
48
48
|
CHECKSUMS
|
|
49
49
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
50
|
+
bundler (4.0.17) sha256=214e21431b5665dd2f99df8a5511c6b151d7a72e8015c8b38f8b775b61cbb6c1
|
|
50
51
|
json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
|
|
51
52
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
52
53
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
@@ -61,9 +62,9 @@ CHECKSUMS
|
|
|
61
62
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
62
63
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
63
64
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
64
|
-
yard (0.9.
|
|
65
|
-
yard-lint (1.
|
|
66
|
-
zeitwerk (2.
|
|
65
|
+
yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51
|
|
66
|
+
yard-lint (1.10.2) sha256=3e595709d088a8cc827e706a4146b447eb6a907ee375d06c11077369d00717f6
|
|
67
|
+
zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
|
|
67
68
|
|
|
68
69
|
BUNDLED WITH
|
|
69
|
-
4.0.
|
|
70
|
+
4.0.17
|
data/bin/cli/sqs.rb
CHANGED
|
@@ -12,6 +12,14 @@ module Shoryuken
|
|
|
12
12
|
# @see https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html
|
|
13
13
|
MAX_BATCH_SIZE = 1024 * 1024
|
|
14
14
|
|
|
15
|
+
# Long-poll wait (seconds) used when draining a queue in find_all, so a
|
|
16
|
+
# short-poll empty batch doesn't end dump/mv prematurely.
|
|
17
|
+
FIND_ALL_WAIT_SECONDS = 1
|
|
18
|
+
|
|
19
|
+
# Number of consecutive empty long-poll batches before find_all concludes
|
|
20
|
+
# the queue is drained.
|
|
21
|
+
FIND_ALL_MAX_EMPTY_BATCHES = 3
|
|
22
|
+
|
|
15
23
|
namespace :sqs
|
|
16
24
|
class_option :endpoint, aliases: '-e', type: :string, default: ENV['SHORYUKEN_SQS_ENDPOINT'], desc: 'Endpoint URL'
|
|
17
25
|
|
|
@@ -146,6 +154,13 @@ module Shoryuken
|
|
|
146
154
|
value[:string_value].bytesize
|
|
147
155
|
elsif value[:binary_value]
|
|
148
156
|
value[:binary_value].bytesize
|
|
157
|
+
else
|
|
158
|
+
# List-type attributes (string_list_values /
|
|
159
|
+
# binary_list_values) carry no scalar value here and
|
|
160
|
+
# are intentionally not requeued (see
|
|
161
|
+
# normalize_dump_message). Size them as 0 rather than
|
|
162
|
+
# crashing the whole batch with `nil + Integer`.
|
|
163
|
+
0
|
|
149
164
|
end
|
|
150
165
|
name_size + data_type_size + value_size
|
|
151
166
|
end
|
|
@@ -163,6 +178,25 @@ module Shoryuken
|
|
|
163
178
|
# @return [Integer] the number of messages received
|
|
164
179
|
def find_all(url, limit, &block)
|
|
165
180
|
count = 0
|
|
181
|
+
empty_batches = 0
|
|
182
|
+
# Callers (dump/mv) collect messages and only delete them after this
|
|
183
|
+
# method returns (so a failed dump/mv deletes nothing), which means
|
|
184
|
+
# nothing is removed from the queue during the drain. Two consequences:
|
|
185
|
+
#
|
|
186
|
+
# 1. A large drain can outlast the queue's visibility timeout, at
|
|
187
|
+
# which point already-received messages become visible again and
|
|
188
|
+
# are handed back on a later receive. `seen` maps message id to
|
|
189
|
+
# the first yielded message so those re-reads are not yielded,
|
|
190
|
+
# counted, or dumped/moved twice - but the newest receipt handle
|
|
191
|
+
# is copied onto that message, because SQS only honors the most
|
|
192
|
+
# recently received handle for deletion (an older one can report
|
|
193
|
+
# success yet leave the message in the queue).
|
|
194
|
+
# 2. Because messages stay in flight until the caller deletes them,
|
|
195
|
+
# a queue larger than SQS's in-flight limit (~120k standard /
|
|
196
|
+
# ~20k FIFO) cannot be drained in a single pass: once the limit
|
|
197
|
+
# is reached receive returns empty and find_all stops. No message
|
|
198
|
+
# is lost - re-run dump/mv to drain the remainder.
|
|
199
|
+
seen = {}
|
|
166
200
|
batch_size = limit > 10 ? 10 : limit
|
|
167
201
|
|
|
168
202
|
loop do
|
|
@@ -172,16 +206,46 @@ module Shoryuken
|
|
|
172
206
|
messages = sqs.receive_message(
|
|
173
207
|
queue_url: url,
|
|
174
208
|
max_number_of_messages: batch_size,
|
|
209
|
+
# Long poll: short polling (the default wait_time_seconds: 0)
|
|
210
|
+
# samples only a subset of SQS hosts and routinely returns an empty
|
|
211
|
+
# batch even when the queue still has messages, which made dump/mv
|
|
212
|
+
# stop early and miss messages on real (distributed) SQS.
|
|
213
|
+
wait_time_seconds: FIND_ALL_WAIT_SECONDS,
|
|
175
214
|
attribute_names: ['All'],
|
|
176
215
|
message_attribute_names: ['All']
|
|
177
216
|
).messages || []
|
|
178
217
|
|
|
179
|
-
messages
|
|
218
|
+
# Split first-time messages from re-reads (same id handed back after
|
|
219
|
+
# the visibility timeout lapsed). A batch of only re-reads counts as
|
|
220
|
+
# empty so the drain still terminates.
|
|
221
|
+
fresh = []
|
|
222
|
+
messages.each do |message|
|
|
223
|
+
if (previous = seen[message.message_id])
|
|
224
|
+
# Keep the newest receipt handle on the already-yielded message
|
|
225
|
+
# so the caller's deferred batch_delete targets a handle SQS
|
|
226
|
+
# still accepts.
|
|
227
|
+
previous.receipt_handle = message.receipt_handle
|
|
228
|
+
else
|
|
229
|
+
seen[message.message_id] = message
|
|
230
|
+
fresh << message
|
|
231
|
+
end
|
|
232
|
+
end
|
|
180
233
|
|
|
181
|
-
|
|
234
|
+
fresh.each(&block)
|
|
235
|
+
|
|
236
|
+
count += fresh.size
|
|
182
237
|
|
|
183
238
|
break if count >= limit
|
|
184
|
-
|
|
239
|
+
|
|
240
|
+
# Even with long polling an occasional empty batch is possible while
|
|
241
|
+
# messages remain, so only give up after several consecutive batches
|
|
242
|
+
# that yielded no new messages.
|
|
243
|
+
if fresh.empty?
|
|
244
|
+
empty_batches += 1
|
|
245
|
+
break if empty_batches >= FIND_ALL_MAX_EMPTY_BATCHES
|
|
246
|
+
else
|
|
247
|
+
empty_batches = 0
|
|
248
|
+
end
|
|
185
249
|
end
|
|
186
250
|
|
|
187
251
|
count
|
data/bin/integrations
CHANGED
|
@@ -70,21 +70,23 @@ class IntegrationRunner
|
|
|
70
70
|
def run_specs(specs)
|
|
71
71
|
results = []
|
|
72
72
|
|
|
73
|
-
specs.
|
|
73
|
+
specs.each_with_index do |spec, idx|
|
|
74
|
+
prefix = "[#{idx + 1}/#{specs.size}]"
|
|
75
|
+
puts "#{prefix} RUNNING #{spec[:name]}"
|
|
76
|
+
$stdout.flush
|
|
77
|
+
|
|
74
78
|
result = run_spec(spec)
|
|
75
79
|
results << result
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
end
|
|
81
|
+
elapsed = result[:duration] ? format('%.1fs', result[:duration]) : '?'
|
|
82
|
+
status = if result[:skipped] then 'SKIPPED'
|
|
83
|
+
elsif result[:success] then 'OK '
|
|
84
|
+
else 'FAILED '
|
|
85
|
+
end
|
|
86
|
+
puts "#{prefix} #{status} #{spec[:name]} (#{elapsed})"
|
|
84
87
|
$stdout.flush
|
|
85
88
|
end
|
|
86
89
|
|
|
87
|
-
puts
|
|
88
90
|
results
|
|
89
91
|
end
|
|
90
92
|
|
|
@@ -132,34 +134,50 @@ class IntegrationRunner
|
|
|
132
134
|
output = []
|
|
133
135
|
start_time = Time.now
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
Bundler.with_unbundled_env do
|
|
138
|
+
# Use non-block popen so we control when waitpid runs. The block form
|
|
139
|
+
# calls io.close (which calls waitpid internally) as part of its own
|
|
140
|
+
# ensure, so if the child is still running when Timeout::Error fires,
|
|
141
|
+
# io.close blocks indefinitely before we can rescue the error.
|
|
142
|
+
io = IO.popen(env, cmd, chdir: spec[:directory], err: [:child, :out])
|
|
143
|
+
timed_out = false
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
Timeout.timeout(TIMEOUT) do
|
|
147
|
+
io.each_line { |line| output << line }
|
|
148
|
+
end
|
|
149
|
+
rescue Timeout::Error
|
|
150
|
+
timed_out = true
|
|
151
|
+
ensure
|
|
152
|
+
# Kill before closing: io.close calls waitpid, which blocks if the
|
|
153
|
+
# child is still alive. Sending SIGKILL first guarantees the child
|
|
154
|
+
# exits promptly so waitpid (inside io.close) returns immediately.
|
|
155
|
+
begin
|
|
156
|
+
Process.kill('KILL', io.pid)
|
|
157
|
+
rescue Errno::ESRCH
|
|
158
|
+
# Child already exited on its own — nothing to kill
|
|
144
159
|
end
|
|
160
|
+
io.close rescue nil
|
|
145
161
|
end
|
|
146
162
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
if timed_out
|
|
164
|
+
{
|
|
165
|
+
spec: spec,
|
|
166
|
+
success: false,
|
|
167
|
+
exit_code: -1,
|
|
168
|
+
error: 'Timeout',
|
|
169
|
+
duration: Time.now - start_time,
|
|
170
|
+
output: output.join
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
{
|
|
174
|
+
spec: spec,
|
|
175
|
+
success: $?.success?,
|
|
176
|
+
exit_code: $?.exitstatus,
|
|
177
|
+
duration: Time.now - start_time,
|
|
178
|
+
output: output.join
|
|
179
|
+
}
|
|
180
|
+
end
|
|
163
181
|
end
|
|
164
182
|
end
|
|
165
183
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Shoryuken
|
|
4
4
|
module ActiveJob
|
|
5
|
-
# Adds an accessor for SQS SendMessage parameters on ActiveJob jobs
|
|
6
|
-
#
|
|
7
|
-
# these parameters when enqueueing jobs; other adapters can ignore them.
|
|
5
|
+
# Adds an accessor for SQS SendMessage parameters on ActiveJob jobs (instances of ActiveJob::Base). Shoryuken ActiveJob
|
|
6
|
+
# queue adapters use these parameters when enqueueing jobs; other adapters can ignore them.
|
|
8
7
|
module SQSSendMessageParametersAccessor
|
|
9
8
|
extend ActiveSupport::Concern
|
|
10
9
|
|
|
@@ -14,14 +13,12 @@ module Shoryuken
|
|
|
14
13
|
end
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
# Initializes SQS SendMessage parameters on instances of ActiveJob::Base
|
|
18
|
-
#
|
|
19
|
-
# as when using ActiveJob::Base.set.
|
|
16
|
+
# Initializes SQS SendMessage parameters on instances of ActiveJob::Base to the empty hash, and populates it whenever
|
|
17
|
+
# `#enqueue` is called, such as when using ActiveJob::Base.set.
|
|
20
18
|
module SQSSendMessageParametersSupport
|
|
21
19
|
# Initializes a new ActiveJob instance with empty SQS parameters
|
|
22
20
|
#
|
|
23
|
-
# Uses argument forwarding (...) to properly pass all arguments including
|
|
24
|
-
# keyword arguments to the base class.
|
|
21
|
+
# Uses argument forwarding (...) to properly pass all arguments including keyword arguments to the base class.
|
|
25
22
|
def initialize(...)
|
|
26
23
|
super(...)
|
|
27
24
|
self.sqs_send_message_parameters = {}
|