Split pgstat_bestart() into three different routines
authorMichael Paquier <michael@paquier.xyz>
Tue, 4 Mar 2025 05:09:44 +0000 (14:09 +0900)
committerMichael Paquier <michael@paquier.xyz>
Tue, 4 Mar 2025 05:09:44 +0000 (14:09 +0900)
pgstat_bestart(), used post-authentication to set up a backend entry
in the PgBackendStatus array, so as its data becomes visible in
pg_stat_activity and related catalogs, has its logic divided into three
routines with this commit, called in order at different steps of the
backend initialization:
* pgstat_bestart_initial() sets up the backend entry with a minimal
amount of information, reporting it with a new BackendState called
STATE_STARTING while waiting for backend initialization and client
authentication to complete.  The main benefit that this offers is
observability, so as it is possible to monitor the backend activity
during authentication.  This step happens earlier than in the logic
prior to this commit.  pgstat_beinit() happens earlier as well, before
authentication.
* pgstat_bestart_security() reports the SSL/GSS status of the
connection, once authentication completes.  Auxiliary processes, for
example, do not need to call this step, hence it is optional.  This
step is called after performing authentication, same as previously.
* pgstat_bestart_final() reports the user and database IDs, takes the
entry out of STATE_STARTING, and reports its application_name.  This is
called as the last step of the three, once authentication completes.

An injection point is added, with a test checking that the "starting"
phase of a backend entry is visible in pg_stat_activity.  Some follow-up
patches are planned to take advantage of this refactoring with more
information provided in backend entries during authentication (LDAP
hanging was a problem for the author, initially).

Author: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/CAOYmi+=60deN20WDyCoHCiecgivJxr=98s7s7-C8SkXwrCfHXg@mail.gmail.com

doc/src/sgml/monitoring.sgml
src/backend/postmaster/auxprocess.c
src/backend/utils/activity/backend_status.c
src/backend/utils/adt/pgstatfuncs.c
src/backend/utils/init/postinit.c
src/include/utils/backend_status.h
src/test/authentication/Makefile
src/test/authentication/meson.build
src/test/authentication/t/007_pre_auth.pl [new file with mode: 0644]

index 9178f1d34efdccaa91c28e26825af2a1f9edb12c..16646f560e8dce69a7f06faf781ea128e25fff54 100644 (file)
@@ -899,6 +899,12 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        Current overall state of this backend.
        Possible values are:
        <itemizedlist>
+        <listitem>
+         <para>
+          <literal>starting</literal>: The backend is in initial startup. Client
+          authentication is performed during this phase.
+         </para>
+        </listitem>
         <listitem>
         <para>
           <literal>active</literal>: The backend is executing a query.
index ff366ceb0fc7ef9f9295f30fe7a2f7c14eaa3634..4f6795f72650fdf001e4e409514a063f6609c7c0 100644 (file)
@@ -78,7 +78,8 @@ AuxiliaryProcessMainCommon(void)
 
    /* Initialize backend status information */
    pgstat_beinit();
-   pgstat_bestart();
+   pgstat_bestart_initial();
+   pgstat_bestart_final();
 
    /* register a before-shutdown callback for LWLock cleanup */
    before_shmem_exit(ShutdownAuxiliaryProcess, 0);
index 5f68ef26adc69f9c2c43616933f004c71118e6d1..7681b4ba5a99803e1424e6cfadb5ed7085c65f56 100644 (file)
@@ -255,29 +255,22 @@ pgstat_beinit(void)
 
 
 /* ----------
- * pgstat_bestart() -
+ * pgstat_bestart_initial() -
  *
- * Initialize this backend's entry in the PgBackendStatus array.
- * Called from InitPostgres.
+ * Initialize this backend's entry in the PgBackendStatus array.  Called
+ * from InitPostgres and AuxiliaryProcessMain.
  *
- * Apart from auxiliary processes, MyDatabaseId, session userid, and
- * application_name must already be set (hence, this cannot be combined
- * with pgstat_beinit).  Note also that we must be inside a transaction
- * if this isn't an aux process, as we may need to do encoding conversion
- * on some strings.
- *----------
+ * Clears out a new pgstat entry, initializing it to suitable defaults and
+ * reporting STATE_STARTING.  Backends should continue filling in any
+ * transport security details as needed with pgstat_bestart_security(), and
+ * must finally exit STATE_STARTING by calling pgstat_bestart_final().
+ * ----------
  */
 void
