Repository navigation
fix(model): take SQL Server advisory locks for the session, outside a transaction - #4325
Conversation
… transaction On SQL Server, withAdvisoryLock() outside a transaction failed with "The statement or function must be executed in the context of a user transaction": sp_getapplock used its default Transaction owner. The docblock claimed full support and the adapter reported none. The lock is now owned by the database session (sp_getapplock @LockOwner = 'Session'), as on MySQL and PostgreSQL, and goes through the same verified release (#4197): - the acquire returns @@spid from the same batch; - a release runs only on the session that holds the lock (APPLOCK_MODE), since sp_releaseapplock errors on any other; - the held check reads the holder from sys.dm_tran_locks. Without the VIEW SERVER (PERFORMANCE) STATE permission it falls back to whether any session holds the lock. $supportsAdvisoryLocks() is true. The transaction path (#4198) shares the session acquire. Fixes #4220 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Review @ 163b681: source looks good, one LOW. Final verdict after matrix 37273906928Release lands on the holding session. This reuses #4197's verified release unchanged.
The #4222 transaction path still works.
Holder read. Findings
Docs and guide. They match the code:
On the matrix I'll read |
…language The advisory-lock held check fell back only when the error message contained "permission": never on a server whose messages aren't in English, and never on Adobe, whose message is just "Error Executing Database Query.". It now keys on SQL Server's error number (300, 297, 229), read where each engine keeps it: nativeErrorCode on Lucee and Adobe, the driver exception's cause chain on BoxLang, errorCode for a thrown error. The message and detail text remain a fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Wheels Test Results219 815 tests 215 280 ✅ 1h 20m 30s ⏱️ Results for commit 2a8bdd0. |
Final: APPROVE @ 2a8bdd0 (matrix 37275206192, all 32 legs read; PR CI read)Delta since 163b681. My LOW is fixed.
Matrix
PR CI: 26 pass, 3 skipping, 0 fail, 0 pending. No open findings. The two INFOs (the text fallback alongside a known number, and ending the cause walk on the first cause without |
Summary
Closes #4220.
On SQL Server,
withAdvisoryLock()called outside a transaction failed with "The statement or function must be executed in the context of a user transaction."sp_getapplockused its default@LockOwner = 'Transaction'. The adapter reported$supportsAdvisoryLocks()as false, which only the specs read, and thewithAdvisoryLock()docblock claimed "Full support".The lock is now owned by the database session (
sp_getapplock @LockOwner = 'Session'), as on MySQL and PostgreSQL. It goes through the same verified release (#4197):$acquireAdvisoryLockSession()returns@@SPID, read in the same batch, as the holder. The statussp_getapplockreturns is mapped as before:-1throwsWheels.AdvisoryLockTimeout;Wheels.AdvisoryLockError.$tryReleaseAdvisoryLock()releases only whenAPPLOCK_MODEshows this pooled session holds the lock.sp_releaseapplockraises an error on any other session.$isAdvisoryLockHeld()reads the holder fromsys.dm_tran_locks. An application lock appears there as<db>:[<first 32 characters of the name>]:(<hash>); I measured this on SQL Server 17.VIEW SERVER STATE(SQL Server 2022:VIEW SERVER PERFORMANCE STATE). I measured that a login without it gets error 300.APPLOCK_MODEfor this session, sinceAPPLOCK_TESTgrants a session its own lock, andAPPLOCK_TESTfor the others.nativeErrorCodeon Lucee and Adobe, the driver exception's cause chain on BoxLang, anderrorCodefor a thrown error. Message and detail text are only a fallback; on Adobe,messageis just "Error Executing Database Query."$supportsAdvisoryLocks()is now true. The transaction path (withAdvisoryLock(): hold one connection for the call, using transaction-scoped locks #4198, feat(model): opt-in transaction-scoped advisory locks for withAdvisoryLock #4222) shares the session acquire, so the two can't drift.locking.cfm,Base.cfcand the adapter.Docs
changelog.d/4220-sqlserver-standalone-advisory-lock.fixed.md.withAdvisoryLock()works on SQL Server outside a transaction", including the permission note.Tests
advisoryLockExclusionSpecnow runs its session-lock checks on SQL Server too:sys.dm_tran_locksunreadable (mocked permission error), a lock held by another session is still seen as held, and as free once it's released.$sqlServerErrorNumber()reads each field shape, and a real driver error reports 208 on each engine.AdapterSqlGenerationSpecexpects SQL Server's$supportsAdvisoryLocks()to be true.lockingSpec, which skipped when support was false, now runs on SQL Server.RED on develop, Lucee 7 × SQL Server, only the specs copied in: 7 checks fail or error. The
withAdvisoryLock()checks error with "The statement or function must be executed in the context of a user transaction."GREEN locally, all with 0 fail / 0 error:
On Adobe, the mock's exception type had to be custom, because Adobe won't
cfthrowits built-inDatabasetype. The message is still the real permission-denied text, which is what the adapter checks.After rebasing onto develop (which brought #4323's guide edit), I reran internal and model on Lucee 7 × SQL Server; both pass. The guides build passes.
🤖 Generated with Claude Code