diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index e011789fe36..ee4cb870667 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -1318,16 +1318,16 @@ ApplyLauncherMain(Datum main_arg) can_update_xmin &= sub->enabled; /* - * Reset the slot's xmin to the safe decoding horizon if - * it is not valid, or if this database newly appears - * among the retained databases: the xmin may be newer - * than the new database's oldest active transaction ID, - * violating the per-database invariant checked in - * get_candidate_xid(). The set is keyed on - * subretentionactive, so that a subscription resuming - * retention is also treated as a new arrival. One call - * per cycle is enough, as the horizon is a safe seed for - * every database. + * A retaining apply worker starts with the slot's xmin as + * its oldest_nonremovable_xid (see + * logicalrep_worker_launch()), so a database whose oldest + * active transaction ID precedes that xmin would trip the + * assertion in get_candidate_xid(). Reset the xmin before + * starting the worker, which also seeds it on the first + * cycle. The set of retained databases is keyed on + * subretentionactive, so a subscription resuming retention + * is also treated as a new arrival. One call per cycle is + * enough, as a single reset covers all such databases. */ if (!reset_done && (!TransactionIdIsValid(MyReplicationSlot->data.xmin) || @@ -1349,9 +1349,7 @@ ApplyLauncherMain(Datum main_arg) /* * Compute the minimum xmin required to protect dead tuples * required for conflict detection among all running apply - * workers. One subscription that cannot contribute is enough to - * prevent the slot's xmin from advancing, so stop collecting once - * that happens. + * workers. */ can_update_xmin = can_update_xmin && collect_min_nonremovable_xid(sub, w, &xmin); @@ -1461,13 +1459,14 @@ ApplyLauncherMain(Datum main_arg) } /* - * Fold this subscription's minimum non-removable transaction ID into *xmin, - * for subscriptions that are actively retaining conflict information. + * Determine the minimum non-removable transaction ID required for conflict + * detection, accumulating the contribution of the given subscription in + * *xmin. Subscriptions that are not actively retaining conflict information + * are ignored. * * Returns false if the slot's xmin cannot be advanced in this cycle, which is * the case when the apply worker is not running (worker is NULL) or does not - * yet hold a valid oldest_nonremovable_xid. Subscriptions that are not - * retaining place no constraint on the xmin and return true. + * yet hold a valid oldest_nonremovable_xid. * * The caller must hold LogicalRepWorkerLock, to prevent accessing invalid * worker data in scenarios where a worker might exit and reset its state @@ -1572,14 +1571,11 @@ update_conflict_slot_xmin(TransactionId new_xmin) /* * Reset the xmin of the conflict detection slot to the cluster-wide safe - * decoding horizon, which is a safe seed for an apply worker in any - * database. + * decoding horizon, which accounts for all running transactions and is + * therefore safe for an apply worker in any database. Called when the slot is + * created and when a database newly needs retention. * - * This is used both for the initial setup and when a database newly appears - * among the databases with actively-retaining subscriptions; see - * ApplyLauncherMain(). An already-valid xmin is only moved backwards, as - * advancing it here would bypass the advancement protocol; regressing is - * safe, and the workers will advance the xmin again in later cycles. + * The xmin is left unchanged if it is already at or before the horizon. */ static void reset_conflict_slot_xmin_to_safe_horizon(void) @@ -1597,7 +1593,10 @@ reset_conflict_slot_xmin_to_safe_horizon(void) /* * Nothing to do if the current xmin is valid and not newer than the - * horizon. + * horizon. The xmin must not be advanced here, as advancing is only safe + * once the apply workers confirm that all concurrent transactions have + * been applied. Regressing it only retains more than necessary, and the + * workers will advance it again in later cycles. */ if (TransactionIdIsValid(old_xmin) && TransactionIdPrecedesOrEquals(old_xmin, xmin_horizon))