Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wdICt-0040gb-1Z for pgpool-hackers@arkaria.postgresql.org; Sat, 27 Jun 2026 01:51:44 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wdICq-00DmEh-0t for pgpool-hackers@arkaria.postgresql.org; Sat, 27 Jun 2026 01:51:40 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wdICq-00DmEX-01 for pgpool-hackers@lists.postgresql.org; Sat, 27 Jun 2026 01:51:40 +0000 Received: from meldrar.postgresql.org ([2a02:c0:301:0:ffff::31]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wdICn-00000000R78-0EfI for pgpool-hackers@lists.postgresql.org; Sat, 27 Jun 2026 01:51:39 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Content-Transfer-Encoding:Content-Type: Mime-Version:References:In-Reply-To:From:Subject:Cc:To:Message-Id:Date:Sender :Reply-To:Content-ID:Content-Description; bh=eypfpMJm72YOUJraK0jHbAixR7AojEzCdlKWjm5Xy1c=; b=kr/eASCG5cJ+VyP5vV8CYpbq8s 3Hmu6rvY83ICdx/9GLtCmu5V6AJ+b7mnRqw3HHBpKdXAnhHCx4flf5LzZ8gVlKF7zQFtoLCH0RLEd eQqYUbDZibRXZaQ8k7Q/1i2j1JVJ2o0FgNJ5wzZStA4AEbwL1ni57M3s6xvM0r6bsIEM+vUrBe5oi cYzt19sIRIv+HCMy4HFRh5nfzMRKabBuT3TmjiQHcaUt5D3fHFxdR0/mMlBOof/R1z/+qRedEmIAB 0fPkMyu9PSXMFDsNlJRt393+eJX5K2Z/yTBQGcwxkfbT2rP/jCon3xMealnppGOZiS/lic1tqioP5 +N8S+4dQ==; Received: from [2409:11:4120:300:4a50:57ef:8777:c35a] (helo=localhost) by meldrar.postgresql.org with esmtpsa (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wdICh-006hEO-27; Sat, 27 Jun 2026 01:51:36 +0000 Date: Sat, 27 Jun 2026 10:51:08 +0900 (JST) Message-Id: <20260627.105108.665094997299401916.ishii@postgresql.org> To: nadav@tailorbrands.com Cc: pgpool-hackers@lists.postgresql.org Subject: Re: Proposal: Recent mutated table tracking in memory From: Tatsuo Ishii In-Reply-To: References: <20260626.074028.806271918741516663.ishii@postgresql.org> X-Mailer: Mew version 6.8 on Emacs 29.3 Mime-Version: 1.0 Content-Type: Text/Plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-Host-Lookup-Failed: Reverse DNS lookup failed for 2409:11:4120:300:4a50:57ef:8777:c35a (failed) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Hi Nadav, > Hi Tatsuo, > > Sorry -- my wording was misleading. No, unpatched pgpool does NOT have > this problem, and you're right that your test shows no hang on master. > The hang is entirely in the new code my patch adds, not in pgpool > itself. > > To be precise: the marking code that hung is the dml_adaptive_global > branch in handle_query_context() (CommandComplete.c) that my patch > introduces. It is gated on disable_load_balance_on_write = > dml_adaptive_global, which only exists with the patch applied, so master > never runs it. > > It also doesn't fire for your exact sequence, for two reasons: > > 1. You ran it on master, which doesn't contain the branch at all. > 2. Even on a patched build, your statements are inside an explicit > transaction (BEGIN ... COMMIT), and that branch only runs for > autocommit statements (the "!is_in_transaction" case). > > What actually hung, on a patched build with dml_adaptive_global set in > streaming replication, was a *bare autocommit* statement (no surrounding > transaction) whose table OID cannot be served from the relcache -- for > example: > > DROP TABLE IF EXISTS non_existent_table; -- no BEGIN/COMMIT > > Because the name is unresolvable, pool_table_name_to_oid() does not cache > it (no_cache_if_zero), so every lookup is a relcache miss. The marking > branch calls pool_extract_table_oids() -> ... -> do_query() to resolve > the OID, but handle_query_context() runs before the statement's > CommandComplete response has been read from the backend, so injecting > that query desyncs the protocol and the session blocks. Backtrace: > > #2 pool_read2 (blocked in read()) > #3 do_query "SELECT > COALESCE(pg_catalog.to_regclass('...')::oid, 0)" > #4 pool_search_relcache (relcache miss) > #5 pool_table_name_to_oid > #6 pool_extract_table_oids query_cache/pool_memqcache.c > #7 handle_query_context protocol/CommandComplete.c > (!is_in_transaction branch) > #8 CommandComplete > > This is the same class of issue as the one behind your original review > comment: resolving OIDs at CommandComplete time means doing backend I/O > while a response is in flight. > > v6-0001 fixes it for good by resolving the written tables' OIDs (and the > database OID) at DML *routing* time -- when the connection is idle and > do_query is safe, the same point the feature already resolves OIDs for > the SELECT staleness check. handle_query_context() then only marks the > already-resolved OIDs and does no backend I/O at all, for both the > autocommit and the COMMIT paths. > > Verified on a PG16 streaming-replication cluster with dml_adaptive_global: > the bare "DROP TABLE IF EXISTS non_existent_table;" and your > "BEGIN; DROP TABLE IF EXISTS ...; COMMIT;" both return promptly, the > deferred-constraint COMMIT failure leaves the table unmarked (reads keep > load-balancing to the standby), and a successful autocommit or COMMIT > write marks the table and forces reads to the primary. Understood. > Sorry again for the confusing earlier phrasing. No problem. Thank you for detailed explanation! So, my review comments are below. Mostly cosmetic coding style suggestions. diff --git a/src/context/pool_query_context.c b/src/context/pool_query_context.c index 73bc64338..a7756ec8b 100644 --- a/src/context/pool_query_context.c +++ b/src/context/pool_query_context.c @@ -1848,20 +1849,26 @@ is_in_list(char *name, List *list) static bool is_select_object_in_temp_write_list(Node *node, void *context) { - if (node == NULL || pool_config->disable_load_balance_on_write != DLBOW_DML_ADAPTIVE) + if (node == NULL || + !DLBOW_IS_DML_ADAPTIVE(pool_config->disable_load_balance_on_write)) return false; if (IsA(node, RangeVar)) { RangeVar *rgv = (RangeVar *) node; - POOL_SESSION_CONTEXT *session_context = pool_get_session_context(false); + POOL_SESSION_CONTEXT *session_context; - if (pool_config->disable_load_balance_on_write == DLBOW_DML_ADAPTIVE && session_context->is_in_transaction) + session_context = pool_get_session_context(false); + + if (session_context->is_in_transaction) { ereport(DEBUG1, - (errmsg("is_select_object_in_temp_write_list: \"%s\", found relation \"%s\"", (char *) context, rgv->relname))); + (errmsg("is_select_object_in_temp_write_list:" + " \"%s\", found relation \"%s\"", + (char *) context, rgv->relname))); I guess you aimed to shorten the message so that it does not exceed line length limit (78). But IMO the rule is not applied to message format strings because this kind of style (divide the message line into multiple lines) is hard to read. I suggest to restore to former style. Same suggestion is applied to all other similar places. @@ -2169,6 +2275,153 @@ where_to_send_main_replica(POOL_QUERY_CONTEXT *query_context, char *query, Node { pool_set_node_to_be_sent(query_context, PRIMARY_NODE_ID); } + + /* + * Check track table mutation for recently written tables. If + * in cold start or any table was recently written, route to + * primary to avoid stale reads. + */ + else if (pool_config->disable_load_balance_on_write == + DLBOW_DML_ADAPTIVE_GLOBAL) + { This "else if" branch is too large and hard to read. You should move the code of the branch to a subroutine and call it from here. diff --git a/src/include/utils/pool_track_table_mutation.h b/src/include/utils/pool_track_table_mutation.h new file mode 100644 index 000000000..dfbac666d --- /dev/null +++ b/src/include/utils/pool_track_table_mutation.h @@ -0,0 +1,167 @@ +/* -*-pgsql-c-*- */ +/* + * pgpool: a language independent connection pool server for PostgreSQL + * written by Tatsuo Ishii + * + * Copyright (c) 2003-2026 PgPool Global Development Group Since this file is new, the Copyright year should be just 2026. + * Copyright (c) 2026 PgPool Global Development Group diff --git a/src/protocol/CommandComplete.c b/src/protocol/CommandComplete.c index 1f63a0e8d..76e77fea0 100644 --- a/src/protocol/CommandComplete.c +++ b/src/protocol/CommandComplete.c diff --git a/src/utils/pool_track_table_mutation.c b/src/utils/pool_track_table_mutation.c new file mode 100644 index 000000000..e7771e7bf --- /dev/null +++ b/src/utils/pool_track_table_mutation.c @@ -0,0 +1,902 @@ +/* -*-pgsql-c-*- */ +/* + * pgpool: a language independent connection pool server for PostgreSQL + * written by Tatsuo Ishii + * + * Copyright (c) 2003-2026 PgPool Global Development Group Since this file is new, the Copyright year should be just 2026. + * Copyright (c) 2026 PgPool Global Development Group +/* ---------------- + * Hash functions + * ---------------- + */ + +/* + * FNV-1a hash for table/database oid pair + */ Can you please add more comments regarding FNV-1a? I'm not sure everyone is famimilar with FNV-1a (at lease I am not). +/* + * Get current time + */ +static void +get_current_time(struct timeval *tv) +{ + gettimeofday(tv, NULL); +} Just out of curiosity, why you don't call gettimeofday directly? Regards, -- Tatsuo Ishii SRA OSS K.K. English: http://www.sraoss.co.jp/index_en/ Japanese:http://www.sraoss.co.jp