Skip to content

fix: pin connection for advisory lock lifecycle#1168

Open
thetutlage wants to merge 7 commits into
22.xfrom
fix/advisory-lock-connection-pinning
Open

fix: pin connection for advisory lock lifecycle#1168
thetutlage wants to merge 7 commits into
22.xfrom
fix/advisory-lock-connection-pinning

Conversation

@thetutlage

@thetutlage thetutlage commented Mar 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Advisory locks are session-scoped (bound to the database connection that acquired them), but rawQuery picks a random connection from the pool each time. This causes releaseAdvisoryLock to fail with E_UNABLE_RELEASE_LOCK when it lands on a different connection than getAdvisoryLock.
  • When pool max > 1, the migration runner now pins a dedicated connection and passes it to the dialect's lock methods. With pool max = 1, pinning is skipped (unnecessary and would starve other queries).
  • Dialect getAdvisoryLock and releaseAdvisoryLock accept an optional connection parameter to use a specific connection instead of acquiring one from the pool.

Closes #1153

Test plan

  • Pool of 1: holding a connection starves the pool (proves pinning must be conditional)
  • Pool of 2 without pinned connection: release fails (proves the bug)
  • Pool of 2 with pinned connection: release succeeds (proves the fix)
  • Migration runner with pool of 4: full migration cycle completes
  • Migration runner with pool of 1: full migration cycle completes
  • All existing migration and query client tests pass

Summary by CodeRabbit

  • Bug Fixes

    • Improved migration reliability when database connection pools have multiple connections.
    • Advisory locks are now safely acquired and released within the same session to prevent incorrect unlocks.
    • Improved handling of lock contention so migrations fail with the proper error status and no partial table creation.
  • Tests

    • Added advisory-lock test coverage across constrained pool sizes, including successful lock reuse, release failures without the same connection, rollbacks, and contention scenarios.

Allow overwriting existing files during stub generation by passing
--force to any make:* command. The make:model command forwards the
flag to sub-commands (migration, controller, transformer, factory).
…ELEASE_LOCK

Advisory locks (PG_TRY_ADVISORY_LOCK/PG_ADVISORY_UNLOCK in PostgreSQL,
GET_LOCK/RELEASE_LOCK in MySQL) are session-scoped — they are bound to
the specific database connection that acquired them. Previously, both
acquire and release used rawQuery which picks a random connection from
the pool each time, causing the release to fail when it lands on a
different connection.

When the pool has more than one connection, the migration runner now
pins a dedicated connection and passes it through to the dialect's lock
methods. With a single-connection pool, pinning is skipped since it
would starve other queries and isn't needed (only one connection exists).

Closes #1153
@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request

@github-actions github-actions Bot added the Stale label Apr 20, 2026
@simoneNEMO

Copy link
Copy Markdown

Hey @thetutlage, I went through this and looks good to me (minus the unrelated commit ofc). It is very hard to test since we got issues related to this only 3 times in 2 years and we do daily deployments but I will keep an eye when this get merged.

@github-actions github-actions Bot removed the Stale label Apr 26, 2026
@github-actions

Copy link
Copy Markdown

This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request

@github-actions github-actions Bot added the Stale label May 17, 2026
@github-actions

Copy link
Copy Markdown

This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request

@github-actions github-actions Bot closed this May 22, 2026
@thetutlage thetutlage removed the Stale label May 28, 2026
@thetutlage thetutlage reopened this May 28, 2026
When acquiring the advisory lock failed (for example, another process
already holds it), the migration runner still attempted to release it
during shutdown. The release then failed and threw E_UNABLE_RELEASE_LOCK,
masking the real E_UNABLE_ACQUIRE_LOCK error and causing run() to reject
instead of surfacing the failure via migrator.error.

Track whether the lock was actually acquired and skip the release when it
was not, so the original acquire error is preserved and run() resolves.

Also add migrator coverage for a pool of 2 (the tightest pinning case),
rollback with a multi-connection pool, and lock contention.
@thetutlage thetutlage self-assigned this Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27d5d855-d38f-4714-9404-db236b009d28

📥 Commits

Reviewing files that changed from the base of the PR and between 466e0c7 and 28e6502.

📒 Files selected for processing (1)
  • src/migration/runner.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/migration/runner.ts

📝 Walkthrough

Walkthrough

