Skip to content

Issue 5755 change advisory lock to try advisory lock#5809

Open
onno-vos-dev wants to merge 2 commits into
prisma:mainfrom
onno-vos-dev:issue-5755-change-advisory-lock-to-try-advisory-lock
Open

Issue 5755 change advisory lock to try advisory lock#5809
onno-vos-dev wants to merge 2 commits into
prisma:mainfrom
onno-vos-dev:issue-5755-change-advisory-lock-to-try-advisory-lock

Conversation

@onno-vos-dev
Copy link
Copy Markdown

No description provided.

onnovos added 2 commits May 6, 2026 11:32
… concurrency issues when multiple nodes compete for the same advisory lock
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Updated PostgreSQL migration locking mechanism to use non-blocking approach, returning clear error message when another migration is in progress instead of blocking with timeout.

Walkthrough

Replaced the PostgreSQL blocking advisory lock mechanism with a non-blocking approach using pg_try_advisory_lock. When the lock cannot be acquired, an error is returned instead of waiting. Supporting imports were adjusted and tests were updated to handle the new error result patterns.

Changes

Advisory Lock Non-Blocking Refactor

Layer / File(s) Summary
Core Lock Implementation
schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs
Replaced blocking pg_advisory_lock (with timeout) with non-blocking pg_try_advisory_lock; returns an error when lock cannot be acquired instead of timing out. Added Url and DatabaseSchemaInconsistent imports to support the new error-handling flow.
Import Cleanup
schema-engine/connectors/sql-schema-connector/src/flavour/postgres/connector/wasm/mod.rs
Removed the now-unused ADVISORY_LOCK_TIMEOUT import; kept Circumstances and PostgresProvider imports.
Test Updates
schema-engine/sql-migration-tests/tests/migrations/advisory_locking.rs
Added Queryable import; refactored three concurrent test branches to use `.map(

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the motivation for switching from blocking to non-blocking advisory locks and how this addresses concurrency issues.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: replacing advisory lock with try advisory lock to address concurrency issues, which aligns with the actual code changes across the three modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs`:
- Around line 383-386: The current parsing of pg_try_advisory_lock result (the
let acquired = ... expression) uses unwrap_or(false), which treats parsing
failures as "lock not acquired"; change this to explicitly distinguish parsing
errors from a contended lock by: attempt to parse the boolean from
result.first().and_then(|row| row.at(0).and_then(|v| v.as_bool())) and if None
or a parsing error occurs, return/propagate a distinct error (e.g., a
parse/driver error) instead of treating it as false; only treat a successfully
parsed false as "lock contended". Ensure the code paths and error messages that
use the acquired variable (the parsing expression and the subsequent contention
check around pg_try_advisory_lock) are updated to handle and surface the parse
error separately.
- Around line 378-395: Extract the hardcoded advisory lock id (72707369) into a
named const (e.g., ADVISORY_LOCK_ID) and use that const when constructing the
SQL passed to connection.query_raw and when composing the
ConnectorError::from_msg string (and update the nearby comment to reference the
const as well); ensure the SQL string is formatted to include the const value
and the error message is built with the same const so the id is not duplicated
in literals anywhere in this block (look for connection.query_raw(...) and
ConnectorError::from_msg(...) to apply the change).

In `@schema-engine/sql-migration-tests/tests/migrations/advisory_locking.rs`:
- Around line 100-138: The test postgres_advisory_lock_contention_fails_fast
currently may leak the advisory lock if unwrap_err() or the assertion panics;
wrap the dedicated connection/lock_holder in a small RAII guard (e.g.,
LockHolderGuard) that holds the Quaint connection and issues "SELECT
pg_advisory_unlock(72707369)" in its Drop implementation so the unlock always
runs, replace the explicit tok(lock_holder.raw_cmd(...)) call with constructing
the guard, and relax the assertion by checking for the stable parts of the error
(e.g., "Another instance" plus the production constant if available such as an
ADVISORY_LOCK message constant in postgres.rs, otherwise keep a minimal
substring) instead of coupling to the literal "pg_try_advisory_lock".
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce5a656e-4499-4de0-bab8-f45c81e38fe3

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6e192 and 3315714.

📒 Files selected for processing (3)
  • schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs
  • schema-engine/connectors/sql-schema-connector/src/flavour/postgres/connector/wasm/mod.rs
  • schema-engine/sql-migration-tests/tests/migrations/advisory_locking.rs

Comment on lines +378 to +395
let result = connection
.query_raw("SELECT pg_try_advisory_lock(72707369)", &[])
.await
.map_err(imp::quaint_error_mapper(params))?;