-pgstat_bestart(void)
+pgstat_bestart_initial(void)
 {
    volatile PgBackendStatus *vbeentry = MyBEEntry;
    PgBackendStatus lbeentry;
-#ifdef USE_SSL
-   PgBackendSSLStatus lsslstatus;
-#endif
-#ifdef ENABLE_GSS
-   PgBackendGSSStatus lgssstatus;
-#endif
 
    /* pgstats state must be initialized from pgstat_beinit() */
    Assert(vbeentry != NULL);
@@ -297,14 +290,6 @@ pgstat_bestart(void)
           unvolatize(PgBackendStatus *, vbeentry),
           sizeof(PgBackendStatus));
 
-   /* These structs can just start from zeroes each time, though */
-#ifdef USE_SSL
-   memset(&lsslstatus, 0, sizeof(lsslstatus));
-#endif
-#ifdef ENABLE_GSS
-   memset(&lgssstatus, 0, sizeof(lgssstatus));
-#endif
-
    /*
     * Now fill in all the fields of lbeentry, except for strings that are
     * out-of-line data.  Those have to be handled separately, below.
@@ -315,15 +300,8 @@ pgstat_bestart(void)
    lbeentry.st_activity_start_timestamp = 0;
    lbeentry.st_state_start_timestamp = 0;
    lbeentry.st_xact_start_timestamp = 0;
-   lbeentry.st_databaseid = MyDatabaseId;
-
-   /* We have userid for client-backends, wal-sender and bgworker processes */
-   if (lbeentry.st_backendType == B_BACKEND
-       || lbeentry.st_backendType == B_WAL_SENDER
-       || lbeentry.st_backendType == B_BG_WORKER)
-       lbeentry.st_userid = GetSessionUserId();
-   else
-       lbeentry.st_userid = InvalidOid;
+   lbeentry.st_databaseid = InvalidOid;
+   lbeentry.st_userid = InvalidOid;
 
    /*
     * We may not have a MyProcPort (eg, if this is the autovacuum process).
@@ -336,46 +314,10 @@ pgstat_bestart(void)
    else
        MemSet(&lbeentry.st_clientaddr, 0, sizeof(lbeentry.st_clientaddr));
 
-#ifdef USE_SSL
-   if (MyProcPort && MyProcPort->ssl_in_use)
-   {
-       lbeentry.st_ssl = true;
-       lsslstatus.ssl_bits = be_tls_get_cipher_bits(MyProcPort);
-       strlcpy(lsslstatus.ssl_version, be_tls_get_version(MyProcPort), NAMEDATALEN);
-       strlcpy(lsslstatus.ssl_cipher, be_tls_get_cipher(MyProcPort), NAMEDATALEN);
-       be_tls_get_peer_subject_name(MyProcPort, lsslstatus.ssl_client_dn, NAMEDATALEN);
-       be_tls_get_peer_serial(MyProcPort, lsslstatus.ssl_client_serial, NAMEDATALEN);
-       be_tls_get_peer_issuer_name(MyProcPort, lsslstatus.ssl_issuer_dn, NAMEDATALEN);
-   }
-   else
-   {
-       lbeentry.st_ssl = false;
-   }
-#else
    lbeentry.st_ssl = false;
-#endif
-
-#ifdef ENABLE_GSS
-   if (MyProcPort && MyProcPort->gss != NULL)
-   {
-       const char *princ = be_gssapi_get_princ(MyProcPort);
-
-       lbeentry.st_gss = true;
-       lgssstatus.gss_auth = be_gssapi_get_auth(MyProcPort);
-       lgssstatus.gss_enc = be_gssapi_get_enc(MyProcPort);
-       lgssstatus.gss_delegation = be_gssapi_get_delegation(MyProcPort);
-       if (princ)
-           strlcpy(lgssstatus.gss_princ, princ, NAMEDATALEN);
-   }
-   else
-   {
-       lbeentry.st_gss = false;
-   }
-#else
    lbeentry.st_gss = false;
-#endif
 
-   lbeentry.st_state = STATE_UNDEFINED;
+   lbeentry.st_state = STATE_STARTING;
    lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
    lbeentry.st_progress_command_target = InvalidOid;
    lbeentry.st_query_id = UINT64CONST(0);
@@ -417,14 +359,138 @@ pgstat_bestart(void)
    lbeentry.st_clienthostname[NAMEDATALEN - 1] = '\0';
    lbeentry.st_activity_raw[pgstat_track_activity_query_size - 1] = '\0';
 
+   /* These structs can just start from zeroes each time */
 #ifdef USE_SSL