Migration advisory locks now use pinned write connections across acquisition and release. PostgreSQL and MySQL dialect APIs accept optional connections, while migration and query-client tests cover pool sizing, contention, rollback, and cleanup.

Changes

Session-bound advisory locks

Layer / File(s) Summary
Advisory-lock connection contracts
src/types/database.ts, src/dialects/pg.ts, src/dialects/mysql.ts
Dialect signatures accept optional connections, and lock queries use write clients with connection binding.
Pinned migration lock lifecycle
src/migration/runner.ts
MigrationRunner tracks acquisition state, pins connections for multi-connection pools, and releases them after unlock or failure.
Advisory-lock behavior validation
test/migrations/migrator.spec.ts, test/database/query_client.spec.ts
Tests cover pool sizes, pinned acquire/release behavior, rollback, contention, and database cleanup aliases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MigrationRunner
  participant WritePool
  participant DatabaseSession
  MigrationRunner->>WritePool: acquire pinned write connection
  MigrationRunner->>DatabaseSession: acquire advisory lock
  DatabaseSession-->>MigrationRunner: lock_status
  MigrationRunner->>DatabaseSession: release advisory lock on same connection
  DatabaseSession-->>MigrationRunner: lock_status
  MigrationRunner->>WritePool: return pinned connection
Loading

Poem

I’m a bunny with a lock and a key,
Pinned to one session, safe as can be.
Acquire, release, then hop away,
Tests guard the burrow night and day. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely matches the main change: pinning the advisory lock connection lifecycle.
Linked Issues check ✅ Passed The PR addresses #1153 by keeping advisory lock acquire/release on the same connection for PostgreSQL and MySQL, with supporting tests.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are evident; the test and runner updates support the advisory-lock fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/advisory-lock-connection-pinning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thetutlage thetutlage added the Type: Bug The issue has indentified a bug label Jul 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
commands/make_model.ts (1)

48-57: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Duplicate alias for --transformer and --controller.

The --transformer flag uses alias: 'c', which is already assigned to --controller (line 43). This conflict means -c will not behave correctly. Consider changing the transformer alias to 't' or removing it.

🐛 Proposed fix
   `@flags.boolean`({
     name: 'transformer',
-    alias: 'c',
+    alias: 't',
     description: 'Generate the transformer for the model',
   })
   declare transformer: boolean
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@commands/make_model.ts` around lines 48 - 57, Update the transformer flag
declaration in commands/make_model.ts to remove the duplicate alias `c`; assign
it the unique alias `t` or omit the alias, while preserving the existing
controller flag alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dialects/pg.ts`:
- Around line 255-258: Parameterize all advisory-lock SQL inputs to prevent
injection: update getAdvisoryLock and the PostgreSQL unlock flow in
src/dialects/pg.ts (sites 255-258 and 271-274) to bind key, and update the MySQL
GET_LOCK and RELEASE_LOCK flows in src/dialects/mysql.ts (sites 252-255 and
268-269) to bind key and timeout through the query builder rather than
interpolating values into SQL.

In `@src/migration/runner.ts`:
- Around line 304-313: Move the acquire:lock emission out of the try/catch
surrounding lock acquisition in the migration runner. Keep getAdvisoryLock,
failure handling, and lockAcquired assignment inside the try, then emit
acquire:lock only after the catch block so listener errors cannot trigger
releaseLockConnection or leave inconsistent lock state.

In `@test/database/query_client.spec.ts`:
- Around line 539-559: Rename the cleanup callback parameter in all three
affected .run callbacks to avoid shadowing the file-level cleanup declaration,
and update each corresponding cleanup invocation to use the new alias. Preserve
the existing cleanup behavior and async teardown logic.

---

Outside diff comments:
In `@commands/make_model.ts`:
- Around line 48-57: Update the transformer flag declaration in
commands/make_model.ts to remove the duplicate alias `c`; assign it the unique
alias `t` or omit the alias, while preserving the existing controller flag
alias.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3daa64c2-b677-48e3-96ef-06fbc4ac0a25

📥 Commits

Reviewing files that changed from the base of the PR and between 1a88178 and 71cd4c2.

