agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v1 1/2] Report wait event for cost-based vacuum delay 245+ messages / 4 participants [nested] [flat]
* [PATCH v1 1/2] Report wait event for cost-based vacuum delay @ 2020-03-21 01:47 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Justin Pryzby @ 2020-03-21 01:47 UTC (permalink / raw) --- doc/src/sgml/monitoring.sgml | 2 ++ src/backend/commands/vacuum.c | 2 ++ src/backend/postmaster/pgstat.c | 3 +++ src/include/pgstat.h | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 5bffdcce10..46c99a55b7 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -1507,6 +1507,8 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser (<filename>pg_wal</filename>, archive or stream) before trying again to retrieve WAL data, at recovery. </entry> + <entry><literal>VacuumDelay</literal></entry> + <entry>Waiting in a cost-based vacuum delay point.</entry> </row> <row> <entry morerows="68"><literal>IO</literal></entry> diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index d625d17bf4..59731d687f 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -2019,7 +2019,9 @@ vacuum_delay_point(void) if (msec > VacuumCostDelay * 4) msec = VacuumCostDelay * 4; + pgstat_report_wait_start(WAIT_EVENT_VACUUM_DELAY); pg_usleep((long) (msec * 1000)); + pgstat_report_wait_end(); VacuumCostBalance = 0; diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c index d29c211a76..742ec07b59 100644 --- a/src/backend/postmaster/pgstat.c +++ b/src/backend/postmaster/pgstat.c @@ -3824,6 +3824,9 @@ pgstat_get_wait_timeout(WaitEventTimeout w) case WAIT_EVENT_RECOVERY_RETRIEVE_RETRY_INTERVAL: event_name = "RecoveryRetrieveRetryInterval"; break; + case WAIT_EVENT_VACUUM_DELAY: + event_name = "VacuumDelay"; + break; /* no default case, so that compiler will warn */ } diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 851d0a7246..4db40e23cc 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -848,7 +848,8 @@ typedef enum WAIT_EVENT_BASE_BACKUP_THROTTLE = PG_WAIT_TIMEOUT, WAIT_EVENT_PG_SLEEP, WAIT_EVENT_RECOVERY_APPLY_DELAY, - WAIT_EVENT_RECOVERY_RETRIEVE_RETRY_INTERVAL + WAIT_EVENT_RECOVERY_RETRIEVE_RETRY_INTERVAL, + WAIT_EVENT_VACUUM_DELAY, } WaitEventTimeout; /* ---------- -- 2.17.0 --9jxsPFA5p3P2qPhR Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v1-0002-vacuum-to-report-time-spent-in-cost-based-delay.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v19] Avoid orphaned objects dependencies @ 2024-03-29 15:43 Bertrand Drouvot <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Bertrand Drouvot @ 2024-03-29 15:43 UTC (permalink / raw) It's currently possible to create orphaned objects dependencies, for example: Scenario 1: session 1: begin; drop schema schem; session 2: create a function in the schema schem session 1: commit; With the above, the function created in session 2 would be linked to a non existing schema. Scenario 2: session 1: begin; create a function in the schema schem session 2: drop schema schem; session 1: commit; With the above, the function created in session 1 would be linked to a non existing schema. To avoid those scenarios, a new lock (that conflicts with a lock taken by DROP) has been put in place before the dependencies are being recorded. With this in place, the drop schema in scenario 2 would be locked. Also, after the new lock attempt, the patch checks that the object still exists: with this in place session 2 in scenario 1 would be locked and would report an error once session 1 committs (that would not be the case should session 1 abort the transaction). The patch takes into account any type of objects except the ones that are pinned (they are not droppable because the system requires it). A special case is done for objects that belong to the RelationRelationId class. For those, we should be in one of the two following cases that would already prevent the relation to be dropped: 1. The relation is already locked (could be an existing relation or a relation that we are creating). 2. The relation is protected indirectly (i.e an index protected by a lock on its table, a table protected by a lock on a function that depends of the table...) To avoid any risks for the RelationRelationId class case, we acquire a lock if there is none. That may add unnecessary lock for 2. but that's worth it. The patch adds a few tests for some dependency cases (that would currently produce orphaned objects): - schema and function (as the above scenarios) - alter a dependency (function and schema) - function and arg type - function and return type - function and function - domain and domain - table and type - server and foreign data wrapper --- src/backend/catalog/aclchk.c | 1 + src/backend/catalog/dependency.c | 201 ++++++++++++++++++ src/backend/catalog/heap.c | 7 + src/backend/catalog/index.c | 26 +++ src/backend/catalog/objectaddress.c | 57 +++++ src/backend/catalog/pg_aggregate.c | 9 + src/backend/catalog/pg_attrdef.c | 1 + src/backend/catalog/pg_cast.c | 5 + src/backend/catalog/pg_collation.c | 1 + src/backend/catalog/pg_constraint.c | 26 +++ src/backend/catalog/pg_conversion.c | 2 + src/backend/catalog/pg_depend.c | 39 +++- src/backend/catalog/pg_operator.c | 19 ++ src/backend/catalog/pg_proc.c | 17 +- src/backend/catalog/pg_publication.c | 11 + src/backend/catalog/pg_range.c | 6 + src/backend/catalog/pg_type.c | 39 ++++ src/backend/catalog/toasting.c | 1 + src/backend/commands/alter.c | 4 + src/backend/commands/amcmds.c | 1 + src/backend/commands/cluster.c | 7 + src/backend/commands/event_trigger.c | 1 + src/backend/commands/extension.c | 5 + src/backend/commands/foreigncmds.c | 7 + src/backend/commands/functioncmds.c | 6 + src/backend/commands/indexcmds.c | 2 + src/backend/commands/opclasscmds.c | 18 ++ src/backend/commands/operatorcmds.c | 30 +++ src/backend/commands/policy.c | 2 + src/backend/commands/proclang.c | 3 + src/backend/commands/sequence.c | 2 + src/backend/commands/statscmds.c | 10 + src/backend/commands/tablecmds.c | 32 ++- src/backend/commands/trigger.c | 29 ++- src/backend/commands/tsearchcmds.c | 73 ++++++- src/backend/commands/typecmds.c | 84 ++++++++ src/backend/rewrite/rewriteDefine.c | 1 + src/backend/utils/errcodes.txt | 1 + src/include/catalog/dependency.h | 3 + src/include/catalog/objectaddress.h | 1 + .../expected/test_dependencies_locks.out | 129 +++++++++++ src/test/isolation/isolation_schedule | 1 + .../specs/test_dependencies_locks.spec | 89 ++++++++ src/test/regress/expected/alter_table.out | 11 +- 44 files changed, 998 insertions(+), 22 deletions(-) 39.7% src/backend/catalog/ 30.5% src/backend/commands/ 17.0% src/test/isolation/expected/ 10.5% src/test/isolation/specs/ diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 02a754cc30a..420641633c5 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -1341,6 +1341,7 @@ SetDefaultACL(InternalDefaultACL *iacls) referenced.objectId = iacls->nspid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, iacls->nspid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); } } diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..34884aa8950 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1519,6 +1519,69 @@ AcquireDeletionLock(const ObjectAddress *object, int flags) } } +/* + * LockNotPinnedObjectById + * + * Lock the object that we are about to record a dependency on. + * After it's locked, verify that it hasn't been dropped while we + * weren't looking. If the object has been dropped, this function + * does not return! + */ +void +LockNotPinnedObjectById(const ObjectAddress *object) +{ + if (isObjectPinned(object)) + return; + + if (object->classId == RelationRelationId) + { + Assert(!IsSharedRelation(object->objectId)); + + /* + * We must be in one of the two following cases that would already + * prevent the relation to be dropped: 1. The relation is already + * locked (could be an existing relation or a relation that we are + * creating). 2. The relation is protected indirectly (i.e an index + * protected by a lock on its table, a table protected by a lock on a + * function that depends of the table...). To avoid any risks, acquire + * a lock if there is none. That may add unnecessary lock for 2. but + * that's worth it. + */ + if (!CheckRelationOidLockedByMe(object->objectId, AccessShareLock, true)) + LockRelationOid(object->objectId, AccessShareLock); + } + else + { + /* assume we should lock the whole object not a sub-object */ + LockDatabaseObject(object->classId, object->objectId, 0, AccessShareLock); + } + + /* check if object still exists */ + if (!ObjectByIdExist(object)) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_DOES_NOT_EXIST), + errmsg("dependent object does not exist"), + errdetail("Class OID is %u and object OID is %u", + object->classId, object->objectId))); + + return; +} + +/* + * LockNotPinnedObject + * + * Lock the object that we are about to record a dependency on. + */ +void +LockNotPinnedObject(Oid classid, Oid objid) +{ + ObjectAddress object; + + ObjectAddressSet(object, classid, objid); + + LockNotPinnedObjectById(&object); +} + /* * ReleaseDeletionLock - release an object deletion lock * @@ -1730,6 +1793,7 @@ find_expr_references_walker(Node *node, if (rte->rtekind == RTE_RELATION) { /* If it's a plain relation, reference this column */ + LockNotPinnedObject(RelationRelationId, rte->relid); add_object_address(RelationRelationId, rte->relid, var->varattno, context->addrs); } @@ -1756,6 +1820,7 @@ find_expr_references_walker(Node *node, Oid objoid; /* A constant must depend on the constant's datatype */ + LockNotPinnedObject(TypeRelationId, con->consttype); add_object_address(TypeRelationId, con->consttype, 0, context->addrs); @@ -1767,8 +1832,11 @@ find_expr_references_walker(Node *node, */ if (OidIsValid(con->constcollid) && con->constcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, con->constcollid); add_object_address(CollationRelationId, con->constcollid, 0, context->addrs); + } /* * If it's a regclass or similar literal referring to an existing @@ -1785,59 +1853,83 @@ find_expr_references_walker(Node *node, objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(PROCOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(ProcedureRelationId, objoid); add_object_address(ProcedureRelationId, objoid, 0, context->addrs); + } break; case REGOPEROID: case REGOPERATOROID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(OPEROID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(OperatorRelationId, objoid); add_object_address(OperatorRelationId, objoid, 0, context->addrs); + } break; case REGCLASSOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(RELOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(RelationRelationId, objoid); add_object_address(RelationRelationId, objoid, 0, context->addrs); + } break; case REGTYPEOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(TYPEOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(TypeRelationId, objoid); add_object_address(TypeRelationId, objoid, 0, context->addrs); + } break; case REGCOLLATIONOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(COLLOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(CollationRelationId, objoid); add_object_address(CollationRelationId, objoid, 0, context->addrs); + } break; case REGCONFIGOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(TSCONFIGOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(TSConfigRelationId, objoid); add_object_address(TSConfigRelationId, objoid, 0, context->addrs); + } break; case REGDICTIONARYOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(TSDICTOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(TSDictionaryRelationId, objoid); add_object_address(TSDictionaryRelationId, objoid, 0, context->addrs); + } break; case REGNAMESPACEOID: objoid = DatumGetObjectId(con->constvalue); if (SearchSysCacheExists1(NAMESPACEOID, ObjectIdGetDatum(objoid))) + { + LockNotPinnedObject(NamespaceRelationId, objoid); add_object_address(NamespaceRelationId, objoid, 0, context->addrs); + } break; /* @@ -1859,18 +1951,23 @@ find_expr_references_walker(Node *node, Param *param = (Param *) node; /* A parameter must depend on the parameter's datatype */ + LockNotPinnedObject(TypeRelationId, param->paramtype); add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); /* and its collation, just as for Consts */ if (OidIsValid(param->paramcollid) && param->paramcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, param->paramcollid); add_object_address(CollationRelationId, param->paramcollid, 0, context->addrs); + } } else if (IsA(node, FuncExpr)) { FuncExpr *funcexpr = (FuncExpr *) node; + LockNotPinnedObject(ProcedureRelationId, funcexpr->funcid); add_object_address(ProcedureRelationId, funcexpr->funcid, 0, context->addrs); /* fall through to examine arguments */ @@ -1879,6 +1976,7 @@ find_expr_references_walker(Node *node, { OpExpr *opexpr = (OpExpr *) node; + LockNotPinnedObject(OperatorRelationId, opexpr->opno); add_object_address(OperatorRelationId, opexpr->opno, 0, context->addrs); /* fall through to examine arguments */ @@ -1887,6 +1985,7 @@ find_expr_references_walker(Node *node, { DistinctExpr *distinctexpr = (DistinctExpr *) node; + LockNotPinnedObject(OperatorRelationId, distinctexpr->opno); add_object_address(OperatorRelationId, distinctexpr->opno, 0, context->addrs); /* fall through to examine arguments */ @@ -1895,6 +1994,7 @@ find_expr_references_walker(Node *node, { NullIfExpr *nullifexpr = (NullIfExpr *) node; + LockNotPinnedObject(OperatorRelationId, nullifexpr->opno); add_object_address(OperatorRelationId, nullifexpr->opno, 0, context->addrs); /* fall through to examine arguments */ @@ -1903,6 +2003,7 @@ find_expr_references_walker(Node *node, { ScalarArrayOpExpr *opexpr = (ScalarArrayOpExpr *) node; + LockNotPinnedObject(OperatorRelationId, opexpr->opno); add_object_address(OperatorRelationId, opexpr->opno, 0, context->addrs); /* fall through to examine arguments */ @@ -1911,6 +2012,7 @@ find_expr_references_walker(Node *node, { Aggref *aggref = (Aggref *) node; + LockNotPinnedObject(ProcedureRelationId, aggref->aggfnoid); add_object_address(ProcedureRelationId, aggref->aggfnoid, 0, context->addrs); /* fall through to examine arguments */ @@ -1919,6 +2021,7 @@ find_expr_references_walker(Node *node, { WindowFunc *wfunc = (WindowFunc *) node; + LockNotPinnedObject(ProcedureRelationId, wfunc->winfnoid); add_object_address(ProcedureRelationId, wfunc->winfnoid, 0, context->addrs); /* fall through to examine arguments */ @@ -1935,8 +2038,11 @@ find_expr_references_walker(Node *node, */ if (sbsref->refrestype != sbsref->refcontainertype && sbsref->refrestype != sbsref->refelemtype) + { + LockNotPinnedObject(TypeRelationId, sbsref->refrestype); add_object_address(TypeRelationId, sbsref->refrestype, 0, context->addrs); + } /* fall through to examine arguments */ } else if (IsA(node, SubPlan)) @@ -1960,16 +2066,25 @@ find_expr_references_walker(Node *node, * anywhere else in the expression. */ if (OidIsValid(reltype)) + { + LockNotPinnedObject(RelationRelationId, reltype); add_object_address(RelationRelationId, reltype, fselect->fieldnum, context->addrs); + } else + { + LockNotPinnedObject(TypeRelationId, fselect->resulttype); add_object_address(TypeRelationId, fselect->resulttype, 0, context->addrs); + } /* the collation might not be referenced anywhere else, either */ if (OidIsValid(fselect->resultcollid) && fselect->resultcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, fselect->resultcollid); add_object_address(CollationRelationId, fselect->resultcollid, 0, context->addrs); + } } else if (IsA(node, FieldStore)) { @@ -1980,53 +2095,76 @@ find_expr_references_walker(Node *node, if (OidIsValid(reltype)) { ListCell *l; + bool locked = false; foreach(l, fstore->fieldnums) + { + if (!locked) + { + LockNotPinnedObject(RelationRelationId, reltype); + locked = true; + } add_object_address(RelationRelationId, reltype, lfirst_int(l), context->addrs); + } } else + { + LockNotPinnedObject(TypeRelationId, fstore->resulttype); add_object_address(TypeRelationId, fstore->resulttype, 0, context->addrs); + } } else if (IsA(node, RelabelType)) { RelabelType *relab = (RelabelType *) node; /* since there is no function dependency, need to depend on type */ + LockNotPinnedObject(TypeRelationId, relab->resulttype); add_object_address(TypeRelationId, relab->resulttype, 0, context->addrs); /* the collation might not be referenced anywhere else, either */ if (OidIsValid(relab->resultcollid) && relab->resultcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, relab->resultcollid); add_object_address(CollationRelationId, relab->resultcollid, 0, context->addrs); + } } else if (IsA(node, CoerceViaIO)) { CoerceViaIO *iocoerce = (CoerceViaIO *) node; /* since there is no exposed function, need to depend on type */ + LockNotPinnedObject(TypeRelationId, iocoerce->resulttype); add_object_address(TypeRelationId, iocoerce->resulttype, 0, context->addrs); /* the collation might not be referenced anywhere else, either */ if (OidIsValid(iocoerce->resultcollid) && iocoerce->resultcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, iocoerce->resultcollid); add_object_address(CollationRelationId, iocoerce->resultcollid, 0, context->addrs); + } } else if (IsA(node, ArrayCoerceExpr)) { ArrayCoerceExpr *acoerce = (ArrayCoerceExpr *) node; /* as above, depend on type */ + LockNotPinnedObject(TypeRelationId, acoerce->resulttype); add_object_address(TypeRelationId, acoerce->resulttype, 0, context->addrs); /* the collation might not be referenced anywhere else, either */ if (OidIsValid(acoerce->resultcollid) && acoerce->resultcollid != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, acoerce->resultcollid); add_object_address(CollationRelationId, acoerce->resultcollid, 0, context->addrs); + } /* fall through to examine arguments */ } else if (IsA(node, ConvertRowtypeExpr)) @@ -2034,6 +2172,7 @@ find_expr_references_walker(Node *node, ConvertRowtypeExpr *cvt = (ConvertRowtypeExpr *) node; /* since there is no function dependency, need to depend on type */ + LockNotPinnedObject(TypeRelationId, cvt->resulttype); add_object_address(TypeRelationId, cvt->resulttype, 0, context->addrs); } @@ -2041,6 +2180,7 @@ find_expr_references_walker(Node *node, { CollateExpr *coll = (CollateExpr *) node; + LockNotPinnedObject(CollationRelationId, coll->collOid); add_object_address(CollationRelationId, coll->collOid, 0, context->addrs); } @@ -2048,6 +2188,7 @@ find_expr_references_walker(Node *node, { RowExpr *rowexpr = (RowExpr *) node; + LockNotPinnedObject(TypeRelationId, rowexpr->row_typeid); add_object_address(TypeRelationId, rowexpr->row_typeid, 0, context->addrs); } @@ -2058,11 +2199,13 @@ find_expr_references_walker(Node *node, foreach(l, rcexpr->opnos) { + LockNotPinnedObject(OperatorRelationId, lfirst_oid(l)); add_object_address(OperatorRelationId, lfirst_oid(l), 0, context->addrs); } foreach(l, rcexpr->opfamilies) { + LockNotPinnedObject(OperatorFamilyRelationId, lfirst_oid(l)); add_object_address(OperatorFamilyRelationId, lfirst_oid(l), 0, context->addrs); } @@ -2072,6 +2215,7 @@ find_expr_references_walker(Node *node, { CoerceToDomain *cd = (CoerceToDomain *) node; + LockNotPinnedObject(TypeRelationId, cd->resulttype); add_object_address(TypeRelationId, cd->resulttype, 0, context->addrs); } @@ -2079,6 +2223,7 @@ find_expr_references_walker(Node *node, { NextValueExpr *nve = (NextValueExpr *) node; + LockNotPinnedObject(RelationRelationId, nve->seqid); add_object_address(RelationRelationId, nve->seqid, 0, context->addrs); } @@ -2087,19 +2232,26 @@ find_expr_references_walker(Node *node, OnConflictExpr *onconflict = (OnConflictExpr *) node; if (OidIsValid(onconflict->constraint)) + { + LockNotPinnedObject(ConstraintRelationId, onconflict->constraint); add_object_address(ConstraintRelationId, onconflict->constraint, 0, context->addrs); + } /* fall through to examine arguments */ } else if (IsA(node, SortGroupClause)) { SortGroupClause *sgc = (SortGroupClause *) node; + LockNotPinnedObject(OperatorRelationId, sgc->eqop); add_object_address(OperatorRelationId, sgc->eqop, 0, context->addrs); if (OidIsValid(sgc->sortop)) + { + LockNotPinnedObject(OperatorRelationId, sgc->sortop); add_object_address(OperatorRelationId, sgc->sortop, 0, context->addrs); + } return false; } else if (IsA(node, WindowClause)) @@ -2107,15 +2259,24 @@ find_expr_references_walker(Node *node, WindowClause *wc = (WindowClause *) node; if (OidIsValid(wc->startInRangeFunc)) + { + LockNotPinnedObject(ProcedureRelationId, wc->startInRangeFunc); add_object_address(ProcedureRelationId, wc->startInRangeFunc, 0, context->addrs); + } if (OidIsValid(wc->endInRangeFunc)) + { + LockNotPinnedObject(ProcedureRelationId, wc->endInRangeFunc); add_object_address(ProcedureRelationId, wc->endInRangeFunc, 0, context->addrs); + } if (OidIsValid(wc->inRangeColl) && wc->inRangeColl != DEFAULT_COLLATION_OID) + { + LockNotPinnedObject(CollationRelationId, wc->inRangeColl); add_object_address(CollationRelationId, wc->inRangeColl, 0, context->addrs); + } /* fall through to examine substructure */ } else if (IsA(node, CTECycleClause)) @@ -2123,14 +2284,23 @@ find_expr_references_walker(Node *node, CTECycleClause *cc = (CTECycleClause *) node; if (OidIsValid(cc->cycle_mark_type)) + { + LockNotPinnedObject(TypeRelationId, cc->cycle_mark_type); add_object_address(TypeRelationId, cc->cycle_mark_type, 0, context->addrs); + } if (OidIsValid(cc->cycle_mark_collation)) + { + LockNotPinnedObject(CollationRelationId, cc->cycle_mark_collation); add_object_address(CollationRelationId, cc->cycle_mark_collation, 0, context->addrs); + } if (OidIsValid(cc->cycle_mark_neop)) + { + LockNotPinnedObject(OperatorRelationId, cc->cycle_mark_neop); add_object_address(OperatorRelationId, cc->cycle_mark_neop, 0, context->addrs); + } /* fall through to examine substructure */ } else if (IsA(node, Query)) @@ -2163,6 +2333,7 @@ find_expr_references_walker(Node *node, switch (rte->rtekind) { case RTE_RELATION: + LockNotPinnedObject(RelationRelationId, rte->relid); add_object_address(RelationRelationId, rte->relid, 0, context->addrs); break; @@ -2230,12 +2401,19 @@ find_expr_references_walker(Node *node, rte = rt_fetch(query->resultRelation, query->rtable); if (rte->rtekind == RTE_RELATION) { + bool locked = false; + foreach(lc, query->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); if (tle->resjunk) continue; /* ignore junk tlist items */ + if (!locked) + { + LockNotPinnedObject(RelationRelationId, rte->relid); + locked = true; + } add_object_address(RelationRelationId, rte->relid, tle->resno, context->addrs); } @@ -2247,6 +2425,7 @@ find_expr_references_walker(Node *node, */ foreach(lc, query->constraintDeps) { + LockNotPinnedObject(ConstraintRelationId, lfirst_oid(lc)); add_object_address(ConstraintRelationId, lfirst_oid(lc), 0, context->addrs); } @@ -2281,16 +2460,25 @@ find_expr_references_walker(Node *node, */ foreach(ct, rtfunc->funccoltypes) { + LockNotPinnedObject(TypeRelationId, lfirst_oid(ct)); add_object_address(TypeRelationId, lfirst_oid(ct), 0, context->addrs); } foreach(ct, rtfunc->funccolcollations) { Oid collid = lfirst_oid(ct); + bool locked = false; if (OidIsValid(collid) && collid != DEFAULT_COLLATION_OID) + { + if (!locked) + { + LockNotPinnedObject(CollationRelationId, collid); + locked = true; + } add_object_address(CollationRelationId, collid, 0, context->addrs); + } } } else if (IsA(node, TableFunc)) @@ -2303,22 +2491,32 @@ find_expr_references_walker(Node *node, */ foreach(ct, tf->coltypes) { + LockNotPinnedObject(TypeRelationId, lfirst_oid(ct)); add_object_address(TypeRelationId, lfirst_oid(ct), 0, context->addrs); } foreach(ct, tf->colcollations) { Oid collid = lfirst_oid(ct); + bool locked = false; if (OidIsValid(collid) && collid != DEFAULT_COLLATION_OID) + { + if (!locked) + { + LockNotPinnedObject(CollationRelationId, collid); + locked = true; + } add_object_address(CollationRelationId, collid, 0, context->addrs); + } } } else if (IsA(node, TableSampleClause)) { TableSampleClause *tsc = (TableSampleClause *) node; + LockNotPinnedObject(ProcedureRelationId, tsc->tsmhandler); add_object_address(ProcedureRelationId, tsc->tsmhandler, 0, context->addrs); /* fall through to examine arguments */ @@ -2369,9 +2567,12 @@ process_function_rte_ref(RangeTblEntry *rte, AttrNumber attnum, Assert(attnum - atts_done <= tupdesc->natts); if (OidIsValid(reltype)) /* can this fail? */ + { + LockNotPinnedObject(RelationRelationId, reltype); add_object_address(RelationRelationId, reltype, attnum - atts_done, context->addrs); + } return; } /* Nothing to do; function's result type is handled elsewhere */ diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 57ef466acce..aaf6949a226 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -837,6 +837,7 @@ AddNewAttributeTuples(Oid new_rel_oid, /* Add dependency info */ ObjectAddressSubSet(myself, RelationRelationId, new_rel_oid, i + 1); ObjectAddressSet(referenced, TypeRelationId, attr->atttypid); + LockNotPinnedObject(TypeRelationId, attr->atttypid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* The default collation is pinned, so don't bother recording it */ @@ -845,6 +846,7 @@ AddNewAttributeTuples(Oid new_rel_oid, { ObjectAddressSet(referenced, CollationRelationId, attr->attcollation); + LockNotPinnedObject(CollationRelationId, attr->attcollation); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -1452,11 +1454,13 @@ heap_create_with_catalog(const char *relname, ObjectAddressSet(referenced, NamespaceRelationId, relnamespace); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(NamespaceRelationId, relnamespace); if (reloftypeid) { ObjectAddressSet(referenced, TypeRelationId, reloftypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, reloftypeid); } /* @@ -1472,6 +1476,7 @@ heap_create_with_catalog(const char *relname, { ObjectAddressSet(referenced, AccessMethodRelationId, accessmtd); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(AccessMethodRelationId, accessmtd); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -3799,6 +3804,7 @@ StorePartitionKey(Relation rel, { ObjectAddressSet(referenced, OperatorClassRelationId, partopclass[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, partopclass[i]); /* The default collation is pinned, so don't bother recording it */ if (OidIsValid(partcollation[i]) && @@ -3806,6 +3812,7 @@ StorePartitionKey(Relation rel, { ObjectAddressSet(referenced, CollationRelationId, partcollation[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, partcollation[i]); } } diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 7377912b41e..cf442e7f21f 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1117,6 +1117,7 @@ index_create(Relation heapRelation, else { bool have_simple_col = false; + bool locked_object = false; addrs = new_object_addresses(); @@ -1129,6 +1130,12 @@ index_create(Relation heapRelation, heapRelationId, indexInfo->ii_IndexAttrNumbers[i]); add_exact_object_address(&referenced, addrs); + + if (!locked_object) + { + LockNotPinnedObject(RelationRelationId, heapRelationId); + locked_object = true; + } have_simple_col = true; } } @@ -1144,6 +1151,8 @@ index_create(Relation heapRelation, ObjectAddressSet(referenced, RelationRelationId, heapRelationId); add_exact_object_address(&referenced, addrs); + + LockNotPinnedObject(RelationRelationId, heapRelationId); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_AUTO); @@ -1159,9 +1168,13 @@ index_create(Relation heapRelation, if (OidIsValid(parentIndexRelid)) { ObjectAddressSet(referenced, RelationRelationId, parentIndexRelid); + + LockNotPinnedObject(RelationRelationId, parentIndexRelid); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, heapRelationId); + + LockNotPinnedObject(RelationRelationId, heapRelationId); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } @@ -1177,6 +1190,7 @@ index_create(Relation heapRelation, { ObjectAddressSet(referenced, CollationRelationId, collationIds[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, collationIds[i]); } } @@ -1185,6 +1199,7 @@ index_create(Relation heapRelation, { ObjectAddressSet(referenced, OperatorClassRelationId, opclassIds[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, opclassIds[i]); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -1995,6 +2010,14 @@ index_constraint_create(Relation heapRelation, */ ObjectAddressSet(myself, ConstraintRelationId, conOid); ObjectAddressSet(idxaddr, RelationRelationId, indexRelationId); + + /* + * CommandCounterIncrement() here to ensure the new constraint entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + LockNotPinnedObject(ConstraintRelationId, conOid); recordDependencyOn(&idxaddr, &myself, DEPENDENCY_INTERNAL); /* @@ -2006,9 +2029,12 @@ index_constraint_create(Relation heapRelation, ObjectAddress referenced; ObjectAddressSet(referenced, ConstraintRelationId, parentConstraintId); + LockNotPinnedObject(ConstraintRelationId, parentConstraintId); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(heapRelation)); + + LockNotPinnedObject(RelationRelationId, RelationGetRelid(heapRelation)); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index d8eb8d3deaa..f3e82671df5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2595,6 +2595,63 @@ get_object_namespace(const ObjectAddress *address) return oid; } +/* + * ObjectByIdExist + * + * Return whether the given object exists. + * + * Works for most catalogs, if no special processing is needed. + */ +bool +ObjectByIdExist(const ObjectAddress *address) +{ + HeapTuple tuple; + int cache; + const ObjectPropertyType *property; + + property = get_object_property_data(address->classId); + + cache = property->oid_catcache_id; + + if (cache >= 0) + { + /* Fetch tuple from syscache. */ + tuple = SearchSysCache1(cache, ObjectIdGetDatum(address->objectId)); + + if (!HeapTupleIsValid(tuple)) + { + return false; + } + + ReleaseSysCache(tuple); + + return true; + } + else + { + Relation rel; + ScanKeyData skey[1]; + SysScanDesc scan; + + rel = table_open(address->classId, AccessShareLock); + + ScanKeyInit(&skey[0], + get_object_attnum_oid(address->classId), + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(address->objectId)); + + scan = systable_beginscan(rel, get_object_oid_index(address->classId), true, + NULL, 1, skey); + + /* we expect exactly one match */ + tuple = systable_getnext(scan); + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return (HeapTupleIsValid(tuple)); + } +} + /* * Return ObjectType for the given object type as given by * getObjectTypeDescription; if no valid ObjectType code exists, but it's a diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c index bcf4050f5b1..ce5865fac69 100644 --- a/src/backend/catalog/pg_aggregate.c +++ b/src/backend/catalog/pg_aggregate.c @@ -748,12 +748,14 @@ AggregateCreate(const char *aggName, /* Depends on transition function */ ObjectAddressSet(referenced, ProcedureRelationId, transfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, transfn); /* Depends on final function, if any */ if (OidIsValid(finalfn)) { ObjectAddressSet(referenced, ProcedureRelationId, finalfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, finalfn); } /* Depends on combine function, if any */ @@ -761,6 +763,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, combinefn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, combinefn); } /* Depends on serialization function, if any */ @@ -768,6 +771,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, serialfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, serialfn); } /* Depends on deserialization function, if any */ @@ -775,6 +779,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, deserialfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, deserialfn); } /* Depends on forward transition function, if any */ @@ -782,6 +787,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, mtransfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, mtransfn); } /* Depends on inverse transition function, if any */ @@ -789,6 +795,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, minvtransfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, minvtransfn); } /* Depends on final function, if any */ @@ -796,6 +803,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, ProcedureRelationId, mfinalfn); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, mfinalfn); } /* Depends on sort operator, if any */ @@ -803,6 +811,7 @@ AggregateCreate(const char *aggName, { ObjectAddressSet(referenced, OperatorRelationId, sortop); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorRelationId, sortop); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/catalog/pg_attrdef.c b/src/backend/catalog/pg_attrdef.c index 91dafc81021..91da30c504e 100644 --- a/src/backend/catalog/pg_attrdef.c +++ b/src/backend/catalog/pg_attrdef.c @@ -178,6 +178,7 @@ StoreAttrDefault(Relation rel, AttrNumber attnum, colobject.objectId = RelationGetRelid(rel); colobject.objectSubId = attnum; + LockNotPinnedObject(RelationRelationId, RelationGetRelid(rel)); recordDependencyOn(&defobject, &colobject, attgenerated ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/catalog/pg_cast.c b/src/backend/catalog/pg_cast.c index 1773c9c5491..90513a39ac6 100644 --- a/src/backend/catalog/pg_cast.c +++ b/src/backend/catalog/pg_cast.c @@ -97,16 +97,19 @@ CastCreate(Oid sourcetypeid, Oid targettypeid, /* dependency on source type */ ObjectAddressSet(referenced, TypeRelationId, sourcetypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, sourcetypeid); /* dependency on target type */ ObjectAddressSet(referenced, TypeRelationId, targettypeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, targettypeid); /* dependency on function */ if (OidIsValid(funcid)) { ObjectAddressSet(referenced, ProcedureRelationId, funcid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, funcid); } /* dependencies on casts required for function */ @@ -114,11 +117,13 @@ CastCreate(Oid sourcetypeid, Oid targettypeid, { ObjectAddressSet(referenced, CastRelationId, incastid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CastRelationId, incastid); } if (OidIsValid(outcastid)) { ObjectAddressSet(referenced, CastRelationId, outcastid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CastRelationId, outcastid); } record_object_address_dependencies(&myself, addrs, behavior); diff --git a/src/backend/catalog/pg_collation.c b/src/backend/catalog/pg_collation.c index 469635b3580..fa6fd7bf791 100644 --- a/src/backend/catalog/pg_collation.c +++ b/src/backend/catalog/pg_collation.c @@ -218,6 +218,7 @@ CollationCreate(const char *collname, Oid collnamespace, referenced.classId = NamespaceRelationId; referenced.objectId = collnamespace; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, collnamespace); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on owner */ diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index ac80652baf2..79b2df100e6 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -264,17 +264,26 @@ CreateConstraintEntry(const char *constraintName, if (constraintNTotalKeys > 0) { + bool locked_object = false; + for (i = 0; i < constraintNTotalKeys; i++) { ObjectAddressSubSet(relobject, RelationRelationId, relId, constraintKey[i]); add_exact_object_address(&relobject, addrs_auto); + + if (!locked_object) + { + LockNotPinnedObject(RelationRelationId, relId); + locked_object = true; + } } } else { ObjectAddressSet(relobject, RelationRelationId, relId); add_exact_object_address(&relobject, addrs_auto); + LockNotPinnedObject(RelationRelationId, relId); } } @@ -287,6 +296,7 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(domobject, TypeRelationId, domainId); add_exact_object_address(&domobject, addrs_auto); + LockNotPinnedObject(TypeRelationId, domainId); } record_object_address_dependencies(&conobject, addrs_auto, @@ -306,17 +316,26 @@ CreateConstraintEntry(const char *constraintName, if (foreignNKeys > 0) { + bool locked_object = false; + for (i = 0; i < foreignNKeys; i++) { ObjectAddressSubSet(relobject, RelationRelationId, foreignRelId, foreignKey[i]); add_exact_object_address(&relobject, addrs_normal); + + if (!locked_object) + { + LockNotPinnedObject(RelationRelationId, foreignRelId); + locked_object = true; + } } } else { ObjectAddressSet(relobject, RelationRelationId, foreignRelId); add_exact_object_address(&relobject, addrs_normal); + LockNotPinnedObject(RelationRelationId, foreignRelId); } } @@ -332,6 +351,7 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(relobject, RelationRelationId, indexRelId); add_exact_object_address(&relobject, addrs_normal); + LockNotPinnedObject(RelationRelationId, indexRelId); } if (foreignNKeys > 0) @@ -351,15 +371,18 @@ CreateConstraintEntry(const char *constraintName, { oprobject.objectId = pfEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, pfEqOp[i]); if (ppEqOp[i] != pfEqOp[i]) { oprobject.objectId = ppEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, ppEqOp[i]); } if (ffEqOp[i] != pfEqOp[i]) { oprobject.objectId = ffEqOp[i]; add_exact_object_address(&oprobject, addrs_normal); + LockNotPinnedObject(OperatorRelationId, ffEqOp[i]); } } } @@ -1119,9 +1142,12 @@ ConstraintSetParentConstraint(Oid childConstrId, ObjectAddressSet(depender, ConstraintRelationId, childConstrId); ObjectAddressSet(referenced, ConstraintRelationId, parentConstrId); + LockNotPinnedObject(ConstraintRelationId, parentConstrId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, childTableId); + + LockNotPinnedObject(RelationRelationId, childTableId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC); } else diff --git a/src/backend/catalog/pg_conversion.c b/src/backend/catalog/pg_conversion.c index 04cc375caea..5ac365ebd04 100644 --- a/src/backend/catalog/pg_conversion.c +++ b/src/backend/catalog/pg_conversion.c @@ -116,12 +116,14 @@ ConversionCreate(const char *conname, Oid connamespace, referenced.classId = ProcedureRelationId; referenced.objectId = conproc; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, conproc); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on namespace */ referenced.classId = NamespaceRelationId; referenced.objectId = connamespace; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, connamespace); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* create dependency on owner */ diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index c8b11f887e2..fdafffb8934 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -20,19 +20,20 @@ #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" +#include "catalog/pg_auth_members.h" #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" #include "catalog/pg_extension.h" #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" +#include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" -static bool isObjectPinned(const ObjectAddress *object); - /* * Record a dependency between 2 objects via their respective ObjectAddress. @@ -99,6 +100,37 @@ recordMultipleDependencies(const ObjectAddress *depender, slot_init_count = 0; for (i = 0; i < nreferenced; i++, referenced++) { +#ifdef USE_ASSERT_CHECKING + if (!isObjectPinned(referenced)) + { + if (referenced->classId != RelationRelationId) + { + LOCKTAG tag; + + SET_LOCKTAG_OBJECT(tag, + MyDatabaseId, + referenced->classId, + referenced->objectId, + 0); + /* assert the referenced object is locked */ + Assert(LockHeldByMe(&tag, AccessShareLock, false)); + } + else + { + Assert(!IsSharedRelation(referenced->objectId)); + + /* + * Assert the referenced object is locked if it should be + * visible (see the comment related to LockNotPinnedObject() + * in TypeCreate()). + */ + Assert(!ObjectByIdExist(referenced) || + CheckRelationOidLockedByMe(referenced->objectId, + AccessShareLock, true)); + } + } +#endif + /* * If the referenced object is pinned by the system, there's no real * need to record dependencies on it. This saves lots of space in @@ -238,6 +270,7 @@ recordDependencyOnCurrentExtension(const ObjectAddress *object, extension.objectId = CurrentExtensionObject; extension.objectSubId = 0; + LockNotPinnedObject(ExtensionRelationId, CurrentExtensionObject); recordDependencyOn(object, &extension, DEPENDENCY_EXTENSION); } } @@ -705,7 +738,7 @@ changeDependenciesOn(Oid refClassId, Oid oldRefObjectId, * The passed subId, if any, is ignored; we assume that only whole objects * are pinned (and that this implies pinning their components). */ -static bool +bool isObjectPinned(const ObjectAddress *object) { return IsPinnedObject(object->classId, object->objectId); diff --git a/src/backend/catalog/pg_operator.c b/src/backend/catalog/pg_operator.c index bfcfa643464..ba6aa332187 100644 --- a/src/backend/catalog/pg_operator.c +++ b/src/backend/catalog/pg_operator.c @@ -251,6 +251,16 @@ OperatorShellMake(const char *operatorName, values[Anum_pg_operator_oprrest - 1] = ObjectIdGetDatum(InvalidOid); values[Anum_pg_operator_oprjoin - 1] = ObjectIdGetDatum(InvalidOid); + /* Lock dependent objects */ + if (OidIsValid(operatorNamespace)) + LockNotPinnedObject(NamespaceRelationId, operatorNamespace); + + if (OidIsValid(leftTypeId)) + LockNotPinnedObject(TypeRelationId, leftTypeId); + + if (OidIsValid(rightTypeId)) + LockNotPinnedObject(TypeRelationId, rightTypeId); + /* * create a new operator tuple */ @@ -513,6 +523,15 @@ OperatorCreate(const char *operatorName, CatalogTupleInsert(pg_operator_desc, tup); } + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, operatorNamespace); + LockNotPinnedObject(TypeRelationId, leftTypeId); + LockNotPinnedObject(TypeRelationId, rightTypeId); + LockNotPinnedObject(TypeRelationId, operResultType); + LockNotPinnedObject(ProcedureRelationId, procedureId); + LockNotPinnedObject(ProcedureRelationId, restrictionId); + LockNotPinnedObject(ProcedureRelationId, joinId); + /* Add dependencies for the entry */ address = makeOperatorDependencies(tup, true, isUpdate); diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index fe0490259e9..c3999bdce87 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -593,6 +593,13 @@ ProcedureCreate(const char *procedureName, if (is_update) deleteDependencyRecordsFor(ProcedureRelationId, retval, true); + /* + * CommandCounterIncrement() here to ensure the new function entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + addrs = new_object_addresses(); ObjectAddressSet(myself, ProcedureRelationId, retval); @@ -600,20 +607,24 @@ ProcedureCreate(const char *procedureName, /* dependency on namespace */ ObjectAddressSet(referenced, NamespaceRelationId, procNamespace); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(NamespaceRelationId, procNamespace); /* dependency on implementation language */ ObjectAddressSet(referenced, LanguageRelationId, languageObjectId); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(LanguageRelationId, languageObjectId); /* dependency on return type */ ObjectAddressSet(referenced, TypeRelationId, returnType); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, returnType); /* dependency on transform used by return type, if any */ if ((trfid = get_transform_oid(returnType, languageObjectId, true))) { ObjectAddressSet(referenced, TransformRelationId, trfid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TransformRelationId, trfid); } /* dependency on parameter types */ @@ -621,12 +632,14 @@ ProcedureCreate(const char *procedureName, { ObjectAddressSet(referenced, TypeRelationId, allParams[i]); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, allParams[i]); /* dependency on transform used by parameter type, if any */ if ((trfid = get_transform_oid(allParams[i], languageObjectId, true))) { ObjectAddressSet(referenced, TransformRelationId, trfid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TransformRelationId, trfid); } } @@ -635,6 +648,7 @@ ProcedureCreate(const char *procedureName, { ObjectAddressSet(referenced, ProcedureRelationId, prosupport); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, prosupport); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -674,9 +688,6 @@ ProcedureCreate(const char *procedureName, ArrayType *set_items = NULL; int save_nestlevel = 0; - /* Advance command counter so new tuple can be seen by validator */ - CommandCounterIncrement(); - /* * Set per-function configuration parameters so that the validation is * done with the environment the function expects. However, if diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 41ffd494c81..8922b8547e7 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -441,6 +441,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, referenced; List *relids = NIL; int i; + bool locked = false; rel = table_open(PublicationRelRelationId, RowExclusiveLock); @@ -503,10 +504,13 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, /* Add dependency on the publication */ ObjectAddressSet(referenced, PublicationRelationId, pubid); + LockNotPinnedObject(PublicationRelationId, pubid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the relation */ ObjectAddressSet(referenced, RelationRelationId, relid); + + LockNotPinnedObject(RelationRelationId, relid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the objects mentioned in the qualifications */ @@ -520,6 +524,11 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, while ((i = bms_next_member(attnums, i)) >= 0) { ObjectAddressSubSet(referenced, RelationRelationId, relid, i); + if (!locked) + { + LockNotPinnedObject(RelationRelationId, relid); + locked = true; + } recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -717,10 +726,12 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists) /* Add dependency on the publication */ ObjectAddressSet(referenced, PublicationRelationId, pubid); + LockNotPinnedObject(PublicationRelationId, pubid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Add dependency on the schema */ ObjectAddressSet(referenced, NamespaceRelationId, schemaid); + LockNotPinnedObject(NamespaceRelationId, schemaid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* Close the table */ diff --git a/src/backend/catalog/pg_range.c b/src/backend/catalog/pg_range.c index 8df73e7ab71..e1538c21468 100644 --- a/src/backend/catalog/pg_range.c +++ b/src/backend/catalog/pg_range.c @@ -70,26 +70,31 @@ RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, ObjectAddressSet(referenced, TypeRelationId, rangeSubType); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, rangeSubType); ObjectAddressSet(referenced, OperatorClassRelationId, rangeSubOpclass); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(OperatorClassRelationId, rangeSubOpclass); if (OidIsValid(rangeCollation)) { ObjectAddressSet(referenced, CollationRelationId, rangeCollation); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(CollationRelationId, rangeCollation); } if (OidIsValid(rangeCanonical)) { ObjectAddressSet(referenced, ProcedureRelationId, rangeCanonical); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, rangeCanonical); } if (OidIsValid(rangeSubDiff)) { ObjectAddressSet(referenced, ProcedureRelationId, rangeSubDiff); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, rangeSubDiff); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); @@ -99,6 +104,7 @@ RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, referencing.classId = TypeRelationId; referencing.objectId = multirangeTypeOid; referencing.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, rangeTypeOid); recordDependencyOn(&referencing, &myself, DEPENDENCY_INTERNAL); table_close(pg_range, RowExclusiveLock); diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c index b36f81afb9d..77ff5f06030 100644 --- a/src/backend/catalog/pg_type.c +++ b/src/backend/catalog/pg_type.c @@ -157,6 +157,12 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) * Create dependencies. We can/must skip this in bootstrap mode. */ if (!IsBootstrapProcessingMode()) + { + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, typeNamespace); + LockNotPinnedObject(ProcedureRelationId, F_SHELL_IN); + LockNotPinnedObject(ProcedureRelationId, F_SHELL_OUT); + GenerateTypeDependencies(tup, pg_type_desc, NULL, @@ -166,6 +172,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) false, true, /* make extension dependency */ false); + } /* Post creation hook for new shell type */ InvokeObjectPostCreateHook(TypeRelationId, typoid, 0); @@ -494,6 +501,37 @@ TypeCreate(Oid newTypeOid, * Create dependencies. We can/must skip this in bootstrap mode. */ if (!IsBootstrapProcessingMode()) + { + /* + * CommandCounterIncrement() here to ensure the new type entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, typeNamespace); + LockNotPinnedObject(ProcedureRelationId, inputProcedure); + LockNotPinnedObject(ProcedureRelationId, outputProcedure); + LockNotPinnedObject(ProcedureRelationId, receiveProcedure); + LockNotPinnedObject(ProcedureRelationId, sendProcedure); + LockNotPinnedObject(ProcedureRelationId, typmodinProcedure); + LockNotPinnedObject(ProcedureRelationId, typmodoutProcedure); + LockNotPinnedObject(ProcedureRelationId, analyzeProcedure); + LockNotPinnedObject(ProcedureRelationId, subscriptProcedure); + LockNotPinnedObject(TypeRelationId, baseType); + LockNotPinnedObject(CollationRelationId, typeCollation); + LockNotPinnedObject(TypeRelationId, elementType); + + /* + * No need to call LockRelationOid() (through LockNotPinnedObject()) + * on relationOid as relationOid is set to an InvalidOid or to a new + * Oid not added to pg_class yet (In heap_create_with_catalog(), + * AddNewRelationType() is called before AddNewRelationTuple()). + */ + if (relationKind == RELKIND_COMPOSITE_TYPE) + LockNotPinnedObject(TypeRelationId, typeObjectId); + GenerateTypeDependencies(tup, pg_type_desc, (defaultTypeBin ? @@ -505,6 +543,7 @@ TypeCreate(Oid newTypeOid, isDependentType, true, /* make extension dependency */ rebuildDeps); + } /* Post creation hook for new type */ InvokeObjectPostCreateHook(TypeRelationId, typeObjectId, 0); diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 874a8fc89ad..b8fb22af6ae 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -383,6 +383,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, toastobject.objectId = toast_relid; toastobject.objectSubId = 0; + LockNotPinnedObject(RelationRelationId, relOid); recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 78c1d4e1b84..e068b127c28 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -499,7 +499,10 @@ ExecAlterObjectDependsStmt(AlterObjectDependsStmt *stmt, ObjectAddress *refAddre currexts = getAutoExtensionsOfObject(address.classId, address.objectId); if (!list_member_oid(currexts, refAddr.objectId)) + { + LockNotPinnedObject(refAddr.classId, refAddr.objectId); recordDependencyOn(&address, &refAddr, DEPENDENCY_AUTO_EXTENSION); + } } return address; @@ -803,6 +806,7 @@ AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid) pfree(replaces); /* update dependency to point to the new schema */ + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(classId, objid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for object %u", diff --git a/src/backend/commands/amcmds.c b/src/backend/commands/amcmds.c index 58ed9d216cc..eb9b86d5633 100644 --- a/src/backend/commands/amcmds.c +++ b/src/backend/commands/amcmds.c @@ -104,6 +104,7 @@ CreateAccessMethod(CreateAmStmt *stmt) referenced.objectId = amhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, amhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c index 99193f5c886..c1a7ecbfab8 100644 --- a/src/backend/commands/cluster.c +++ b/src/backend/commands/cluster.c @@ -1274,6 +1274,7 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, */ if (relam1 != relam2) { + LockNotPinnedObject(AccessMethodRelationId, relam2); if (changeDependencyFor(RelationRelationId, r1, AccessMethodRelationId, @@ -1282,6 +1283,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", get_namespace_name(get_rel_namespace(r1)), get_rel_name(r1)); + + LockNotPinnedObject(AccessMethodRelationId, relam1); if (changeDependencyFor(RelationRelationId, r2, AccessMethodRelationId, @@ -1383,6 +1386,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, { baseobject.objectId = r1; toastobject.objectId = relform1->reltoastrelid; + + LockNotPinnedObject(RelationRelationId, r1); recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } @@ -1391,6 +1396,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, { baseobject.objectId = r2; toastobject.objectId = relform2->reltoastrelid; + + LockNotPinnedObject(RelationRelationId, r2); recordDependencyOn(&toastobject, &baseobject, DEPENDENCY_INTERNAL); } diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..14dab63ef0d 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -327,6 +327,7 @@ insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtO referenced.classId = ProcedureRelationId; referenced.objectId = funcoid; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, funcoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* Depend on extension, if any. */ diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index ba540e3de5b..01813ac71d5 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -2035,6 +2035,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner, ObjectAddressSet(nsp, NamespaceRelationId, schemaOid); add_exact_object_address(&nsp, refobjs); + LockNotPinnedObject(NamespaceRelationId, schemaOid); foreach(lc, requiredExtensions) { @@ -2043,6 +2044,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner, ObjectAddressSet(otherext, ExtensionRelationId, reqext); add_exact_object_address(&otherext, refobjs); + LockNotPinnedObject(ExtensionRelationId, reqext); } /* Record all of them (this includes duplicate elimination) */ @@ -3079,6 +3081,7 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o table_close(extRel, RowExclusiveLock); /* update dependency to point to the new schema */ + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(ExtensionRelationId, extensionOid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for extension %s", @@ -3369,6 +3372,7 @@ ApplyExtensionUpdates(Oid extensionOid, otherext.objectId = reqext; otherext.objectSubId = 0; + LockNotPinnedObject(ExtensionRelationId, reqext); recordDependencyOn(&myself, &otherext, DEPENDENCY_NORMAL); } @@ -3525,6 +3529,7 @@ ExecAlterExtensionContentsRecurse(AlterExtensionContentsStmt *stmt, /* * OK, add the dependency. */ + LockNotPinnedObject(extension.classId, extension.objectId); recordDependencyOn(&object, &extension, DEPENDENCY_EXTENSION); /* diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index c14e038d54f..d6cced22e7a 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -642,6 +642,7 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -650,6 +651,7 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwvalidator; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwvalidator); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -811,6 +813,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwhandler; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwhandler); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -819,6 +822,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = fdwvalidator; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, fdwvalidator); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -951,6 +955,7 @@ CreateForeignServer(CreateForeignServerStmt *stmt) referenced.classId = ForeignDataWrapperRelationId; referenced.objectId = fdw->fdwid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignDataWrapperRelationId, fdw->fdwid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); recordDependencyOnOwner(ForeignServerRelationId, srvId, ownerId); @@ -1195,6 +1200,7 @@ CreateUserMapping(CreateUserMappingStmt *stmt) referenced.classId = ForeignServerRelationId; referenced.objectId = srv->serverid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignServerRelationId, srv->serverid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); if (OidIsValid(useId)) @@ -1472,6 +1478,7 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid) referenced.classId = ForeignServerRelationId; referenced.objectId = server->serverid; referenced.objectSubId = 0; + LockNotPinnedObject(ForeignServerRelationId, server->serverid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); table_close(ftrel, RowExclusiveLock); diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index b9fd7683abb..e2155511478 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -1461,6 +1461,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) /* Add or replace dependency on support function */ if (OidIsValid(procForm->prosupport)) { + LockNotPinnedObject(ProcedureRelationId, newsupport); if (changeDependencyFor(ProcedureRelationId, funcOid, ProcedureRelationId, procForm->prosupport, newsupport) != 1) @@ -1474,6 +1475,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) referenced.classId = ProcedureRelationId; referenced.objectId = newsupport; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, newsupport); recordDependencyOn(&address, &referenced, DEPENDENCY_NORMAL); } @@ -1990,21 +1992,25 @@ CreateTransform(CreateTransformStmt *stmt) /* dependency on language */ ObjectAddressSet(referenced, LanguageRelationId, langid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(LanguageRelationId, langid); /* dependency on type */ ObjectAddressSet(referenced, TypeRelationId, typeid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(TypeRelationId, typeid); /* dependencies on functions */ if (OidIsValid(fromsqlfuncid)) { ObjectAddressSet(referenced, ProcedureRelationId, fromsqlfuncid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, fromsqlfuncid); } if (OidIsValid(tosqlfuncid)) { ObjectAddressSet(referenced, ProcedureRelationId, tosqlfuncid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, tosqlfuncid); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 5b1753d4681..f601d590d8f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -4488,8 +4488,10 @@ IndexSetParentIndex(Relation partitionIdx, Oid parentOid) ObjectAddressSet(parentIdx, RelationRelationId, parentOid); ObjectAddressSet(partitionTbl, RelationRelationId, partitionIdx->rd_index->indrelid); + LockNotPinnedObject(RelationRelationId, parentOid); recordDependencyOn(&partIdx, &parentIdx, DEPENDENCY_PARTITION_PRI); + LockNotPinnedObject(RelationRelationId, partitionIdx->rd_index->indrelid); recordDependencyOn(&partIdx, &partitionTbl, DEPENDENCY_PARTITION_SEC); } diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c index 2c325badf94..72b347e9802 100644 --- a/src/backend/commands/opclasscmds.c +++ b/src/backend/commands/opclasscmds.c @@ -299,12 +299,14 @@ CreateOpFamily(CreateOpFamilyStmt *stmt, const char *opfname, referenced.classId = AccessMethodRelationId; referenced.objectId = amoid; referenced.objectSubId = 0; + LockNotPinnedObject(AccessMethodRelationId, amoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* dependency on namespace */ referenced.classId = NamespaceRelationId; referenced.objectId = namespaceoid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, namespaceoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on owner */ @@ -726,18 +728,21 @@ DefineOpClass(CreateOpClassStmt *stmt) referenced.classId = NamespaceRelationId; referenced.objectId = namespaceoid; referenced.objectSubId = 0; + LockNotPinnedObject(NamespaceRelationId, namespaceoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on opfamily */ referenced.classId = OperatorFamilyRelationId; referenced.objectId = opfamilyoid; referenced.objectSubId = 0; + LockNotPinnedObject(OperatorFamilyRelationId, opfamilyoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); /* dependency on indexed datatype */ referenced.classId = TypeRelationId; referenced.objectId = typeoid; referenced.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, typeoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); /* dependency on storage datatype */ @@ -746,6 +751,7 @@ DefineOpClass(CreateOpClassStmt *stmt) referenced.classId = TypeRelationId; referenced.objectId = storageoid; referenced.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, storageoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } @@ -1487,6 +1493,13 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, heap_freetuple(tup); + /* + * CommandCounterIncrement() here to ensure the new operator entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + /* Make its dependencies */ myself.classId = AccessMethodOperatorRelationId; myself.objectId = entryoid; @@ -1497,6 +1510,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectSubId = 0; /* see comments in amapi.h about dependency strength */ + LockNotPinnedObject(OperatorRelationId, op->object); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); @@ -1505,6 +1519,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = op->refobjid; referenced.objectSubId = 0; + LockNotPinnedObject(referenced.classId, op->refobjid); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); @@ -1538,6 +1553,7 @@ storeOperators(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = op->sortfamily; referenced.objectSubId = 0; + LockNotPinnedObject(OperatorFamilyRelationId, op->sortfamily); recordDependencyOn(&myself, &referenced, op->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); } @@ -1621,6 +1637,7 @@ storeProcedures(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectSubId = 0; /* see comments in amapi.h about dependency strength */ + LockNotPinnedObject(ProcedureRelationId, proc->object); recordDependencyOn(&myself, &referenced, proc->ref_is_hard ? DEPENDENCY_NORMAL : DEPENDENCY_AUTO); @@ -1629,6 +1646,7 @@ storeProcedures(List *opfamilyname, Oid amoid, Oid opfamilyoid, referenced.objectId = proc->refobjid; referenced.objectSubId = 0; + LockNotPinnedObject(referenced.classId, proc->refobjid); recordDependencyOn(&myself, &referenced, proc->ref_is_hard ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/commands/operatorcmds.c b/src/backend/commands/operatorcmds.c index 673648f1fc6..59a4715fffe 100644 --- a/src/backend/commands/operatorcmds.c +++ b/src/backend/commands/operatorcmds.c @@ -33,6 +33,7 @@ #include "access/htup_details.h" #include "access/table.h" +#include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_namespace.h" @@ -656,11 +657,15 @@ AlterOperator(AlterOperatorStmt *stmt) { replaces[Anum_pg_operator_oprrest - 1] = true; values[Anum_pg_operator_oprrest - 1] = ObjectIdGetDatum(restrictionOid); + if (OidIsValid(restrictionOid)) + LockNotPinnedObject(ProcedureRelationId, restrictionOid); } if (updateJoin) { replaces[Anum_pg_operator_oprjoin - 1] = true; values[Anum_pg_operator_oprjoin - 1] = ObjectIdGetDatum(joinOid); + if (OidIsValid(joinOid)) + LockNotPinnedObject(ProcedureRelationId, joinOid); } if (OidIsValid(commutatorOid)) { @@ -688,6 +693,31 @@ AlterOperator(AlterOperatorStmt *stmt) CatalogTupleUpdate(catalog, &tup->t_self, tup); + + /* Lock dependent objects */ + oprForm = (Form_pg_operator) GETSTRUCT(tup); + + if (OidIsValid(oprForm->oprnamespace)) + LockNotPinnedObject(NamespaceRelationId, oprForm->oprnamespace); + + if (OidIsValid(oprForm->oprleft)) + LockNotPinnedObject(TypeRelationId, oprForm->oprleft); + + if (OidIsValid(oprForm->oprright)) + LockNotPinnedObject(TypeRelationId, oprForm->oprright); + + if (OidIsValid(oprForm->oprresult)) + LockNotPinnedObject(TypeRelationId, oprForm->oprresult); + + if (OidIsValid(oprForm->oprcode)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprcode); + + if (OidIsValid(oprForm->oprrest)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprrest); + + if (OidIsValid(oprForm->oprjoin)) + LockNotPinnedObject(ProcedureRelationId, oprForm->oprjoin); + address = makeOperatorDependencies(tup, false, true); if (OidIsValid(commutatorOid) || OidIsValid(negatorOid)) diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c index 83056960fe4..cf5e1947929 100644 --- a/src/backend/commands/policy.c +++ b/src/backend/commands/policy.c @@ -722,6 +722,7 @@ CreatePolicy(CreatePolicyStmt *stmt) myself.objectId = policy_id; myself.objectSubId = 0; + LockNotPinnedObject(RelationRelationId, table_id); recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); recordDependencyOnExpr(&myself, qual, qual_pstate->p_rtable, @@ -1053,6 +1054,7 @@ AlterPolicy(AlterPolicyStmt *stmt) myself.objectId = policy_id; myself.objectSubId = 0; + LockNotPinnedObject(RelationRelationId, table_id); recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); recordDependencyOnExpr(&myself, qual, qual_parse_rtable, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c index 5036ac03639..f4a7e46b714 100644 --- a/src/backend/commands/proclang.c +++ b/src/backend/commands/proclang.c @@ -190,12 +190,14 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) /* dependency on the PL handler function */ ObjectAddressSet(referenced, ProcedureRelationId, handlerOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, handlerOid); /* dependency on the inline handler function, if any */ if (OidIsValid(inlineOid)) { ObjectAddressSet(referenced, ProcedureRelationId, inlineOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, inlineOid); } /* dependency on the validator function, if any */ @@ -203,6 +205,7 @@ CreateProceduralLanguage(CreatePLangStmt *stmt) { ObjectAddressSet(referenced, ProcedureRelationId, valOid); add_exact_object_address(&referenced, addrs); + LockNotPinnedObject(ProcedureRelationId, valOid); } record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c index b13ee2b745d..31dba914b87 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -1691,6 +1691,8 @@ process_owned_by(Relation seqrel, List *owned_by, bool for_identity) depobject.classId = RelationRelationId; depobject.objectId = RelationGetRelid(seqrel); depobject.objectSubId = 0; + + LockNotPinnedObject(RelationRelationId, RelationGetRelid(tablerel)); recordDependencyOn(&depobject, &refobject, deptype); } diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index a817821bf6d..5b7950d5823 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -88,6 +88,7 @@ CreateStatistics(CreateStatsStmt *stmt) bool build_mcv; bool build_expressions; bool requested_type = false; + bool locked_object = false; int i; ListCell *cell; ListCell *cell2; @@ -536,6 +537,12 @@ CreateStatistics(CreateStatsStmt *stmt) for (i = 0; i < nattnums; i++) { ObjectAddressSubSet(parentobject, RelationRelationId, relid, attnums[i]); + + if (!locked_object) + { + LockNotPinnedObject(RelationRelationId, relid); + locked_object = true; + } recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO); } @@ -553,6 +560,8 @@ CreateStatistics(CreateStatsStmt *stmt) if (!nattnums) { ObjectAddressSet(parentobject, RelationRelationId, relid); + + LockNotPinnedObject(RelationRelationId, relid); recordDependencyOn(&myself, &parentobject, DEPENDENCY_AUTO); } @@ -573,6 +582,7 @@ CreateStatistics(CreateStatsStmt *stmt) * than the underlying table(s). */ ObjectAddressSet(parentobject, NamespaceRelationId, namespaceId); + LockNotPinnedObject(NamespaceRelationId, namespaceId); recordDependencyOn(&myself, &parentobject, DEPENDENCY_NORMAL); recordDependencyOnOwner(StatisticExtRelationId, statoid, stxowner); diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 18f64db6e39..77b4d99f711 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3527,6 +3527,7 @@ StoreCatalogInheritance1(Oid relationId, Oid parentOid, childobject.objectId = relationId; childobject.objectSubId = 0; + LockNotPinnedObject(RelationRelationId, parentOid); recordDependencyOn(&childobject, &parentobject, child_dependency_type(child_is_partition)); @@ -7434,7 +7435,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * Add needed dependency entries for the new column. */ + LockNotPinnedObject(TypeRelationId, attribute->atttypid); add_column_datatype_dependency(myrelid, newattnum, attribute->atttypid); + LockNotPinnedObject(CollationRelationId, attribute->attcollation); add_column_collation_dependency(myrelid, newattnum, attribute->attcollation); /* @@ -10491,11 +10494,16 @@ addFkConstraint(addFkConstraintSides fkside, Assert(fkside != addFkBothSides); if (fkside == addFkReferencedSide) + { + LockNotPinnedObject(ConstraintRelationId, parentConstr); recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL); + } else { + LockNotPinnedObject(ConstraintRelationId, parentConstr); recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel)); + LockNotPinnedObject(RelationRelationId, RelationGetRelid(rel)); recordDependencyOn(&address, &referenced, DEPENDENCY_PARTITION_SEC); } } @@ -13969,7 +13977,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, table_close(attrelation, RowExclusiveLock); /* Install dependencies on new datatype and collation */ + LockNotPinnedObject(TypeRelationId, targettype); add_column_datatype_dependency(RelationGetRelid(rel), attnum, targettype); + LockNotPinnedObject(CollationRelationId, targetcollid); add_column_collation_dependency(RelationGetRelid(rel), attnum, targetcollid); /* @@ -15547,6 +15557,7 @@ ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) */ ObjectAddressSet(relobj, RelationRelationId, reloid); ObjectAddressSet(referenced, AccessMethodRelationId, rd_rel->relam); + LockNotPinnedObject(AccessMethodRelationId, rd_rel->relam); recordDependencyOn(&relobj, &referenced, DEPENDENCY_NORMAL); } else if (OidIsValid(oldAccessMethodId) && @@ -15566,6 +15577,7 @@ ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId) OidIsValid(rd_rel->relam)); /* Both are valid, so update the dependency */ + LockNotPinnedObject(AccessMethodRelationId, rd_rel->relam); changeDependencyFor(RelationRelationId, reloid, AccessMethodRelationId, oldAccessMethodId, rd_rel->relam); @@ -17288,6 +17300,7 @@ ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode) typeobj.classId = TypeRelationId; typeobj.objectId = typeid; typeobj.objectSubId = 0; + LockNotPinnedObject(TypeRelationId, typeid); recordDependencyOn(&tableobj, &typeobj, DEPENDENCY_NORMAL); /* Update pg_class.reloftype */ @@ -18060,14 +18073,17 @@ AlterRelationNamespaceInternal(Relation classRel, Oid relOid, /* Update dependency on schema if caller said so */ - if (hasDependEntry && - changeDependencyFor(RelationRelationId, - relOid, - NamespaceRelationId, - oldNspOid, - newNspOid) != 1) - elog(ERROR, "could not change schema dependency for relation \"%s\"", - NameStr(classForm->relname)); + if (hasDependEntry) + { + LockNotPinnedObject(NamespaceRelationId, newNspOid); + if (changeDependencyFor(RelationRelationId, + relOid, + NamespaceRelationId, + oldNspOid, + newNspOid) != 1) + elog(ERROR, "could not change schema dependency for relation \"%s\"", + NameStr(classForm->relname)); + } } else UnlockTuple(classRel, &classTup->t_self, InplaceUpdateTupleLock); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 7a5ffe32f60..3c1863c0e5b 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -1020,8 +1020,6 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, ((Form_pg_class) GETSTRUCT(tuple))->relhastriggers = true; CatalogTupleUpdate(pgrel, &tuple->t_self, tuple); - - CommandCounterIncrement(); } else CacheInvalidateRelcacheByTuple(tuple); @@ -1029,6 +1027,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, heap_freetuple(tuple); table_close(pgrel, RowExclusiveLock); + /* + * CommandCounterIncrement() here to ensure the new trigger entry is + * visible when LockNotPinnedObject() will check its existence before + * recording the dependencies. + */ + CommandCounterIncrement(); + /* * If we're replacing a trigger, flush all the old dependencies before * recording new ones. @@ -1047,6 +1052,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ProcedureRelationId; referenced.objectId = funcoid; referenced.objectSubId = 0; + LockNotPinnedObject(ProcedureRelationId, funcoid); recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); if (isInternal && OidIsValid(constraintOid)) @@ -1060,6 +1066,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ConstraintRelationId; referenced.objectId = constraintOid; referenced.objectSubId = 0; + LockNotPinnedObject(ConstraintRelationId, constraintOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); } else @@ -1072,6 +1079,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = RelationRelationId; referenced.objectId = RelationGetRelid(rel); referenced.objectSubId = 0; + + LockNotPinnedObject(RelationRelationId, RelationGetRelid(rel)); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); if (OidIsValid(constrrelid)) @@ -1079,6 +1088,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = RelationRelationId; referenced.objectId = constrrelid; referenced.objectSubId = 0; + + LockNotPinnedObject(RelationRelationId, constrrelid); recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); } /* Not possible to have an index dependency in this case */ @@ -1093,6 +1104,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, referenced.classId = ConstraintRelationId; referenced.objectId = constraintOid; referenced.objectSubId = 0; + LockNotPinnedObject(TriggerRelationId, trigoid); recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL); } @@ -1102,8 +1114,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, if (OidIsValid(parentTriggerOid)) { ObjectAddressSet(referenced, TriggerRelationId, parentTriggerOid); + LockNotPinnedObject(TriggerRelationId, parentTriggerOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel)); + + LockNotPinnedObject(RelationRelationId, RelationGetRelid(rel)); recordDependencyOn(&myself, &referenced, DEPENDENCY_PARTITION_SEC); } } @@ -1112,12 +1127,19 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, if (columns != NULL) { int i; + bool locked_object = false; referenced.classId = RelationRelationId; referenced.objectId = RelationGetRelid(rel); for (i = 0; i < ncolumns; i++) { referenced.objectSubId = columns[i]; + + if (!locked_object) + { + LockNotPinnedObject(RelationRelationId, RelationGetRelid(rel)); + locked_object = true; + } recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } } @@ -1257,9 +1279,12 @@ TriggerSetParentTrigger(Relation trigRel, ObjectAddressSet(depender, TriggerRelationId, childTrigId); ObjectAddressSet(referenced, TriggerRelationId, parentTrigId); + LockNotPinnedObject(TriggerRelationId, parentTrigId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI); ObjectAddressSet(referenced, RelationRelationId, childTableId); + + LockNotPinnedObject(RelationRelationId, childTableId); recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC); } else diff --git a/src/backend/commands/tsearchcmds.c b/src/backend/commands/tsearchcmds.c index ab16d42ad56..e36c3da1023 100644 --- a/src/backend/commands/tsearchcmds.c +++ b/src/backend/commands/tsearchcmds.c @@ -214,6 +214,7 @@ DefineTSParser(List *names, List *parameters) namestrcpy(&pname, prsname); values[Anum_pg_ts_parser_prsname - 1] = NameGetDatum(&pname); values[Anum_pg_ts_parser_prsnamespace - 1] = ObjectIdGetDatum(namespaceoid); + LockNotPinnedObject(NamespaceRelationId, namespaceoid); /* * loop over the definition list and extract the information we need. @@ -224,28 +225,48 @@ DefineTSParser(List *names, List *parameters) if (strcmp(defel->defname, "start") == 0) { + Oid procoid; + values[Anum_pg_ts_parser_prsstart - 1] = get_ts_parser_func(defel, Anum_pg_ts_parser_prsstart); + procoid = DatumGetObjectId(values[Anum_pg_ts_parser_prsstart - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "gettoken") == 0) { + Oid procoid; + values[Anum_pg_ts_parser_prstoken - 1] = get_ts_parser_func(defel, Anum_pg_ts_parser_prstoken); + procoid = DatumGetObjectId(values[Anum_pg_ts_parser_prstoken - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "end") == 0) { + Oid procoid; + values[Anum_pg_ts_parser_prsend - 1] = get_ts_parser_func(defel, Anum_pg_ts_parser_prsend); + procoid = DatumGetObjectId(values[Anum_pg_ts_parser_prsend - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "headline") == 0) { + Oid procoid; + values[Anum_pg_ts_parser_prsheadline - 1] = get_ts_parser_func(defel, Anum_pg_ts_parser_prsheadline); + procoid = DatumGetObjectId(values[Anum_pg_ts_parser_prsheadline - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "lextypes") == 0) { + Oid procoid; + values[Anum_pg_ts_parser_prslextype - 1] = get_ts_parser_func(defel, Anum_pg_ts_parser_prslextype); + procoid = DatumGetObjectId(values[Anum_pg_ts_parser_prslextype - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else ereport(ERROR, @@ -474,6 +495,10 @@ DefineTSDictionary(List *names, List *parameters) CatalogTupleInsert(dictRel, tup); + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, namespaceoid); + LockNotPinnedObject(TSTemplateRelationId, templId); + address = makeDictionaryDependencies(tup); /* Post creation hook for new text search dictionary */ @@ -723,6 +748,7 @@ DefineTSTemplate(List *names, List *parameters) namestrcpy(&dname, tmplname); values[Anum_pg_ts_template_tmplname - 1] = NameGetDatum(&dname); values[Anum_pg_ts_template_tmplnamespace - 1] = ObjectIdGetDatum(namespaceoid); + LockNotPinnedObject(NamespaceRelationId, namespaceoid); /* * loop over the definition list and extract the information we need. @@ -733,15 +759,23 @@ DefineTSTemplate(List *names, List *parameters) if (strcmp(defel->defname, "init") == 0) { + Oid procoid; + values[Anum_pg_ts_template_tmplinit - 1] = get_ts_template_func(defel, Anum_pg_ts_template_tmplinit); nulls[Anum_pg_ts_template_tmplinit - 1] = false; + procoid = DatumGetObjectId(values[Anum_pg_ts_template_tmplinit - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else if (strcmp(defel->defname, "lexize") == 0) { + Oid procoid; + values[Anum_pg_ts_template_tmpllexize - 1] = get_ts_template_func(defel, Anum_pg_ts_template_tmpllexize); nulls[Anum_pg_ts_template_tmpllexize - 1] = false; + procoid = DatumGetObjectId(values[Anum_pg_ts_template_tmpllexize - 1]); + LockNotPinnedObject(ProcedureRelationId, procoid); } else ereport(ERROR, @@ -998,6 +1032,10 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied) values[Anum_pg_ts_config_cfgowner - 1] = ObjectIdGetDatum(GetUserId()); values[Anum_pg_ts_config_cfgparser - 1] = ObjectIdGetDatum(prsOid); + /* Lock dependent objects */ + LockNotPinnedObject(NamespaceRelationId, namespaceoid); + LockNotPinnedObject(TSParserRelationId, prsOid); + tup = heap_form_tuple(cfgRel->rd_att, values, nulls); CatalogTupleInsert(cfgRel, tup); @@ -1063,6 +1101,7 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied) slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = cfgmap->mapseqno; slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = cfgmap->mapdict; + LockNotPinnedObject(TSDictionaryRelationId, cfgmap->mapdict); ExecStoreVirtualTuple(slot[slot_stored_count]); slot_stored_count++; @@ -1156,9 +1195,13 @@ ObjectAddress AlterTSConfiguration(AlterTSConfigurationStmt *stmt) { HeapTuple tup; + Form_pg_ts_config cfg; Oid cfgId; Relation relMap; ObjectAddress address; + ScanKeyData skey; + SysScanDesc scan; + HeapTuple maptup; /* Find the configuration */ tup = GetTSConfigTuple(stmt->cfgname); @@ -1168,7 +1211,8 @@ AlterTSConfiguration(AlterTSConfigurationStmt *stmt) errmsg("text search configuration \"%s\" does not exist", NameListToString(stmt->cfgname)))); - cfgId = ((Form_pg_ts_config) GETSTRUCT(tup))->oid; + cfg = (Form_pg_ts_config) GETSTRUCT(tup); + cfgId = cfg->oid; /* must be owner */ if (!object_ownercheck(TSConfigRelationId, cfgId, GetUserId())) @@ -1183,6 +1227,28 @@ AlterTSConfiguration(AlterTSConfigurationStmt *stmt) else if (stmt->tokentype) DropConfigurationMapping(stmt, tup, relMap); + /* Lock dependent objects */ + + ScanKeyInit(&skey, + Anum_pg_ts_config_map_mapcfg, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(cfgId)); + + scan = systable_beginscan(relMap, TSConfigMapIndexId, true, + NULL, 1, &skey); + + while (HeapTupleIsValid((maptup = systable_getnext(scan)))) + { + Form_pg_ts_config_map cfgmap = (Form_pg_ts_config_map) GETSTRUCT(maptup); + + LockNotPinnedObject(TSDictionaryRelationId, cfgmap->mapdict); + } + + systable_endscan(scan); + + LockNotPinnedObject(NamespaceRelationId, cfg->cfgnamespace); + LockNotPinnedObject(TSParserRelationId, cfg->cfgparser); + /* Update dependencies */ makeConfigurationDependencies(tup, true, relMap); @@ -1414,6 +1480,8 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, repl_val[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictNew); repl_repl[Anum_pg_ts_config_map_mapdict - 1] = true; + LockNotPinnedObject(TSDictionaryRelationId, dictNew); + newtup = heap_modify_tuple(maptup, RelationGetDescr(relMap), repl_val, repl_null, repl_repl); @@ -1456,6 +1524,9 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = Int32GetDatum(j + 1); slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictIds[j]); + /* Lock dependent objects */ + LockNotPinnedObject(TSDictionaryRelationId, dictIds[j]); + ExecStoreVirtualTuple(slot[slotCount]); slotCount++; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 3cb3ca1cca1..e36ea6d97e0 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1820,6 +1820,7 @@ makeRangeConstructors(const char *name, Oid namespace, * that they go away silently when the type is dropped. Note that * pg_dump depends on this choice to avoid dumping the constructors. */ + LockNotPinnedObject(TypeRelationId, rangeOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); } } @@ -1885,6 +1886,7 @@ makeMultirangeConstructors(const char *name, Oid namespace, * that they go away silently when the type is dropped. Note that pg_dump * depends on this choice to avoid dumping the constructors. */ + LockNotPinnedObject(TypeRelationId, multirangeOid); recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); pfree(argtypes); @@ -2698,6 +2700,45 @@ AlterDomainDefault(List *names, Node *defaultRaw) CatalogTupleUpdate(rel, &tup->t_self, newtuple); + /* Lock dependent objects */ + typTup = (Form_pg_type) GETSTRUCT(newtuple); + + if (OidIsValid(typTup->typnamespace)) + LockNotPinnedObject(NamespaceRelationId, typTup->typnamespace); + + if (OidIsValid(typTup->typinput)) + LockNotPinnedObject(ProcedureRelationId, typTup->typinput); + + if (OidIsValid(typTup->typoutput)) + LockNotPinnedObject(ProcedureRelationId, typTup->typoutput); + + if (OidIsValid(typTup->typreceive)) + LockNotPinnedObject(ProcedureRelationId, typTup->typreceive); + + if (OidIsValid(typTup->typsend)) + LockNotPinnedObject(ProcedureRelationId, typTup->typsend); + + if (OidIsValid(typTup->typmodin)) + LockNotPinnedObject(ProcedureRelationId, typTup->typmodin); + + if (OidIsValid(typTup->typmodout)) + LockNotPinnedObject(ProcedureRelationId, typTup->typmodout); + + if (OidIsValid(typTup->typanalyze)) + LockNotPinnedObject(ProcedureRelationId, typTup->typanalyze); + + if (OidIsValid(typTup->typsubscript)) + LockNotPinnedObject(ProcedureRelationId, typTup->typsubscript); + + if (OidIsValid(typTup->typbasetype)) + LockNotPinnedObject(TypeRelationId, typTup->typbasetype); + + if (OidIsValid(typTup->typcollation)) + LockNotPinnedObject(CollationRelationId, typTup->typcollation); + + if (OidIsValid(typTup->typelem)) + LockNotPinnedObject(TypeRelationId, typTup->typelem); + /* Rebuild dependencies */ GenerateTypeDependencies(newtuple, rel, @@ -4263,10 +4304,13 @@ AlterTypeNamespaceInternal(Oid typeOid, Oid nspOid, if (oldNspOid != nspOid && (isCompositeType || typform->typtype != TYPTYPE_COMPOSITE) && !isImplicitArray) + { + LockNotPinnedObject(NamespaceRelationId, nspOid); if (changeDependencyFor(TypeRelationId, typeOid, NamespaceRelationId, oldNspOid, nspOid) != 1) elog(ERROR, "could not change schema dependency for type \"%s\"", format_type_be(typeOid)); + } InvokeObjectPostAlterHook(TypeRelationId, typeOid, 0); @@ -4558,6 +4602,7 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray, SysScanDesc scan; ScanKeyData key[1]; HeapTuple domainTup; + Form_pg_type typeForm; /* Since this function recurses, it could be driven to stack overflow */ check_stack_depth(); @@ -4606,6 +4651,45 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray, newtup = heap_modify_tuple(tup, RelationGetDescr(catalog), values, nulls, replaces); + /* Lock dependent objects */ + typeForm = (Form_pg_type) GETSTRUCT(newtup); + + if (OidIsValid(typeForm->typnamespace)) + LockNotPinnedObject(NamespaceRelationId, typeForm->typnamespace); + + if (OidIsValid(typeForm->typinput)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typinput); + + if (OidIsValid(typeForm->typoutput)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typoutput); + + if (OidIsValid(typeForm->typreceive)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typreceive); + + if (OidIsValid(typeForm->typsend)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typsend); + + if (OidIsValid(typeForm->typmodin)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typmodin); + + if (OidIsValid(typeForm->typmodout)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typmodout); + + if (OidIsValid(typeForm->typanalyze)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typanalyze); + + if (OidIsValid(typeForm->typsubscript)) + LockNotPinnedObject(ProcedureRelationId, typeForm->typsubscript); + + if (OidIsValid(typeForm->typbasetype)) + LockNotPinnedObject(TypeRelationId, typeForm->typbasetype); + + if (OidIsValid(typeForm->typcollation)) + LockNotPinnedObject(CollationRelationId, typeForm->typcollation); + + if (OidIsValid(typeForm->typelem)) + LockNotPinnedObject(TypeRelationId, typeForm->typelem); + CatalogTupleUpdate(catalog, &newtup->t_self, newtup); /* Rebuild dependencies for this type */ diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 8aa90b0d6fb..14a6468efca 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -155,6 +155,7 @@ InsertRule(const char *rulname, referenced.objectId = eventrel_oid; referenced.objectSubId = 0; + LockNotPinnedObject(RelationRelationId, eventrel_oid); recordDependencyOn(&myself, &referenced, (evtype == CMD_SELECT) ? DEPENDENCY_INTERNAL : DEPENDENCY_AUTO); diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index c96aa7c49ef..c664ae4977b 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -277,6 +277,7 @@ Section: Class 28 - Invalid Authorization Specification Section: Class 2B - Dependent Privilege Descriptors Still Exist 2B000 E ERRCODE_DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST dependent_privilege_descriptors_still_exist +2BP02 E ERRCODE_DEPENDENT_OBJECTS_DOES_NOT_EXIST dependent_objects_does_not_exist 2BP01 E ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST dependent_objects_still_exist Section: Class 2D - Invalid Transaction Termination diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 0ea7ccf5243..73ee2c6a787 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -101,6 +101,8 @@ typedef struct ObjectAddresses ObjectAddresses; /* in dependency.c */ extern void AcquireDeletionLock(const ObjectAddress *object, int flags); +extern void LockNotPinnedObjectById(const ObjectAddress *object); +extern void LockNotPinnedObject(Oid classid, Oid objid); extern void ReleaseDeletionLock(const ObjectAddress *object); @@ -172,6 +174,7 @@ extern long changeDependenciesOf(Oid classId, Oid oldObjectId, extern long changeDependenciesOn(Oid refClassId, Oid oldRefObjectId, Oid newRefObjectId); +extern bool isObjectPinned(const ObjectAddress *object); extern Oid getExtensionOfObject(Oid classId, Oid objectId); extern List *getAutoExtensionsOfObject(Oid classId, Oid objectId); diff --git a/src/include/catalog/objectaddress.h b/src/include/catalog/objectaddress.h index 630434b73cf..34d5428433b 100644 --- a/src/include/catalog/objectaddress.h +++ b/src/include/catalog/objectaddress.h @@ -53,6 +53,7 @@ extern void check_object_ownership(Oid roleid, Node *object, Relation relation); extern Oid get_object_namespace(const ObjectAddress *address); +extern bool ObjectByIdExist(const ObjectAddress *address); extern bool is_objectclass_supported(Oid class_id); extern const char *get_object_class_descr(Oid class_id); diff --git a/src/test/isolation/expected/test_dependencies_locks.out b/src/test/isolation/expected/test_dependencies_locks.out new file mode 100644 index 00000000000..820680f5e16 --- /dev/null +++ b/src/test/isolation/expected/test_dependencies_locks.out @@ -0,0 +1,129 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin s1_create_function_in_schema s2_drop_schema s1_commit +step s1_begin: BEGIN; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_schema: DROP SCHEMA testschema; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_schema: <... completed> +ERROR: cannot drop schema testschema because other objects depend on it + +starting permutation: s2_begin s2_drop_schema s1_create_function_in_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_schema: DROP SCHEMA testschema; +step s1_create_function_in_schema: CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_in_schema: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_alter_function_schema s2_drop_alterschema s1_commit +step s1_begin: BEGIN; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; +step s2_drop_alterschema: DROP SCHEMA alterschema; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_alterschema: <... completed> +ERROR: cannot drop schema alterschema because other objects depend on it + +starting permutation: s2_begin s2_drop_alterschema s1_alter_function_schema s2_commit +step s2_begin: BEGIN; +step s2_drop_alterschema: DROP SCHEMA alterschema; +step s1_alter_function_schema: ALTER FUNCTION public.falter() SET SCHEMA alterschema; <waiting ...> +step s2_commit: COMMIT; +step s1_alter_function_schema: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_function_with_argtype s2_drop_foo_type s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; +step s2_drop_foo_type: DROP TYPE public.foo; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_foo_type: <... completed> +ERROR: cannot drop type foo because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_type s1_create_function_with_argtype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_type: DROP TYPE public.foo; +step s1_create_function_with_argtype: CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_argtype: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_function_with_rettype s2_drop_foo_rettype s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; +step s2_drop_foo_rettype: DROP DOMAIN id; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_foo_rettype: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_foo_rettype s1_create_function_with_rettype s2_commit +step s2_begin: BEGIN; +step s2_drop_foo_rettype: DROP DOMAIN id; +step s1_create_function_with_rettype: CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_rettype: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_function_with_function s2_drop_function_f s1_commit +step s1_begin: BEGIN; +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; +step s2_drop_function_f: DROP FUNCTION f(); <waiting ...> +step s1_commit: COMMIT; +step s2_drop_function_f: <... completed> +ERROR: cannot drop function f() because other objects depend on it + +starting permutation: s2_begin s2_drop_function_f s1_create_function_with_function s2_commit +step s2_begin: BEGIN; +step s2_drop_function_f: DROP FUNCTION f(); +step s1_create_function_with_function: CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; <waiting ...> +step s2_commit: COMMIT; +step s1_create_function_with_function: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_domain_with_domain s2_drop_domain_id s1_commit +step s1_begin: BEGIN; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; +step s2_drop_domain_id: DROP DOMAIN id; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_domain_id: <... completed> +ERROR: cannot drop type id because other objects depend on it + +starting permutation: s2_begin s2_drop_domain_id s1_create_domain_with_domain s2_commit +step s2_begin: BEGIN; +step s2_drop_domain_id: DROP DOMAIN id; +step s1_create_domain_with_domain: CREATE DOMAIN idid as id; <waiting ...> +step s2_commit: COMMIT; +step s1_create_domain_with_domain: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_table_with_type s2_drop_footab_type s1_commit +step s1_begin: BEGIN; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); +step s2_drop_footab_type: DROP TYPE public.footab; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_footab_type: <... completed> +ERROR: cannot drop type footab because other objects depend on it + +starting permutation: s2_begin s2_drop_footab_type s1_create_table_with_type s2_commit +step s2_begin: BEGIN; +step s2_drop_footab_type: DROP TYPE public.footab; +step s1_create_table_with_type: CREATE TABLE tabtype(a footab); <waiting ...> +step s2_commit: COMMIT; +step s1_create_table_with_type: <... completed> +ERROR: dependent object does not exist + +starting permutation: s1_begin s1_create_server_with_fdw_wrapper s2_drop_fdw_wrapper s1_commit +step s1_begin: BEGIN; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; <waiting ...> +step s1_commit: COMMIT; +step s2_drop_fdw_wrapper: <... completed> +ERROR: cannot drop foreign-data wrapper fdw_wrapper because other objects depend on it + +starting permutation: s2_begin s2_drop_fdw_wrapper s1_create_server_with_fdw_wrapper s2_commit +step s2_begin: BEGIN; +step s2_drop_fdw_wrapper: DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; +step s1_create_server_with_fdw_wrapper: CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; <waiting ...> +step s2_commit: COMMIT; +step s1_create_server_with_fdw_wrapper: <... completed> +ERROR: dependent object does not exist diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 143109aa4da..0e80dfecfb3 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -115,3 +115,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: test_dependencies_locks diff --git a/src/test/isolation/specs/test_dependencies_locks.spec b/src/test/isolation/specs/test_dependencies_locks.spec new file mode 100644 index 00000000000..5d04dfe9dc6 --- /dev/null +++ b/src/test/isolation/specs/test_dependencies_locks.spec @@ -0,0 +1,89 @@ +setup +{ + CREATE SCHEMA testschema; + CREATE SCHEMA alterschema; + CREATE TYPE public.foo as enum ('one', 'two'); + CREATE TYPE public.footab as enum ('three', 'four'); + CREATE DOMAIN id AS int; + CREATE FUNCTION f() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FUNCTION public.falter() RETURNS int LANGUAGE SQL RETURN 1; + CREATE FOREIGN DATA WRAPPER fdw_wrapper; +} + +teardown +{ + DROP FUNCTION IF EXISTS testschema.foo(); + DROP FUNCTION IF EXISTS fooargtype(num foo); + DROP FUNCTION IF EXISTS footrettype(); + DROP FUNCTION IF EXISTS foofunc(); + DROP FUNCTION IF EXISTS public.falter(); + DROP FUNCTION IF EXISTS alterschema.falter(); + DROP DOMAIN IF EXISTS idid; + DROP SERVER IF EXISTS srv_fdw_wrapper; + DROP TABLE IF EXISTS tabtype; + DROP SCHEMA IF EXISTS testschema; + DROP SCHEMA IF EXISTS alterschema; + DROP TYPE IF EXISTS public.foo; + DROP TYPE IF EXISTS public.footab; + DROP DOMAIN IF EXISTS id; + DROP FUNCTION IF EXISTS f(); + DROP FOREIGN DATA WRAPPER IF EXISTS fdw_wrapper; +} + +session "s1" + +step "s1_begin" { BEGIN; } +step "s1_create_function_in_schema" { CREATE FUNCTION testschema.foo() RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_argtype" { CREATE FUNCTION fooargtype(num foo) RETURNS int AS 'select 1' LANGUAGE sql; } +step "s1_create_function_with_rettype" { CREATE FUNCTION footrettype() RETURNS id LANGUAGE sql RETURN 1; } +step "s1_create_function_with_function" { CREATE FUNCTION foofunc() RETURNS int LANGUAGE SQL RETURN f() + 1; } +step "s1_alter_function_schema" { ALTER FUNCTION public.falter() SET SCHEMA alterschema; } +step "s1_create_domain_with_domain" { CREATE DOMAIN idid as id; } +step "s1_create_table_with_type" { CREATE TABLE tabtype(a footab); } +step "s1_create_server_with_fdw_wrapper" { CREATE SERVER srv_fdw_wrapper FOREIGN DATA WRAPPER fdw_wrapper; } +step "s1_commit" { COMMIT; } + +session "s2" + +step "s2_begin" { BEGIN; } +step "s2_drop_schema" { DROP SCHEMA testschema; } +step "s2_drop_alterschema" { DROP SCHEMA alterschema; } +step "s2_drop_foo_type" { DROP TYPE public.foo; } +step "s2_drop_foo_rettype" { DROP DOMAIN id; } +step "s2_drop_footab_type" { DROP TYPE public.footab; } +step "s2_drop_function_f" { DROP FUNCTION f(); } +step "s2_drop_domain_id" { DROP DOMAIN id; } +step "s2_drop_fdw_wrapper" { DROP FOREIGN DATA WRAPPER fdw_wrapper RESTRICT; } +step "s2_commit" { COMMIT; } + +# function - schema +permutation "s1_begin" "s1_create_function_in_schema" "s2_drop_schema" "s1_commit" +permutation "s2_begin" "s2_drop_schema" "s1_create_function_in_schema" "s2_commit" + +# alter function - schema +permutation "s1_begin" "s1_alter_function_schema" "s2_drop_alterschema" "s1_commit" +permutation "s2_begin" "s2_drop_alterschema" "s1_alter_function_schema" "s2_commit" + +# function - argtype +permutation "s1_begin" "s1_create_function_with_argtype" "s2_drop_foo_type" "s1_commit" +permutation "s2_begin" "s2_drop_foo_type" "s1_create_function_with_argtype" "s2_commit" + +# function - rettype +permutation "s1_begin" "s1_create_function_with_rettype" "s2_drop_foo_rettype" "s1_commit" +permutation "s2_begin" "s2_drop_foo_rettype" "s1_create_function_with_rettype" "s2_commit" + +# function - function +permutation "s1_begin" "s1_create_function_with_function" "s2_drop_function_f" "s1_commit" +permutation "s2_begin" "s2_drop_function_f" "s1_create_function_with_function" "s2_commit" + +# domain - domain +permutation "s1_begin" "s1_create_domain_with_domain" "s2_drop_domain_id" "s1_commit" +permutation "s2_begin" "s2_drop_domain_id" "s1_create_domain_with_domain" "s2_commit" + +# table - type +permutation "s1_begin" "s1_create_table_with_type" "s2_drop_footab_type" "s1_commit" +permutation "s2_begin" "s2_drop_footab_type" "s1_create_table_with_type" "s2_commit" + +# server - foreign data wrapper +permutation "s1_begin" "s1_create_server_with_fdw_wrapper" "s2_drop_fdw_wrapper" "s1_commit" +permutation "s2_begin" "s2_drop_fdw_wrapper" "s1_create_server_with_fdw_wrapper" "s2_commit" diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 362f38856d2..7c162c0d455 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -2862,11 +2862,12 @@ begin; alter table alterlock2 add constraint alterlock2nv foreign key (f1) references alterlock (f1) NOT VALID; select * from my_locks order by 1; - relname | max_lockmode -------------+----------------------- - alterlock | ShareRowExclusiveLock - alterlock2 | ShareRowExclusiveLock -(2 rows) + relname | max_lockmode +----------------+----------------------- + alterlock | ShareRowExclusiveLock + alterlock2 | ShareRowExclusiveLock + alterlock_pkey | AccessShareLock +(3 rows) commit; begin; -- 2.34.1 --jlrXVwdftypL847/-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v3 6/7] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/commands/tablecmds.c | 4 ++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- src/include/catalog/pg_retired.dat | 3 ++- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1824faab231..a6f7cdf3a36 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..12c4ce08437 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17480,8 +17480,8 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), false); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 4f2c93f1ee2..d87b361a093 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index ffec11c5539..4304ab220ba 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3986,9 +3986,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8518,7 +8515,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', diff --git a/src/include/catalog/pg_retired.dat b/src/include/catalog/pg_retired.dat index 90928a9cb00..8b5f58850f6 100644 --- a/src/include/catalog/pg_retired.dat +++ b/src/include/catalog/pg_retired.dat @@ -21,6 +21,7 @@ { oid => '1573', proname => 'pg_get_ruledef' }, { oid => '1640', proname => 'pg_get_viewdef' }, { oid => '1641', proname => 'pg_get_viewdef' }, -{ oid => '1643', proname => 'pg_get_indexdef' } +{ oid => '1643', proname => 'pg_get_indexdef' }, +{ oid => '1716', proname => 'pg_get_expr' } ] -- 2.43.0 --zd8Z8rlPOcTi77n/ Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0007-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v4 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 7 +++---- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b04b0dbd2a0..a523a512949 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17477,8 +17477,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 03550f0d80b..a70807e84ca 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2574,23 +2574,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1d1b24a8f4c..69c1e5bb264 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3987,9 +3987,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8531,7 +8528,9 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.43.0 --eEEy3EHc57lA8spa Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v4-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v5 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 7 +++---- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0ce2e81f9c2..fad4202b1ff 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17694,8 +17694,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1afa2531f30..972f89eafbc 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e148b1ad8e3..f36f4625e50 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4000,9 +4000,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8595,7 +8592,9 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.52.0 --m23X5uHFNxthGTU3 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v5-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v7 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index eec09ba1ded..73eb6322daf 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17695,8 +17695,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2d39bce7fd7..707f83d4310 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 39139fd9e3b..855529509de 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8638,9 +8635,11 @@ pronargdefaults => '1', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.52.0 --GzYugJnip6WHHvTk Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v7-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v8 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a1845240a98..f063f744f6d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17730,8 +17730,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d83be8cf77d..e60478677e0 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7e452aa8c27..71bd2aaf3d9 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8605,9 +8602,11 @@ proargmodes => '{i,v}', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.53.0 --iWwo79nqhOvOs5kf Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v8-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v8.1 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a1845240a98..f063f744f6d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17730,8 +17730,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d83be8cf77d..e60478677e0 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e610a6213ec..df43469be3d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8605,9 +8602,11 @@ proargmodes => '{i,v}', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.53.0 --AcCatzXgbvyipyEy Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v8.1-0006-Handle-pg_get_triggerdef-default-args-in-system.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v1 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1f89dcc7908..81d210a9c45 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 416144c49f5..9effa02fa2e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a9431ea4037..10c5286bd7d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3977,9 +3977,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8502,7 +8499,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.51.2 --IRGsBYp1UN8d6WrT Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v1-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v1 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1f89dcc7908..81d210a9c45 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 416144c49f5..9effa02fa2e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a9431ea4037..10c5286bd7d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3977,9 +3977,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8502,7 +8499,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.51.2 --IRGsBYp1UN8d6WrT Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v1-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v3 6/7] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/commands/tablecmds.c | 4 ++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- src/include/catalog/pg_retired.dat | 3 ++- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1824faab231..a6f7cdf3a36 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..12c4ce08437 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17480,8 +17480,8 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), false); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 4f2c93f1ee2..d87b361a093 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index ffec11c5539..4304ab220ba 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3986,9 +3986,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8518,7 +8515,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', diff --git a/src/include/catalog/pg_retired.dat b/src/include/catalog/pg_retired.dat index 90928a9cb00..8b5f58850f6 100644 --- a/src/include/catalog/pg_retired.dat +++ b/src/include/catalog/pg_retired.dat @@ -21,6 +21,7 @@ { oid => '1573', proname => 'pg_get_ruledef' }, { oid => '1640', proname => 'pg_get_viewdef' }, { oid => '1641', proname => 'pg_get_viewdef' }, -{ oid => '1643', proname => 'pg_get_indexdef' } +{ oid => '1643', proname => 'pg_get_indexdef' }, +{ oid => '1716', proname => 'pg_get_expr' } ] -- 2.43.0 --zd8Z8rlPOcTi77n/ Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v3-0007-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v4 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 7 +++---- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index b04b0dbd2a0..a523a512949 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17477,8 +17477,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 03550f0d80b..a70807e84ca 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2574,23 +2574,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1d1b24a8f4c..69c1e5bb264 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3987,9 +3987,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8531,7 +8528,9 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.43.0 --eEEy3EHc57lA8spa Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v4-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v5 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 7 +++---- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0ce2e81f9c2..fad4202b1ff 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17694,8 +17694,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1afa2531f30..972f89eafbc 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e148b1ad8e3..f36f4625e50 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4000,9 +4000,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8595,7 +8592,9 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.52.0 --m23X5uHFNxthGTU3 Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v5-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v7 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index eec09ba1ded..73eb6322daf 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17695,8 +17695,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2d39bce7fd7..707f83d4310 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 39139fd9e3b..855529509de 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8638,9 +8635,11 @@ pronargdefaults => '1', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.52.0 --GzYugJnip6WHHvTk Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v7-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v8 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a1845240a98..f063f744f6d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17730,8 +17730,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d83be8cf77d..e60478677e0 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7e452aa8c27..71bd2aaf3d9 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8605,9 +8602,11 @@ proargmodes => '{i,v}', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.53.0 --iWwo79nqhOvOs5kf Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v8-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v8.1 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use proargdefaults to handle the optional pretty argument. That also means any direct function calls now needs to set the pretty parameter. --- src/backend/commands/tablecmds.c | 5 +++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 9 ++++----- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index a1845240a98..f063f744f6d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17730,8 +17730,9 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), + BoolGetDatum(false)); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d83be8cf77d..e60478677e0 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2932,23 +2932,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e610a6213ec..df43469be3d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -4006,9 +4006,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8605,9 +8602,11 @@ proargmodes => '{i,v}', proargdefaults => '{NULL}', prosrc => 'pg_get_database_ddl' }, { oid => '2509', - descr => 'deparse an encoded expression with pretty-print option', + descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', + proargnames => '{expr,relation,pretty}', proargdefaults => '{false}', + prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.53.0 --AcCatzXgbvyipyEy Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v8.1-0006-Handle-pg_get_triggerdef-default-args-in-system.patch ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v2 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/commands/tablecmds.c | 4 ++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1824faab231..a6f7cdf3a36 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..12c4ce08437 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17480,8 +17480,8 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), false); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 9faf340f0c6..305e3e80c41 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 96eb7baf8e0..3a0be2d4b2a 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3986,9 +3986,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8518,7 +8515,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.43.0 --3/nz5wg6+DYwHsLU Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH v2 5/6] Handle pg_get_expr default args in system_functions.sql @ 2025-12-09 19:17 Mark Wong <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Mark Wong @ 2025-12-09 19:17 UTC (permalink / raw) Modernize pg_get_expr to use CREATE OR REPLACE FUNCTION to handle the optional pretty argument. --- src/backend/catalog/system_functions.sql | 7 +++++++ src/backend/commands/tablecmds.c | 4 ++-- src/backend/utils/adt/ruleutils.c | 17 ----------------- src/include/catalog/pg_proc.dat | 5 +---- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 1824faab231..a6f7cdf3a36 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -692,6 +692,13 @@ LANGUAGE INTERNAL PARALLEL SAFE AS 'pg_get_constraintdef'; +CREATE OR REPLACE FUNCTION + pg_get_expr(expr pg_node_tree, relation oid, pretty bool DEFAULT false) +RETURNS TEXT +LANGUAGE INTERNAL +PARALLEL SAFE +AS 'pg_get_expr'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..12c4ce08437 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -17480,8 +17480,8 @@ decompile_conbin(HeapTuple contup, TupleDesc tupdesc) if (isnull) elog(ERROR, "null conbin for constraint %u", con->oid); - expr = DirectFunctionCall2(pg_get_expr, attr, - ObjectIdGetDatum(con->conrelid)); + expr = DirectFunctionCall3(pg_get_expr, attr, + ObjectIdGetDatum(con->conrelid), false); return TextDatumGetCString(expr); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 9faf340f0c6..305e3e80c41 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2573,23 +2573,6 @@ decompile_column_index_array(Datum column_index_array, Oid relId, */ Datum pg_get_expr(PG_FUNCTION_ARGS) -{ - text *expr = PG_GETARG_TEXT_PP(0); - Oid relid = PG_GETARG_OID(1); - text *result; - int prettyFlags; - - prettyFlags = PRETTYFLAG_INDENT; - - result = pg_get_expr_worker(expr, relid, prettyFlags); - if (result) - PG_RETURN_TEXT_P(result); - else - PG_RETURN_NULL(); -} - -Datum -pg_get_expr_ext(PG_FUNCTION_ARGS) { text *expr = PG_GETARG_TEXT_PP(0); Oid relid = PG_GETARG_OID(1); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 96eb7baf8e0..3a0be2d4b2a 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3986,9 +3986,6 @@ { oid => '1662', descr => 'trigger description', proname => 'pg_get_triggerdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_triggerdef' }, -{ oid => '1716', descr => 'deparse an encoded expression', - proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, { oid => '1665', descr => 'name of sequence for a serial column', proname => 'pg_get_serial_sequence', provolatile => 's', prorettype => 'text', proargtypes => 'text text', prosrc => 'pg_get_serial_sequence' }, @@ -8518,7 +8515,7 @@ { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', - proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr_ext' }, + proargtypes => 'pg_node_tree oid bool', prosrc => 'pg_get_expr' }, { oid => '2510', descr => 'get the prepared statements for this session', proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', -- 2.43.0 --3/nz5wg6+DYwHsLU Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0006-Handle-pg_get_triggerdef-default-args-in-system_f.patch" ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
* [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. @ 2026-05-12 10:27 Antonin Houska <[email protected]> 0 siblings, 0 replies; 245+ messages in thread From: Antonin Houska @ 2026-05-12 10:27 UTC (permalink / raw) The fix: https://www.postgresql.org/message-id/77611.1778055944%40localhost --- src/backend/access/transam/xact.c | 2 + src/backend/commands/repack_worker.c | 2 + src/test/isolation/isolationtester.c | 9 +- .../expected/repack_running_xacts.out | 81 ++++++++++++ .../specs/repack_running_xacts.spec | 119 ++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/repack_running_xacts.out create mode 100644 src/test/modules/injection_points/specs/repack_running_xacts.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 5586fbe5b07..b63ee166028 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -2428,6 +2429,7 @@ CommitTransaction(void) * must be done _before_ releasing locks we hold and _after_ * RecordTransactionCommit. */ + INJECTION_POINT("before-end-transaction", NULL); ProcArrayEndTransaction(MyProc, latestXid); /* diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index c40f8c98e06..6835626d677 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -26,6 +26,7 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "tcop/tcopprot.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #define REPL_PLUGIN_NAME "pgrepack" @@ -233,6 +234,7 @@ repack_setup_logical_decoding(Oid relid) * Neither prepare_write nor do_write callback nor update_progress is * useful for us. */ + INJECTION_POINT("before-create-decoding-context", NULL); ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index 440c875b8ac..8f17ee412c9 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -216,15 +216,22 @@ main(int argc, char **argv) * exactly expect concurrent use of test tables. However, autovacuum will * occasionally take AccessExclusiveLock to truncate a table, and we must * ignore that transient wait. + * + * If the session's backend is blocked, and if its background worker is + * waiting on an injection point, we assume that the injection point is + * the reason for the backend to be blocked. That's what we check in the + * second query of the UNION. XXX Should we use a separate query for that? */ initPQExpBuffer(&wait_query); appendPQExpBufferStr(&wait_query, + "WITH blocking(res) AS (" "SELECT pg_catalog.pg_isolation_test_session_is_blocked($1, '{"); /* The spec syntax requires at least one session; assume that here. */ appendPQExpBufferStr(&wait_query, conns[1].backend_pid_str); for (i = 2; i < nconns; i++) appendPQExpBuffer(&wait_query, ",%s", conns[i].backend_pid_str); - appendPQExpBufferStr(&wait_query, "}')"); + appendPQExpBufferStr(&wait_query, "}') UNION " + "SELECT pg_catalog.pg_isolation_test_session_is_blocked(pid, '{}') FROM pg_stat_activity WHERE leader_pid=$1) SELECT bool_or(res) FROM blocking"); res = PQprepare(conns[0].conn, PREP_WAITING, wait_query.data, 0, NULL); if (PQresultStatus(res) != PGRES_COMMAND_OK) diff --git a/src/test/modules/injection_points/expected/repack_running_xacts.out b/src/test/modules/injection_points/expected/repack_running_xacts.out new file mode 100644 index 00000000000..271fe2b97cb --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_running_xacts.out @@ -0,0 +1,81 @@ +Parsed test spec with 5 sessions + +starting permutation: repack s3_assign_xid wakeup_bcdc s4_changes s4_attach s4_commit s3_commit s5_assign_xid wakeup_bet s5_commit check +injection_points_attach +----------------------- + +(1 row) + +step repack: + REPACK (CONCURRENTLY) repack_test; + <waiting ...> +step s3_assign_xid: + BEGIN; + INSERT INTO aux VALUES (1); + +step wakeup_bcdc: + SELECT injection_points_wakeup('before-create-decoding-context'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s4_changes: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); + +step s4_attach: + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); + +injection_points_set_local +-------------------------- + +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s4_commit: + COMMIT; + <waiting ...> +step s3_commit: + COMMIT; + +step s5_assign_xid: + BEGIN; + INSERT INTO aux VALUES (2); + +step wakeup_bet: + SELECT injection_points_wakeup('before-end-transaction'); + +injection_points_wakeup +----------------------- + +(1 row) + +step repack: <... completed> +step s4_commit: <... completed> +step s5_commit: + COMMIT; + +step check: + TABLE repack_test; + +i|j +-+- +(0 rows) + +injection_points_detach +----------------------- + +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/specs/repack_running_xacts.spec b/src/test/modules/injection_points/specs/repack_running_xacts.spec new file mode 100644 index 00000000000..1f878514046 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_running_xacts.spec @@ -0,0 +1,119 @@ +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + CREATE TABLE aux(i int); +} + +teardown +{ + DROP TABLE repack_test; + DROP TABLE aux; + DROP EXTENSION injection_points; +} + +session s1 +setup +{ + SELECT injection_points_attach('before-create-decoding-context', 'wait'); +} +step repack +{ + REPACK (CONCURRENTLY) repack_test; +} +step check +{ + TABLE repack_test; +} +teardown +{ + SELECT injection_points_detach('before-create-decoding-context'); +} + +session s2 +step wakeup_bcdc +{ + SELECT injection_points_wakeup('before-create-decoding-context'); +} +step wakeup_bet +{ + SELECT injection_points_wakeup('before-end-transaction'); +} + +session s3 +step s3_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (1); +} +step s3_commit +{ + COMMIT; +} + +session s4 +step s4_changes +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (1, 1); +} +# Do not attach in the setup section, that would be too soon. +step s4_attach +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('before-end-transaction', 'wait'); +} +step s4_commit +{ + COMMIT; +} +teardown +{ + SELECT injection_points_detach('before-end-transaction'); +} + +session s5 +step s5_assign_xid +{ + BEGIN; + INSERT INTO aux VALUES (2); +} +step s5_commit +{ + COMMIT; +} + +permutation +repack +# Assign XID so that a running transaction prevents the snapshot builder from +# reaching CONSISTENT state immediately. It will wait for this to complete +# after having reached BUILDING_SNAPSHOT. +s3_assign_xid +# Let the decoding setup start. +wakeup_bcdc +# Likewise, the snapshot builder will wait for the s4's xact to complete after +# having reached FULL_SNAPSHOT. This is the problematic transaction, so let it +# do some changes. +s4_changes +# Attach to the 'before-end-transaction' injection point that s4 will need +# during commit. +s4_attach +# Only write commit record for s4, but do not remove the xact from procarray +# yet. Thus the snapshot builder still needs to wait. +s4_commit +# Let the snapshot builder proceed to FULL_SNAPSHOT. +s3_commit +# Start another transaction so that CONSISTENT is not reached "directly", +# i.e. due to no running transaction. It's important here that builder->xmin +# does not advance. +s5_assign_xid +# Remove s4 xact from procarray, and thus reach the CONSISTENT state. Since +# the COMMIT appeared in WAL too early (i.e. when the snapshot builder state +# did not allow decoding of COMMIT records yet), the snapshot builder will +# consider s4 running. This is also due to returning from +# SnapBuildProcessRunningXacts() too early, w/o advancing builder->xmin. +wakeup_bet +# s5 is not needed anymore +s5_commit +# Show that the data changes performed by s4 are lost. +check -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 245+ messages in thread
end of thread, other threads:[~2026-05-12 10:27 UTC | newest] Thread overview: 245+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2020-03-21 01:47 [PATCH v1 1/2] Report wait event for cost-based vacuum delay Justin Pryzby <[email protected]> 2024-03-29 15:43 [PATCH v19] Avoid orphaned objects dependencies Bertrand Drouvot <[email protected]> 2025-12-09 19:17 [PATCH v8.1 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v8 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v8 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v1 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v8.1 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v1 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v7 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v3 6/7] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v7 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v2 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v2 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v5 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v4 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v5 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v4 5/6] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2025-12-09 19:17 [PATCH v3 6/7] Handle pg_get_expr default args in system_functions.sql Mark Wong <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]> 2026-05-12 10:27 [PATCH] Test to demonstrate bug in commit 0d3dba38c7 and to verify a fix. Antonin Houska <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox