postgres.git / summary / log / commit / refs

commit    9ff68679b50f291d1c38e09748721a9732439508
Author:   Tom Lane <tgl@sss.pgh.pa.us>
Date:     Wed Feb 19 21:35:15 2025 +0000
First released in: 18.0, 17.5

    Fix crash in brininsertcleanup during logical replication.
    
    Logical replication crashes if the subscriber's partitioned table
    has a BRIN index.  There are two independently blamable causes,
    and this patch fixes both:
    
    1. brininsertcleanup fails if called twice for the same IndexInfo,
    because it half-destroys its BrinInsertState but leaves it still
    linked from ii_AmCache.  brininsert would also fail in that state,
    so it's pretty hard to see any advantage to this coding.  Fully
    remove the BrinInsertState, instead, so that a new brininsert
    call would create a new cache.
    
    2. A logical replication subscriber sometimes does ExecOpenIndices
    twice on the same ResultRelInfo, followed by doing ExecCloseIndices
    twice; the second call reaches the brininsertcleanup bug.  Quite
    aside from tickling unexpected cases in aminsertcleanup methods,
    this seems very wasteful, because the IndexInfos built in the
    first ExecOpenIndices call are just lost during the second call,
    and have to be rebuilt at possibly-nontrivial cost.  We should
    establish a coding rule that you don't do that.
    
    The problematic coding is that when the target table is partitioned,
    apply_handle_tuple_routing calls ExecFindPartition which does
    ExecOpenIndices (and expects that ExecCleanupTupleRouting will
    close the indexes again).  Using the ResultRelInfo made by
    ExecFindPartition, it calls apply_handle_delete_internal or
    apply_handle_insert_internal, both of which think they need to do
    ExecOpenIndices/ExecCloseIndices for themselves.  They do in the main
    non-partitioned code paths, but not here.  The simplest fix is to pull
    their ExecOpenIndices/ExecCloseIndices calls out and put them in the
    call sites for the non-partitioned cases.  (We could have refactored
    apply_handle_update_internal similarly, but I did not do so today
    because there's no bug there: the partitioned code path doesn't
    call it.)
    
    Also, remove the always-duplicative open/close calls within
    apply_handle_tuple_routing itself.
    
    Since brininsertcleanup and indeed the whole aminsertcleanup mechanism
    are new in v17, there's no observable bug in older branches.  A case
    could be made for trying to avoid these duplicative open/close calls
    in the older branches, but for now it seems not worth the trouble and
    risk of new bugs.
    
    Bug: #18815
    Reported-by: Sergey Belyashov <sergey.belyashov@gmail.com>
    Discussion: https://postgr.es/m/18815-2a0407cc7f40b327@postgresql.org
    Backpatch-through: 17