📒 Files selected for processing (16)
  • commands/make_factory.ts
  • commands/make_migration.ts
  • commands/make_model.ts
  • commands/make_seeder.ts
  • src/database/main.ts
  • src/dialects/mysql.ts
  • src/dialects/pg.ts
  • src/migration/runner.ts
  • src/migration/schema_dump/schema_states/sqlite.ts
  • src/query_client/index.ts
  • src/transaction_client/index.ts
  • src/types/database.ts
  • src/types/model.ts
  • src/types/querybuilder.ts
  • test/database/query_client.spec.ts
  • test/migrations/migrator.spec.ts

Comment thread src/dialects/pg.ts
Comment on lines +255 to +258
async getAdvisoryLock(key: string, _timeout?: number, connection?: any): Promise<boolean> {
const query = this.client
.getWriteClient()
.raw(`SELECT PG_TRY_ADVISORY_LOCK('${key}') as lock_status;`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file map =="
git ls-files 'src/dialects/*' 'src/**' | sed -n '1,200p'

echo
echo "== pg.ts relevant section =="
sed -n '230,290p' src/dialects/pg.ts

echo
echo "== mysql.ts relevant section =="
sed -n '230,290p' src/dialects/mysql.ts

echo
echo "== search advisory lock usages =="
rg -n "getAdvisoryLock|releaseAdvisoryLock|PG_TRY_ADVISORY_LOCK|PG_ADVISORY_UNLOCK|GET_LOCK|RELEASE_LOCK" src

echo
echo "== search for caller inputs around advisory lock keys =="
rg -n "getAdvisoryLock\(|releaseAdvisoryLock\(" src

Repository: adonisjs/lucid

Length of output: 13146


Parameterize advisory-lock SQL

  • src/dialects/pg.ts: bind key in PG_TRY_ADVISORY_LOCK / PG_ADVISORY_UNLOCK.
  • src/dialects/mysql.ts: bind key and timeout in GET_LOCK, and bind key in RELEASE_LOCK.

The current string interpolation exposes this public API to SQL injection.

📍 Affects 2 files
  • src/dialects/pg.ts#L255-L258 (this comment)
  • src/dialects/pg.ts#L271-L274
  • src/dialects/mysql.ts#L252-L255
  • src/dialects/mysql.ts#L268-L269
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dialects/pg.ts` around lines 255 - 258, Parameterize all advisory-lock
SQL inputs to prevent injection: update getAdvisoryLock and the PostgreSQL
unlock flow in src/dialects/pg.ts (sites 255-258 and 271-274) to bind key, and
update the MySQL GET_LOCK and RELEASE_LOCK flows in src/dialects/mysql.ts (sites
252-255 and 268-269) to bind key and timeout through the query builder rather
than interpolating values into SQL.

Comment thread src/migration/runner.ts
Comment thread test/database/query_client.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/database/query_client.spec.ts (1)

569-599: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Pin the connection for this lock test. It still depends on pool ordering: if acquireConnection() returns the session that ran getAdvisoryLock(), releaseAdvisoryLock() will succeed and the assertion flips.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/database/query_client.spec.ts` around lines 569 - 599, Update the
“release advisory lock fails with pool of 2 without pinned connection” test to
explicitly pin or otherwise retain the connection used by getAdvisoryLock before
testing releaseAdvisoryLock. Remove the pool-ordering-dependent heldConnection
approach, ensure release runs on a different connection, and preserve the
expected false result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/database/query_client.spec.ts`:
- Around line 569-599: Update the “release advisory lock fails with pool of 2
without pinned connection” test to explicitly pin or otherwise retain the
connection used by getAdvisoryLock before testing releaseAdvisoryLock. Remove
the pool-ordering-dependent heldConnection approach, ensure release runs on a
different connection, and preserve the expected false result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70cd0533-b9b5-4dbf-aef6-f55321cf14ac

📥 Commits

Reviewing files that changed from the base of the PR and between 71cd4c2 and 466e0c7.

📒 Files selected for processing (4)
  • src/dialects/mysql.ts
  • src/dialects/pg.ts
  • src/types/database.ts
  • test/database/query_client.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/dialects/pg.ts
  • src/dialects/mysql.ts

A throwing acquire:lock listener would trigger the catch block, which
releases the pinned connection back to the pool while the advisory lock
is still held on it, leaving the lock stranded and masking the listener
error during shutdown. Emit the event only after the lock is confirmed
acquired, outside the try/catch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug The issue has indentified a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Advisory lock for migrations acquires and releases on different pool connections, causing E_UNABLE_RELEASE_LOCK

2 participants