diff --git a/src/backend/access/common/toast_dictionary.c b/src/backend/access/common/toast_dictionary.c index 240c408e26..dbcbced460 100644 --- a/src/backend/access/common/toast_dictionary.c +++ b/src/backend/access/common/toast_dictionary.c @@ -62,6 +62,7 @@ #include "access/tableam.h" #include "access/toast_compression.h" #include "access/toast_dictionary.h" +#include "access/xact.h" #include "catalog/catalog.h" #include "catalog/indexing.h" #include "catalog/objectaddress.h" @@ -78,6 +79,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/resowner.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/timestamp.h" @@ -1155,7 +1157,11 @@ dict_count(Relation drel, Oid relid, AttrNumber attnum) * manual trainer), reuses ANALYZE's already-fetched sample rows, and installs * a new current dictionary only when a held-out size test says a candidate * beats the current one by the configured margin. The write path is never - * involved. Any error is logged and swallowed so it cannot abort ANALYZE. + * involved. The work runs in an internal subtransaction: an error rolls it + * back, releasing whatever the failed step held (a TOAST scan, buffer pins, + * a half-done dict_install()), is reported as a WARNING, and ANALYZE goes on. + * A query cancel or a pending interrupt is re-thrown instead, so ANALYZE stops + * as it would anywhere else. */ void toast_dictionary_maybe_evolve(Relation onerel, AttrNumber attnum, @@ -1166,6 +1172,7 @@ toast_dictionary_maybe_evolve(Relation onerel, AttrNumber attnum, const DictCompressRoutine *routine; MemoryContext evolvecxt, oldcxt; + ResourceOwner oldowner; TupleDesc tupdesc = RelationGetDescr(onerel); /* (a) opted in, and still a dictionary column? */ @@ -1183,13 +1190,18 @@ toast_dictionary_maybe_evolve(Relation onerel, AttrNumber attnum, /* * Everything below runs in a short-lived context so that samples, the * candidate dictionary, and fetched current-dictionary bytes are all - * freed on return, and in a PG_TRY so a codec or catalog error degrades - * to "no evolution this round" rather than aborting ANALYZE. + * freed on return, and in an internal subtransaction so that a codec or + * catalog error degrades to "no evolution this round" rather than + * aborting ANALYZE, without leaking what the failed step held. */ - evolvecxt = AllocSetContextCreate(CurrentMemoryContext, + oldcxt = CurrentMemoryContext; + oldowner = CurrentResourceOwner; + evolvecxt = AllocSetContextCreate(oldcxt, "Dictionary evolution", ALLOCSET_DEFAULT_SIZES); - oldcxt = MemoryContextSwitchTo(evolvecxt); + + BeginInternalSubTransaction(NULL); + MemoryContextSwitchTo(evolvecxt); PG_TRY(); { @@ -1361,7 +1373,9 @@ toast_dictionary_maybe_evolve(Relation onerel, AttrNumber attnum, (int64) totalrows, candbuf, candlen); done: - ; + ReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcxt); + CurrentResourceOwner = oldowner; } PG_CATCH(); { @@ -1370,18 +1384,31 @@ done: MemoryContextSwitchTo(oldcxt); edata = CopyErrorData(); FlushErrorState(); - ereport(LOG, + RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(oldcxt); + CurrentResourceOwner = oldowner; + MemoryContextDelete(evolvecxt); + + /* + * A cancel (including statement_timeout) must stop ANALYZE as it + * would anywhere else. ProcessInterrupts() clears QueryCancelPending + * before raising it, so test the error code; also re-throw if an + * interrupt is still pending. + */ + if (edata->sqlerrcode == ERRCODE_QUERY_CANCELED || + QueryCancelPending || ProcDiePending) + ReThrowError(edata); + + ereport(WARNING, (errmsg("automatic dictionary evolution for column \"%s\" of \"%s\" was skipped: %s", NameStr(att->attname), RelationGetRelationName(onerel), edata->message))); FreeErrorData(edata); - MemoryContextDelete(evolvecxt); return; } PG_END_TRY(); - MemoryContextSwitchTo(oldcxt); MemoryContextDelete(evolvecxt); }