fix(datatables): keep an instance database a settings save is waiting to name

Cleanup for a database whose setup failed took the lock first, read no user,
and dropped it while a save blocked on that same lock was about to commit a
reference to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-09-21 10:03:58 +02:00
co-authored by Claude Opus 5
parent 538d098a79
commit 953c6903bb
+29 -2
View File
@@ -1525,12 +1525,39 @@ async fn drop_if_unused_on(
dbname: &str,
) -> error::Result<Vec<String>> {
let users = instance_database_users(conn, dbname).await?;
if users.is_empty() {
drop_custom_instance_database_on(conn, dbname).await?;
if !users.is_empty() {
return Ok(users);
}
// A settings save naming this database takes the same lock, so one already waiting on it is
// about to name a database this would drop: its own read of the users happens after the drop,
// and it would commit a reference to nothing. Keeping it is what the save expects, and the
// name is then reported as still in use.
if let Some(waiter) = waiting_for_instance_database(conn, dbname).await? {
return Ok(vec![waiter]);
}
drop_custom_instance_database_on(conn, dbname).await?;
Ok(users)
}
/// A transaction blocked on `dbname`'s instance-database lock, named as its `pid`. The lock is
/// taken by key, so `pg_locks` reports it split across `classid` and `objid`.
async fn waiting_for_instance_database(
conn: &mut sqlx::PgConnection,
dbname: &str,
) -> error::Result<Option<String>> {
let pid: Option<i32> = sqlx::query_scalar(
"SELECT l.pid FROM pg_locks l
WHERE l.locktype = 'advisory' AND NOT l.granted
AND l.classid = ((hashtext('instance_database:' || $1)::bigint >> 32) & 4294967295)::oid
AND l.objid = (hashtext('instance_database:' || $1)::bigint & 4294967295)::oid
LIMIT 1",
)
.bind(dbname)
.fetch_optional(&mut *conn)
.await?;
Ok(pid.map(|pid| format!("a request waiting to name it (pid {pid})")))
}
/// The workspaces naming instance database `dbname` as a data table or a ducklake catalog. Taken
/// under [`lock_instance_databases`] for `dbname`, the answer holds until that lock is released.
///