From 0ca3311b261086ae86afb3b5877842a13728c808 Mon Sep 17 00:00:00 2001 From: Chris Munns Date: Fri, 24 Jul 2026 14:27:47 -0400 Subject: [PATCH] Re-drive index/constraint creation for split tables on --resume For a table split into multiple parallel COPY parts, index and constraint creation is gated on a per-run "who finished the last part" election stored in the s_table_parts_done / s_table_indexes_done SQLite tables (tableoid, pid). The gate only fires for the worker whose getpid() matches the stored pid. On --resume the stored pid belongs to a dead prior-run process, so no live worker matches and the index/constraint enqueue is never re-driven. A table whose parts all finished but whose PK constraint was not yet built in the prior run stays with a bare leaf index, and pg_restore post-data then fails to attach it (relation "..._pkey" already exists / cannot attach index / no unique constraint matching given keys). Clear both election tables once at the start of a --resume clone, single-threaded and before any table-data worker is forked, so a fresh election runs among live workers. These tables only hold per-run election tokens; progress comes from the summary table (done_time_epoch), so clearing them loses no progress, and re-enqueue is idempotent. --- src/bin/pgcopydb/cli_clone_follow.c | 15 +++++++++++ src/bin/pgcopydb/copydb.h | 1 + src/bin/pgcopydb/copydb_schema.c | 39 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/bin/pgcopydb/cli_clone_follow.c b/src/bin/pgcopydb/cli_clone_follow.c index b0ded2b2..ced0f1e1 100644 --- a/src/bin/pgcopydb/cli_clone_follow.c +++ b/src/bin/pgcopydb/cli_clone_follow.c @@ -773,6 +773,21 @@ cloneDB(CopyDataSpec *copySpecs) return false; } + /* + * On --resume the per-run part/index election tables still hold the pids of + * dead prior-run workers. Clear them now, single-threaded and before any + * table-data worker is forked, so a fresh election among live workers + * re-drives index/constraint creation for split (multi-part) tables. + */ + if (copySpecs->resume) + { + if (!copydb_resume_reset_part_election(copySpecs)) + { + /* errors have already been logged */ + return false; + } + } + /* now register in the catalogs the already known startTime */ if (!summary_start_timing(sourceDB, TIMING_SECTION_TOTAL)) { diff --git a/src/bin/pgcopydb/copydb.h b/src/bin/pgcopydb/copydb.h index b41454cb..cfd73b81 100644 --- a/src/bin/pgcopydb/copydb.h +++ b/src/bin/pgcopydb/copydb.h @@ -413,6 +413,7 @@ bool copydb_prepare_sequence_specs(CopyDataSpec *specs, PGSQL *pgsql, bool reset /* copydb_schema.c */ bool copydb_fetch_schema_and_prepare_specs(CopyDataSpec *specs); +bool copydb_resume_reset_part_election(CopyDataSpec *specs); bool copydb_objectid_is_filtered_out(CopyDataSpec *specs, uint32_t catalogOid, uint32_t oid, diff --git a/src/bin/pgcopydb/copydb_schema.c b/src/bin/pgcopydb/copydb_schema.c index 2aada56b..b9d4ee3f 100644 --- a/src/bin/pgcopydb/copydb_schema.c +++ b/src/bin/pgcopydb/copydb_schema.c @@ -179,6 +179,45 @@ copydb_fetch_schema_and_prepare_specs(CopyDataSpec *specs) } +/* + * copydb_resume_reset_part_election clears the per-run "who finished last" + * election tables (s_table_parts_done, s_table_indexes_done) so that a + * --resume run holds a fresh election among live workers. + * + * These tables only ever store per-run election tokens (a tableoid and the + * getpid() of the worker that observed the last part/index as done); progress + * itself lives in the summary table (done_time_epoch). On resume the stored + * pids belong to dead prior-run processes, so no live worker matches and the + * index/constraint enqueue for a split (multi-part) table is never re-driven. + * Clearing the tokens loses no progress and re-enqueue is idempotent + * (IF NOT EXISTS / doneTime / foundConstraintOnTarget checks). + * + * Must run single-threaded before any COPY/table-data worker is forked, or the + * workers would race and wipe freshly-elected tokens. + */ +bool +copydb_resume_reset_part_election(CopyDataSpec *specs) +{ + DatabaseCatalog *sourceDB = &(specs->catalogs.source); + + log_notice("Resetting stale part/index election state for --resume"); + + if (!catalog_execute(sourceDB, "delete from s_table_parts_done")) + { + /* errors have already been logged */ + return false; + } + + if (!catalog_execute(sourceDB, "delete from s_table_indexes_done")) + { + /* errors have already been logged */ + return false; + } + + return true; +} + + /* * copydb_fetch_source_catalog_setup initializes our local catalog cache and * checks the setup and cache state.