Skip to content

fix(core): scope CacheInvalidator lifetime and clear rolled-back pending entries - #2603

Merged
glennjacobs merged 3 commits into
lunarphp:2.xfrom
wakqasahmed:fix/cache-invalidator-rollback-2561
Aug 25, 2026
Merged

glennjacobs merged 3 commits into
lunarphp:2.xfrom
wakqasahmed:fix/cache-invalidator-rollback-2561

Conversation

@wakqasahmed

Copy link
Copy Markdown
Contributor

Closes #2561

Summary

CacheInvalidator is documented as request-scoped but was bound as a singleton, letting pending state outlive a request/job under Octane or long-lived workers. It's now bound with $app->scoped(...).

Separately, record() added targets to $pending and registered an afterCommit callback, but a rollback discarded the callback while leaving the $pending entry in place. A later autocommit mutation, a later committed transaction, or a later transaction touching the same target could then flush an invalidation event for a change that was rolled back.

The fix tracks the transaction level each pending entry was first (or most shallowly) touched at, and listens for Illuminate\Database\Events\TransactionRolledBack per connection to drop only the entries whose shallowest touch happened inside the frame being discarded. An entry also touched by a surviving outer frame is kept (accepted over-invalidation, consistent with the class's existing reliability bias documented in its docblock).

Changes

  • packages/core/src/LunarServiceProvider.php: bind CacheInvalidator as scoped instead of singleton.
  • packages/core/src/Cache/CacheInvalidator.php: record the transaction level per pending entry and register a rollback listener that prunes entries whose frame was discarded.
  • tests/core/Feature/CacheInvalidationTest.php: added regression coverage for
    • root rollback followed by an autocommit mutation
    • root rollback followed by a later committed transaction
    • a later transaction for the same target after a rollback
    • nested rollback followed by outer commit
    • the same target touched in both outer and nested frames surviving a nested rollback

Test plan

  • Reproduced the bug: reverting the source changes while keeping the new tests fails a root rollback does not leak into a later autocommit mutation (event dispatched 2 times instead of 1).
  • All 25 tests in tests/core/Feature/CacheInvalidationTest.php pass with the fix applied (ran via vendor/bin/pest, PHP 8.4, sqlite).

@wakqasahmed

Copy link
Copy Markdown
Contributor Author

Hi @glennjacobs @alecritson — noticed this PR doesn't have a reviewer assigned yet — it's been about 6 days, CI is green and it's mergeable. Would you (or whoever's best placed) be able to take a look when you get a chance, or point me to who should? Thanks!

@glennjacobs

Copy link
Copy Markdown
Contributor

@wakqasahmed on holiday at the moment. Will be hitting all the PRs on my return at the end of next week.

@wakqasahmed

Copy link
Copy Markdown
Contributor Author

Sounds good, thanks — no rush, I'll wait for your review whenever you're back.

glennjacobs and others added 2 commits August 25, 2026 13:03
The recorder registered a permanent closure on the shared dispatcher per
instance; under the scoped binding every request/job scope in a long-lived
worker added another listener retaining its dead instance. The provider now
wires a single process-lifetime listener that resolves the scoped recorder
at dispatch time, and pruneRolledBack joins the contract.

Co-Authored-By: Claude Fable 5 <[email protected]>
@glennjacobs
glennjacobs merged commit 5bed272 into lunarphp:2.x Aug 25, 2026
21 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Roadmap Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants