Hi,
On Wed, Aug 05, 2026 at 08:22:55PM -0500, Sami Imseih wrote:
> I also added an ample amount of test coverage in stats.sql and also testing
> the cascading cases in test_custom_var_stats.
I'm not sure this test exercises the need for more than one extra pass.
It creates the cascade entries in the order B, C, A. So, during the first
extra pass, B is processed before C and C can be flushed during that same
pass.
Also, B and C are checked only after COMMIT, so the transaction end flush
could hide a failure of the in transaction rescan.
What about creating the entries in the order C, B, A and checking B and C
before COMMIT?
> I do create 2 new kinds to
> test the cascade, which means we need to reserve 2 new Kind IDs.
> If that is a problem, we can leave these tests out perhaps?
>
> ```
> +/*
> + * Kind IDs for cascade flush test (A -> B -> C).
> + * Tests that pgStatPendingFlushExtra handles multi-level dependencies.
> + */
> +#define PGSTAT_KIND_CASCADE_B 27
> +#define PGSTAT_KIND_CASCADE_C 28
> ```
Do we need separate kinds? Could B and C be represented as two objects belonging
to one custom kind?
A few more comments:
=== 1
pgstat_relation_flush_cb() now does:
+ tabentry->dead_tuples += lstats->counts.delta_dead_tuples -
lstats->flushed.delta_dead_tuples;
and then records the cumulative value as flushed:
+ /*
+ * Record what was flushed. Transactional counters are retained until
the
+ * transaction boundary.
+ */
+ if (flush_txn)
+ {
+ lstats->flushed = lstats->counts;
But pgstat_report_analyze() still subtracts the whole counts.delta_dead_tuples
value, so it can remove dead tuple statistics that were already published.
I think that pgstat_report_analyze() should subtract only the unflushed delta,
means:
deadtuples -= rel->pgstat_info->counts.delta_dead_tuples -
rel->pgstat_info->flushed.delta_dead_tuples;
=== 2
The truncate handling resets the flushed.changed_tuples value:
+if (lstats->counts.truncdropped && !lstats->flushed.truncdropped)
+{
+ tabentry->live_tuples = 0;
+ tabentry->dead_tuples = 0;
+ tabentry->ins_since_vacuum = 0;
+ lstats->flushed.delta_live_tuples = 0;
+ lstats->flushed.delta_dead_tuples = 0;
+ lstats->flushed.changed_tuples = 0;
+}
However, changed_tuples is cumulative and truncate does not reset the shared
mod_since_analyze counter. Resetting flushed.changed_tuples to zero can
therefore publish changes that were already published.
I don't think flushed.changed_tuples should be reset here.
=== 3
The relation statistics reset is prevented by:
+if (lstats->counts.truncdropped && !lstats->flushed.truncdropped)
After the first full flush, this copies counts into flushed:
+ if (flush_txn)
+ {
+ lstats->flushed = lstats->counts;
+ return PGSTAT_FLUSH_DONE;
+ }
Both truncdropped values are then true. A subsequent truncate leaves
counts.truncdropped true, so the condition does not become true again and the
statistics reset is skipped.
What about doing this instead?
"
if (flush_txn)
{
lstats->flushed = lstats->counts;
lstats->counts.truncdropped = false;
lstats->flushed.truncdropped = false;
return PGSTAT_FLUSH_DONE;
}
"
=== 4
+ if (result == PGSTAT_FLUSH_DONE && xact_boundary)
pgstat_delete_pending_entry(entry_ref);
else
have_pending = true;
that means that now due to the extra loop:
+ /*
+ * Second scan (see above) for dependent entries populated after they were
+ * already visited.
+ */
+ while (pgStatPendingFlushExtra && IsTransactionOrTransactionBlock())
+ {
it invokes every retained callback again. Because pending->count is not cleared,
the same value is published twice. I think that can be an issue for custom
stats: the new test clears pending values:
+ memset(pending_entry, 0, sizeof(*pending_entry));
but that would be a new requirement for all custom callbacks.
At minimum that should be documented but I think a cleaner fix would be to
separate pending lifetime from queue membership, so that PGSTAT_FLUSH_DONE
removes
an entry from the work queue while retaining its storage.
=== 5
In the extra scan, next is set before the callback:
+ next = dlist_has_next(&pgStatPending, cur) ?
+ dlist_next_node(&pgStatPending, cur) : NULL;
+
+ kind_info->flush_pending_cb(entry_ref, nowait, xact_boundary);
so, if the current tail callback creates a new dependent entry then it is
processed
only if the callback also sets pgStatPendingFlushExtra, although the flag is
documented for updating an entry already visited. I think that next should be
set after the callback call (like the first scan).
=== 6
+ /*
+ * Second scan (see above) for dependent entries populated after they were
+ * already visited.
+ */
+ while (pgStatPendingFlushExtra && IsTransactionOrTransactionBlock())
+ {
What if:
A flushes into B and requests another pass
B flushes into A and requests another pass
Wouldn't that loop forever? That's not the case for core stats, but a custom
stats callback could create such a loop.
I'm not sure we can do much with the current global flag except document that
the dependencies must be acyclic.
Also, does that loop need CFI?
=== 7
+extern bool pgStatPendingFlushExtra;
missing PGDLLIMPORT?
=== 8
- proparallel => 'r', prorettype => 'void', proargtypes => '',
+ proparallel => 'u', prorettype => 'void', proargtypes => '',
I think that would need a bump catalog version, add a XXX in the commit message
to not forget about it?
also "descr => 'statistics: force stats to be flushed after the next commit',"
should be updated?
=== 9
Should we also add test to verify that the function double counting bug is
solved?
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com