Repository navigation
fix(model): take MySQL and PostgreSQL advisory locks with the shared connection options - #4331
Conversation
…connection options
The default (non-transaction) withAdvisoryLock() methods on MySQL and
PostgreSQL built {datasource, username, password} by hand, even with an
empty username and password, while model queries and the transactional
path use $advisoryLockConnection(). They now use it too, like SQL Server.
That helper resolves the datasource like $performQuery: a tenant
(non-shared) model now takes its lock on the active tenant's datasource
instead of the application's default one. Two tenants on different
databases using the same lock name no longer exclude each other; a lock
taken through a shared model stays on the default datasource.
Fixes #4223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
…k-connection-helper
Review @ 8795ea2: one MEDIUM (the guide's consequence statement is only half true). Final verdict after matrix 37283279616 and PR CIWhat changed
orch1's checks
Finding
On the matrix I'll read |
…atasource withAdvisoryLock() first waits on an application-server lock named after the model's configured datasource and the lock name. With a tenant model's lock now taken on the tenant's datasource, two tenants using the same lock name on one server still queued behind each other there, and could time out. The wait is now keyed on the effective datasource: another tenant's datasource is another connection pool, which can't borrow the session holding the lock (#4197), so its callers needn't wait. The docs note that MySQL lock names are server-wide, so tenant databases on the same MySQL server still share a lock name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
Final: APPROVE @ 216a0e1 (matrix 37284943699, all 32 legs read; PR CI read)Delta since 8795ea2. My MEDIUM is fixed.
Matrix
PR CI: 26 pass, 3 skipping, 0 fail, 0 pending. No open findings. |
Fixes #4223
Summary
The default (non-transaction) advisory-lock methods on MySQL and PostgreSQL built their connection options by hand:
{datasource, username, password}, even when the username and password are empty. Model queries, the transactional path (#4222) and SQL Server's lock methods (#4325) use the shared$advisoryLockConnection()helper instead, which:$performQuery()does;$effectiveDataSource().On Adobe CF the two option forms use different connection pools. The default path never needs the lock on the callback's connection, so the old form wasn't a correctness bug there, but the two paths followed different rules for the same queries.
Change
$acquireAdvisoryLockSession,$releaseAdvisoryLock,$tryReleaseAdvisoryLockand$isAdvisoryLockHeldinMySQLModel.cfcandPostgreSQLModel.cfcnow pass$advisoryLockConnection().Behaviour change: tenant models lock on the tenant database
Because the helper resolves the datasource the way the model's queries do, a tenant (non-shared) model's
withAdvisoryLock()now takes the lock on the active tenant's datasource. In 4.1 it took the lock on the application's default datasource. SQL Server andwithAdvisoryLock(transaction = true)already worked this way.withAdvisoryLock()through a shared model (sharedModel()), which keeps the default datasource whichever tenant is active.Docs.
changedfragment.The in-application wait follows the lock's datasource too
Review finding:
withAdvisoryLock()first waits on an application-server lock ($advisoryLockLocalName, #4197) named after the model's configured datasource. So on one server, two tenants using the same lock name still queued behind each other there, and could hitWheels.AdvisoryLockTimeout, though their database locks no longer conflict.The name is now keyed on
adapter.$effectiveDataSource(). Another tenant's datasource is another connection pool, which can't borrow the session holding the lock, so #4197's protection still holds. This also applies to SQL Server andtransaction = true.MySQL.
GET_LOCKnames are server-wide, so tenant databases on the same MySQL server still share a lock name in the database. PostgreSQL and SQL Server locks are per database. The upgrade guide, the multi-tenancy guide and the fragment say so.Tests
New
vendor/wheels/tests/specs/model/advisoryLockTenantSpec.cfc(MySQL, PostgreSQL and SQL Server; it callsskip()elsewhere), with a newSharedAuthortest model (the authors table withsharedModel()):model("author").withAdvisoryLock()fails naming that datasource, and the callback never runs.Wheels.AdvisoryLockTimeout("Another caller in this application").$advisoryLockConnection().datasourceis the default datasource, the lock is held while the callback runs, and it's released afterwards. This is the global-lock pattern the docs show.advisoryLockExclusionSpec(concurrency, leak, holder-scoped release, load probe) is the regression check.RED (the new spec on develop's adapters, Lucee 7 + MySQL): the tenant spec fails, because the callback ran with the lock taken on the default datasource. The shared-model spec passes on develop too, as expected, since that behaviour doesn't change.
GREEN,
modeldirectory (pass / fail / error;advisoryLockTenantSpec2/2 on every leg listed):SQLite:
internalspecs: 113 passed;modeldirectory: 1372 / 0 / 0.Other checks: the complexity gate passes, and the guides build passes.
After merging develop (with feat(model): run long date and timestamp IN lists on SQL Server through STRING_SPLIT #4329): Lucee 7 + MySQL
modeldirectory 1427 / 0 / 0,advisoryLockTenantSpec2/2.After the review fix (
modeldirectory;advisoryLockTenantSpec4/4 on each leg):8795ea2a2, Lucee 7 + MySQL): the two new specs fail. Tenant B waits behind tenant A and times out, and both tenants get the same wait name.internalspecs: 113 passed;modeldirectory: 1377 / 0 / 0. The thread code compiles there; the database-lock specs skip.🤖 Generated with Claude Code