Repository navigation
fix(model): roll back a Wheels transaction when the request ends with abort inside it - #4335
Conversation
… abort inside it The rollback for a failed transaction method lived in the finally/catch of a try that also has a catch clause. On BoxLang, abort skips a finally whose try has a catch clause, so a request that ended with abort inside invokeWithTransaction() (or a nested call, a savepoint unit, or withAdvisoryLock(transaction = true)) closed the transaction with the writes made before the abort. Lucee and Adobe ColdFusion rolled back. $transactionBody() now runs the method through $invokeTransactionMethod(): a catch-free try/finally around an inner try/catch that records a normal throw. When the method neither returned nor threw, the outer finally runs transaction action="rollback". Normal completion is unchanged. CLAUDE.md gains cross-engine invariant 22 for the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…abort-rollback Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # vendor/wheels/tests/routes.cfm
…ly on abort The release comments in $advisoryLockTransactionBody() said a transaction-action statement in a finally makes BoxLang skip it on abort. Measured: the trigger is a catch clause on the try; transaction actions make no difference. The comments now say so and point at cross-engine invariant 22, and note that invokeWithTransaction() performs the abort rollback on BoxLang. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Review @ b0645e0: source looks good. Final verdict after matrix 37304944278 (BoxLang legs especially) and PR CI
Branch freshness. The branch is behind develop, but develop hasn't touched any of the PR's files since its base, and On the matrix I'll read |
Wheels Test Results222 196 tests 217 264 ✅ 1h 22m 31s ⏱️ Results for commit b0645e0. |
Final: APPROVE @ b0645e0 (matrix 37304944278, all 32 legs read; PR CI read)Correction to point 3 of my source review. I said nothing up the chain issues a second rollback. That's true of the plain
Matrix
PR CI: 18 pass, 3 skipping, 0 fail, 0 pending. No open findings. The INFOs stand: the probe's |
…onal advisory-lock body (#4338) * refactor(model): drop the abort rollback from the transactional advisory-lock body $advisoryLockTransactionBody() rolled the transaction back in its outer finally when the callback neither committed nor threw, i.e. when the request ended with abort. Since #4335, invokeWithTransaction() itself rolls back on abort ($invokeTransactionMethod()), and this method only runs through invokeWithTransaction(), so that rollback is redundant. Removing it also drops the two flags only it read and the outer catch that only set one of them, which leaves the outer try catch-free (cross-engine invariant 22). The inner catch-free release on abort or throw and the release after a failed commit are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(core): drop the locking.cfm exception from the try/catch/finally guard $advisoryLockTransactionBody() no longer has a try with both a catch and a finally, so the guard needs no exceptions. With develop's locking.cfm and an empty allowlist the guard fails at the old outer try; with this branch it passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
When a request ends with
abortinside a Wheels transaction, the transaction is now rolled back on every supported engine. That coversinvokeWithTransaction(), a nested call that joins it, a savepoint unit, andwithAdvisoryLock(transaction = true), which runs throughinvokeWithTransaction(). A transaction that completes normally (it returns, returns false, or throws) behaves exactly as before.The rollback for a failed method lived in the
finally/catchof atrythat also has acatchclause. On BoxLang,abortskips afinallywhosetryhas acatchclause, so on that path the transaction was closed with the writes made before the abort still in it. Lucee and Adobe ColdFusion already rolled back.Change
vendor/wheels/model/transactions.cfm:$transactionBody()now runs the method through a new$invokeTransactionMethod().try/finallywith no catch clause, around an innertry/catchthat only records a normal throw and rethrows it.abort), the outerfinallyrunstransaction action="rollback".vendor/wheels/model/locking.cfm: the comments on fix(model): release withAdvisoryLock(transaction=true) lock when the callback aborts #4330's releasefinallynow name the actual condition: thefinallymust belong to atrywith nocatchclause. They also note thatinvokeWithTransaction()now performs the abort rollback for this path. Comment-only, in its own commit.CLAUDE.md: a new cross-engine invariant (22) records the rule and the nested try pattern.fixedfragment.Tests
New
vendor/wheels/tests/specs/model/transactionAbortRollbackSpec.cfc. Each scenario runs as its own request through$testClient, via a test controller (TransactionAbortProbe) and a route. Each aborting scenario checks a marker set right before the abort, so it can't pass if the request never reached the write.invokeWithTransaction()leaves 0 rows;RED (this spec on develop, BoxLang + MySQL): the three abort scenarios fail with the writes kept. The two normal-completion scenarios pass. On develop the abort scenarios pass on Lucee and Adobe ColdFusion.
GREEN,
modeldirectory after merging develop (pass / fail / error):advisoryLockAbortReleaseSpecpasses on every leg where it applies (it skips its transaction case on PostgreSQL, and the whole spec on SQLite).internalspecs: 113 passed.modeldirectory: 1385 / 0 / 0, with the new spec 5/5.Raw
transaction {}blocks aren't managed by Wheels and aren't changed here.🤖 Generated with Claude Code