| From: | Sami Imseih <samimseih(at)gmail(dot)com> |
|---|---|
| To: | Bertrand Drouvot <bertranddrouvot(dot)pg(at)gmail(dot)com> |
| Cc: | pgsql-hackers <pgsql-hackers(at)postgresql(dot)org>, Kyotaro Horiguchi <horikyota(dot)ntt(at)gmail(dot)com>, Michael Paquier <michael(at)paquier(dot)xyz>, Lukas Fittl <lukas(at)fittl(dot)com> |
| Subject: | Re: pgstat: Flush some statistics within running transactions, take 2 |
| Date: | 2026-08-05 02:49:30 |
| Message-ID: | CAA5RZ0tikSiK_DUm0UFPyq5LD6ygPAC3qbjMnV8KK7WXjYebXw@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Thanks for the findings!
> Thanks for the new patch version!
>
> Some comments:
>
> === 1
>
> > I moved the protection into
> > pgstat_flush_pending_entries() itself, so all callbacks are protected.
>
> > if (result == PGSTAT_FLUSH_DONE && xact_boundary)
> > pgstat_delete_pending_entry(entry_ref);
> > else
> > have_pending = true;
>
> That produces a corner case for database and relation stats, for example:
>
> "
> CREATE TABLE flush_db_lag AS
> SELECT i FROM generate_series(1, 100) AS g(i);
>
> SELECT pg_stat_force_next_flush();
>
> BEGIN;
> SET LOCAL stats_fetch_consistency = none;
> SELECT 1 FROM pg_class LIMIT 1;
> SELECT pg_stat_force_next_flush();
> SELECT pg_stat_reset();
> SELECT pg_stat_get_db_tuples_returned(5) AS db_before,
> pg_stat_get_tuples_returned(16384) AS rel_before;
>
> SELECT count(*) FROM flush_db_lag;
> SELECT pg_stat_force_next_flush();
>
> SELECT pg_stat_get_db_tuples_returned(5) AS db_after_first,
> pg_stat_get_tuples_returned(16384) AS rel_after_first;
>
> SELECT pg_stat_force_next_flush();
>
> SELECT pg_stat_get_db_tuples_returned(5) AS db_after_second,
> pg_stat_get_tuples_returned(16384) AS rel_after_second;
> "
>
> Produces:
>
> db_after_first | rel_after_first
> ----------------+-----------------
> 11 | 100
>
> db_after_second | rel_after_second
> -----------------+------------------
> 117 | 100
>
>
> We can see that after the first force, relation statistics contains 100 tuples,
> but the database aggregate does not (while the second force adds them).
>
> This is because it keeps the flushed database entry in the pending list. When the
> relation callback later adds counters to that already visited entry, it is not
> requeued because entry_ref->pending is non-NULL. Then, those counters wait
> until the next flush.
>
> That could also happen for custom stats that updates an already visited retained
> entry.
>
> One option could be to keep pending entry memory allocated, but track queue
> membership separately and requeue any entry that receives new counters after
> being processed.
I only looked at this finding so far. After spending some time
thinking about it, doing cross-kind accumulation inside
flush_pending_cb seems problematic by design.
```
bool
pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait)
...
....
/* The entry was successfully flushed, add the same to database stats */
dbentry = pgstat_prep_database_pending(dboid);
dbentry->tuples_returned += lstats->counts.tuples_returned;
dbentry->tuples_fetched += lstats->counts.tuples_fetched;
dbentry->tuples_inserted += lstats->counts.tuples_inserted;
dbentry->tuples_updated += lstats->counts.tuples_updated;
dbentry->tuples_deleted += lstats->counts.tuples_deleted;
dbentry->blocks_fetched += lstats->counts.blocks_fetched;
dbentry->blocks_hit += lstats->counts.blocks_hit;
return true;
```
Right now with only flushing at the end of transaction, we can get
away with it because flushed entries are deleted, so dependent
entries get re-created at the tail and flushed in the same pass.
But the mid-transaction case exposes the ordering dependency, since
entries are not deleted and not re-visited in the same pass.
As you call out, custom stats could hit this too.
I think the fix is to separate cross-kind accumulation from the
flush itself. Rather than having flush_pending_cb call
pgstat_prep_database_pending() while we're iterating the pending
list, what do you think of adding a post_flush_pending_cb that
runs after a successful flush? At a transaction boundary this
works naturally. The flushed relation entry gets deleted, the
callback re-creates the database entry, and is guaranteed to
be visited.
For mid-transaction flushing where entries stay on the list, a
dependent entry that was already visited won't be reached again
in the first pass because it's not a new entry. So, if we are
mid-transaction and still have pending data, we can take a second
pass to handle the stats accumulated during the post flush
callback.
Adding post_flush_pending_cb can go in as a pre-requisite
commit. This is also better in terms of separation of
responsibilities between flushing the kinds stats and post
flush actions.
What do you think?
--
Sami
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Yugo Nagata | 2026-08-05 03:00:16 | Re: Rename EXISTS-to-ANY converted subplan to exists_to_any |
| Previous Message | Neil Chen | 2026-08-05 02:46:25 | Re: Fix archive restore race that could unlink WAL before rename |