From 748e746b388f6bc35b2d829521f11f0864daa314 Mon Sep 17 00:00:00 2001 From: mhenrixon Date: Fri, 21 Aug 2026 10:40:58 +0200 Subject: [PATCH] Detect advisory locks at query_parser = auto on single-primary clusters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At the auto level (the default), the regex fallback in RegexParser::use_parser matched only the base session-control command set. On clusters where nothing else engages the query parser — a single primary, no sharding, no replicas — advisory lock statements therefore bypassed the parser entirely: the client was never pinned to its backend, pg_advisory_unlock could route to a different server connection (the WARNING is silently swallowed), and the session lock stranded on the pooled backend until it happened to be closed. Sharded and read/write-split clusters were unaffected because the parser is already fully engaged there. Match the advisory regex set (base + advisory) at auto, so session advisory locks pin, unlock correctly, and get cleaned up by pg_advisory_unlock_all when a client disconnects mid-hold. Fixes #1407 --- pgdog/src/frontend/regex_parser.rs | 35 +++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/pgdog/src/frontend/regex_parser.rs b/pgdog/src/frontend/regex_parser.rs index f3899428e..2326a9961 100644 --- a/pgdog/src/frontend/regex_parser.rs +++ b/pgdog/src/frontend/regex_parser.rs @@ -60,9 +60,17 @@ impl RegexParser { /// Check if we should enable the parser just for this request. pub(crate) fn use_parser(&self, request: &ClientRequest) -> bool { - let with_locks = self.level == QueryParserLevel::SessionControlAndLocks; - let session_control = - self.level == QueryParserLevel::SessionControl || self.level == QueryParserLevel::Auto; + // Auto must detect advisory locks even when nothing else needs the + // parser (single primary, no sharding): session advisory locks taken + // through a transaction-mode pool are a correctness hazard — without + // detection the client isn't pinned, the unlock can land on a + // different backend, and the lock strands on the pooled server + // connection. + let with_locks = matches!( + self.level, + QueryParserLevel::SessionControlAndLocks | QueryParserLevel::Auto + ); + let session_control = self.level == QueryParserLevel::SessionControl; if (with_locks || session_control) && let Ok(Some(query)) = request.query() @@ -198,6 +206,27 @@ mod test { )); } + #[test] + fn test_advisory_lock_auto_level() { + // Auto must detect advisory locks even on clusters where nothing else + // enables the parser (single primary, no sharding); otherwise session + // locks taken through a transaction-mode pool strand on pooled + // backends when the unlock routes to a different server connection. + let l = QueryParserLevel::Auto; + assert!(matches_at("SELECT pg_advisory_lock(1)", l)); + assert!(matches_at("SELECT pg_try_advisory_lock(1, 2)", l)); + assert!(matches_at( + "SELECT pg_try_advisory_lock(1, 2) AS t0abc /* app lock */", + l + )); + assert!(matches_at("SELECT pg_advisory_unlock(1, 2)", l)); + assert!(matches_at("SELECT pg_advisory_unlock_all()", l)); + // Base session-control statements still match at Auto. + assert!(matches_at("NOTIFY test_channel", l)); + // Plain queries still bypass the parser at Auto. + assert!(!matches_at("SELECT 1", l)); + } + #[test] fn test_scan_prefix_limit() { let l = QueryParserLevel::SessionControlAndLocks;