From 4d5d8ec0945c1da179e35d1654c79aa6c697b983 Mon Sep 17 00:00:00 2001 From: fox0430 Date: Tue, 14 Jul 2026 19:40:52 +0900 Subject: [PATCH] fix: reject transaction-level advisory lock calls outside a transaction --- async_postgres/pg_advisory_lock.nim | 14 +++++++++++++- tests/test_advisory_lock.nim | 30 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/async_postgres/pg_advisory_lock.nim b/async_postgres/pg_advisory_lock.nim index d7ef202..146b0cd 100644 --- a/async_postgres/pg_advisory_lock.nim +++ b/async_postgres/pg_advisory_lock.nim @@ -56,7 +56,7 @@ import std/[macros, importutils] -import async_backend, pg_types, pg_connection, pg_client +import async_backend, pg_protocol, pg_types, pg_connection, pg_client privateAccess(PgConnection) @@ -89,14 +89,26 @@ template unlockSessionLock( dec conn.heldSessionLocks released +proc ensureXactScope(conn: PgConnection) {.inline.} = + # In tsIdle the acquire's implicit tx would commit and drop the lock before + # body runs, silently losing mutual exclusion. + if conn.txStatus != tsInTransaction: + raise newException( + PgStateError, + "transaction-level advisory lock requires an active transaction " & "(txStatus: " & + $conn.txStatus & "); wrap the call in withTransaction", + ) + template acquireXactLock( conn: PgConnection, sql: string, params: seq[PgParam], t: Duration ) = + ensureXactScope(conn) discard await conn.queryValue(sql, params, timeout = t) template tryXactLock( conn: PgConnection, sql: string, params: seq[PgParam], t: Duration ): bool = + ensureXactScope(conn) await conn.queryValue(bool, sql, params, timeout = t) # Session-level exclusive locks diff --git a/tests/test_advisory_lock.nim b/tests/test_advisory_lock.nim index 9885d6c..af53904 100644 --- a/tests/test_advisory_lock.nim +++ b/tests/test_advisory_lock.nim @@ -166,6 +166,30 @@ suite "Advisory Lock: transaction-level (int64)": waitFor t() + test "xact lock outside transaction raises PgStateError": + proc t() {.async.} = + let conn = await connect(plainConfig()) + defer: + await conn.close() + doAssertRaises(PgStateError): + waitFor conn.advisoryLockXact(40010'i64) + doAssertRaises(PgStateError): + waitFor conn.advisoryLockXactShared(40011'i64) + doAssertRaises(PgStateError): + discard waitFor conn.advisoryTryLockXact(40012'i64) + doAssertRaises(PgStateError): + discard waitFor conn.advisoryTryLockXactShared(40013'i64) + doAssertRaises(PgStateError): + waitFor conn.advisoryLockXact(1'i32, 2'i32) + doAssertRaises(PgStateError): + waitFor conn.advisoryLockXactShared(3'i32, 4'i32) + doAssertRaises(PgStateError): + discard waitFor conn.advisoryTryLockXact(5'i32, 6'i32) + doAssertRaises(PgStateError): + discard waitFor conn.advisoryTryLockXactShared(7'i32, 8'i32) + + waitFor t() + suite "Advisory Lock: two-key (int32, int32)": test "lock and unlock": proc t() {.async.} = @@ -228,8 +252,9 @@ suite "Advisory Lock: two-key (int32, int32)": await conn2.close() conn1.withTransaction: await conn1.advisoryLockXactShared(9'i32, 10'i32) - let acquired = await conn2.advisoryTryLockXactShared(9'i32, 10'i32) + let acquired = await conn2.advisoryTryLockShared(9'i32, 10'i32) doAssert acquired + discard await conn2.advisoryUnlockShared(9'i32, 10'i32) waitFor t() @@ -392,8 +417,9 @@ suite "Advisory Lock: withAdvisoryLockXact template": await conn2.close() conn1.withTransaction: conn1.withAdvisoryLockXactShared(17'i32, 18'i32): - let acquired = await conn2.advisoryTryLockXactShared(17'i32, 18'i32) + let acquired = await conn2.advisoryTryLockShared(17'i32, 18'i32) doAssert acquired + discard await conn2.advisoryUnlockShared(17'i32, 18'i32) waitFor t()