let acquired = result
.first()
.and_then(|row| row.at(0).and_then(|v| v.as_bool()))
.unwrap_or(false);

if !acquired {
return Err(ConnectorError::from_msg(
"Could not acquire the postgres advisory lock (SELECT pg_try_advisory_lock(72707369)). \
Another instance is likely running migrations against this database. \
See https://pris.ly/d/migrate-advisory-locking for details."
.to_owned(),
));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Hardcoded lock id duplicated between the SQL and the error message.

72707369 is now repeated in three places (SQL literal, error message, and the comment on Line 376). If anyone ever rotates this id, the error string will silently drift. Extract a const and format both strings from it.

♻️ Suggested refactor
+const MIGRATE_ADVISORY_LOCK_ID: i64 = 72707369;
+
 ...
-            let result = connection
-                .query_raw("SELECT pg_try_advisory_lock(72707369)", &[])
-                .await
-                .map_err(imp::quaint_error_mapper(params))?;
+            let result = connection
+                .query_raw(
+                    &format!("SELECT pg_try_advisory_lock({MIGRATE_ADVISORY_LOCK_ID})"),
+                    &[],
+                )
+                .await
+                .map_err(imp::quaint_error_mapper(params))?;
 ...
-            if !acquired {
-                return Err(ConnectorError::from_msg(
-                    "Could not acquire the postgres advisory lock (SELECT pg_try_advisory_lock(72707369)). \
-                     Another instance is likely running migrations against this database. \
-                     See https://pris.ly/d/migrate-advisory-locking for details."
-                        .to_owned(),
-                ));
-            }
+            if !acquired {
+                return Err(ConnectorError::from_msg(format!(
+                    "Could not acquire the postgres advisory lock (SELECT pg_try_advisory_lock({MIGRATE_ADVISORY_LOCK_ID})). \
+                     Another instance is likely running migrations against this database. \
+                     See https://pris.ly/d/migrate-advisory-locking for details."
+                )));
+            }

Note the test in advisory_locking.rs only asserts on the substrings "pg_try_advisory_lock" and "Another instance", so this refactor stays test-compatible.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let result = connection
.query_raw("SELECT pg_try_advisory_lock(72707369)", &[])
.await
.map_err(imp::quaint_error_mapper(params))?;
let acquired = result
.first()
.and_then(|row| row.at(0).and_then(|v| v.as_bool()))
.unwrap_or(false);
if !acquired {
return Err(ConnectorError::from_msg(
"Could not acquire the postgres advisory lock (SELECT pg_try_advisory_lock(72707369)). \
Another instance is likely running migrations against this database. \
See https://pris.ly/d/migrate-advisory-locking for details."
.to_owned(),
));
}
const MIGRATE_ADVISORY_LOCK_ID: i64 = 72707369;
let result = connection
.query_raw(
&format!("SELECT pg_try_advisory_lock({MIGRATE_ADVISORY_LOCK_ID})"),
&[],
)
.await
.map_err(imp::quaint_error_mapper(params))?;
let acquired = result
.first()
.and_then(|row| row.at(0).and_then(|v| v.as_bool()))
.unwrap_or(false);
if !acquired {
return Err(ConnectorError::from_msg(format!(
"Could not acquire the postgres advisory lock (SELECT pg_try_advisory_lock({MIGRATE_ADVISORY_LOCK_ID})). \
Another instance is likely running migrations against this database. \
See https://pris.ly/d/migrate-advisory-locking for details."
)));
}
🤖 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 `@schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs` around
lines 378 - 395, Extract the hardcoded advisory lock id (72707369) into a named
const (e.g., ADVISORY_LOCK_ID) and use that const when constructing the SQL
passed to connection.query_raw and when composing the ConnectorError::from_msg
string (and update the nearby comment to reference the const as well); ensure
the SQL string is formatted to include the const value and the error message is
built with the same const so the id is not duplicated in literals anywhere in
this block (look for connection.query_raw(...) and ConnectorError::from_msg(...)
to apply the change).

Comment on lines +383 to +386
let acquired = result
.first()
.and_then(|row| row.at(0).and_then(|v| v.as_bool()))
.unwrap_or(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

unwrap_or(false) will misreport unparseable results as “another instance is running”.

If pg_try_advisory_lock ever returns no row or a non-boolean (e.g. due to an unexpected driver shape), the code silently treats that as “lock not acquired” and surfaces the contention error to the user — which is misleading and undebuggable. Distinguish "couldn’t parse the result" from "lock was contended":

🛡️ Proposed fix
-            let acquired = result
-                .first()
-                .and_then(|row| row.at(0).and_then(|v| v.as_bool()))
-                .unwrap_or(false);
+            let acquired = result
+                .first()
+                .and_then(|row| row.at(0).and_then(|v| v.as_bool()))
+                .ok_or_else(|| {
+                    ConnectorError::from_msg(
+                        "Unexpected result from pg_try_advisory_lock(72707369): \
+                         expected a single boolean column."
+                            .to_owned(),
+                    )
+                })?;
🤖 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 `@schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs` around
lines 383 - 386, The current parsing of pg_try_advisory_lock result (the let
acquired = ... expression) uses unwrap_or(false), which treats parsing failures
as "lock not acquired"; change this to explicitly distinguish parsing errors
from a contended lock by: attempt to parse the boolean from
result.first().and_then(|row| row.at(0).and_then(|v| v.as_bool())) and if None
or a parsing error occurs, return/propagate a distinct error (e.g., a
parse/driver error) instead of treating it as false; only treat a successfully
parsed false as "lock contended". Ensure the code paths and error messages that
use the acquired variable (the parsing expression and the subsequent contention
check around pg_try_advisory_lock) are updated to handle and surface the parse
error separately.

Comment on lines +100 to 138
// Regression test for https://github.com/prisma/prisma-engines/issues/5755.
// We hold a session-level advisory lock on `72707369` from an external
// connection, then try to apply a migration. The schema engine must fail fast
// with an advisory-lock error instead of blocking (and potentially deadlocking
// against concurrent operations such as `CREATE INDEX CONCURRENTLY`).
#[test_connector(tags(Postgres), exclude(CockroachDb))]
fn postgres_advisory_lock_contention_fails_fast(mut api: TestApi) {
let mut me = api.new_engine();
let migrations_directory = api.create_migrations_directory();

let dm = api.datamodel_with_provider(
r#"
model Cat {
id Int @id
inBox Boolean
}
"#,
);

me.create_migration("01initial", &dm, &migrations_directory)
.draft(true)
.send_sync();

// Acquire a dedicated connection (separate from the engine's connection)
// and grab the same advisory lock the schema engine uses.
let lock_holder = tok(quaint::single::Quaint::new(api.connection_string())).unwrap();
tok(lock_holder.raw_cmd("SELECT pg_advisory_lock(72707369)")).unwrap();

assert_eq!(empty_results_count, 2);
let err = tok(async { me.apply_migrations(&migrations_directory).send().await }).unwrap_err();

// Release the lock so the test database teardown is not blocked.
tok(lock_holder.raw_cmd("SELECT pg_advisory_unlock(72707369)")).unwrap();

let msg = err.to_string();
assert!(
msg.contains("pg_try_advisory_lock") && msg.contains("Another instance"),
"unexpected error: {msg}"
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Solid regression test, but consider tightening lock cleanup and message coupling.

Two small things:

  1. Lines 128/134–137: if apply_migrations unexpectedly succeeds, unwrap_err() panics before the explicit pg_advisory_unlock runs; same for any later assertion failure. In practice the session-level lock releases when lock_holder is dropped (Quaint::single is a single connection), so this is safe today, but a small RAII-style guard or Drop helper would make it robust against future refactors that share the connection.
  2. Lines 134–137: the assertion couples the test to the literal substring "pg_try_advisory_lock" from the production error message. If you adopt the suggested constant-based formatting in postgres.rs, this still passes; just be aware the substring match is the only contract.

Otherwise the test correctly reproduces the #5755 contention scenario by holding the lock from a separate session and asserting fast failure.

🤖 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 `@schema-engine/sql-migration-tests/tests/migrations/advisory_locking.rs`
around lines 100 - 138, The test postgres_advisory_lock_contention_fails_fast
currently may leak the advisory lock if unwrap_err() or the assertion panics;
wrap the dedicated connection/lock_holder in a small RAII guard (e.g.,
LockHolderGuard) that holds the Quaint connection and issues "SELECT
pg_advisory_unlock(72707369)" in its Drop implementation so the unlock always
runs, replace the explicit tok(lock_holder.raw_cmd(...)) call with constructing
the guard, and relax the assertion by checking for the stable parts of the error
(e.g., "Another instance" plus the production constant if available such as an
ADVISORY_LOCK message constant in postgres.rs, otherwise keep a minimal
substring) instead of coupling to the literal "pg_try_advisory_lock".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants