Skip to content

[2.x] fix: avoid duplicate ModelNotFoundException logs on orphaned queued jobs - #4634

Merged
imorland merged 1 commit into
2.xfrom
im/abstractjob-delete-missing-models
May 6, 2026
Merged

[2.x] fix: avoid duplicate ModelNotFoundException logs on orphaned queued jobs#4634
imorland merged 1 commit into
2.xfrom
im/abstractjob-delete-missing-models

Conversation

@imorland

@imorland imorland commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

When a queued job's serialized model is deleted between dispatch and worker pickup (a common race for notification fan-out — mentions, subscriptions, FoF byobu/follow-tags, etc.), the worker logs two ModelNotFoundException entries even though the race is handled correctly.

The flow (CallQueuedHandler):

  1. Worker unserialize()s payload → firstOrFail() throws ModelNotFoundException (caught silently).
  2. handleModelNotFound() runs. With deleteWhenMissingModels unset (default false), it falls through to $job->fail($e).
  3. fail() calls getCommand()unserialize() againModelNotFoundException (this time not caught, gets logged).

Setting public bool $deleteWhenMissingModels = true; on AbstractJob makes the handler $job->delete() the job instead, skipping the noisy second-deserialize path. The job is already off the queue and any unique-job lock has already been released.

Fixes #4615

Why apply at the AbstractJob level

All current AbstractJob subclasses (12 across core + bundled extensions) follow the same pattern: notify-X-about-Y, send-mail-about-Y, index-Y-for-search, run-composer-command, gdpr-export-for-user, etc. None of them does anything useful if the subject model has been deleted — there's no meaningful retry path.

Subclasses that genuinely want the failed-and-retry behavior can opt out with public bool $deleteWhenMissingModels = false;.

Changes

Test plan

  • Manual: dispatch a queued job whose model is then deleted before the worker reaches it (post a reply that triggers a notification, immediately delete the post). Confirm only the original ModelNotFoundException is logged once at INFO/DEBUG level (Laravel's caught-and-handled path), not twice at ERROR.
  • Existing queue test suite passes.

Notes

  • Pairs with [2.x] fix: add missing getHidden() to Log\Context\Repository stub #4633 (getHidden() stub fix) — once that's merged, this PR removes the remaining log noise from the same orphaned-job scenario.
  • This is a behavior change for any third-party extension whose AbstractJob subclass was relying on failed() being called on missing models. That should be vanishingly rare given Flarum's job patterns, and the override is one line. Worth calling out in upgrade notes.

When a queued job's serialized model is deleted between dispatch and
worker pickup, Laravel's CallQueuedHandler catches the unserialize
ModelNotFoundException via handleModelNotFound. Without
deleteWhenMissingModels, it then routes the job to fail(), which
re-deserializes the payload and throws ModelNotFoundException a second
time — this one is not caught and gets logged.

Set deleteWhenMissingModels = true on AbstractJob so the handler
delete()s the job (already off the queue, lock already released) instead
of going through the noisy fail() path. All current AbstractJob
subclasses (notification fan-out, mail sends, search index, package
manager, gdpr export, etc.) are no-ops when their subject model is
missing — there's no useful retry path.

Subclasses that genuinely want to be retried/failed on missing models
can override with `public bool $deleteWhenMissingModels = false;`.

Fixes #4615
@imorland
imorland requested a review from a team as a code owner May 6, 2026 20:00
@imorland imorland added this to the 2.0.0-rc.2 milestone May 6, 2026
@imorland
imorland merged commit b6cf89d into 2.x May 6, 2026
25 checks passed
@imorland
imorland deleted the im/abstractjob-delete-missing-models branch May 6, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbstractJob should set deleteWhenMissingModels=true to avoid secondary ModelNotFoundException on orphaned queued jobs

1 participant