From 711863b09f54db89f74ebfbe822d25322668e7e4 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Mon, 4 Aug 2025 08:14:02 +0000
Subject: [PATCH v2] Adding per backend commit and rollback counters

This commit adds 2 functions: pg_stat_get_backend_xact_commit() and
pg_stat_get_backend_xact_rollback() to report the number of transactions that
have been committed/rolled back for a given backend PID.

It relies on the existing per backend statistics that has been added in
9aea73fc61d.
---
 doc/src/sgml/monitoring.sgml                | 36 +++++++++++++
 src/backend/utils/activity/pgstat_backend.c | 60 +++++++++++++++++++++
 src/backend/utils/activity/pgstat_xact.c    |  1 +
 src/backend/utils/adt/pgstatfuncs.c         | 22 ++++++++
 src/include/catalog/pg_proc.dat             |  9 ++++
 src/include/pgstat.h                        |  8 +++
 src/include/utils/pgstat_internal.h         |  4 +-
 src/test/regress/expected/stats.out         | 17 ++++++
 src/test/regress/sql/stats.sql              | 10 ++++
 9 files changed, 166 insertions(+), 1 deletion(-)
  26.1% doc/src/sgml/
  29.1% src/backend/utils/activity/
  11.9% src/backend/utils/adt/
   9.4% src/include/catalog/
   3.9% src/include/utils/
   3.3% src/include/
   8.9% src/test/regress/expected/
   7.1% src/test/regress/sql/

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 3f4a27a736e..77b41e02b82 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4921,6 +4921,42 @@ description | Waiting for a newly initialized WAL file to reach durable storage
        </para></entry>
       </row>
 
+      <row>
+       <entry id="pg-stat-get-backend-xact-commit" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_stat_get_backend_xact_commit</primary>
+        </indexterm>
+        <function>pg_stat_get_backend_xact_commit</function> ( <type>integer</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the number of transactions that have been committed by the backend
+        with the specified process ID.
+       </para>
+       <para>
+        The function does not return statistics for the checkpointer,
+        the background writer, the startup process and the autovacuum launcher.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry id="pg-stat-get-backend-xact-rollback" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_stat_get_backend_xact_rollback</primary>
+        </indexterm>
+        <function>pg_stat_get_backend_xact_rollback</function> ( <type>integer</type> )
+        <returnvalue>bigint</returnvalue>
+       </para>
+       <para>
+        Returns the number of transactions that have been rolled back by the backend
+        with the specified process ID.
+       </para>
+       <para>
+        The function does not return statistics for the checkpointer,
+        the background writer, the startup process and the autovacuum launcher.
+       </para></entry>
+      </row>
+
       <row>
        <entry id="pg-stat-get-backend-wal" role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 8714a85e2d9..12ae9a8d321 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -47,6 +47,11 @@ static bool backend_has_iostats = false;
  */
 static WalUsage prevBackendWalUsage;
 
+/*
+ * For backend commit and rollback statistics.
+ */
+static bool backend_has_xactstats = false;
+
 /*
  * Utility routines to report I/O stats for backends, kept here to avoid
  * exposing PendingBackendStats to the outside world.
@@ -259,6 +264,34 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref)
 	prevBackendWalUsage = pgWalUsage;
 }
 
+/*
+ * Flush out locally pending backend xact statistics.  Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_xact(PgStat_EntryRef *entry_ref)
+{
+	PgStatShared_Backend *shbackendent;
+
+	/*
+	 * This function can be called even if nothing at all has happened for
+	 * XACT statistics.  In this case, avoid unnecessarily modifying the stats
+	 * entry.
+	 */
+	if (!backend_has_xactstats)
+		return;
+
+	shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+
+	shbackendent->stats.xact_commit += PendingBackendStats.pending_xact_commit;
+	shbackendent->stats.xact_rollback += PendingBackendStats.pending_xact_rollback;
+
+	PendingBackendStats.pending_xact_commit = 0;
+	PendingBackendStats.pending_xact_rollback = 0;
+
+	backend_has_xactstats = false;
+}
+
 /*
  * Flush out locally pending backend statistics
  *
@@ -283,6 +316,10 @@ pgstat_flush_backend(bool nowait, bits32 flags)
 		pgstat_backend_wal_have_pending())
 		has_pending_data = true;
 
+	/* Some XACT data pending? */
+	if ((flags & PGSTAT_BACKEND_FLUSH_XACT) && backend_has_xactstats)
+		has_pending_data = true;
+
 	if (!has_pending_data)
 		return false;
 