src/backend/access/brin/brin.c | 8 ++++--- src/backend/replication/logical/worker.c | 40 +++++++++++++++++++++----------- src/test/subscription/t/013_partition.pl | 4 ++++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c index 4265687afa4..60320440fc5 100644 --- a/src/backend/access/brin/brin.c +++ b/src/backend/access/brin/brin.c @@ -511,16 +511,18 @@ brininsertcleanup(Relation index, IndexInfo *indexInfo) BrinInsertState *bistate = (BrinInsertState *) indexInfo->ii_AmCache; /* bail out if cache not initialized */ - if (indexInfo->ii_AmCache == NULL) + if (bistate == NULL) return; + /* do this first to avoid dangling pointer if we fail partway through */ + indexInfo->ii_AmCache = NULL; + /* * Clean up the revmap. Note that the brinDesc has already been cleaned up * as part of its own memory context. */ brinRevmapTerminate(bistate->bis_rmAccess); - bistate->bis_rmAccess = NULL; - bistate->bis_desc = NULL; + pfree(bistate); } /* diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index f09ab41c605..31ab69ea13a 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -2454,8 +2454,13 @@ apply_handle_insert(StringInfo s) apply_handle_tuple_routing(edata, remoteslot, NULL, CMD_INSERT); else - apply_handle_insert_internal(edata, edata->targetRelInfo, - remoteslot); + { + ResultRelInfo *relinfo = edata->targetRelInfo; + + ExecOpenIndices(relinfo, true); + apply_handle_insert_internal(edata, relinfo, remoteslot); + ExecCloseIndices(relinfo); + } finish_edata(edata); @@ -2482,16 +2487,18 @@ apply_handle_insert_internal(ApplyExecutionData *edata, { EState *estate = edata->estate; - /* We must open indexes here. */ - ExecOpenIndices(relinfo, true); + /* Caller should have opened indexes already. */ + Assert(relinfo->ri_IndexRelationDescs != NULL || + !relinfo->ri_RelationDesc->rd_rel->relhasindex || + RelationGetIndexList(relinfo->ri_RelationDesc) == NIL); + + /* Caller will not have done this bit. */ + Assert(relinfo->ri_onConflictArbiterIndexes == NIL); InitConflictIndexes(relinfo); /* Do the insert. */ TargetPrivilegesCheck(relinfo->ri_RelationDesc, ACL_INSERT); ExecSimpleRelationInsert(relinfo, estate, remoteslot); - - /* Cleanup. */ - ExecCloseIndices(relinfo); } /* @@ -2816,8 +2823,14 @@ apply_handle_delete(StringInfo s) apply_handle_tuple_routing(edata, remoteslot, NULL, CMD_DELETE); else - apply_handle_delete_internal(edata, edata->targetRelInfo, + { + ResultRelInfo *relinfo = edata->targetRelInfo; + + ExecOpenIndices(relinfo, false); + apply_handle_delete_internal(edata, relinfo, remoteslot, rel->localindexoid); + ExecCloseIndices(relinfo); + } finish_edata(edata); @@ -2851,7 +2864,11 @@ apply_handle_delete_internal(ApplyExecutionData *edata, bool found; EvalPlanQualInit(&epqstate, estate, NULL, NIL, -1, NIL); - ExecOpenIndices(relinfo, false); + + /* Caller should have opened indexes already. */ + Assert(relinfo->ri_IndexRelationDescs != NULL || + !localrel->rd_rel->relhasindex || + RelationGetIndexList(localrel) == NIL); found = FindReplTupleInLocalRel(edata, localrel, remoterel, localindexoid, remoteslot, &localslot); @@ -2892,7 +2909,6 @@ apply_handle_delete_internal(ApplyExecutionData *edata, } /* Cleanup. */ - ExecCloseIndices(relinfo); EvalPlanQualEnd(&epqstate); } @@ -3131,7 +3147,6 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, * work already done above to find the local tuple in the * partition. */ - ExecOpenIndices(partrelinfo, true); InitConflictIndexes(partrelinfo); EvalPlanQualSetSlot(&epqstate, remoteslot_part); @@ -3181,8 +3196,6 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, get_namespace_name(RelationGetNamespace(partrel_new)), RelationGetRelationName(partrel_new)); - ExecOpenIndices(partrelinfo, false); - /* DELETE old tuple found in the old partition. */ EvalPlanQualSetSlot(&epqstate, localslot); TargetPrivilegesCheck(partrelinfo->ri_RelationDesc, ACL_DELETE); @@ -3217,7 +3230,6 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, remoteslot_part); } - ExecCloseIndices(partrelinfo); EvalPlanQualEnd(&epqstate); } break; diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl index 14a3beae6e4..61b0cb4aa1a 100644 --- a/src/test/subscription/t/013_partition.pl +++ b/src/test/subscription/t/013_partition.pl @@ -49,6 +49,10 @@ $node_publisher->safe_psql('postgres', $node_subscriber1->safe_psql('postgres', "CREATE TABLE tab1 (c text, a int PRIMARY KEY, b text) PARTITION BY LIST (a)" ); +# make a BRIN index to test aminsertcleanup logic in subscriber +$node_subscriber1->safe_psql('postgres', + "CREATE INDEX tab1_c_brin_idx ON tab1 USING brin (c)" +); $node_subscriber1->safe_psql('postgres', "CREATE TABLE tab1_1 (b text, c text DEFAULT 'sub1_tab1', a int NOT NULL)" ); [parent: a1b4f289beec]