Repository navigation
fix(solid_queue): don't perform the job twice when the guard clause raises - #3
Merged
antoinefink merged 2 commits intoAug 11, 2026
Merged
Conversation
…aises
`ActiveJobExtensions#perform_now` used a method-level `rescue`, which also
covered its own guard clause:
return super unless Sentry.initialized? && using_solid_queue_adapter?
On that early-return path `job_executed` is still nil — Ruby registers the
local from the lexical position of its assignment, so the rescue reads it as
nil rather than raising NameError. `raise if job_executed` therefore did not
fire: the original exception was swallowed and the fallback `super` performed
the job a *second time on the same instance*.
Observable whenever the guard is false (any non-solid_queue adapter, or Sentry
not initialized):
- `executions` reaches 2 for a single invocation
- side effects run twice — duplicate enqueues, doubled API calls and logs
- the error that finally propagates comes from the *second* run, masking the
real one; an ActiveJob::Continuable job raises InvalidStepError on the
replay because its step state was already recorded
This hits every ActiveJob test in a host app, since the test adapter reports
`queue_adapter_name == "test"`, and any environment using a different adapter
(e.g. `:inline` on staging). Production on solid_queue was unaffected.
Scope the rescue to a `begin` block around the instrumented section only, so
the guard's own `super` is no longer covered by it.
Adds regression coverage for both early-return paths — Sentry uninitialized,
and a non-solid_queue adapter — asserting that a raising job is performed
exactly once and propagates its own error. Both new examples fail on the old
code with `expected: 1, got: 2`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`perform_now` is a wrapper, so the invariant that matters is how many times it
calls the wrapped job. The suite asserted almost entirely on outcomes — return
value, error class — which is structurally blind to a duplicate run: an
idempotent job produces an identical outcome either way. That is how the
method-level rescue performed every job twice without failing any of the 62
examples.
Adds counting fixtures and converts the blind assertions:
- the two "safety rescue" fallback examples used HappyJob and checked only
`result == "happy"`; they now use CountingHappyJob and assert runs == 1, so
a fallback that re-runs an already-executed job fails
- "re-raises job errors even when safety rescue is active" checked only the
error class; it now asserts the job ran once, so mistaking a job error for
an instrumentation failure fails
- adds a fully instrumented raising case (guard passes) asserting one run,
executions == 1, and exactly one Sentry event
These new assertions pass with and without the perform_now fix — the guard-true
paths were never affected by that bug. They are forward-looking guards: a future
change that breaks `job_executed` on the normal path would otherwise go
unnoticed, exactly as the original defect did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
antoinefink
merged commit Aug 11, 2026
af613a7
into
feat/sentry-solid-queue
112 of 141 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
ActiveJobExtensions#perform_nowused a method-levelrescue, which in Ruby wraps the entire method body — including its own guard clause:On the early-return path
job_executedis stillnil. Ruby registers a local from the lexical position of its assignment, so the rescue reads it asnilrather than raisingNameError— which is why this failed silently.raise if job_executednever fires, the original exception is swallowed, and the fallbacksuperperforms the job a second time on the same instance.Impact
Whenever the guard is false — any non-
solid_queueadapter, or Sentry not initialized:executionsreaches2for a single invocationActiveJob::Continuablejob raisesInvalidStepErroron the replay, because its step state was already recordedThis hits every ActiveJob test in a host app (the test adapter reports
queue_adapter_name == "test") and any environment on a different adapter — e.g.:inlineon staging. Production onsolid_queuewas unaffected, since the guard passes there and the normal path runs.The fix
Scope the rescue to a
beginblock around the instrumented section only, so the guard's ownsuperis no longer covered by it. The fallback-to-uninstrumented-superbehaviour is preserved for genuine instrumentation failures.Tests
Adds regression coverage for both early-return paths — Sentry uninitialized, and a non-solid_queue adapter — asserting a raising job is performed exactly once and propagates its own error.
Both new examples fail on the old code with
expected: 1, got: 2, and pass with the fix. Full gem suite: 62 examples, 0 failures. RuboCop clean.Note that the
raise_errorassertions alone pass either way — only the execution counter catches the duplication. That is precisely why this went unnoticed.🤖 Generated with Claude Code