@@ -298,6 +335,9 @@ pgstat_flush_backend(bool nowait, bits32 flags)
 	if (flags & PGSTAT_BACKEND_FLUSH_WAL)
 		pgstat_flush_backend_entry_wal(entry_ref);
 
+	if (flags & PGSTAT_BACKEND_FLUSH_XACT)
+		pgstat_flush_backend_entry_xact(entry_ref);
+
 	pgstat_unlock_entry(entry_ref);
 
 	return false;
@@ -400,3 +440,23 @@ pgstat_backend_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts)
 {
 	((PgStatShared_Backend *) header)->stats.stat_reset_timestamp = ts;
 }
+
+void
+AtEOXact_PgStat_Backend(bool isCommit, bool parallel)
+{
+	/* Don't count parallel worker transaction stats */
+	if (!parallel)
+	{
+		/*
+		 * Count transaction commit or abort.  (We use counters, not just
+		 * bools, in case the reporting message isn't sent right away.)
+		 */
+		if (isCommit)
+			PendingBackendStats.pending_xact_commit++;
+		else
+			PendingBackendStats.pending_xact_rollback++;
+
+		backend_has_xactstats = true;
+		pgstat_report_fixed = true;
+	}
+}
diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c
index bc9864bd8d9..cd1c501c165 100644
--- a/src/backend/utils/activity/pgstat_xact.c
+++ b/src/backend/utils/activity/pgstat_xact.c
@@ -42,6 +42,7 @@ AtEOXact_PgStat(bool isCommit, bool parallel)
 	PgStat_SubXactStatus *xact_state;
 
 	AtEOXact_PgStat_Database(isCommit, parallel);
+	AtEOXact_PgStat_Backend(isCommit, parallel);
 
 	/* handle transactional stats information */
 	xact_state = pgStatXactStack;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index c756c2bebaa..6436129f516 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1606,6 +1606,28 @@ pg_stat_get_backend_io(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+#define PG_STAT_GET_BACKENDENTRY_INT64(stat)					\
+Datum															\
+CppConcat(pg_stat_get_backend_,stat)(PG_FUNCTION_ARGS)			\
+{																\
+	int			pid;											\
+	PgStat_Backend *backend_stats;								\
+																\
+	pid = PG_GETARG_INT32(0);									\
+	backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);\
+																\
+	if (!backend_stats)											\
+		PG_RETURN_NULL();										\
+	else														\
+		PG_RETURN_INT64(backend_stats->stat);					\
+}
+
+/* pg_stat_get_backend_xact_commit */
+PG_STAT_GET_BACKENDENTRY_INT64(xact_commit)
+
+/* pg_stat_get_backend_xact_rollback */
+PG_STAT_GET_BACKENDENTRY_INT64(xact_rollback)
+
 /*
  * pg_stat_wal_build_tuple
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 118d6da1ace..f5085d4e016 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6010,6 +6010,15 @@
   proargnames => '{backend_pid,backend_type,object,context,reads,read_bytes,read_time,writes,write_bytes,write_time,writebacks,writeback_time,extends,extend_bytes,extend_time,hits,evictions,reuses,fsyncs,fsync_time,stats_reset}',
   prosrc => 'pg_stat_get_backend_io' },
 
+{ oid => '8170', descr => 'statistics: backend transactions committed',
+  proname => 'pg_stat_get_backend_xact_commit', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'int4',
+  prosrc => 'pg_stat_get_backend_xact_commit' },
+{ oid => '8916', descr => 'statistics: backend transactions rolled back',
+  proname => 'pg_stat_get_backend_xact_rollback', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'int4',
+  prosrc => 'pg_stat_get_backend_xact_rollback' },
+
 { oid => '1136', descr => 'statistics: information about WAL activity',
   proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
   proparallel => 'r', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 202bd2d5ace..d3491faaff2 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -490,6 +490,8 @@ typedef struct PgStat_Backend
 	TimestampTz stat_reset_timestamp;
 	PgStat_BktypeIO io_stats;
 	PgStat_WalCounters wal_counters;
+	PgStat_Counter xact_commit;
+	PgStat_Counter xact_rollback;
 } PgStat_Backend;
 
 /* ---------
@@ -502,6 +504,12 @@ typedef struct PgStat_BackendPending
 	 * Backend statistics store the same amount of IO data as PGSTAT_KIND_IO.
 	 */
 	PgStat_PendingIO pending_io;
