From 6e1772e469bd00bce64394766d999019f8f802f5 Mon Sep 17 00:00:00 2001 From: Chris Munns Date: Sat, 25 Jul 2026 12:41:25 -0400 Subject: [PATCH 1/2] follow: release the source snapshot connection after the catalog read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pgcopydb follow` (standalone, e.g. `follow --resume`) opened a source connection to hold a snapshot for the initial catalog read, then never closed it — it stayed idle in transaction on the source for the entire follow run (which can be days). On a --resume run the catalog read is a cache hit, so the connection is opened and never even used, yet still held. The follow phase streams from the replication slot, and the database setup and sequence-reset steps each open their own connection (via copydb_copy_snapshot), so nothing needs this connection after the catalog read. Close it right after, mirroring clone --follow, which already closes its snapshot once the base copy is done. Gate the close on the live connection rather than sourceSnapshot.state: when the catalog read runs, copydb_fetch_schema_and_prepare_specs commits and finishes this connection itself but leaves state as SET, so state is unreliable. A non-NULL connection means the fetch left it open and it is ours to close; a NULL connection means it was already torn down. It holds no snapshot in the --resume case (backend_xmin is NULL), so this is a connection-hygiene fix, not a vacuum-horizon fix. --- src/bin/pgcopydb/cli_clone_follow.c | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/bin/pgcopydb/cli_clone_follow.c b/src/bin/pgcopydb/cli_clone_follow.c index ced0f1e1..5175707b 100644 --- a/src/bin/pgcopydb/cli_clone_follow.c +++ b/src/bin/pgcopydb/cli_clone_follow.c @@ -645,6 +645,37 @@ cli_follow(int argc, char **argv) exit(EXIT_CODE_SOURCE); } + /* + * The source snapshot connection is only needed for the one-time catalog + * read just above. The follow phase itself streams from the replication + * slot, and the database setup and sequence-reset steps each open their + * own connection (they call copydb_copy_snapshot, which copies the + * snapshot metadata into a fresh connection). Nothing uses this connection + * again. + * + * Release it now instead of holding it until the process exits. Otherwise + * a follow-only run (e.g. `follow --resume`, where the catalog read is a + * cache hit and this connection is never even used) leaves an + * idle-in-transaction connection open on the source for the entire, + * potentially days-long, follow run. This mirrors clone --follow, which + * already closes the snapshot once the base copy is done. + * + * Gate on the live connection, not sourceSnapshot.state: when the catalog + * read did run it commits and finishes this connection itself (see + * pgsql_commit in copydb_fetch_schema_and_prepare_specs) but leaves state + * as SET, so state is unreliable here. A non-NULL connection means the + * fetch left it open (the cache-hit / not-consistent cases) and it is ours + * to close. + */ + if (copySpecs.sourceSnapshot.pgsql.connection != NULL) + { + if (!copydb_close_snapshot(©Specs)) + { + /* errors have already been logged */ + exit(EXIT_CODE_SOURCE); + } + } + if (!follow_main_loop(©Specs, &specs)) { /* errors have already been logged */ From 06a6ded9feffc4d5be07ded86e19ce124daf072c Mon Sep 17 00:00:00 2001 From: Chris Munns Date: Tue, 4 Aug 2026 15:42:51 +0200 Subject: [PATCH 2/2] Trim the snapshot-close comment --- src/bin/pgcopydb/cli_clone_follow.c | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/bin/pgcopydb/cli_clone_follow.c b/src/bin/pgcopydb/cli_clone_follow.c index 5175707b..d0c19b4c 100644 --- a/src/bin/pgcopydb/cli_clone_follow.c +++ b/src/bin/pgcopydb/cli_clone_follow.c @@ -646,26 +646,10 @@ cli_follow(int argc, char **argv) } /* - * The source snapshot connection is only needed for the one-time catalog - * read just above. The follow phase itself streams from the replication - * slot, and the database setup and sequence-reset steps each open their - * own connection (they call copydb_copy_snapshot, which copies the - * snapshot metadata into a fresh connection). Nothing uses this connection - * again. - * - * Release it now instead of holding it until the process exits. Otherwise - * a follow-only run (e.g. `follow --resume`, where the catalog read is a - * cache hit and this connection is never even used) leaves an - * idle-in-transaction connection open on the source for the entire, - * potentially days-long, follow run. This mirrors clone --follow, which - * already closes the snapshot once the base copy is done. - * - * Gate on the live connection, not sourceSnapshot.state: when the catalog - * read did run it commits and finishes this connection itself (see - * pgsql_commit in copydb_fetch_schema_and_prepare_specs) but leaves state - * as SET, so state is unreliable here. A non-NULL connection means the - * fetch left it open (the cache-hit / not-consistent cases) and it is ours - * to close. + * The snapshot connection is only needed for the catalog read above; + * nothing in the follow phase uses it. Close it now so a follow-only run + * doesn't hold it idle-in-transaction for the whole run. Gate on the live + * connection, not sourceSnapshot.state, which the catalog read leaves stale. */ if (copySpecs.sourceSnapshot.pgsql.connection != NULL) {