-   memcpy(lbeentry.st_sslstatus, &lsslstatus, sizeof(PgBackendSSLStatus));
+   memset(lbeentry.st_sslstatus, 0, sizeof(PgBackendSSLStatus));
 #endif
 #ifdef ENABLE_GSS
-   memcpy(lbeentry.st_gssstatus, &lgssstatus, sizeof(PgBackendGSSStatus));
+   memset(lbeentry.st_gssstatus, 0, sizeof(PgBackendGSSStatus));
 #endif
 
    PGSTAT_END_WRITE_ACTIVITY(vbeentry);
+}
+
+/* ----------
+ * pgstat_bestart_security() -
+ *
+ * Fill in SSL and GSS information for the pgstat entry.  This is the second
+ * optional step taken when filling a backend's entry, not required for
+ * auxiliary processes.
+ *
+ * This should only be called from backends with a MyProcPort.
+ * ----------
+ */
+void
+pgstat_bestart_security(void)
+{
+   volatile PgBackendStatus *beentry = MyBEEntry;
+   bool        ssl = false;
+   bool        gss = false;
+#ifdef USE_SSL
+   PgBackendSSLStatus lsslstatus;
+   PgBackendSSLStatus *st_sslstatus;
+#endif
+#ifdef ENABLE_GSS
+   PgBackendGSSStatus lgssstatus;
+   PgBackendGSSStatus *st_gssstatus;
+#endif
+
+   /* pgstats state must be initialized from pgstat_beinit() */
+   Assert(beentry != NULL);
+   Assert(MyProcPort);         /* otherwise there's no point */
+
+#ifdef USE_SSL
+   st_sslstatus = beentry->st_sslstatus;
+   memset(&lsslstatus, 0, sizeof(lsslstatus));
+
+   if (MyProcPort->ssl_in_use)
+   {
+       ssl = true;
+       lsslstatus.ssl_bits = be_tls_get_cipher_bits(MyProcPort);
+       strlcpy(lsslstatus.ssl_version, be_tls_get_version(MyProcPort), NAMEDATALEN);
+       strlcpy(lsslstatus.ssl_cipher, be_tls_get_cipher(MyProcPort), NAMEDATALEN);
+       be_tls_get_peer_subject_name(MyProcPort, lsslstatus.ssl_client_dn, NAMEDATALEN);
+       be_tls_get_peer_serial(MyProcPort, lsslstatus.ssl_client_serial, NAMEDATALEN);
+       be_tls_get_peer_issuer_name(MyProcPort, lsslstatus.ssl_issuer_dn, NAMEDATALEN);
+   }
+#endif
+
+#ifdef ENABLE_GSS
+   st_gssstatus = beentry->st_gssstatus;
+   memset(&lgssstatus, 0, sizeof(lgssstatus));
+
+   if (MyProcPort->gss != NULL)
+   {
+       const char *princ = be_gssapi_get_princ(MyProcPort);
+
+       gss = true;
+       lgssstatus.gss_auth = be_gssapi_get_auth(MyProcPort);
+       lgssstatus.gss_enc = be_gssapi_get_enc(MyProcPort);
+       lgssstatus.gss_delegation = be_gssapi_get_delegation(MyProcPort);
+       if (princ)
+           strlcpy(lgssstatus.gss_princ, princ, NAMEDATALEN);
+   }
+#endif
+
+   /*
+    * Update my status entry, following the protocol of bumping
+    * st_changecount before and after.  We use a volatile pointer here to
+    * ensure the compiler doesn't try to get cute.
+    */
+   PGSTAT_BEGIN_WRITE_ACTIVITY(beentry);
+
+   beentry->st_ssl = ssl;
+   beentry->st_gss = gss;
+
+#ifdef USE_SSL
+   memcpy(st_sslstatus, &lsslstatus, sizeof(PgBackendSSLStatus));
+#endif
+#ifdef ENABLE_GSS
+   memcpy(st_gssstatus, &lgssstatus, sizeof(PgBackendGSSStatus));
+#endif
+
+   PGSTAT_END_WRITE_ACTIVITY(beentry);
+}
+
+/* ----------
+ * pgstat_bestart_final() -
+ *
+ * Finalizes the state of this backend's entry by filling in the user and
+ * database IDs, clearing STATE_STARTING, and reporting the application_name.
+ *
+ * We must be inside a transaction if this is not an auxiliary process, as
+ * we may need to do encoding conversion.
+ * ----------
+ */
+void
+pgstat_bestart_final(void)
+{
+   volatile PgBackendStatus *beentry = MyBEEntry;
+   Oid         userid;
+
+   /* pgstats state must be initialized from pgstat_beinit() */
+   Assert(beentry != NULL);
+
+   /* We have userid for client-backends, wal-sender and bgworker processes */
+   if (MyBackendType == B_BACKEND
+       || MyBackendType == B_WAL_SENDER
+       || MyBackendType == B_BG_WORKER)
+       userid = GetSessionUserId();
+   else
+       userid = InvalidOid;
+
+   /*
+    * Update my status entry, following the protocol of bumping
+    * st_changecount before and after.  We use a volatile pointer here to
+    * ensure the compiler doesn't try to get cute.
+    */
+   PGSTAT_BEGIN_WRITE_ACTIVITY(beentry);
+
+   beentry->st_databaseid = MyDatabaseId;
+   beentry->st_userid = userid;
+   beentry->st_state = STATE_UNDEFINED;
+
+   PGSTAT_END_WRITE_ACTIVITY(beentry);
 
    /* Create the backend statistics entry */
    if (pgstat_tracks_backend_bktype(MyBackendType))
index 0212d8d5906b730e709ca8bd7f055eeb19dc4bc3..9172e1cb9d23f4781354e8b48ec15120529ff6ec 100644 (file)
@@ -393,6 +393,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 
            switch (beentry->st_state)
            {
+               case STATE_STARTING:
+                   values[4] = CStringGetTextDatum("starting");
+                   break;
                case STATE_IDLE:
                    values[4] = CStringGetTextDatum("idle");
                    break;
index 318600d6d02e2785071a3d5abd14baa76c6d0ea6..b428a59bdd26b4bf23d6d06f772c2a02f9deba65 100644 (file)
@@ -59,6 +59,7 @@
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
 #include "utils/portal.h"
@@ -718,6 +719,20 @@ InitPostgres(const char *in_dbname, Oid dboid,
     */
    InitProcessPhase2();
 
+   /* Initialize status reporting */
+   pgstat_beinit();
+
+   /*
+    * And initialize an entry in the PgBackendStatus array.  That way, if
+    * LWLocks or third-party authentication should happen to hang, it is
+    * possible to retrieve some information about what is going on.
+    */
+   if (!bootstrap)
+   {
+       pgstat_bestart_initial();
+       INJECTION_POINT("init-pre-auth");
+   }
+
    /*
     * Initialize my entry in the shared-invalidation manager's array of
     * per-backend data.
@@ -786,9 +801,6 @@ InitPostgres(const char *in_dbname, Oid dboid,
    /* Initialize portal manager */
    EnablePortalManager();
 
-   /* Initialize status reporting */
-   pgstat_beinit();
-
    /*
     * Load relcache entries for the shared system catalogs.  This must create
     * at least entries for pg_database and catalogs used for authentication.
@@ -809,8 +821,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
    /* The autovacuum launcher is done here */
    if (AmAutoVacuumLauncherProcess())
    {
-       /* report this backend in the PgBackendStatus array */
-       pgstat_bestart();
+       /* fill in the remainder of this entry in the PgBackendStatus array */
+       pgstat_bestart_final();
 
        return;
    }
@@ -884,6 +896,14 @@ InitPostgres(const char *in_dbname, Oid dboid,
        am_superuser = superuser();
    }
 
+   /* Report any SSL/GSS details for the session. */
+   if (MyProcPort != NULL)
+   {
+       Assert(!bootstrap);
+
+       pgstat_bestart_security();
+   }
+
    /*
     * Binary upgrades only allowed super-user connections
     */
@@ -953,8 +973,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
        /* initialize client encoding */
        InitializeClientEncoding();
 
-       /* report this backend in the PgBackendStatus array */
-       pgstat_bestart();
+       /* fill in the remainder of this entry in the PgBackendStatus array */
+       pgstat_bestart_final();
 
        /* close the transaction we started above */
        CommitTransactionCommand();
@@ -997,7 +1017,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
         */
        if (!bootstrap)
        {
-           pgstat_bestart();
+           pgstat_bestart_final();
            CommitTransactionCommand();
        }
        return;
@@ -1197,9 +1217,9 @@ InitPostgres(const char *in_dbname, Oid dboid,
    if ((flags & INIT_PG_LOAD_SESSION_LIBS) != 0)
        process_session_preload_libraries();
 
-   /* report this backend in the PgBackendStatus array */
+   /* fill in the remainder of this entry in the PgBackendStatus array */
    if (!bootstrap)
-       pgstat_bestart();
+       pgstat_bestart_final();
 
    /* close the transaction we started above */
    if (!bootstrap)
index d3d4ff6c5c9a841d1450ef552688e5bcd91e0d66..1c9b4fe14d0628a6d8c653fb80c26bad47f945af 100644 (file)
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
    STATE_UNDEFINED,
+   STATE_STARTING,
    STATE_IDLE,
    STATE_RUNNING,
    STATE_IDLEINTRANSACTION,
@@ -309,7 +310,9 @@ extern void BackendStatusShmemInit(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
-extern void pgstat_bestart(void);
+extern void pgstat_bestart_initial(void);
+extern void pgstat_bestart_security(void);
+extern void pgstat_bestart_final(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
 
index c4022dc829b670c0def6d4c3cb5c7c5e1a9f68f0..8b5beced08061183fb5870c2bc5c9c36f44fa7f5 100644 (file)
@@ -13,6 +13,10 @@ subdir = src/test/authentication
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
 check:
    $(prove_check)
 
index f6e48411c1167fbb0ae77b07c36d640ef01f1f32..800b3a5ff40fbeebb5f493deb40df31215a3228f 100644 (file)
@@ -5,6 +5,9 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_password.pl',
       't/002_saslprep.pl',
@@ -12,6 +15,7 @@ tests += {
       't/004_file_inclusion.pl',
       't/005_sspi.pl',
       't/006_login_trigger.pl',
+      't/007_pre_auth.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
new file mode 100644 (file)
index 0000000..a638226
--- /dev/null
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2021-2025, PostgreSQL Global Development Group
+
+# Tests for connection behavior prior to authentication.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+   plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf(
+   'postgresql.conf', q[
+log_connections = on
+]);
+
+$node->start;
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node->check_extension('injection_points'))
+{
+   plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+
+# Connect to the server and inject a waitpoint.
+my $psql = $node->background_psql('postgres');
+$psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
+
+# From this point on, all new connections will hang during startup, just before
+# authentication. Use the $psql connection handle for server interaction.
+my $conn = $node->background_psql('postgres', wait => 0);
+
+# Wait for the connection to show up.
+my $pid;
+while (1)
+{
+   $pid = $psql->query(
+       "SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
+   last if $pid ne "";
+
+   usleep(100_000);
+}
+
+note "backend $pid is authenticating";
+ok(1, 'authenticating connections are recorded in pg_stat_activity');
+
+# Detach the waitpoint and wait for the connection to complete.
+$psql->query_safe("SELECT injection_points_wakeup('init-pre-auth');");
+$conn->wait_connect();
+
+# Make sure the pgstat entry is updated eventually.
+while (1)
+{
+   my $state =
+     $psql->query("SELECT state FROM pg_stat_activity WHERE pid = $pid;");
+   last if $state eq "idle";
+
+   note "state for backend $pid is '$state'; waiting for 'idle'...";
+   usleep(100_000);
+}
+
+ok(1, 'authenticated connections reach idle state in pg_stat_activity');
+
+$psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
+$psql->quit();
+$conn->quit();
+
+done_testing();