+
+	/*
+	 * Xact statistics pending flush.
+	 */
+	PgStat_Counter pending_xact_commit;
+	PgStat_Counter pending_xact_rollback;
 } PgStat_BackendPending;
 
 /*
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 6cf00008f63..23ea8ffd618 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -616,12 +616,14 @@ extern void pgstat_archiver_snapshot_cb(void);
 /* flags for pgstat_flush_backend() */
 #define PGSTAT_BACKEND_FLUSH_IO		(1 << 0)	/* Flush I/O statistics */
 #define PGSTAT_BACKEND_FLUSH_WAL   (1 << 1) /* Flush WAL statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL   (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL)
+#define PGSTAT_BACKEND_FLUSH_XACT   (1 << 2)	/* Flush xact statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL   (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_XACT)
 
 extern bool pgstat_flush_backend(bool nowait, bits32 flags);
 extern bool pgstat_backend_flush_cb(bool nowait);
 extern void pgstat_backend_reset_timestamp_cb(PgStatShared_Common *header,
 											  TimestampTz ts);
+extern void AtEOXact_PgStat_Backend(bool isCommit, bool parallel);
 
 /*
  * Functions in pgstat_bgwriter.c
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 605f5070376..0d316f94e40 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -135,11 +135,28 @@ INSERT INTO trunc_stats_test1 DEFAULT VALUES;
 INSERT INTO trunc_stats_test1 DEFAULT VALUES;
 UPDATE trunc_stats_test1 SET id = id + 10 WHERE id IN (1, 2);
 DELETE FROM trunc_stats_test1 WHERE id = 3;
+-- in passing, check that backend's commit is incrementing
+SELECT pg_stat_get_backend_xact_commit AS xact_commit_before
+  FROM pg_stat_get_backend_xact_commit(pg_backend_pid()) \gset
 BEGIN;
 UPDATE trunc_stats_test1 SET id = id + 100;
 TRUNCATE trunc_stats_test1;
 INSERT INTO trunc_stats_test1 DEFAULT VALUES;
 COMMIT;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_stat_get_backend_xact_commit AS xact_commit_after
+  FROM pg_stat_get_backend_xact_commit(pg_backend_pid()) \gset
+SELECT :xact_commit_after > :xact_commit_before;
+ ?column? 
+----------
+ t
+(1 row)
+
 -- use a savepoint: 1 insert, 1 live
 BEGIN;
 INSERT INTO trunc_stats_test2 DEFAULT VALUES;
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 54e72866344..d629140d880 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -58,12 +58,22 @@ INSERT INTO trunc_stats_test1 DEFAULT VALUES;
 UPDATE trunc_stats_test1 SET id = id + 10 WHERE id IN (1, 2);
 DELETE FROM trunc_stats_test1 WHERE id = 3;
 
+-- in passing, check that backend's commit is incrementing
+SELECT pg_stat_get_backend_xact_commit AS xact_commit_before
+  FROM pg_stat_get_backend_xact_commit(pg_backend_pid()) \gset
+
 BEGIN;
 UPDATE trunc_stats_test1 SET id = id + 100;
 TRUNCATE trunc_stats_test1;
 INSERT INTO trunc_stats_test1 DEFAULT VALUES;
 COMMIT;
 
+SELECT pg_stat_force_next_flush();
+SELECT pg_stat_get_backend_xact_commit AS xact_commit_after
+  FROM pg_stat_get_backend_xact_commit(pg_backend_pid()) \gset
+
+SELECT :xact_commit_after > :xact_commit_before;
+
 -- use a savepoint: 1 insert, 1 live
 BEGIN;
 INSERT INTO trunc_stats_test2 DEFAULT VALUES;
-- 
2.34.1

