]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix PANIC with track_functions due to concurrent drop of pgstats entries
authorMichael Paquier <michael@paquier.xyz>
Thu, 18 Jun 2026 02:49:34 +0000 (11:49 +0900)
committerMichael Paquier <michael@paquier.xyz>
Thu, 18 Jun 2026 02:49:34 +0000 (11:49 +0900)
pgstat_drop_entry_internal() generates an ERROR if facing a pgstats
entry already marked as dropped.  With a workload doing a lot of
concurrent CALL and DROP/CREATE PROCEDURE, it could be possible for
AtEOXact_PgStat_DroppedStats(), that wants to do transactional drops, to
find entries that are already dropped, after a commit record has been
written.  In this case, ERRORs are upgraded to PANIC, taking down the
server.

This issue is fixed by making pgstat_drop_entry() optionally more
tolerant to concurrent drops, adding to the routine a missing_ok option
to make some of its callers more tolerant (spoiler: some of the callers
want a strict behavior, like replication slots and backend stats).
pgstat_drop_entry_internal() cannot be called anymore for an entry
marked as dropped, hence its error is replaced by an assertion.
Functions are handled as a special case in core; this problem could also
apply to custom stats kinds depending on what an extension does.
track_functions is costly when enabled (disabled by default), which is
perhaps the main reason why this has not be found yet.

A similar version of this patch has been proposed by Sami Imseih on a
different thread for a feature in development.  This version has tweaked
here by me for the sake of fixing this issue.

Reported-by: zhanglihui <zlh21343@163.com>
Author: Sami Imseih <samimseih@gmail.com>
Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Discussion: https://postgr.es/m/19520-73873648d44793cf@postgresql.org
Backpatch-through: 15

src/backend/utils/activity/pgstat.c
src/backend/utils/activity/pgstat_function.c
src/backend/utils/activity/pgstat_replslot.c
src/backend/utils/activity/pgstat_shmem.c
src/backend/utils/activity/pgstat_xact.c
src/include/utils/pgstat_internal.h
src/test/modules/injection_points/injection_stats.c

index 9113ee9d6a9505ea004239c65dd7734311c74ddf..d997be2e1d931b1ef8a152225ec9b7134efdb128 100644 (file)
@@ -620,7 +620,7 @@ pgstat_shutdown_hook(int code, Datum arg)
        dlist_init(&pgStatPending);
 
        /* drop the backend stats entry */
-       if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber))
+       if (!pgstat_drop_entry(PGSTAT_KIND_BACKEND, InvalidOid, MyProcNumber, false))
                pgstat_request_entry_refs_gc();
 
        pgstat_detach_shmem();
index 6214f93d36e0c3a49557b103b105aa94e98c416f..f763a0d6f5bab1ef631adc58db0755e0e986c1fc 100644 (file)
@@ -113,7 +113,7 @@ pgstat_init_function_usage(FunctionCallInfo fcinfo,
                if (!SearchSysCacheExists1(PROCOID, ObjectIdGetDatum(fcinfo->flinfo->fn_oid)))
                {
                        pgstat_drop_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId,
-                                                         fcinfo->flinfo->fn_oid);
+                                                         fcinfo->flinfo->fn_oid, true);
                        ereport(ERROR, errcode(ERRCODE_UNDEFINED_FUNCTION),
                                        errmsg("function call to dropped function"));
                }
index ccfb11c49bf82793180ca1f031ab54d1eb1ba2e3..746b97a0b5af9e110de3738e695fa40982175963 100644 (file)
@@ -158,7 +158,7 @@ pgstat_drop_replslot(ReplicationSlot *slot)
        Assert(LWLockHeldByMeInMode(ReplicationSlotAllocationLock, LW_EXCLUSIVE));
 
        if (!pgstat_drop_entry(PGSTAT_KIND_REPLSLOT, InvalidOid,
-                                                  ReplicationSlotIndex(slot)))
+                                                  ReplicationSlotIndex(slot), false))
                pgstat_request_entry_refs_gc();
 }
 
index 2b7f783ef7c453c4e09c3bc47e60f5e68e6b35e1..414b66be2dc46f111b5ae877448b5c11e9ffcc10 100644 (file)
@@ -898,14 +898,7 @@ pgstat_drop_entry_internal(PgStatShared_HashEntry *shent,
         * Signal that the entry is dropped - this will eventually cause other
         * backends to release their references.
         */
-       if (shent->dropped)
-               elog(ERROR,
-                        "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u",
-                        pgstat_get_kind_info(shent->key.kind)->name,
-                        shent->key.dboid,
-                        shent->key.objid,
-                        pg_atomic_read_u32(&shent->refcount),
-                        pg_atomic_read_u32(&shent->generation));
+       Assert(!shent->dropped);
        shent->dropped = true;
 
        /* release refcount marking entry as not dropped */
@@ -981,13 +974,16 @@ pgstat_drop_database_and_contents(Oid dboid)
  * This routine returns false if the stats entry of the dropped object could
  * not be freed, true otherwise.
  *
+ * If missing_ok is true, skip entries that have been concurrently dropped.
+ *
  * The callers of this function should call pgstat_request_entry_refs_gc()
  * if the stats entry could not be freed, to ensure that this entry's memory
  * can be reclaimed later by a different backend calling
  * pgstat_gc_entry_refs().
  */
 bool
-pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
+pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
+                                 bool missing_ok)
 {
        PgStat_HashKey key;
        PgStatShared_HashEntry *shent;
@@ -1015,6 +1011,20 @@ pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid)
        shent = dshash_find(pgStatLocal.shared_hash, &key, true);
        if (shent)
        {
+               if (shent->dropped)
+               {
+                       if (!missing_ok)
+                               elog(ERROR,
+                                        "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u",
+                                        pgstat_get_kind_info(shent->key.kind)->name,
+                                        shent->key.dboid,
+                                        shent->key.objid,
+                                        pg_atomic_read_u32(&shent->refcount),
+                                        pg_atomic_read_u32(&shent->generation));
+                       dshash_release_lock(pgStatLocal.shared_hash, shent);
+                       return true;
+               }
+
                freed = pgstat_drop_entry_internal(shent, NULL);
 
                /*
index bc9864bd8d9d0abc732bbf8bcff6460b0c79b816..b058c978dcf39fb2aaa778c8590ccebe9d4d4015 100644 (file)
@@ -85,7 +85,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit)
                         * Transaction that dropped an object committed. Drop the stats
                         * too.
                         */
-                       if (!pgstat_drop_entry(it->kind, it->dboid, objid))
+                       if (!pgstat_drop_entry(it->kind, it->dboid, objid, true))
                                not_freed_count++;
                }
                else if (!isCommit && pending->is_create)
@@ -94,7 +94,7 @@ AtEOXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state, bool isCommit)
                         * Transaction that created an object aborted. Drop the stats
                         * associated with the object.
                         */
-                       if (!pgstat_drop_entry(it->kind, it->dboid, objid))
+                       if (!pgstat_drop_entry(it->kind, it->dboid, objid, true))
                                not_freed_count++;
                }
 
@@ -160,7 +160,7 @@ AtEOSubXact_PgStat_DroppedStats(PgStat_SubXactStatus *xact_state,
                         * Subtransaction creating a new stats object aborted. Drop the
                         * stats object.
                         */
-                       if (!pgstat_drop_entry(it->kind, it->dboid, objid))
+                       if (!pgstat_drop_entry(it->kind, it->dboid, objid, true))
                                not_freed_count++;
                        pfree(pending);
                }
@@ -323,7 +323,7 @@ pgstat_execute_transactional_drops(int ndrops, struct xl_xact_stats_item *items,
                xl_xact_stats_item *it = &items[i];
                uint64          objid = ((uint64) it->objid_hi) << 32 | it->objid_lo;
 
-               if (!pgstat_drop_entry(it->kind, it->dboid, objid))
+               if (!pgstat_drop_entry(it->kind, it->dboid, objid, true))
                        not_freed_count++;
        }
 
index 6cf00008f6333575f3f160fab6f32c61f7c2d4c7..4f840c38f0721d9725c9e383e653aa79255d12e2 100644 (file)
@@ -708,7 +708,8 @@ extern PgStat_EntryRef *pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64
 extern bool pgstat_lock_entry(PgStat_EntryRef *entry_ref, bool nowait);
 extern bool pgstat_lock_entry_shared(PgStat_EntryRef *entry_ref, bool nowait);
 extern void pgstat_unlock_entry(PgStat_EntryRef *entry_ref);
-extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid);
+extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
+                                                         bool missing_ok);
 extern void pgstat_drop_all_entries(void);
 extern void pgstat_drop_matching_entries(bool (*do_drop) (PgStatShared_HashEntry *, Datum),
                                                                                 Datum match_data);
index ca8df4ad217ab91fc5dbf18665135ced42d8cc84..435a0484a4ba5a025e0c12e19a8d2a4c5f896a5e 100644 (file)
@@ -150,7 +150,7 @@ pgstat_drop_inj(const char *name)
                return;
 
        if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, InvalidOid,
-                                                  PGSTAT_INJ_IDX(name)))
+                                                  PGSTAT_INJ_IDX(name), false))
                pgstat_request_entry_refs_gc();
 }