[PATCH] pg_stat_activity: make slow/hanging authentication more visible

Started by Jacob Championover 1 year ago66 messages
#1Jacob Champion
jacob.champion@enterprisedb.com
2 attachment(s)

Hi all,

Recently I dealt with a server where PAM had hung a connection
indefinitely, suppressing our authentication timeout and preventing a
clean shutdown. Worse, the xmin that was pinned by the opening
transaction cascaded to replicas and started messing things up
downstream.

The DBAs didn't know what was going on, because pg_stat_activity
doesn't report the authenticating connection or its open transaction.
It just looked like a Postgres bug. And while talking about it with
Euler, he mentioned he'd seen similar "invisible" hangs with
misbehaving LDAP deployments. I think we can do better to show DBAs
what's happening.

0001, attached, changes InitPostgres() to report a nearly-complete
pgstat entry before entering client authentication, then fills it in
the rest of the way once we know who the user is. Here's a sample
entry for a client that's hung during a SCRAM exchange:

=# select * from pg_stat_activity where state = 'authenticating';
-[ RECORD 1 ]----+------------------------------
datid |
datname |
pid | 745662
leader_pid |
usesysid |
usename |
application_name |
client_addr | 127.0.0.1
client_hostname |
client_port | 38304
backend_start | 2024-05-06 11:25:23.905923-07
xact_start |
query_start |
state_change |
wait_event_type | Client
wait_event | ClientRead
state | authenticating
backend_xid |
backend_xmin | 784
query_id |
query |
backend_type | client backend

0002 goes even further, and adds wait events for various forms of
external authentication, but it's not fully baked. The intent is for a
DBA to be able to see when a bunch of connections are piling up
waiting for PAM/Kerberos/whatever. (I'm also motivated by my OAuth
patchset, where there's a server-side plugin that we have no control
over, and we'd want to be able to correctly point fingers at it if
things go wrong.)

= Open Issues, Idle Thoughts =

Maybe it's wishful thinking, but it'd be cool if a misbehaving
authentication exchange did not impact replicas in any way. Is there a
way to make that opening transaction lighterweight?

0001 may be a little too much code. There are only two parts of
pgstat_bestart() that need to be modified: omit the user ID, and fill
in the state as 'authenticating' rather than unknown. I could just add
the `pre_auth` boolean to the signature of pgstat_bestart() directly,
if we don't mind adjusting all the call sites. We could also avoid
changing the signature entirely, and just assume that we're
authenticating if SessionUserId isn't set. That felt like a little too
much global magic to me, though.

Would anyone like me to be more aggressive, and create a pgstat entry
as soon as we have the opening transaction? Or... as soon as a
connection is made?

0002 is abusing the "IPC" wait event class. If the general idea seems
okay, maybe we could add an "External" class that encompasses the
general idea of "it's not our fault, it's someone else's"?

I had trouble deciding how granular to make the areas that are covered
by the new wait events. Ideally they would kick in only when we call
out to an external system, but for some authentication types, that's a
lot of calls to wrap. On the other extreme, we don't want to go too
high in the call stack and accidentally nest wait events (such as
those generated during pq_getmessage()). What I have now is not very
principled.

I haven't decided how to test these patches. Seems like a potential
use case for injection points, but I think I'd need to preload an
injection library rather than using the existing extension. Does that
seem like an okay way to go?

Thanks,
--Jacob

Attachments:

0001-pgstat-report-in-earlier-with-STATE_AUTHENTICATING.patchapplication/octet-stream; name=0001-pgstat-report-in-earlier-with-STATE_AUTHENTICATING.patchDownload
From 1535930adde98162152223c1d215c1ccb0f0a9e0 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH 1/2] pgstat: report in earlier with STATE_AUTHENTICATING

Add pgstat_bestart_pre_auth(), which reports an 'authenticating' state
while waiting for client authentication to complete. Since we hold a
transaction open across that call, and some authentication methods call
out to external systems, having a pg_stat_activity entry helps DBAs
debug when things go badly wrong.
---
 src/backend/utils/activity/backend_status.c | 37 ++++++++++++++++++---
 src/backend/utils/adt/pgstatfuncs.c         |  3 ++
 src/backend/utils/init/postinit.c           |  9 +++++
 src/include/utils/backend_status.h          |  2 ++
 4 files changed, 47 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 1ccf4c6d83..c996049bbe 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -71,6 +71,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -271,6 +272,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -320,9 +349,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -377,7 +406,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_AUTHENTICATING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 3876339ee1..f34e4a1643 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 
 			switch (beentry->st_state)
 			{
+				case STATE_AUTHENTICATING:
+					values[4] = CStringGetTextDatum("authenticating");
+					break;
 				case STATE_IDLE:
 					values[4] = CStringGetTextDatum("idle");
 					break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 0805398e24..4f10e29b3d 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -921,6 +921,15 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
+		/*
+		 * Authentication can take a while, during which time we're holding a
+		 * transaction open. Fill in enough of a backend status so that DBAs can
+		 * observe what's going on. (The later call to pgstat_bestart() will
+		 * fill in the rest of the status after we've authenticated.)
+		 */
+		pgstat_bestart_pre_auth();
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 7b7f6f59d0..f673c6a6ac 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_AUTHENTICATING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void CreateSharedBackendStatus(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
-- 
2.34.1

0002-WIP-report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=0002-WIP-report-external-auth-calls-as-wait-events.patchDownload
From 9faa86a4597227c5837c306c01a7a0e5466e4ea5 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH 2/2] WIP: report external auth calls as wait events

Introduce new WAIT_EVENT_AUTHN_* types for various external
authentication systems, to make it obvious what's going wrong if one of
those systems hangs.

TODO:
- don't abuse the IPC wait event group like this
- test
---
 src/backend/libpq/auth.c                      | 54 +++++++++++++++----
 .../utils/activity/wait_event_names.txt       |  5 ++
 2 files changed, 49 insertions(+), 10 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c5270..bda80e88f9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_GSSAPI);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,19 +1410,25 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
 	if (!port->hba->compat_realm)
 	{
-		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
-											  domainname, sizeof(domainname),
-											  port->hba->upn_username);
+		int			status;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
+								  domainname, sizeof(domainname),
+								  port->hba->upn_username);
+		pgstat_report_wait_end();
 
 		if (status != STATUS_OK)
 			/* Error already reported from pg_SSPI_make_upn */
@@ -2114,7 +2128,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2127,7 +2143,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2478,7 +2496,11 @@ CheckLDAPAuth(Port *port)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
+	r = InitializeLDAPConnection(port, &ldap);
+	pgstat_report_wait_end();
+
+	if (r == STATUS_ERROR)
 	{
 		/* Error message already sent */
 		pfree(passwd);
@@ -2525,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2550,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2557,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2625,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -2885,12 +2915,16 @@ CheckRADIUSAuth(Port *port)
 	identifiers = list_head(port->hba->radiusidentifiers);
 	foreach(server, port->hba->radiusservers)
 	{
-		int			ret = PerformRadiusTransaction(lfirst(server),
-												   lfirst(secrets),
-												   radiusports ? lfirst(radiusports) : NULL,
-												   identifiers ? lfirst(identifiers) : NULL,
-												   port->user_name,
-												   passwd);
+		int			ret;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_RADIUS);
+		ret = PerformRadiusTransaction(lfirst(server),
+									   lfirst(secrets),
+									   radiusports ? lfirst(radiusports) : NULL,
+									   identifiers ? lfirst(identifiers) : NULL,
+									   port->user_name,
+									   passwd);
+		pgstat_report_wait_end();
 
 		/*------
 		 * STATUS_OK = Login OK
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 87cbca2811..7761c2d71d 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -104,6 +104,11 @@ Section: ClassName - WaitEventIPC
 APPEND_READY	"Waiting for subplan nodes of an <literal>Append</literal> plan node to be ready."
 ARCHIVE_CLEANUP_COMMAND	"Waiting for <xref linkend="guc-archive-cleanup-command"/> to complete."
 ARCHIVE_COMMAND	"Waiting for <xref linkend="guc-archive-command"/> to complete."
+AUTHN_GSSAPI	"Waiting for a response from a Kerberos server via GSSAPI."
+AUTHN_LDAP	"Waiting for a response from an LDAP server."
+AUTHN_PAM	"Waiting for a response from the local PAM service."
+AUTHN_RADIUS	"Waiting for a response from a RADIUS server."
+AUTHN_SSPI	"Waiting for a response from a Windows security provider via SSPI."
 BACKEND_TERMINATION	"Waiting for the termination of another backend."
 BACKUP_WAIT_WAL_ARCHIVE	"Waiting for WAL files required for a backup to be successfully archived."
 BGWORKER_SHUTDOWN	"Waiting for background worker to shut down."
-- 
2.34.1

#2Noah Misch
noah@leadboat.com
In reply to: Jacob Champion (#1)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, May 06, 2024 at 02:23:38PM -0700, Jacob Champion wrote:

=# select * from pg_stat_activity where state = 'authenticating';
-[ RECORD 1 ]----+------------------------------
datid |
datname |
pid | 745662
leader_pid |
usesysid |
usename |
application_name |
client_addr | 127.0.0.1
client_hostname |
client_port | 38304
backend_start | 2024-05-06 11:25:23.905923-07
xact_start |
query_start |
state_change |
wait_event_type | Client
wait_event | ClientRead
state | authenticating
backend_xid |
backend_xmin | 784
query_id |
query |
backend_type | client backend

That looks like a reasonable user experience. Is any field newly-nullable?

= Open Issues, Idle Thoughts =

Maybe it's wishful thinking, but it'd be cool if a misbehaving
authentication exchange did not impact replicas in any way. Is there a
way to make that opening transaction lighterweight?

You could release the xmin before calling PAM or LDAP. If you've copied all
relevant catalog content to local memory, that's fine to do. That said, it
may be more fruitful to arrange for authentication timeout to cut through PAM
etc. Hanging connection slots hurt even if they lack an xmin. I assume it
takes an immediate shutdown to fix them?

Would anyone like me to be more aggressive, and create a pgstat entry
as soon as we have the opening transaction? Or... as soon as a
connection is made?

All else being equal, I'd like backends to have one before taking any lmgr
lock or snapshot.

I haven't decided how to test these patches. Seems like a potential
use case for injection points, but I think I'd need to preload an
injection library rather than using the existing extension. Does that
seem like an okay way to go?

Yes.

Thanks,
nm

#3Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Noah Misch (#2)
4 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Sun, Jun 30, 2024 at 10:48 AM Noah Misch <noah@leadboat.com> wrote:

That looks like a reasonable user experience. Is any field newly-nullable?

Technically I think the answer is no, since backends such as walwriter
already have null database and user fields. It's new for a client
backend to have nulls there, though.

That said, it
may be more fruitful to arrange for authentication timeout to cut through PAM
etc.

That seems mostly out of our hands -- the misbehaving modules are free
to ignore our signals (and do). Is there another way to force the
issue?

Hanging connection slots hurt even if they lack an xmin.

Oh, would releasing the xmin not really move the needle, then?

I assume it
takes an immediate shutdown to fix them?

That's my understanding, yeah.

Would anyone like me to be more aggressive, and create a pgstat entry
as soon as we have the opening transaction? Or... as soon as a
connection is made?

All else being equal, I'd like backends to have one before taking any lmgr
lock or snapshot.

I can look at this for the next patchset version.

I haven't decided how to test these patches. Seems like a potential
use case for injection points, but I think I'd need to preload an
injection library rather than using the existing extension. Does that
seem like an okay way to go?

Yes.

I misunderstood how injection points worked. No preload module needed,
so v2 adds a waitpoint and a test along with a couple of needed tweaks
to BackgroundPsql. I think 0001 should probably be applied
independently.

Thanks,
--Jacob

Attachments:

v2-0001-BackgroundPsql-handle-empty-query-results.patchapplication/octet-stream; name=v2-0001-BackgroundPsql-handle-empty-query-results.patchDownload
From 014f42c62659dbf302b85b9265ab0d6b081b08b3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:11:56 -0700
Subject: [PATCH v2 1/4] BackgroundPsql: handle empty query results

There won't be a newline at the end of an empty query result. (Before
this fix, the $banner showed up in the result, leading to confusing
debugging sessions.)

recovery/t/037_invalid_database was relying on the non-empty query
results, so I have switched those cases to use "bare" calls to
query_safe() instead.
---
 src/test/perl/PostgreSQL/Test/BackgroundPsql.pm |  2 +-
 src/test/recovery/t/037_invalid_database.pl     | 12 ++++--------
 2 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 3c2aca1c5d..2760e4bc8d 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -223,7 +223,7 @@ sub query
 	$output = $self->{stdout};
 
 	# remove banner again, our caller doesn't care
-	$output =~ s/\n$banner\n$//s;
+	$output =~ s/\n?$banner\n$//s;
 
 	# clear out output for the next query
 	$self->{stdout} = '';
diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl
index 47f524be4c..954c3684a9 100644
--- a/src/test/recovery/t/037_invalid_database.pl
+++ b/src/test/recovery/t/037_invalid_database.pl
@@ -93,13 +93,12 @@ my $bgpsql = $node->background_psql('postgres', on_error_stop => 0);
 my $pid = $bgpsql->query('SELECT pg_backend_pid()');
 
 # create the database, prevent drop database via lock held by a 2PC transaction
-ok( $bgpsql->query_safe(
+$bgpsql->query_safe(
 		qq(
   CREATE DATABASE regression_invalid_interrupt;
   BEGIN;
   LOCK pg_tablespace;
-  PREPARE TRANSACTION 'lock_tblspc';)),
-	"blocked DROP DATABASE completion");
+  PREPARE TRANSACTION 'lock_tblspc';));
 
 # Try to drop. This will wait due to the still held lock.
 $bgpsql->query_until(qr//, "DROP DATABASE regression_invalid_interrupt;\n");
@@ -126,11 +125,8 @@ is($node->psql('regression_invalid_interrupt', ''),
 
 # To properly drop the database, we need to release the lock previously preventing
 # doing so.
-ok($bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc')),
-	"unblock DROP DATABASE");
-
-ok($bgpsql->query(qq(DROP DATABASE regression_invalid_interrupt)),
-	"DROP DATABASE invalid_interrupt");
+$bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc'));
+$bgpsql->query_safe(qq(DROP DATABASE regression_invalid_interrupt));
 
 $bgpsql->quit();
 
-- 
2.34.1

v2-0002-Test-Cluster-let-background_psql-work-asynchronou.patchapplication/octet-stream; name=v2-0002-Test-Cluster-let-background_psql-work-asynchronou.patchDownload
From 592ef05d30505c4c7a1a84084f80c807fce57476 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:46:55 -0700
Subject: [PATCH v2 2/4] Test::Cluster: let background_psql() work
 asynchronously

Specifying `wait => 0` as a parameter to background_psql() causes it to
return immediately, which lets the client run code during connection.
(This is useful if, for example, connections are blocked on injected
waitpoints.) Clients later call ->wait_connect() manually to complete
the asynchronous connection.
---
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    | 23 ++++++++++++++-----
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 10 +++++++-
 2 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 2760e4bc8d..13489ee95e 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -81,7 +81,7 @@ string. For C<interactive> sessions, IO::Pty is required.
 sub new
 {
 	my $class = shift;
-	my ($interactive, $psql_params, $timeout) = @_;
+	my ($interactive, $psql_params, $timeout, $wait) = @_;
 	my $psql = {
 		'stdin' => '',
 		'stdout' => '',
@@ -119,14 +119,25 @@ sub new
 
 	my $self = bless $psql, $class;
 
-	$self->_wait_connect();
+	$wait = 1 unless defined($wait);
+	if ($wait)
+	{
+		$self->wait_connect();
+	}
 
 	return $self;
 }
 
-# Internal routine for awaiting psql starting up and being ready to consume
-# input.
-sub _wait_connect
+=pod
+
+=item $session->wait_connect
+
+Returns once psql has started up and is ready to consume input.  This is called
+automatically for clients unless requested otherwise in the constructor.
+
+=cut
+
+sub wait_connect
 {
 	my ($self) = @_;
 
@@ -187,7 +198,7 @@ sub reconnect_and_clear
 	$self->{stdin} = '';
 	$self->{stdout} = '';
 
-	$self->_wait_connect();
+	$self->wait_connect();
 }
 
 =pod
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 0135c5a795..759c9d93c2 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2182,6 +2182,12 @@ connection.
 
 If given, it must be an array reference containing additional parameters to B<psql>.
 
+=item wait => 1
+
+By default, this method will not return until connection has completed (or
+failed).  Set B<wait> to 0 to return immediately instead.  (Clients can call the
+session's C<wait_connect> method manually when needed.)
+
 =back
 
 =cut
@@ -2205,13 +2211,15 @@ sub background_psql
 		'-');
 
 	$params{on_error_stop} = 1 unless defined $params{on_error_stop};
+	$params{wait} = 1 unless defined $params{wait};
 	$timeout = $params{timeout} if defined $params{timeout};
 
 	push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
 	push @psql_params, @{ $params{extra_params} }
 	  if defined $params{extra_params};
 
-	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout);
+	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout,
+		$params{wait});
 }
 
 =pod
-- 
2.34.1

v2-0004-WIP-report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v2-0004-WIP-report-external-auth-calls-as-wait-events.patchDownload
From 059f856892fb9e9f75d8ce36bad6533457b0711d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v2 4/4] WIP: report external auth calls as wait events

Introduce new WAIT_EVENT_AUTHN_* types for various external
authentication systems, to make it obvious what's going wrong if one of
those systems hangs.

TODO:
- don't abuse the IPC wait event group like this
- test
---
 src/backend/libpq/auth.c                      | 54 +++++++++++++++----
 .../utils/activity/wait_event_names.txt       |  5 ++
 2 files changed, 49 insertions(+), 10 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c5270..bda80e88f9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_GSSAPI);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,19 +1410,25 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
 	if (!port->hba->compat_realm)
 	{
-		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
-											  domainname, sizeof(domainname),
-											  port->hba->upn_username);
+		int			status;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
+								  domainname, sizeof(domainname),
+								  port->hba->upn_username);
+		pgstat_report_wait_end();
 
 		if (status != STATUS_OK)
 			/* Error already reported from pg_SSPI_make_upn */
@@ -2114,7 +2128,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2127,7 +2143,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2478,7 +2496,11 @@ CheckLDAPAuth(Port *port)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
+	r = InitializeLDAPConnection(port, &ldap);
+	pgstat_report_wait_end();
+
+	if (r == STATUS_ERROR)
 	{
 		/* Error message already sent */
 		pfree(passwd);
@@ -2525,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2550,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2557,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2625,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -2885,12 +2915,16 @@ CheckRADIUSAuth(Port *port)
 	identifiers = list_head(port->hba->radiusidentifiers);
 	foreach(server, port->hba->radiusservers)
 	{
-		int			ret = PerformRadiusTransaction(lfirst(server),
-												   lfirst(secrets),
-												   radiusports ? lfirst(radiusports) : NULL,
-												   identifiers ? lfirst(identifiers) : NULL,
-												   port->user_name,
-												   passwd);
+		int			ret;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_RADIUS);
+		ret = PerformRadiusTransaction(lfirst(server),
+									   lfirst(secrets),
+									   radiusports ? lfirst(radiusports) : NULL,
+									   identifiers ? lfirst(identifiers) : NULL,
+									   port->user_name,
+									   passwd);
+		pgstat_report_wait_end();
 
 		/*------
 		 * STATUS_OK = Login OK
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index db37beeaae..3edbfe4473 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -104,6 +104,11 @@ Section: ClassName - WaitEventIPC
 APPEND_READY	"Waiting for subplan nodes of an <literal>Append</literal> plan node to be ready."
 ARCHIVE_CLEANUP_COMMAND	"Waiting for <xref linkend="guc-archive-cleanup-command"/> to complete."
 ARCHIVE_COMMAND	"Waiting for <xref linkend="guc-archive-command"/> to complete."
+AUTHN_GSSAPI	"Waiting for a response from a Kerberos server via GSSAPI."
+AUTHN_LDAP	"Waiting for a response from an LDAP server."
+AUTHN_PAM	"Waiting for a response from the local PAM service."
+AUTHN_RADIUS	"Waiting for a response from a RADIUS server."
+AUTHN_SSPI	"Waiting for a response from a Windows security provider via SSPI."
 BACKEND_TERMINATION	"Waiting for the termination of another backend."
 BACKUP_WAIT_WAL_ARCHIVE	"Waiting for WAL files required for a backup to be successfully archived."
 BGWORKER_SHUTDOWN	"Waiting for background worker to shut down."
-- 
2.34.1

v2-0003-pgstat-report-in-earlier-with-STATE_AUTHENTICATIN.patchapplication/octet-stream; name=v2-0003-pgstat-report-in-earlier-with-STATE_AUTHENTICATIN.patchDownload
From 22ce00adebdd242e747036d46e1904307287feaa Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v2 3/4] pgstat: report in earlier with STATE_AUTHENTICATING

Add pgstat_bestart_pre_auth(), which reports an 'authenticating' state
while waiting for client authentication to complete. Since we hold a
transaction open across that call, and some authentication methods call
out to external systems, having a pg_stat_activity entry helps DBAs
debug when things go badly wrong.
---
 src/backend/utils/activity/backend_status.c   | 37 +++++++++-
 src/backend/utils/adt/pgstatfuncs.c           |  3 +
 src/backend/utils/init/postinit.c             | 11 +++
 src/include/utils/backend_status.h            |  2 +
 src/test/authentication/Makefile              |  2 +
 src/test/authentication/meson.build           |  4 +
 .../authentication/t/007_injection_points.pl  | 73 +++++++++++++++++++
 7 files changed, 128 insertions(+), 4 deletions(-)
 create mode 100644 src/test/authentication/t/007_injection_points.pl

diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 1ccf4c6d83..c996049bbe 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -71,6 +71,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -271,6 +272,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -320,9 +349,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -377,7 +406,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_AUTHENTICATING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 3876339ee1..f34e4a1643 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 
 			switch (beentry->st_state)
 			{
+				case STATE_AUTHENTICATING:
+					values[4] = CStringGetTextDatum("authenticating");
+					break;
 				case STATE_IDLE:
 					values[4] = CStringGetTextDatum("idle");
 					break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 25867c8bd5..83da2790b2 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -878,6 +879,16 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
+		/*
+		 * Authentication can take a while, during which time we're holding a
+		 * transaction open. Fill in enough of a backend status so that DBAs can
+		 * observe what's going on. (The later call to pgstat_bestart() will
+		 * fill in the rest of the status after we've authenticated.)
+		 */
+		pgstat_bestart_pre_auth();
+		INJECTION_POINT("init-pre-auth");
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 7b7f6f59d0..f673c6a6ac 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_AUTHENTICATING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void CreateSharedBackendStatus(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..7bbc3b6e57 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -13,6 +13,8 @@ subdir = src/test/authentication
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+export enable_injection_points
+
 check:
 	$(prove_check)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..09bad8f2b8 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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_injection_points.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/007_injection_points.pl b/src/test/authentication/t/007_injection_points.pl
new file mode 100644
index 0000000000..96ed691d93
--- /dev/null
+++ b/src/test/authentication/t/007_injection_points.pl
@@ -0,0 +1,73 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Tests requiring injection_points functionality, to check on behavior that
+# would otherwise race against 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;
+$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 in 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 = 'authenticating';");
+	last if $pid ne "";
+
+	usleep(500_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(500_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();
-- 
2.34.1

#4Noah Misch
noah@leadboat.com
In reply to: Jacob Champion (#3)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Jul 08, 2024 at 02:09:21PM -0700, Jacob Champion wrote:

On Sun, Jun 30, 2024 at 10:48 AM Noah Misch <noah@leadboat.com> wrote:

That said, it
may be more fruitful to arrange for authentication timeout to cut through PAM
etc.

That seems mostly out of our hands -- the misbehaving modules are free
to ignore our signals (and do). Is there another way to force the
issue?

Two ways at least (neither of them cheap):
- Invoke PAM in a subprocess, and SIGKILL that process if needed.
- Modify the module to be interruptible.

Hanging connection slots hurt even if they lack an xmin.

Oh, would releasing the xmin not really move the needle, then?

It still moves the needle.

#5Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Noah Misch (#2)
4 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Sun, Jun 30, 2024 at 10:48 AM Noah Misch <noah@leadboat.com> wrote:v

Would anyone like me to be more aggressive, and create a pgstat entry
as soon as we have the opening transaction? Or... as soon as a
connection is made?

All else being equal, I'd like backends to have one before taking any lmgr
lock or snapshot.

v3-0003 pushes the pgstat creation as far back as I felt comfortable,
right after the PGPROC registration by InitProcessPhase2(). That
function does lock the ProcArray, but if it gets held forever due to
some bug, you won't be able to use pg_stat_activity to debug it
anyway. And with this ordering, pg_stat_get_activity() will be able to
retrieve the proc entry by PID without a race.

This approach ends up registering an early entry for more cases than
the original patchset. For example, autovacuum and other background
workers will now briefly get their own "authenticating" state, which
seems like it could potentially confuse people. Should I rename the
state, or am I overthinking it?

You could release the xmin before calling PAM or LDAP. If you've copied all
relevant catalog content to local memory, that's fine to do.

I played with the xmin problem a little bit, but I've shelved it for
now. There's probably a way to do that safely; I just don't understand
enough about the invariants to do it. For example, there's a comment
later on that says

* We established a catalog snapshot while reading pg_authid and/or
* pg_database;

and I'm a little nervous about invalidating the snapshot halfway
through that process. Even if PAM and LDAP don't rely on pg_authid or
other shared catalogs today, shouldn't they be allowed to in the
future, without being coupled to InitPostgres implementation order?
And I don't think we can move the pg_database checks before
authentication.

As for the other patches, I'll ping Andrew about 0001, and 0004
remains in its original WIP state. Anyone excited about that wait
event idea?

Thanks!
--Jacob

Attachments:

v3-0002-Test-Cluster-let-background_psql-work-asynchronou.patchapplication/octet-stream; name=v3-0002-Test-Cluster-let-background_psql-work-asynchronou.patchDownload
From 7f404f5ee8aad2a4286d5251a2712ce978ee3fc2 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:46:55 -0700
Subject: [PATCH v3 2/4] Test::Cluster: let background_psql() work
 asynchronously

Specifying `wait => 0` as a parameter to background_psql() causes it to
return immediately, which lets the client run code during connection.
(This is useful if, for example, connections are blocked on injected
waitpoints.) Clients later call ->wait_connect() manually to complete
the asynchronous connection.
---
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    | 23 ++++++++++++++-----
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 10 +++++++-
 2 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 2760e4bc8d..13489ee95e 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -81,7 +81,7 @@ string. For C<interactive> sessions, IO::Pty is required.
 sub new
 {
 	my $class = shift;
-	my ($interactive, $psql_params, $timeout) = @_;
+	my ($interactive, $psql_params, $timeout, $wait) = @_;
 	my $psql = {
 		'stdin' => '',
 		'stdout' => '',
@@ -119,14 +119,25 @@ sub new
 
 	my $self = bless $psql, $class;
 
-	$self->_wait_connect();
+	$wait = 1 unless defined($wait);
+	if ($wait)
+	{
+		$self->wait_connect();
+	}
 
 	return $self;
 }
 
-# Internal routine for awaiting psql starting up and being ready to consume
-# input.
-sub _wait_connect
+=pod
+
+=item $session->wait_connect
+
+Returns once psql has started up and is ready to consume input.  This is called
+automatically for clients unless requested otherwise in the constructor.
+
+=cut
+
+sub wait_connect
 {
 	my ($self) = @_;
 
@@ -187,7 +198,7 @@ sub reconnect_and_clear
 	$self->{stdin} = '';
 	$self->{stdout} = '';
 
-	$self->_wait_connect();
+	$self->wait_connect();
 }
 
 =pod
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index fe6ebf10f7..134ddfaa70 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2191,6 +2191,12 @@ connection.
 
 If given, it must be an array reference containing additional parameters to B<psql>.
 
+=item wait => 1
+
+By default, this method will not return until connection has completed (or
+failed).  Set B<wait> to 0 to return immediately instead.  (Clients can call the
+session's C<wait_connect> method manually when needed.)
+
 =back
 
 =cut
@@ -2214,13 +2220,15 @@ sub background_psql
 		'-');
 
 	$params{on_error_stop} = 1 unless defined $params{on_error_stop};
+	$params{wait} = 1 unless defined $params{wait};
 	$timeout = $params{timeout} if defined $params{timeout};
 
 	push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
 	push @psql_params, @{ $params{extra_params} }
 	  if defined $params{extra_params};
 
-	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout);
+	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout,
+		$params{wait});
 }
 
 =pod
-- 
2.34.1

v3-0001-BackgroundPsql-handle-empty-query-results.patchapplication/octet-stream; name=v3-0001-BackgroundPsql-handle-empty-query-results.patchDownload
From 8f9315949e997e24f68e41d7450f7883f46bf54e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:11:56 -0700
Subject: [PATCH v3 1/4] BackgroundPsql: handle empty query results

There won't be a newline at the end of an empty query result. (Before
this fix, the $banner showed up in the result, leading to confusing
debugging sessions.)

recovery/t/037_invalid_database was relying on the non-empty query
results, so I have switched those cases to use "bare" calls to
query_safe() instead.
---
 src/test/perl/PostgreSQL/Test/BackgroundPsql.pm |  2 +-
 src/test/recovery/t/037_invalid_database.pl     | 12 ++++--------
 2 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 3c2aca1c5d..2760e4bc8d 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -223,7 +223,7 @@ sub query
 	$output = $self->{stdout};
 
 	# remove banner again, our caller doesn't care
-	$output =~ s/\n$banner\n$//s;
+	$output =~ s/\n?$banner\n$//s;
 
 	# clear out output for the next query
 	$self->{stdout} = '';
diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl
index 47f524be4c..954c3684a9 100644
--- a/src/test/recovery/t/037_invalid_database.pl
+++ b/src/test/recovery/t/037_invalid_database.pl
@@ -93,13 +93,12 @@ my $bgpsql = $node->background_psql('postgres', on_error_stop => 0);
 my $pid = $bgpsql->query('SELECT pg_backend_pid()');
 
 # create the database, prevent drop database via lock held by a 2PC transaction
-ok( $bgpsql->query_safe(
+$bgpsql->query_safe(
 		qq(
   CREATE DATABASE regression_invalid_interrupt;
   BEGIN;
   LOCK pg_tablespace;
-  PREPARE TRANSACTION 'lock_tblspc';)),
-	"blocked DROP DATABASE completion");
+  PREPARE TRANSACTION 'lock_tblspc';));
 
 # Try to drop. This will wait due to the still held lock.
 $bgpsql->query_until(qr//, "DROP DATABASE regression_invalid_interrupt;\n");
@@ -126,11 +125,8 @@ is($node->psql('regression_invalid_interrupt', ''),
 
 # To properly drop the database, we need to release the lock previously preventing
 # doing so.
-ok($bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc')),
-	"unblock DROP DATABASE");
-
-ok($bgpsql->query(qq(DROP DATABASE regression_invalid_interrupt)),
-	"DROP DATABASE invalid_interrupt");
+$bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc'));
+$bgpsql->query_safe(qq(DROP DATABASE regression_invalid_interrupt));
 
 $bgpsql->quit();
 
-- 
2.34.1

v3-0003-pgstat-report-in-earlier-with-STATE_AUTHENTICATIN.patchapplication/octet-stream; name=v3-0003-pgstat-report-in-earlier-with-STATE_AUTHENTICATIN.patchDownload
From a7a7551d09ccbd90bbbc26b75e3be718ec6a085e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v3 3/4] pgstat: report in earlier with STATE_AUTHENTICATING

Add pgstat_bestart_pre_auth(), which reports an 'authenticating' state
while waiting for backend initialization and client authentication to
complete. Since we hold a transaction open for a good amount of that,
and some authentication methods call out to external systems, having a
pg_stat_activity entry helps DBAs debug when things go badly wrong.
---
 src/backend/utils/activity/backend_status.c   | 37 +++++++++-
 src/backend/utils/adt/pgstatfuncs.c           |  3 +
 src/backend/utils/init/postinit.c             | 20 ++++-
 src/include/utils/backend_status.h            |  2 +
 src/test/authentication/Makefile              |  2 +
 src/test/authentication/meson.build           |  4 +
 .../authentication/t/007_injection_points.pl  | 73 +++++++++++++++++++
 7 files changed, 134 insertions(+), 7 deletions(-)
 create mode 100644 src/test/authentication/t/007_injection_points.pl

diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 34a55e2177..d693b2dff7 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -71,6 +71,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -271,6 +272,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -320,9 +349,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -377,7 +406,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_AUTHENTICATING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 3221137123..b007f0d5a1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,9 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 
 			switch (beentry->st_state)
 			{
+				case STATE_AUTHENTICATING:
+					values[4] = CStringGetTextDatum("authenticating");
+					break;
 				case STATE_IDLE:
 					values[4] = CStringGetTextDatum("idle");
 					break;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3b50ce19a2..e2034829e4 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -680,6 +681,21 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That way,
+	 * if LWLocks or third-party authentication should happen to hang, the DBA
+	 * will still be able to see what's going on. (A later call to
+	 * pgstat_bestart() will fill in the rest of the status.)
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_pre_auth();
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -748,9 +764,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.
@@ -848,6 +861,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 97874300c3..18f79bd3ad 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_AUTHENTICATING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void BackendStatusShmemInit(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..7bbc3b6e57 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -13,6 +13,8 @@ subdir = src/test/authentication
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+export enable_injection_points
+
 check:
 	$(prove_check)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..09bad8f2b8 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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_injection_points.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/007_injection_points.pl b/src/test/authentication/t/007_injection_points.pl
new file mode 100644
index 0000000000..96ed691d93
--- /dev/null
+++ b/src/test/authentication/t/007_injection_points.pl
@@ -0,0 +1,73 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Tests requiring injection_points functionality, to check on behavior that
+# would otherwise race against 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;
+$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 in 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 = 'authenticating';");
+	last if $pid ne "";
+
+	usleep(500_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(500_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();
-- 
2.34.1

v3-0004-WIP-report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v3-0004-WIP-report-external-auth-calls-as-wait-events.patchDownload
From 5c85c3e0e99cfb2beaa4f0e0a4a7e1cc4ab91af3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v3 4/4] WIP: report external auth calls as wait events

Introduce new WAIT_EVENT_AUTHN_* types for various external
authentication systems, to make it obvious what's going wrong if one of
those systems hangs.

TODO:
- don't abuse the IPC wait event group like this
- test
---
 src/backend/libpq/auth.c                      | 54 +++++++++++++++----
 .../utils/activity/wait_event_names.txt       |  5 ++
 2 files changed, 49 insertions(+), 10 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2b607c5270..bda80e88f9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_GSSAPI);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,19 +1410,25 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
 	if (!port->hba->compat_realm)
 	{
-		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
-											  domainname, sizeof(domainname),
-											  port->hba->upn_username);
+		int			status;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
+								  domainname, sizeof(domainname),
+								  port->hba->upn_username);
+		pgstat_report_wait_end();
 
 		if (status != STATUS_OK)
 			/* Error already reported from pg_SSPI_make_upn */
@@ -2114,7 +2128,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2127,7 +2143,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2478,7 +2496,11 @@ CheckLDAPAuth(Port *port)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
+	r = InitializeLDAPConnection(port, &ldap);
+	pgstat_report_wait_end();
+
+	if (r == STATUS_ERROR)
 	{
 		/* Error message already sent */
 		pfree(passwd);
@@ -2525,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2550,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2557,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2625,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -2885,12 +2915,16 @@ CheckRADIUSAuth(Port *port)
 	identifiers = list_head(port->hba->radiusidentifiers);
 	foreach(server, port->hba->radiusservers)
 	{
-		int			ret = PerformRadiusTransaction(lfirst(server),
-												   lfirst(secrets),
-												   radiusports ? lfirst(radiusports) : NULL,
-												   identifiers ? lfirst(identifiers) : NULL,
-												   port->user_name,
-												   passwd);
+		int			ret;
+
+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_RADIUS);
+		ret = PerformRadiusTransaction(lfirst(server),
+									   lfirst(secrets),
+									   radiusports ? lfirst(radiusports) : NULL,
+									   identifiers ? lfirst(identifiers) : NULL,
+									   port->user_name,
+									   passwd);
+		pgstat_report_wait_end();
 
 		/*------
 		 * STATUS_OK = Login OK
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d6..6ce81edc33 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -105,6 +105,11 @@ Section: ClassName - WaitEventIPC
 APPEND_READY	"Waiting for subplan nodes of an <literal>Append</literal> plan node to be ready."
 ARCHIVE_CLEANUP_COMMAND	"Waiting for <xref linkend="guc-archive-cleanup-command"/> to complete."
 ARCHIVE_COMMAND	"Waiting for <xref linkend="guc-archive-command"/> to complete."
+AUTHN_GSSAPI	"Waiting for a response from a Kerberos server via GSSAPI."
+AUTHN_LDAP	"Waiting for a response from an LDAP server."
+AUTHN_PAM	"Waiting for a response from the local PAM service."
+AUTHN_RADIUS	"Waiting for a response from a RADIUS server."
+AUTHN_SSPI	"Waiting for a response from a Windows security provider via SSPI."
 BACKEND_TERMINATION	"Waiting for the termination of another backend."
 BACKUP_WAIT_WAL_ARCHIVE	"Waiting for WAL files required for a backup to be successfully archived."
 BGWORKER_SHUTDOWN	"Waiting for background worker to shut down."
-- 
2.34.1

#6Andrew Dunstan
andrew@dunslane.net
In reply to: Jacob Champion (#5)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On 2024-08-29 Th 4:44 PM, Jacob Champion wrote:

As for the other patches, I'll ping Andrew about 0001,

Patch 0001 looks sane to me.

cheers

andrew

--
Andrew Dunstan
EDB:https://www.enterprisedb.com

#7Michael Paquier
michael@paquier.xyz
In reply to: Andrew Dunstan (#6)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Aug 30, 2024 at 04:10:32PM -0400, Andrew Dunstan wrote:

On 2024-08-29 Th 4:44 PM, Jacob Champion wrote:

As for the other patches, I'll ping Andrew about 0001,

Patch 0001 looks sane to me.

So does 0002 to me. I'm not much a fan of the addition of
pgstat_bestart_pre_auth(), which is just a shortcut to set a different
state in the backend entry to tell that it is authenticating. Is
authenticating the term for this state of the process startups,
actually? Could it be more transparent to use a "startup" or
"starting"" state instead that gets also used by pgstat_bestart() in
the case of the patch where !pre_auth?

The addition of the new wait event states in 0004 is a good idea,
indeed, and these can be seen in pg_stat_activity once we get out of
PGSTAT_END_WRITE_ACTIVITY() (err.. Right?).
--
Michael

#8Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#7)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Sun, Sep 1, 2024 at 5:10 PM Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Aug 30, 2024 at 04:10:32PM -0400, Andrew Dunstan wrote:

Patch 0001 looks sane to me.

So does 0002 to me.

Thanks both!

I'm not much a fan of the addition of
pgstat_bestart_pre_auth(), which is just a shortcut to set a different
state in the backend entry to tell that it is authenticating. Is
authenticating the term for this state of the process startups,
actually? Could it be more transparent to use a "startup" or
"starting"" state instead

Yeah, I think I should rename that. Especially if we adopt new wait
states to make it obvious where we're stuck.

"startup", "starting", "initializing", "connecting"...?

that gets also used by pgstat_bestart() in
the case of the patch where !pre_auth?

To clarify, do you want me to just add the new boolean directly to
pgstat_bestart()'s parameter list?

The addition of the new wait event states in 0004 is a good idea,
indeed,

Thanks! Any thoughts on the two open questions for it?:
1) Should we add a new wait event class rather than reusing IPC?
2) Is the level at which I've inserted calls to
pgstat_report_wait_start()/_end() sane and maintainable?

and these can be seen in pg_stat_activity once we get out of
PGSTAT_END_WRITE_ACTIVITY() (err.. Right?).

It doesn't look like pgstat_report_wait_start() uses that machinery.

--Jacob

#9Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#8)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 03, 2024 at 02:47:57PM -0700, Jacob Champion wrote:

On Sun, Sep 1, 2024 at 5:10 PM Michael Paquier <michael@paquier.xyz> wrote:

that gets also used by pgstat_bestart() in
the case of the patch where !pre_auth?

To clarify, do you want me to just add the new boolean directly to
pgstat_bestart()'s parameter list?

No. My question was about splitting pgstat_bestart() and
pgstat_bestart_pre_auth() in a cleaner way, because authenticated
connections finish by calling both, meaning that we do twice the same
setup for backend entries depending on the authentication path taken.
That seems like a waste.

The addition of the new wait event states in 0004 is a good idea,
indeed,

Thanks! Any thoughts on the two open questions for it?:
1) Should we add a new wait event class rather than reusing IPC?

A new category would be more adapted. IPC is not adapted because are
not waiting for another server process. Perhaps just use a new
"Authentication" class, as in "The server is waiting for an
authentication operation to complete"?

2) Is the level at which I've inserted calls to
pgstat_report_wait_start()/_end() sane and maintainable?

These don't worry me. You are adding twelve event points with only 5
new wait names. Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

I am not really on board with the test based on injection points
proposed, though. It checks that the "authenticating" flag is set in
pg_stat_activity, but it does nothing else. That seems limited. Or
are you planning for more?
--
Michael

#10Noah Misch
noah@leadboat.com
In reply to: Michael Paquier (#9)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 10, 2024 at 02:29:57PM +0900, Michael Paquier wrote:

You are adding twelve event points with only 5
new wait names. Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

No, I think the patch's level of detail is better. One shouldn't expect the
two ldap_simple_bind_s() calls to have different-enough performance
characteristics to justify exposing that level of detail to the DBA.
ldap_search_s() and InitializeLDAPConnection() differ more, but the DBA mostly
just needs to know the scale of their LDAP responsiveness problem.

(Someday, it might be good to expose the file:line and/or backtrace associated
with a wait, like we do for ereport(). As a way to satisfy rare needs for
more detail, I'd prefer that over giving every pgstat_report_wait_start() a
different name.)

#11Robert Haas
robertmhaas@gmail.com
In reply to: Noah Misch (#10)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 10, 2024 at 1:27 PM Noah Misch <noah@leadboat.com> wrote:

On Tue, Sep 10, 2024 at 02:29:57PM +0900, Michael Paquier wrote:

You are adding twelve event points with only 5
new wait names. Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

No, I think the patch's level of detail is better. One shouldn't expect the
two ldap_simple_bind_s() calls to have different-enough performance
characteristics to justify exposing that level of detail to the DBA.
ldap_search_s() and InitializeLDAPConnection() differ more, but the DBA mostly
just needs to know the scale of their LDAP responsiveness problem.

(Someday, it might be good to expose the file:line and/or backtrace associated
with a wait, like we do for ereport(). As a way to satisfy rare needs for
more detail, I'd prefer that over giving every pgstat_report_wait_start() a
different name.)

I think unique names are a good idea. If a user doesn't care about the
difference between sdgjsA and sdjgsB, they can easily ignore the
trailing suffix, and IME, people typically do that without really
stopping to think about it. If on the other hand the two are lumped
together as sdjgs and a user needs to distinguish them, they can't. So
I see unique names as having much more upside than downside.

--
Robert Haas
EDB: http://www.enterprisedb.com

#12Noah Misch
noah@leadboat.com
In reply to: Robert Haas (#11)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 10, 2024 at 02:51:23PM -0400, Robert Haas wrote:

On Tue, Sep 10, 2024 at 1:27 PM Noah Misch <noah@leadboat.com> wrote:

On Tue, Sep 10, 2024 at 02:29:57PM +0900, Michael Paquier wrote:

You are adding twelve event points with only 5
new wait names. Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

No, I think the patch's level of detail is better. One shouldn't expect the
two ldap_simple_bind_s() calls to have different-enough performance
characteristics to justify exposing that level of detail to the DBA.
ldap_search_s() and InitializeLDAPConnection() differ more, but the DBA mostly
just needs to know the scale of their LDAP responsiveness problem.

(Someday, it might be good to expose the file:line and/or backtrace associated
with a wait, like we do for ereport(). As a way to satisfy rare needs for
more detail, I'd prefer that over giving every pgstat_report_wait_start() a
different name.)

I think unique names are a good idea. If a user doesn't care about the
difference between sdgjsA and sdjgsB, they can easily ignore the
trailing suffix, and IME, people typically do that without really
stopping to think about it. If on the other hand the two are lumped
together as sdjgs and a user needs to distinguish them, they can't. So
I see unique names as having much more upside than downside.

I agree a person can ignore the distinction, but that requires the person to
be consuming the raw event list. It's reasonable to tell your monitoring tool
to give you the top N wait events. Individual AuthnLdap* events may all miss
the cut even though their aggregate would have made the cut. Before you know
to teach that monitoring tool to group AuthnLdap* together, it won't show you
any of those names.

I felt commit c789f0f also chose sub-optimally in this respect, particularly
with the DblinkGetConnect/DblinkConnect pair. I didn't feel strongly enough
to complain at the time, but a rule of "each wait event appears in one
pgstat_report_wait_start()" would be a rule I don't want. One needs
familiarity with the dblink implementation internals to grasp the
DblinkGetConnect/DblinkConnect distinction, and a plausible refactor of dblink
would make those names cease to fit. I see this level of fine-grained naming
as making the event name a sort of stable proxy for FILE:LINE. I'd value
exposing such a proxy, all else being equal, but I don't think wait event
names like AuthLdapBindLdapbinddn/AuthLdapBindUser are the right way. Wait
event names should be more independent of today's code-level details.

#13Michael Paquier
michael@paquier.xyz
In reply to: Noah Misch (#12)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 10, 2024 at 01:58:50PM -0700, Noah Misch wrote:

On Tue, Sep 10, 2024 at 02:51:23PM -0400, Robert Haas wrote:

I think unique names are a good idea. If a user doesn't care about the
difference between sdgjsA and sdjgsB, they can easily ignore the
trailing suffix, and IME, people typically do that without really
stopping to think about it. If on the other hand the two are lumped
together as sdjgs and a user needs to distinguish them, they can't. So
I see unique names as having much more upside than downside.

I agree a person can ignore the distinction, but that requires the person to
be consuming the raw event list. It's reasonable to tell your monitoring tool
to give you the top N wait events. Individual AuthnLdap* events may all miss
the cut even though their aggregate would have made the cut. Before you know
to teach that monitoring tool to group AuthnLdap* together, it won't show you
any of those names.

That's a fair point. I use a bunch of aggregates with group bys for
any monitoring queries looking for event point patterns. In my
experience, when dealing with enough connections, patterns show up
anyway even if there is noise because some of the events that I was
looking for are rather short-term, like a sync events interleaving
with locks storing an average of the events into a secondary table
with some INSERT SELECT.

I felt commit c789f0f also chose sub-optimally in this respect, particularly
with the DblinkGetConnect/DblinkConnect pair. I didn't feel strongly enough
to complain at the time, but a rule of "each wait event appears in one
pgstat_report_wait_start()" would be a rule I don't want. One needs
familiarity with the dblink implementation internals to grasp the
DblinkGetConnect/DblinkConnect distinction, and a plausible refactor of dblink
would make those names cease to fit. I see this level of fine-grained naming
as making the event name a sort of stable proxy for FILE:LINE. I'd value
exposing such a proxy, all else being equal, but I don't think wait event
names like AuthLdapBindLdapbinddn/AuthLdapBindUser are the right way. Wait
event names should be more independent of today's code-level details.

Depends. I'd rather choose more granularity to know exactly which
part of the code I am dealing with, especially in the case of this
thread where these are embedded around external function calls. If,
for example, one notices that a stack of pg_stat_activity scans are
complaining about a specific step in the authentication process, it is
going to offer a much better hint than having to guess which part of
the authentication step is slow, like in LDAP.

Wait event additions are also kind of cheap in terms of maintenance in
core, creating a new translation cost. So I also think there are more
upsides to be wilder here with more points and more granularity.
--
Michael

#14Robert Haas
robertmhaas@gmail.com
In reply to: Noah Misch (#12)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Sep 10, 2024 at 4:58 PM Noah Misch <noah@leadboat.com> wrote:

... a rule of "each wait event appears in one
pgstat_report_wait_start()" would be a rule I don't want.

As the original committer of the wait event stuff, I intended for the
rule that you do not want to be the actual rule. However, I see that I
didn't spell that out anywhere in the commit message, or the commit
itself.

I see this level of fine-grained naming
as making the event name a sort of stable proxy for FILE:LINE. I'd value
exposing such a proxy, all else being equal, but I don't think wait event
names like AuthLdapBindLdapbinddn/AuthLdapBindUser are the right way. Wait
event names should be more independent of today's code-level details.

I don't agree with that. One of the most difficult parts of supporting
PostgreSQL, in my experience, is that it's often very difficult to
find out what has gone wrong when a system starts behaving badly. It
is often necessary to ask customers to install a debugger and do stuff
with it, or give them an instrumented build, in order to determine the
root cause of a problem that in some cases is not even particularly
complicated. While needing to refer to specific source code details
may not be a common experience for the typical end user, it is
extremely common for me. This problem commonly arises with error
messages, because we have lots of error messages that are exactly the
same, although thankfully it has become less common due to "could not
find tuple for THINGY %u" no longer being a message that no longer
typically reaches users. But even when someone has a complaint about
an error message and there are multiple instances of that error
message, I know that:

(1) I can ask them to set the error verbosity to verbose. I don't have
that option for wait events.

(2) The primary function of the error message is to be understandable
to the user, which means that it needs to be written in plain English.
The primary function of a wait event is to make it possible to
understand the behavior of the system and troubleshoot problems, and
it becomes much less effective as soon as it starts saying that thing
A and thing B are so similar that nobody will ever care about the
distinction. It is very hard to be certain of that. When somebody
reports that they've got a whole bunch of wait events on some wait
event that nobody has ever complained about before, I want to go look
at the code in that specific place and try to figure out what's
happening. If I have to start imagining possible scenarios based on 2
or more call sites, or if I have to start by getting them to install a
modified build with those properly split apart and trying to reproduce
the problem, it's a lot harder.

In my experience, the number of distinct wait events that a particular
installation experiences is rarely very large. It is probably measured
in dozens. A user who wishes to disregard the distinction between
similarly-named wait events won't find it prohibitively difficult to
look over the list of all the wait events they ever see and decide
which ones they'd like to merge for reporting purposes. But a user who
really needs things separated out and finds that they aren't is simply
out of luck.

--
Robert Haas
EDB: http://www.enterprisedb.com

#15Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#9)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Sep 9, 2024 at 10:30 PM Michael Paquier <michael@paquier.xyz> wrote:

No. My question was about splitting pgstat_bestart() and
pgstat_bestart_pre_auth() in a cleaner way, because authenticated
connections finish by calling both, meaning that we do twice the same
setup for backend entries depending on the authentication path taken.
That seems like a waste.

I can try to separate them out. I'm a little wary of messing with the
CRITICAL_SECTION guarantees, though. I thought the idea was that you
filled in the entire struct to prevent tearing. (If I've misunderstood
that, please let me know :D)

Perhaps just use a new
"Authentication" class, as in "The server is waiting for an
authentication operation to complete"?

Sounds good.

Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

(I have no strong opinions on this myself, but while the debate is
ongoing, I'll work on a version of the patch with more detailed wait
events. It's easy to collapse them again if that gets the most votes.)

I am not really on board with the test based on injection points
proposed, though. It checks that the "authenticating" flag is set in
pg_stat_activity, but it does nothing else. That seems limited. Or
are you planning for more?

I can test for specific contents of the entry, if you'd like. My
primary goal was to test that an entry shows up if that part of the
code hangs. I think a regression would otherwise go completely
unnoticed.

Thanks!
--Jacob

#16Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#15)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Sep 11, 2024 at 02:29:49PM -0700, Jacob Champion wrote:

On Mon, Sep 9, 2024 at 10:30 PM Michael Paquier <michael@paquier.xyz> wrote:

No. My question was about splitting pgstat_bestart() and
pgstat_bestart_pre_auth() in a cleaner way, because authenticated
connections finish by calling both, meaning that we do twice the same
setup for backend entries depending on the authentication path taken.
That seems like a waste.

I can try to separate them out. I'm a little wary of messing with the
CRITICAL_SECTION guarantees, though. I thought the idea was that you
filled in the entire struct to prevent tearing. (If I've misunderstood
that, please let me know :D)

Hm, yeah. We surely should be careful about the consequences of that.
Setting up twice the structure as the patch proposes is kind of
a weird concept, but it feels to me that we should split that and set
the fields in the pre-auth step and ignore the irrelevant ones, then
complete the rest in a second step. We are going to do that anyway if
we want to be able to have backend entries earlier in the
authentication phase.

Couldn't it be better to have a one-one mapping
instead, adding twelve entries in wait_event_names.txt?

(I have no strong opinions on this myself, but while the debate is
ongoing, I'll work on a version of the patch with more detailed wait
events. It's easy to collapse them again if that gets the most votes.)

Thanks. Robert is arguing upthread about more granularity, which is
also what I understand is the original intention of the wait events.
Noah has a different view. Let's see where it goes but I've given my
opinion.

I can test for specific contents of the entry, if you'd like. My
primary goal was to test that an entry shows up if that part of the
code hangs. I think a regression would otherwise go completely
unnoticed.

Perhaps that would be useful, not sure. Based on my first
impressions, I'd tend to say no to these extra test cycles, but I'm
okay to be proved wrong, as well.
--
Michael

#17Noah Misch
noah@leadboat.com
In reply to: Robert Haas (#14)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Sep 11, 2024 at 09:00:33AM -0400, Robert Haas wrote:

On Tue, Sep 10, 2024 at 4:58 PM Noah Misch <noah@leadboat.com> wrote:

... a rule of "each wait event appears in one
pgstat_report_wait_start()" would be a rule I don't want.

As the original committer of the wait event stuff, I intended for the
rule that you do not want to be the actual rule. However, I see that I
didn't spell that out anywhere in the commit message, or the commit
itself.

I see this level of fine-grained naming
as making the event name a sort of stable proxy for FILE:LINE. I'd value
exposing such a proxy, all else being equal, but I don't think wait event
names like AuthLdapBindLdapbinddn/AuthLdapBindUser are the right way. Wait
event names should be more independent of today's code-level details.

I don't agree with that. One of the most difficult parts of supporting
PostgreSQL, in my experience, is that it's often very difficult to
find out what has gone wrong when a system starts behaving badly. It
is often necessary to ask customers to install a debugger and do stuff
with it, or give them an instrumented build, in order to determine the
root cause of a problem that in some cases is not even particularly
complicated. While needing to refer to specific source code details
may not be a common experience for the typical end user, it is
extremely common for me. This problem commonly arises with error
messages

That is a problem. Half the time, error verbosity doesn't disambiguate enough
for me, and I need backtrace_functions. I now find it hard to believe how
long we coped without backtrace_functions.

I withdraw the objection to "each wait event appears in one
pgstat_report_wait_start()".

#18Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Noah Misch (#17)
5 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi all,

Here's a v4, with a separate wait event for each location. (I could
use some eyes on the specific phrasing I've chosen for each one.)

On Sun, Sep 1, 2024 at 5:10 PM Michael Paquier <michael@paquier.xyz> wrote:

Could it be more transparent to use a "startup" or
"starting"" state instead that gets also used by pgstat_bestart() in
the case of the patch where !pre_auth?

Done. (I've used "starting".)

On Mon, Sep 9, 2024 at 10:30 PM Michael Paquier <michael@paquier.xyz> wrote:

A new category would be more adapted. IPC is not adapted because are
not waiting for another server process. Perhaps just use a new
"Authentication" class, as in "The server is waiting for an
authentication operation to complete"?

Added a new "Auth" class (to cover both authn and authz during
startup), plus documentation.

On Wed, Sep 11, 2024 at 4:42 PM Michael Paquier <michael@paquier.xyz> wrote:

Setting up twice the structure as the patch proposes is kind of
a weird concept, but it feels to me that we should split that and set
the fields in the pre-auth step and ignore the irrelevant ones, then
complete the rest in a second step.

The more I look at this, the more uneasy I feel about the goal. Best I
can tell, the pre-auth step can't ignore irrelevant fields, because
they may contain junk from the previous owner of the shared memory. So
if we want to optimize, we can only change the second step to skip
fields that were already filled in by the pre-auth step.

That has its own problems: not every backend type uses the pre-auth
step in the current patch. Which means a bunch of backends that don't
benefit from the two-step initialization nevertheless have to either
do two PGSTAT_BEGIN_WRITE_ACTIVITY() dances in a row, or else we
duplicate a bunch of the logic to make sure they maintain the same
efficient code path as before.

Finally, if we're okay with all of that, future maintainers need to be
careful about which fields get copied in the first (preauth) step, the
second step, or both. GSS, for example, can be set up during transport
negotiation (first step) or authentication (second step), so we have
to duplicate the logic there. SSL is currently first-step-only, I
think -- but are we sure we want to hardcode the assumption that cert
auth can't change any of those parameters after the transport has been
established? (I've been brainstorming ways we might use TLS 1.3's
post-handshake CertificateRequest, for example.)

So before I commit to this path, I just want to double-check that all
of the above sounds good and non-controversial. :)

--

In the meantime, is anyone willing and able to commit 0001 and/or 0002?

Thanks!
--Jacob

Attachments:

since-v3.diff.txttext/plain; charset=US-ASCII; name=since-v3.diff.txtDownload
1:  8f9315949e = 1:  64289b97e5 BackgroundPsql: handle empty query results
2:  7f404f5ee8 ! 2:  18a9531a25 Test::Cluster: let background_psql() work asynchronously
    @@ src/test/perl/PostgreSQL/Test/Cluster.pm: connection.
      
      =cut
     @@ src/test/perl/PostgreSQL/Test/Cluster.pm: sub background_psql
    - 		'-');
    + 		'-XAtq', '-d', $psql_connstr, '-f', '-');
      
      	$params{on_error_stop} = 1 unless defined $params{on_error_stop};
     +	$params{wait} = 1 unless defined $params{wait};
3:  a7a7551d09 ! 3:  c8071f91d8 pgstat: report in earlier with STATE_AUTHENTICATING
    @@ Metadata
     Author: Jacob Champion <jacob.champion@enterprisedb.com>
     
      ## Commit message ##
    -    pgstat: report in earlier with STATE_AUTHENTICATING
    +    pgstat: report in earlier with STATE_STARTING
     
    -    Add pgstat_bestart_pre_auth(), which reports an 'authenticating' state
    -    while waiting for backend initialization and client authentication to
    +    Add pgstat_bestart_pre_auth(), which reports a 'starting' state while
    +    waiting for backend initialization and client authentication to
         complete. Since we hold a transaction open for a good amount of that,
         and some authentication methods call out to external systems, having a
         pg_stat_activity entry helps DBAs debug when things go badly wrong.
     
    + ## doc/src/sgml/monitoring.sgml ##
    +@@ doc/src/sgml/monitoring.sgml: 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.
    +
      ## src/backend/utils/activity/backend_status.c ##
     @@ src/backend/utils/activity/backend_status.c: static int	localNumBackends = 0;
      static MemoryContext backendStatusSnapContext;
    @@ src/backend/utils/activity/backend_status.c: pgstat_bestart(void)
      #endif
      
     -	lbeentry.st_state = STATE_UNDEFINED;
    -+	lbeentry.st_state = pre_auth ? STATE_AUTHENTICATING : STATE_UNDEFINED;
    ++	lbeentry.st_state = pre_auth ? STATE_STARTING : STATE_UNDEFINED;
      	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
      	lbeentry.st_progress_command_target = InvalidOid;
      	lbeentry.st_query_id = UINT64CONST(0);
    @@ src/backend/utils/adt/pgstatfuncs.c: pg_stat_get_activity(PG_FUNCTION_ARGS)
      
      			switch (beentry->st_state)
      			{
    -+				case STATE_AUTHENTICATING:
    -+					values[4] = CStringGetTextDatum("authenticating");
    ++				case STATE_STARTING:
    ++					values[4] = CStringGetTextDatum("starting");
     +					break;
      				case STATE_IDLE:
      					values[4] = CStringGetTextDatum("idle");
    @@ src/backend/utils/init/postinit.c: InitPostgres(const char *in_dbname, Oid dboid
     +	pgstat_beinit();
     +
     +	/*
    -+	 * This is a convenient time to sketch in a partial pgstat entry. That way,
    -+	 * if LWLocks or third-party authentication should happen to hang, the DBA
    -+	 * will still be able to see what's going on. (A later call to
    ++	 * This is a convenient time to sketch in a partial pgstat entry. That
    ++	 * way, if LWLocks or third-party authentication should happen to hang,
    ++	 * the DBA will still be able to see what's going on. (A later call to
     +	 * pgstat_bestart() will fill in the rest of the status.)
     +	 */
     +	if (!bootstrap)
    @@ src/include/utils/backend_status.h
      typedef enum BackendState
      {
      	STATE_UNDEFINED,
    -+	STATE_AUTHENTICATING,
    ++	STATE_STARTING,
      	STATE_IDLE,
      	STATE_RUNNING,
      	STATE_IDLEINTRANSACTION,
    @@ src/test/authentication/t/007_injection_points.pl (new)
     +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 in authentication. Use the
    -+# $psql connection handle for server interaction.
    ++# 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.
    @@ src/test/authentication/t/007_injection_points.pl (new)
     +while (1)
     +{
     +	$pid = $psql->query(
    -+		"SELECT pid FROM pg_stat_activity WHERE state = 'authenticating';");
    ++		"SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
     +	last if $pid ne "";
     +
     +	usleep(500_000);
    @@ src/test/authentication/t/007_injection_points.pl (new)
     +# Make sure the pgstat entry is updated eventually.
     +while (1)
     +{
    -+	my $state = $psql->query(
    -+		"SELECT state FROM pg_stat_activity WHERE pid = $pid;");
    ++	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'...";
4:  5c85c3e0e9 ! 4:  d14b97cb77 WIP: report external auth calls as wait events
    @@ Metadata
     Author: Jacob Champion <jacob.champion@enterprisedb.com>
     
      ## Commit message ##
    -    WIP: report external auth calls as wait events
    +    Report external auth calls as wait events
     
    -    Introduce new WAIT_EVENT_AUTHN_* types for various external
    -    authentication systems, to make it obvious what's going wrong if one of
    -    those systems hangs.
    +    Introduce a new "Auth" wait class for various external authentication
    +    systems, to make it obvious what's going wrong if one of those systems
    +    hangs. Each new wait event is unique in order to more easily pinpoint
    +    problematic locations in the code.
     
    -    TODO:
    -    - don't abuse the IPC wait event group like this
    -    - test
    +    Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
    +
    + ## doc/src/sgml/monitoring.sgml ##
    +@@ doc/src/sgml/monitoring.sgml: postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
    +        see <xref linkend="wait-event-activity-table"/>.
    +       </entry>
    +      </row>
    ++     <row>
    ++      <entry><literal>Auth</literal></entry>
    ++      <entry>The server process is waiting for an external system to
    ++       authenticate and/or authorize the client connection.
    ++       <literal>wait_event</literal> will identify the specific wait point;
    ++       see <xref linkend="wait-event-auth-table"/>.
    ++      </entry>
    ++     </row>
    +      <row>
    +       <entry><literal>BufferPin</literal></entry>
    +       <entry>The server process is waiting for exclusive access to
     
      ## src/backend/libpq/auth.c ##
     @@
    @@ src/backend/libpq/auth.c: pg_GSS_recvauth(Port *port)
      		elog(DEBUG4, "processing received GSS token of length %u",
      			 (unsigned int) gbuf.length);
      
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_GSSAPI);
    ++		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
      		maj_stat = gss_accept_sec_context(&min_stat,
      										  &port->gss->ctx,
      										  port->gss->cred,
    @@ src/backend/libpq/auth.c: pg_SSPI_recvauth(Port *port)
      	/*
      	 * Acquire a handle to the server credentials.
      	 */
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
    ++	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
      	r = AcquireCredentialsHandle(NULL,
      								 "negotiate",
      								 SECPKG_CRED_INBOUND,
    @@ src/backend/libpq/auth.c: pg_SSPI_recvauth(Port *port)
      		elog(DEBUG4, "processing received SSPI token of length %u",
      			 (unsigned int) buf.len);
      
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
    ++		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
      		r = AcceptSecurityContext(&sspicred,
      								  sspictx,
      								  &inbuf,
    @@ src/backend/libpq/auth.c: pg_SSPI_recvauth(Port *port)
      
      	CloseHandle(token);
      
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
    ++	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
      	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
      						  domainname, &domainnamesize, &accountnameuse))
      		ereport(ERROR,
    @@ src/backend/libpq/auth.c: pg_SSPI_recvauth(Port *port)
     -											  port->hba->upn_username);
     +		int			status;
     +
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_SSPI);
    ++		pgstat_report_wait_start(WAIT_EVENT_SSPI_MAKE_UPN);
     +		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
     +								  domainname, sizeof(domainname),
     +								  port->hba->upn_username);
    @@ src/backend/libpq/auth.c: CheckPAMAuth(Port *port, const char *user, const char
      		return STATUS_ERROR;
      	}
      
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
    ++	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
      	retval = pam_authenticate(pamh, 0);
     +	pgstat_report_wait_end();
      
    @@ src/backend/libpq/auth.c: CheckPAMAuth(Port *port, const char *user, const char
      		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
      	}
      
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_PAM);
    ++	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
      	retval = pam_acct_mgmt(pamh, 0);
     +	pgstat_report_wait_end();
      
    @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      		return STATUS_EOF;		/* client wouldn't send password */
      
     -	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
    ++	pgstat_report_wait_start(WAIT_EVENT_LDAP_INITIALIZE);
     +	r = InitializeLDAPConnection(port, &ldap);
     +	pgstat_report_wait_end();
     +
    @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      		 * Bind with a pre-defined username/password (if available) for
      		 * searching. If none is specified, this turns into an anonymous bind.
      		 */
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
    ++		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
      		r = ldap_simple_bind_s(ldap,
      							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
      							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
    @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      
      		search_message = NULL;
     +
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
    ++		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
      		r = ldap_search_s(ldap,
      						  port->hba->ldapbasedn,
      						  port->hba->ldapscope,
    @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      							port->user_name,
      							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
      
    -+	pgstat_report_wait_start(WAIT_EVENT_AUTHN_LDAP);
    ++	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
      	r = ldap_simple_bind_s(ldap, fulluser, passwd);
     +	pgstat_report_wait_end();
      
    @@ src/backend/libpq/auth.c: CheckRADIUSAuth(Port *port)
     -												   passwd);
     +		int			ret;
     +
    -+		pgstat_report_wait_start(WAIT_EVENT_AUTHN_RADIUS);
    ++		pgstat_report_wait_start(WAIT_EVENT_RADIUS_TRANSACTION);
     +		ret = PerformRadiusTransaction(lfirst(server),
     +									   lfirst(secrets),
     +									   radiusports ? lfirst(radiusports) : NULL,
    @@ src/backend/libpq/auth.c: CheckRADIUSAuth(Port *port)
      		/*------
      		 * STATUS_OK = Login OK
     
    + ## src/backend/utils/activity/wait_event.c ##
    +@@ src/backend/utils/activity/wait_event.c: static const char *pgstat_get_wait_client(WaitEventClient w);
    + static const char *pgstat_get_wait_ipc(WaitEventIPC w);
    + static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
    + static const char *pgstat_get_wait_io(WaitEventIO w);
    ++static const char *pgstat_get_wait_auth(WaitEventAuth w);
    + 
    + 
    + static uint32 local_my_wait_event_info;
    +@@ src/backend/utils/activity/wait_event.c: pgstat_get_wait_event_type(uint32 wait_event_info)
    + 		case PG_WAIT_INJECTIONPOINT:
    + 			event_type = "InjectionPoint";
    + 			break;
    ++		case PG_WAIT_AUTH:
    ++			event_type = "Auth";
    ++			break;
    + 		default:
    + 			event_type = "???";
    + 			break;
    +@@ src/backend/utils/activity/wait_event.c: pgstat_get_wait_event(uint32 wait_event_info)
    + 				event_name = pgstat_get_wait_io(w);
    + 				break;
    + 			}
    ++		case PG_WAIT_AUTH:
    ++			{
    ++				WaitEventAuth w = (WaitEventAuth) wait_event_info;
    ++
    ++				event_name = pgstat_get_wait_auth(w);
    ++				break;
    ++			}
    + 		default:
    + 			event_name = "unknown wait event";
    + 			break;
    +
      ## src/backend/utils/activity/wait_event_names.txt ##
    -@@ src/backend/utils/activity/wait_event_names.txt: Section: ClassName - WaitEventIPC
    - APPEND_READY	"Waiting for subplan nodes of an <literal>Append</literal> plan node to be ready."
    - ARCHIVE_CLEANUP_COMMAND	"Waiting for <xref linkend="guc-archive-cleanup-command"/> to complete."
    - ARCHIVE_COMMAND	"Waiting for <xref linkend="guc-archive-command"/> to complete."
    -+AUTHN_GSSAPI	"Waiting for a response from a Kerberos server via GSSAPI."
    -+AUTHN_LDAP	"Waiting for a response from an LDAP server."
    -+AUTHN_PAM	"Waiting for a response from the local PAM service."
    -+AUTHN_RADIUS	"Waiting for a response from a RADIUS server."
    -+AUTHN_SSPI	"Waiting for a response from a Windows security provider via SSPI."
    - BACKEND_TERMINATION	"Waiting for the termination of another backend."
    - BACKUP_WAIT_WAL_ARCHIVE	"Waiting for WAL files required for a backup to be successfully archived."
    - BGWORKER_SHUTDOWN	"Waiting for background worker to shut down."
    +@@ src/backend/utils/activity/wait_event_names.txt: XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
    + 
    + ABI_compatibility:
    + 
    ++#
    ++# Wait Events - Auth
    ++#
    ++# Use this category when a process is waiting for a third party to
    ++# authenticate/authorize the user.
    ++#
    ++
    ++Section: ClassName - WaitEventAuth
    ++
    ++GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
    ++LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
    ++LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
    ++LDAP_INITIALIZE	"Waiting to initialize an LDAP connection."
    ++LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
    ++PAM_ACCT_MGMT	"Waiting for the local PAM service to validate the user account."
    ++PAM_AUTHENTICATE	"Waiting for the local PAM service to authenticate the user."
    ++RADIUS_TRANSACTION	"Waiting for a RADIUS transaction to complete."
    ++SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
    ++SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
    ++SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's account SID."
    ++SSPI_MAKE_UPN	"Waiting for Windows to translate a Kerberos UPN."
    ++
    ++ABI_compatibility:
    ++
    + #
    + # Wait Events - Timeout
    + #
    +
    + ## src/include/utils/wait_event.h ##
    +@@
    + #define PG_WAIT_TIMEOUT				0x09000000U
    + #define PG_WAIT_IO					0x0A000000U
    + #define PG_WAIT_INJECTIONPOINT		0x0B000000U
    ++#define PG_WAIT_AUTH				0x0C000000U
    + 
    + /* enums for wait events */
    + #include "utils/wait_event_types.h"
    +
    + ## src/test/regress/expected/sysviews.out ##
    +@@ src/test/regress/expected/sysviews.out: select type, count(*) > 0 as ok FROM pg_wait_events
    +    type    | ok 
    + -----------+----
    +  Activity  | t
    ++ Auth      | t
    +  BufferPin | t
    +  Client    | t
    +  Extension | t
    +@@ src/test/regress/expected/sysviews.out: select type, count(*) > 0 as ok FROM pg_wait_events
    +  LWLock    | t
    +  Lock      | t
    +  Timeout   | t
    +-(9 rows)
    ++(10 rows)
    + 
    + -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
    + -- more-or-less working.  We can't test their contents in any great detail
v4-0001-BackgroundPsql-handle-empty-query-results.patchapplication/octet-stream; name=v4-0001-BackgroundPsql-handle-empty-query-results.patchDownload
From 64289b97e571fe660be57273efec360ba11d96ff Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:11:56 -0700
Subject: [PATCH v4 1/4] BackgroundPsql: handle empty query results

There won't be a newline at the end of an empty query result. (Before
this fix, the $banner showed up in the result, leading to confusing
debugging sessions.)

recovery/t/037_invalid_database was relying on the non-empty query
results, so I have switched those cases to use "bare" calls to
query_safe() instead.
---
 src/test/perl/PostgreSQL/Test/BackgroundPsql.pm |  2 +-
 src/test/recovery/t/037_invalid_database.pl     | 12 ++++--------
 2 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 3c2aca1c5d..2760e4bc8d 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -223,7 +223,7 @@ sub query
 	$output = $self->{stdout};
 
 	# remove banner again, our caller doesn't care
-	$output =~ s/\n$banner\n$//s;
+	$output =~ s/\n?$banner\n$//s;
 
 	# clear out output for the next query
 	$self->{stdout} = '';
diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl
index 6d1c711796..e16a3616b2 100644
--- a/src/test/recovery/t/037_invalid_database.pl
+++ b/src/test/recovery/t/037_invalid_database.pl
@@ -96,13 +96,12 @@ my $bgpsql = $node->background_psql('postgres', on_error_stop => 0);
 my $pid = $bgpsql->query('SELECT pg_backend_pid()');
 
 # create the database, prevent drop database via lock held by a 2PC transaction
-ok( $bgpsql->query_safe(
+$bgpsql->query_safe(
 		qq(
   CREATE DATABASE regression_invalid_interrupt;
   BEGIN;
   LOCK pg_tablespace;
-  PREPARE TRANSACTION 'lock_tblspc';)),
-	"blocked DROP DATABASE completion");
+  PREPARE TRANSACTION 'lock_tblspc';));
 
 # Try to drop. This will wait due to the still held lock.
 $bgpsql->query_until(qr//, "DROP DATABASE regression_invalid_interrupt;\n");
@@ -135,11 +134,8 @@ is($node->psql('regression_invalid_interrupt', ''),
 
 # To properly drop the database, we need to release the lock previously preventing
 # doing so.
-ok($bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc')),
-	"unblock DROP DATABASE");
-
-ok($bgpsql->query(qq(DROP DATABASE regression_invalid_interrupt)),
-	"DROP DATABASE invalid_interrupt");
+$bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc'));
+$bgpsql->query_safe(qq(DROP DATABASE regression_invalid_interrupt));
 
 $bgpsql->quit();
 
-- 
2.34.1

v4-0002-Test-Cluster-let-background_psql-work-asynchronou.patchapplication/octet-stream; name=v4-0002-Test-Cluster-let-background_psql-work-asynchronou.patchDownload
From 18a9531a25ad27eebed2f800346f839f8c8d4e72 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Mon, 8 Jul 2024 10:46:55 -0700
Subject: [PATCH v4 2/4] Test::Cluster: let background_psql() work
 asynchronously

Specifying `wait => 0` as a parameter to background_psql() causes it to
return immediately, which lets the client run code during connection.
(This is useful if, for example, connections are blocked on injected
waitpoints.) Clients later call ->wait_connect() manually to complete
the asynchronous connection.
---
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    | 23 ++++++++++++++-----
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 10 +++++++-
 2 files changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 2760e4bc8d..13489ee95e 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -81,7 +81,7 @@ string. For C<interactive> sessions, IO::Pty is required.
 sub new
 {
 	my $class = shift;
-	my ($interactive, $psql_params, $timeout) = @_;
+	my ($interactive, $psql_params, $timeout, $wait) = @_;
 	my $psql = {
 		'stdin' => '',
 		'stdout' => '',
@@ -119,14 +119,25 @@ sub new
 
 	my $self = bless $psql, $class;
 
-	$self->_wait_connect();
+	$wait = 1 unless defined($wait);
+	if ($wait)
+	{
+		$self->wait_connect();
+	}
 
 	return $self;
 }
 
-# Internal routine for awaiting psql starting up and being ready to consume
-# input.
-sub _wait_connect
+=pod
+
+=item $session->wait_connect
+
+Returns once psql has started up and is ready to consume input.  This is called
+automatically for clients unless requested otherwise in the constructor.
+
+=cut
+
+sub wait_connect
 {
 	my ($self) = @_;
 
@@ -187,7 +198,7 @@ sub reconnect_and_clear
 	$self->{stdin} = '';
 	$self->{stdout} = '';
 
-	$self->_wait_connect();
+	$self->wait_connect();
 }
 
 =pod
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 007571e948..aad09cc53e 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2286,6 +2286,12 @@ connection.
 
 If given, it must be an array reference containing additional parameters to B<psql>.
 
+=item wait => 1
+
+By default, this method will not return until connection has completed (or
+failed).  Set B<wait> to 0 to return immediately instead.  (Clients can call the
+session's C<wait_connect> method manually when needed.)
+
 =back
 
 =cut
@@ -2316,13 +2322,15 @@ sub background_psql
 		'-XAtq', '-d', $psql_connstr, '-f', '-');
 
 	$params{on_error_stop} = 1 unless defined $params{on_error_stop};
+	$params{wait} = 1 unless defined $params{wait};
 	$timeout = $params{timeout} if defined $params{timeout};
 
 	push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
 	push @psql_params, @{ $params{extra_params} }
 	  if defined $params{extra_params};
 
-	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout);
+	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params, $timeout,
+		$params{wait});
 }
 
 =pod
-- 
2.34.1

v4-0003-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v4-0003-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From c8071f91d81906f15eed739877ba75a99028713c Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v4 3/4] pgstat: report in earlier with STATE_STARTING

Add pgstat_bestart_pre_auth(), which reports a 'starting' state while
waiting for backend initialization and client authentication to
complete. Since we hold a transaction open for a good amount of that,
and some authentication methods call out to external systems, having a
pg_stat_activity entry helps DBAs debug when things go badly wrong.
---
 doc/src/sgml/monitoring.sgml                  |  6 ++
 src/backend/utils/activity/backend_status.c   | 37 +++++++++-
 src/backend/utils/adt/pgstatfuncs.c           |  3 +
 src/backend/utils/init/postinit.c             | 20 ++++-
 src/include/utils/backend_status.h            |  2 +
 src/test/authentication/Makefile              |  2 +
 src/test/authentication/meson.build           |  4 +
 .../authentication/t/007_injection_points.pl  | 73 +++++++++++++++++++
 8 files changed, 140 insertions(+), 7 deletions(-)
 create mode 100644 src/test/authentication/t/007_injection_points.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d3..81a4a95152 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index bdb3a296ca..d71d7c1b4f 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -69,6 +69,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -269,6 +270,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -318,9 +347,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -375,7 +404,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_STARTING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f7b50e0b5a..c461bbd400 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d..adaa83e745 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -714,6 +715,21 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's going on. (A later call to
+	 * pgstat_bestart() will fill in the rest of the status.)
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_pre_auth();
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -782,9 +798,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.
@@ -882,6 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 97874300c3..8a6d573ce3 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_STARTING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void BackendStatusShmemInit(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..7bbc3b6e57 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -13,6 +13,8 @@ subdir = src/test/authentication
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+export enable_injection_points
+
 check:
 	$(prove_check)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..09bad8f2b8 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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_injection_points.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/007_injection_points.pl b/src/test/authentication/t/007_injection_points.pl
new file mode 100644
index 0000000000..a6176290a6
--- /dev/null
+++ b/src/test/authentication/t/007_injection_points.pl
@@ -0,0 +1,73 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Tests requiring injection_points functionality, to check on behavior that
+# would otherwise race against 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;
+$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(500_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(500_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();
-- 
2.34.1

v4-0004-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v4-0004-Report-external-auth-calls-as-wait-events.patchDownload
From d14b97cb7737e32d9d809794a6671b47788879c0 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v4 4/4] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 54 +++++++++++++++----
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 24 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 90 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 81a4a95152..a148e63711 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 47e8c91606..bbcde591ae 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,19 +1410,25 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
 	if (!port->hba->compat_realm)
 	{
-		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
-											  domainname, sizeof(domainname),
-											  port->hba->upn_username);
+		int			status;
+
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_MAKE_UPN);
+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
+								  domainname, sizeof(domainname),
+								  port->hba->upn_username);
+		pgstat_report_wait_end();
 
 		if (status != STATUS_OK)
 			/* Error already reported from pg_SSPI_make_upn */
@@ -2119,7 +2133,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2132,7 +2148,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2483,7 +2501,11 @@ CheckLDAPAuth(Port *port)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_INITIALIZE);
+	r = InitializeLDAPConnection(port, &ldap);
+	pgstat_report_wait_end();
+
+	if (r == STATUS_ERROR)
 	{
 		/* Error message already sent */
 		pfree(passwd);
@@ -2530,9 +2552,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2555,6 +2580,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2562,6 +2589,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2630,7 +2658,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -2890,12 +2920,16 @@ CheckRADIUSAuth(Port *port)
 	identifiers = list_head(port->hba->radiusidentifiers);
 	foreach(server, port->hba->radiusservers)
 	{
-		int			ret = PerformRadiusTransaction(lfirst(server),
-												   lfirst(secrets),
-												   radiusports ? lfirst(radiusports) : NULL,
-												   identifiers ? lfirst(identifiers) : NULL,
-												   port->user_name,
-												   passwd);
+		int			ret;
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_TRANSACTION);
+		ret = PerformRadiusTransaction(lfirst(server),
+									   lfirst(secrets),
+									   radiusports ? lfirst(radiusports) : NULL,
+									   identifiers ? lfirst(identifiers) : NULL,
+									   port->user_name,
+									   passwd);
+		pgstat_report_wait_end();
 
 		/*------
 		 * STATUS_OK = Login OK
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d930277140..a388999b1a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 8efb4044d6..e5c27a2d2c 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -162,6 +162,30 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_INITIALIZE	"Waiting to initialize an LDAP connection."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+PAM_ACCT_MGMT	"Waiting for the local PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the local PAM service to authenticate the user."
+RADIUS_TRANSACTION	"Waiting for a RADIUS transaction to complete."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's account SID."
+SSPI_MAKE_UPN	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 9f18a753d4..014d536441 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index fad7fc3a7e..030d1a8321 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -179,6 +179,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -187,7 +188,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

#19Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#18)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Nov 01, 2024 at 02:47:38PM -0700, Jacob Champion wrote:

On Sun, Sep 1, 2024 at 5:10 PM Michael Paquier <michael@paquier.xyz> wrote:

Could it be more transparent to use a "startup" or
"starting"" state instead that gets also used by pgstat_bestart() in
the case of the patch where !pre_auth?

Done. (I've used "starting".)

0003 looks much cleaner this way.

Added a new "Auth" class (to cover both authn and authz during
startup), plus documentation.

+PAM_ACCT_MGMT	"Waiting for the local PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the local PAM service to authenticate the user."

Is "local" required for both? Perhaps just use "the PAM service".

+SSPI_LOOKUP_ACCOUNT_SID "Waiting for Windows to find the user's account SID."

We don't document SID in doc/. So perhaps this should add be "SID
(system identifier)".

+SSPI_MAKE_UPN "Waiting for Windows to translate a Kerberos UPN."

UPN is mentioned once in doc/ already. Perhaps this one is OK left
alone..

Except for these tweaks 0004 looks OK.

The more I look at this, the more uneasy I feel about the goal. Best I
can tell, the pre-auth step can't ignore irrelevant fields, because
they may contain junk from the previous owner of the shared memory. So
if we want to optimize, we can only change the second step to skip
fields that were already filled in by the pre-auth step.

That has its own problems: not every backend type uses the pre-auth
step in the current patch. Which means a bunch of backends that don't
benefit from the two-step initialization nevertheless have to either
do two PGSTAT_BEGIN_WRITE_ACTIVITY() dances in a row, or else we
duplicate a bunch of the logic to make sure they maintain the same
efficient code path as before.

Finally, if we're okay with all of that, future maintainers need to be
careful about which fields get copied in the first (preauth) step, the
second step, or both. GSS, for example, can be set up during transport
negotiation (first step) or authentication (second step), so we have
to duplicate the logic there. SSL is currently first-step-only, I
think -- but are we sure we want to hardcode the assumption that cert
auth can't change any of those parameters after the transport has been
established? (I've been brainstorming ways we might use TLS 1.3's
post-handshake CertificateRequest, for example.)

The future field maintenance and what one would need to think more
about in the future is a good point. I still feel slightly uneasy
about the way 0003 is shaped with its new pgstat_bestart_pre_auth(),
but I think that I'm just going to put my hands down on 0003 and see
if I can finish with something I'm a bit more comfortable with. Let's
see..

So before I commit to this path, I just want to double-check that all
of the above sounds good and non-controversial. :)

The goal of the thread is sound.

I'm OK with 0002 to add the wait parameter to BackgroundPsql and be
able to take some actions until a manual wait_connect(). I'll go do
this one. Also perhaps 0001 while on it but I am a bit puzzled by the
removal of the three ok() calls in 037_invalid_database.pl.
--
Michael

#20Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#19)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Nov 06, 2024 at 02:48:31PM +0900, Michael Paquier wrote:

I'm OK with 0002 to add the wait parameter to BackgroundPsql and be
able to take some actions until a manual wait_connect(). I'll go do
this one. Also perhaps 0001 while on it but I am a bit puzzled by the
removal of the three ok() calls in 037_invalid_database.pl.

0002 has been done as ba08edb06545 after adding a bit more
documentation that was missing. 0001 as well with 70291a3c66ec. The
original expectation of 037_invalid_database.pl with the banner data
expected in the output was interesting..

Note that 0003 is lacking an EXTRA_INSTALL in the Makefile of
src/test/authentication/, or the test would fail if doing for example
a `make check` in this path.

The following nit is also required in the script for installcheck, to
skip the test if the module is not installed:
if (!$node->check_extension('injection_points'))
{
plan skip_all => 'Extension injection_points not installed';
}

See src/test/modules/test_misc/t/005_timeouts.pl as one example. (I
know, these are tricky to know about..)

007_injection_points.pl is a name too generic as it could apply in a
lot more places, without being linked to injection points. How about
something like 007_pre_auth.pl?
--
Michael

#21Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#20)
3 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Nov 5, 2024 at 9:48 PM Michael Paquier <michael@paquier.xyz> wrote:

+PAM_ACCT_MGMT  "Waiting for the local PAM service to validate the user account."
+PAM_AUTHENTICATE       "Waiting for the local PAM service to authenticate the user."

Is "local" required for both? Perhaps just use "the PAM service".

Done in v5.

+SSPI_LOOKUP_ACCOUNT_SID "Waiting for Windows to find the user's account SID."

We don't document SID in doc/. So perhaps this should add be "SID
(system identifier)".

I switched to "user's security identifier", which seems to be
search-engine-friendly.

On Wed, Nov 6, 2024 at 7:15 PM Michael Paquier <michael@paquier.xyz> wrote:

0002 has been done as ba08edb06545 after adding a bit more
documentation that was missing. 0001 as well with 70291a3c66ec.

Thanks!

Note that 0003 is lacking an EXTRA_INSTALL in the Makefile of
src/test/authentication/, or the test would fail if doing for example
a `make check` in this path.

The following nit is also required in the script for installcheck, to
skip the test if the module is not installed:
if (!$node->check_extension('injection_points'))
{
plan skip_all => 'Extension injection_points not installed';
}

Fixed.

007_injection_points.pl is a name too generic as it could apply in a
lot more places, without being linked to injection points. How about
something like 007_pre_auth.pl?

Renamed.

Thanks!
--Jacob

Attachments:

since-v4.diff.txttext/plain; charset=US-ASCII; name=since-v4.diff.txtDownload
1:  64289b97e5 < -:  ---------- BackgroundPsql: handle empty query results
2:  18a9531a25 < -:  ---------- Test::Cluster: let background_psql() work asynchronously
3:  c8071f91d8 ! 1:  e755fdccf1 pgstat: report in earlier with STATE_STARTING
    @@ src/test/authentication/Makefile: subdir = src/test/authentication
      top_builddir = ../../..
      include $(top_builddir)/src/Makefile.global
      
    ++EXTRA_INSTALL = src/test/modules/injection_points
    ++
     +export enable_injection_points
     +
      check:
    @@ src/test/authentication/meson.build: tests += {
            't/004_file_inclusion.pl',
            't/005_sspi.pl',
            't/006_login_trigger.pl',
    -+      't/007_injection_points.pl',
    ++      't/007_pre_auth.pl',
          ],
        },
      }
     
    - ## src/test/authentication/t/007_injection_points.pl (new) ##
    + ## src/test/authentication/t/007_pre_auth.pl (new) ##
     @@
     +
     +# Copyright (c) 2021-2024, PostgreSQL Global Development Group
     +
    -+# Tests requiring injection_points functionality, to check on behavior that
    -+# would otherwise race against authentication.
    ++# Tests for connection behavior prior to authentication.
     +
     +use strict;
     +use warnings FATAL => 'all';
    @@ src/test/authentication/t/007_injection_points.pl (new)
     +]);
     +
     +$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.
4:  d14b97cb77 ! 2:  858e95f996 Report external auth calls as wait events
    @@ src/backend/utils/activity/wait_event_names.txt: XACT_GROUP_UPDATE	"Waiting for
     +LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
     +LDAP_INITIALIZE	"Waiting to initialize an LDAP connection."
     +LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
    -+PAM_ACCT_MGMT	"Waiting for the local PAM service to validate the user account."
    -+PAM_AUTHENTICATE	"Waiting for the local PAM service to authenticate the user."
    ++PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
    ++PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
     +RADIUS_TRANSACTION	"Waiting for a RADIUS transaction to complete."
     +SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
     +SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
    -+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's account SID."
    ++SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
     +SSPI_MAKE_UPN	"Waiting for Windows to translate a Kerberos UPN."
     +
     +ABI_compatibility:
v5-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v5-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From e755fdccf16cb4fcd3036e1209291750ffecd261 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v5 1/2] pgstat: report in earlier with STATE_STARTING

Add pgstat_bestart_pre_auth(), which reports a 'starting' state while
waiting for backend initialization and client authentication to
complete. Since we hold a transaction open for a good amount of that,
and some authentication methods call out to external systems, having a
pg_stat_activity entry helps DBAs debug when things go badly wrong.
---
 doc/src/sgml/monitoring.sgml                |  6 ++
 src/backend/utils/activity/backend_status.c | 37 +++++++++-
 src/backend/utils/adt/pgstatfuncs.c         |  3 +
 src/backend/utils/init/postinit.c           | 20 ++++-
 src/include/utils/backend_status.h          |  2 +
 src/test/authentication/Makefile            |  4 +
 src/test/authentication/meson.build         |  4 +
 src/test/authentication/t/007_pre_auth.pl   | 81 +++++++++++++++++++++
 8 files changed, 150 insertions(+), 7 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d3..81a4a95152 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index bdb3a296ca..d71d7c1b4f 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -69,6 +69,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -269,6 +270,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -318,9 +347,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -375,7 +404,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_STARTING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f7b50e0b5a..c461bbd400 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d..adaa83e745 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -714,6 +715,21 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's going on. (A later call to
+	 * pgstat_bestart() will fill in the rest of the status.)
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_pre_auth();
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -782,9 +798,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.
@@ -882,6 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 97874300c3..8a6d573ce3 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_STARTING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void BackendStatusShmemInit(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..d05b15de7c 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..fe4ca51873 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 0000000000..dd554462d3
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2021-2024, 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(500_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(500_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();
-- 
2.34.1

v5-0002-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v5-0002-Report-external-auth-calls-as-wait-events.patchDownload
From 858e95f996589461e2840047fa35675b6f96e46d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v5 2/2] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 54 +++++++++++++++----
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 24 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 90 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 81a4a95152..a148e63711 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 47e8c91606..bbcde591ae 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,19 +1410,25 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
 	if (!port->hba->compat_realm)
 	{
-		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
-											  domainname, sizeof(domainname),
-											  port->hba->upn_username);
+		int			status;
+
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_MAKE_UPN);
+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
+								  domainname, sizeof(domainname),
+								  port->hba->upn_username);
+		pgstat_report_wait_end();
 
 		if (status != STATUS_OK)
 			/* Error already reported from pg_SSPI_make_upn */
@@ -2119,7 +2133,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2132,7 +2148,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2483,7 +2501,11 @@ CheckLDAPAuth(Port *port)
 	if (passwd == NULL)
 		return STATUS_EOF;		/* client wouldn't send password */
 
-	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_INITIALIZE);
+	r = InitializeLDAPConnection(port, &ldap);
+	pgstat_report_wait_end();
+
+	if (r == STATUS_ERROR)
 	{
 		/* Error message already sent */
 		pfree(passwd);
@@ -2530,9 +2552,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2555,6 +2580,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2562,6 +2589,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2630,7 +2658,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -2890,12 +2920,16 @@ CheckRADIUSAuth(Port *port)
 	identifiers = list_head(port->hba->radiusidentifiers);
 	foreach(server, port->hba->radiusservers)
 	{
-		int			ret = PerformRadiusTransaction(lfirst(server),
-												   lfirst(secrets),
-												   radiusports ? lfirst(radiusports) : NULL,
-												   identifiers ? lfirst(identifiers) : NULL,
-												   port->user_name,
-												   passwd);
+		int			ret;
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_TRANSACTION);
+		ret = PerformRadiusTransaction(lfirst(server),
+									   lfirst(secrets),
+									   radiusports ? lfirst(radiusports) : NULL,
+									   identifiers ? lfirst(identifiers) : NULL,
+									   port->user_name,
+									   passwd);
+		pgstat_report_wait_end();
 
 		/*------
 		 * STATUS_OK = Login OK
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d930277140..a388999b1a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b72..a341c6e7c5 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -161,6 +161,30 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_INITIALIZE	"Waiting to initialize an LDAP connection."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_TRANSACTION	"Waiting for a RADIUS transaction to complete."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_MAKE_UPN	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 9f18a753d4..014d536441 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index fad7fc3a7e..030d1a8321 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -179,6 +179,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -187,7 +188,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

#22Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#21)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2024-11-07 09:20:24 -0800, Jacob Champion wrote:

From e755fdccf16cb4fcd3036e1209291750ffecd261 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v5 1/2] pgstat: report in earlier with STATE_STARTING

Add pgstat_bestart_pre_auth(), which reports a 'starting' state while
waiting for backend initialization and client authentication to
complete. Since we hold a transaction open for a good amount of that,
and some authentication methods call out to external systems, having a
pg_stat_activity entry helps DBAs debug when things go badly wrong.

I don't understand why the pgstat_bestart()/pgstat_bestart_pre_auth() split
makes sense. The latter is going to redo most of the work that the former
did. What's the point of that?

Why not have a new function that initializes just the missing additional
information? Or for that matter, why not move most of what pgstat_bestart()
does into pgstat_beinit()?

As-is I'm -1 on this patch, it makes something complicated and fragile even
more so.

From 858e95f996589461e2840047fa35675b6f96e46d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v5 2/2] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

This doesn't really seem like it's actually using wait events to describe
waits. The new wait events cover stuff like memory allocations etc, see
e.g. pg_SSPI_make_upn().

I have some sympathy for that, it'd be nice if we had some generic way to
describe what code is doing - but it doesn't really seem good to use wait
events for that. Right now a backend reporting a wait allows to conclude that
a backend isn't running postgres code and busy or blocked outside of postgres
- but that's not true anymore if you have wait event cover generic things like
memory allocations (or even various library functions).

This isn't just pedantry - all the relevant code really needs to be rewritten
to allow the blocking to happen in an interruptible way, otherwise
authentication timeout etc can't realiably work. Once that's done you can
actually define useful wait events too.

Greetings,

Andres Freund

#23Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#22)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Nov 7, 2024 at 10:12 AM Andres Freund <andres@anarazel.de> wrote:

I don't understand why the pgstat_bestart()/pgstat_bestart_pre_auth() split
makes sense. The latter is going to redo most of the work that the former
did. What's the point of that?

Why not have a new function that initializes just the missing additional
information? Or for that matter, why not move most of what pgstat_bestart()
does into pgstat_beinit()?

I talk about that up above [1]/messages/by-id/CAOYmi+kLzSWrDHZbJg8bWZ94oP_K98mkoEvetgupOBVoy5H_ag@mail.gmail.com. I agree that this is all complicated
and fragile, but at the moment, I think splitting things apart is not
going to reduce the complexity in any way. I'm all ears for a
different approach, though (and it sounds like Michael is taking a
stab at it too).

This doesn't really seem like it's actually using wait events to describe
waits. The new wait events cover stuff like memory allocations etc, see
e.g. pg_SSPI_make_upn().

I've also asked about the "scope" of the waits in the OP [2]/messages/by-id/CAOYmi+=60deN20WDyCoHCiecgivJxr=98s7s7-C8SkXwrCfHXg@mail.gmail.com. I can
move them downwards in the stack, if you'd prefer.

All of these are intended to cover parts of the code that can actually
hang, but for things like SSPI I'm just working off of inspection and
Win32 documentation. So if it's not actually true that some of these
call points can hang, let me know and I can remove them. (For the
particular example you called out, I'm just trying to cover both calls
to TranslateName() in a maintainable place. The documentation says
"TranslateName fails if it cannot bind to Active Directory on a domain
controller." which seemed pretty wait-worthy to me.)

This isn't just pedantry - all the relevant code really needs to be rewritten
to allow the blocking to happen in an interruptible way, otherwise
authentication timeout etc can't realiably work. Once that's done you can
actually define useful wait events too.

I agree that would be amazing! I'm not about to tackle reliable
interrupts for all of the current blocking auth code for v18, though.
I'm just trying to make it observable when we do something that
blocks.

--Jacob

[1]: /messages/by-id/CAOYmi+kLzSWrDHZbJg8bWZ94oP_K98mkoEvetgupOBVoy5H_ag@mail.gmail.com
[2]: /messages/by-id/CAOYmi+=60deN20WDyCoHCiecgivJxr=98s7s7-C8SkXwrCfHXg@mail.gmail.com

#24Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#23)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2024-11-07 10:44:25 -0800, Jacob Champion wrote:

On Thu, Nov 7, 2024 at 10:12 AM Andres Freund <andres@anarazel.de> wrote:

I don't understand why the pgstat_bestart()/pgstat_bestart_pre_auth() split
makes sense. The latter is going to redo most of the work that the former
did. What's the point of that?

Why not have a new function that initializes just the missing additional
information? Or for that matter, why not move most of what pgstat_bestart()
does into pgstat_beinit()?

I talk about that up above [1]. I agree that this is all complicated
and fragile, but at the moment, I think splitting things apart is not
going to reduce the complexity in any way. I'm all ears for a
different approach, though (and it sounds like Michael is taking a
stab at it too).

I think the patch should not be merged as-is. It's just too ugly and fragile.

This doesn't really seem like it's actually using wait events to describe
waits. The new wait events cover stuff like memory allocations etc, see
e.g. pg_SSPI_make_upn().

I've also asked about the "scope" of the waits in the OP [2]. I can
move them downwards in the stack, if you'd prefer.

Well, right now they're just not actually wait events, so yes, they'd need to
be moved down.

I think it might make more sense to use pgstat_report_activity() or such here,
rather than using wait events to describe something that's not a wait.

This isn't just pedantry - all the relevant code really needs to be rewritten
to allow the blocking to happen in an interruptible way, otherwise
authentication timeout etc can't realiably work. Once that's done you can
actually define useful wait events too.

I agree that would be amazing! I'm not about to tackle reliable
interrupts for all of the current blocking auth code for v18, though.
I'm just trying to make it observable when we do something that
blocks.

Well, with that justification we could end up adding wait events for large
swaths of code that might not actually ever wait.

Greetings,

Andres Freund

#25Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#24)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Nov 7, 2024 at 11:41 AM Andres Freund <andres@anarazel.de> wrote:

I think the patch should not be merged as-is. It's just too ugly and fragile.

Understood; I'm trying to find a way forward, and I'm pointing out
that the alternatives I've tried seem to me to be _more_ fragile.

Are there any items in this list that you disagree with/are less
concerned about?

- the pre-auth step must always initialize the entire pgstat struct
- two-step initialization requires two PGSTAT_BEGIN_WRITE_ACTIVITY()
calls for _every_ backend
- two-step initialization requires us to couple against the order that
authentication information is being filled in (pre-auth, post-auth, or
both)

I think it might make more sense to use pgstat_report_activity() or such here,
rather than using wait events to describe something that's not a wait.

I'm not sure why you're saying these aren't waits. If pam_authenticate
is capable of hanging for hours and bringing down a production system,
is that not a "wait"?

I agree that would be amazing! I'm not about to tackle reliable
interrupts for all of the current blocking auth code for v18, though.
I'm just trying to make it observable when we do something that
blocks.

Well, with that justification we could end up adding wait events for large
swaths of code that might not actually ever wait.

If it were hypothetically useful to do so, would that be a problem?
I'm trying not to propose things that aren't actually useful.

--Jacob

#26Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#25)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2024-11-07 12:11:46 -0800, Jacob Champion wrote:

On Thu, Nov 7, 2024 at 11:41 AM Andres Freund <andres@anarazel.de> wrote:

I think the patch should not be merged as-is. It's just too ugly and fragile.

Understood; I'm trying to find a way forward, and I'm pointing out
that the alternatives I've tried seem to me to be _more_ fragile.

Are there any items in this list that you disagree with/are less
concerned about?

- the pre-auth step must always initialize the entire pgstat struct

Correct. And that has to happen exactly once, not twice.

- two-step initialization requires two PGSTAT_BEGIN_WRITE_ACTIVITY()
calls for _every_ backend

That's fine - PGSTAT_BEGIN_WRITE_ACTIVITY() is *extremely* cheap on the write
side. That's the whole point of of the sequence-lock like mechanism.

- two-step initialization requires us to couple against the order that
authentication information is being filled in (pre-auth, post-auth, or
both)

Not sure what you mean with this?

I think it might make more sense to use pgstat_report_activity() or such here,
rather than using wait events to describe something that's not a wait.

I'm not sure why you're saying these aren't waits. If pam_authenticate
is capable of hanging for hours and bringing down a production system,
is that not a "wait"?

It may or may not be. If you increase the iteration count for whatever secret
"hashing" method to be very high, it's not a wait, it's just CPU
use. Similarly, if you have a cpu expensive WHERE clause, that's not a
wait. But if you wait for network IO due to pam using ldap underneath or you
need to read toast values from disk, those are waits.

I agree that would be amazing! I'm not about to tackle reliable
interrupts for all of the current blocking auth code for v18, though.
I'm just trying to make it observable when we do something that
blocks.

Well, with that justification we could end up adding wait events for large
swaths of code that might not actually ever wait.

If it were hypothetically useful to do so, would that be a problem?
I'm trying not to propose things that aren't actually useful.

My point is that you're redefining wait events to be "in some region of code"
and that once you start doing that, there's a lot of other places you could
suddenly use wait events.

But wait events aren't actually suitable for that - they're a *single-depth*
mechanism, which means that if you start waiting, the prior wait event is
lost, and
a) the nested region isn't attributed to the parent while active
b) once the nested wait event is over, the parent isn't reset

Greetings,

Andres Freund

#27Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#26)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Nov 7, 2024 at 1:37 PM Andres Freund <andres@anarazel.de> wrote:

- the pre-auth step must always initialize the entire pgstat struct

Correct. And that has to happen exactly once, not twice.

What goes wrong if it happens twice?

- two-step initialization requires two PGSTAT_BEGIN_WRITE_ACTIVITY()
calls for _every_ backend

That's fine - PGSTAT_BEGIN_WRITE_ACTIVITY() is *extremely* cheap on the write
side. That's the whole point of of the sequence-lock like mechanism.

Okay, cool. I'll retract that concern.

- two-step initialization requires us to couple against the order that
authentication information is being filled in (pre-auth, post-auth, or
both)

Not sure what you mean with this?

In other words: if we split it, people who make changes to the order
that authentication information is determined during startup must know
to keep an eye on this code as well. Whereas with the current
patchset, the layers are decoupled and the order doesn't matter.
Quoting from above:

Finally, if we're okay with all of that, future maintainers need to be
careful about which fields get copied in the first (preauth) step, the
second step, or both. GSS, for example, can be set up during transport
negotiation (first step) or authentication (second step), so we have
to duplicate the logic there. SSL is currently first-step-only, I
think -- but are we sure we want to hardcode the assumption that cert
auth can't change any of those parameters after the transport has been
established? (I've been brainstorming ways we might use TLS 1.3's
post-handshake CertificateRequest, for example.)

If you increase the iteration count for whatever secret
"hashing" method to be very high, it's not a wait, it's just CPU
use.

I don't yet understand why this is a useful distinction to make. I
understand that they are different, but what are the bad consequences
if pg_stat_activity records a CPU busy wait in the same way it records
an I/O wait -- as long as they're not nested?

My point is that you're redefining wait events to be "in some region of code"
and that once you start doing that, there's a lot of other places you could
suddenly use wait events.

But wait events aren't actually suitable for that - they're a *single-depth*
mechanism, which means that if you start waiting, the prior wait event is
lost, and
a) the nested region isn't attributed to the parent while active
b) once the nested wait event is over, the parent isn't reset

I understand that they shouldn't be nested. But as long as they're
not, isn't that fine? And if the concern is that they'll accidentally
get nested, whether in this patch or in the future, can't we just
programmatically assert that we haven't?

Thanks,
--Jacob

#28Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#27)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2024-11-07 14:29:18 -0800, Jacob Champion wrote:

On Thu, Nov 7, 2024 at 1:37 PM Andres Freund <andres@anarazel.de> wrote:

- the pre-auth step must always initialize the entire pgstat struct

Correct. And that has to happen exactly once, not twice.

What goes wrong if it happens twice?

Primarily it's architecturally wrong. For no reason that I can see.

It does actually make things harder - what if somebody added a
pgstat_report_activity() somewhere between the call? It would suddenly get
lost after the second "initialization". Actually, the proposed patch already
has weird, externally visible, consequences - the application name is set,
then suddenly becomes unset, then is set again.

- two-step initialization requires two PGSTAT_BEGIN_WRITE_ACTIVITY()
calls for _every_ backend

That's fine - PGSTAT_BEGIN_WRITE_ACTIVITY() is *extremely* cheap on the write
side. That's the whole point of of the sequence-lock like mechanism.

Okay, cool. I'll retract that concern.

Additionally PGSTAT_BEGIN_WRITE_ACTIVITY() would already happen twice if you
initialize twice...

- two-step initialization requires us to couple against the order that
authentication information is being filled in (pre-auth, post-auth, or
both)

Not sure what you mean with this?

In other words: if we split it, people who make changes to the order
that authentication information is determined during startup must know
to keep an eye on this code as well. Whereas with the current
patchset, the layers are decoupled and the order doesn't matter.
Quoting from above:

Finally, if we're okay with all of that, future maintainers need to be
careful about which fields get copied in the first (preauth) step, the
second step, or both. GSS, for example, can be set up during transport
negotiation (first step) or authentication (second step), so we have
to duplicate the logic there. SSL is currently first-step-only, I
think -- but are we sure we want to hardcode the assumption that cert
auth can't change any of those parameters after the transport has been
established? (I've been brainstorming ways we might use TLS 1.3's
post-handshake CertificateRequest, for example.)

That doesn't seem like a reason to just initialize twice to me. If you have
one initialization step that properly initializes everything to a minimal
default state, you then can have granular functions that set up the user,
database, SSL, GSS information separately.

If you increase the iteration count for whatever secret
"hashing" method to be very high, it's not a wait, it's just CPU
use.

I don't yet understand why this is a useful distinction to make. I
understand that they are different, but what are the bad consequences
if pg_stat_activity records a CPU busy wait in the same way it records
an I/O wait -- as long as they're not nested?

Well, first, because you suddenly can't use wait events anymore to find waits.

But more importantly, because of not having nesting, adding one "coarse" "wait
event" means that anyone adding a wait event at a finer grade suddenly needs
to be aware of all the paths that could lead to the execution of the new
code and change all of them to not use the wait event anymore. It imposes a
tax on measuring actual "out of postgres" wait events.

My point is that you're redefining wait events to be "in some region of code"
and that once you start doing that, there's a lot of other places you could
suddenly use wait events.

But wait events aren't actually suitable for that - they're a *single-depth*
mechanism, which means that if you start waiting, the prior wait event is
lost, and
a) the nested region isn't attributed to the parent while active
b) once the nested wait event is over, the parent isn't reset

I understand that they shouldn't be nested. But as long as they're
not, isn't that fine? And if the concern is that they'll accidentally
get nested, whether in this patch or in the future, can't we just
programmatically assert that we haven't?

One very useful wait event would be for memory allocations that hit the
kernel. Those can take a fairly long time, because they might need to write
dirty buffers to disk before there is enough free memory. Now imagine that we
redefine the system memory allocator (or just postgres') so that all memory
allocations from the kernel use a wait event. Now suddenly all that code that
uses "coarse" wait events suddenly has a *rare* path - because most of the time
we can carve memory out of a larger OS level memory allocation - where wait
events would be nested.

Greetings,

Andres Freund

#29Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#28)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Nov 7, 2024 at 2:56 PM Andres Freund <andres@anarazel.de> wrote:

It does actually make things harder - what if somebody added a
pgstat_report_activity() somewhere between the call? It would suddenly get
lost after the second "initialization". Actually, the proposed patch already
has weird, externally visible, consequences - the application name is set,
then suddenly becomes unset, then is set again.

Oh... I think that alone is enough to change my mind; I neglected the
effects of that little pgstat_report_appname() stinger...

Additionally PGSTAT_BEGIN_WRITE_ACTIVITY() would already happen twice if you
initialize twice...

Sure. I was just trying not to introduce that to _all_ backend code
paths, but it sounds like that's not a concern. (Plus, it turns out to
be four calls, due again to the application_name reporting...)

That doesn't seem like a reason to just initialize twice to me. If you have
one initialization step that properly initializes everything to a minimal
default state, you then can have granular functions that set up the user,
database, SSL, GSS information separately.

I will start work on that then (unless Michael has already beaten me to it?).

But more importantly, because of not having nesting, adding one "coarse" "wait
event" means that anyone adding a wait event at a finer grade suddenly needs
to be aware of all the paths that could lead to the execution of the new
code and change all of them to not use the wait event anymore. It imposes a
tax on measuring actual "out of postgres" wait events.

One very useful wait event would be for memory allocations that hit the
kernel. Those can take a fairly long time, because they might need to write
dirty buffers to disk before there is enough free memory. Now imagine that we
redefine the system memory allocator (or just postgres') so that all memory
allocations from the kernel use a wait event. Now suddenly all that code that
uses "coarse" wait events suddenly has a *rare* path - because most of the time
we can carve memory out of a larger OS level memory allocation - where wait
events would be nested.

Okay, that makes a lot of sense. I will plumb these down as far as I can.

Thanks very much!

--Jacob

#30Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#29)
4 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Nov 7, 2024 at 4:38 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

Oh... I think that alone is enough to change my mind; I neglected the
effects of that little pgstat_report_appname() stinger...

(Note that application_name is not yet set at the site of the first
call, so I think the set-unset-set can't happen after all -- but I
didn't realize that before a lot of digging, which is further evidence
that I need to simplify...)

I will plumb these down as far as I can.

While I work on breaking pgstat_bestart() apart, here is a v6 which
pushes down the "coarse" wait events. No changes to 0001 yet.

I violated the "one event name per call site" rule with
TranslateName(). The call pattern there is "call once to figure out
the buffer length, then call again to fill it in", and IMO that didn't
deserve differentiation. But if anyone objects, I'm happy to change it
(and I'd appreciate some name suggestions in that case).

While I was breaking apart the LDAP events, I noticed that
ldap_unbind() does a lot more than just dropping the connection, so
I've refactored things a bit more in order to wrap all those calls.
That is done separately in 0003, which I will fold into 0002 once I
have confirmation that it's not controversial to anyone.

Thanks!
--Jacob

Attachments:

since-v5.diff.txttext/plain; charset=US-ASCII; name=since-v5.diff.txtDownload
1:  e755fdccf1 = 1:  e755fdccf1 pgstat: report in earlier with STATE_STARTING
2:  858e95f996 ! 2:  3f14df0308 Report external auth calls as wait events
    @@ src/backend/libpq/auth.c: pg_SSPI_recvauth(Port *port)
      
      	free(tokenuser);
      
    - 	if (!port->hba->compat_realm)
    - 	{
    --		int			status = pg_SSPI_make_upn(accountname, sizeof(accountname),
    --											  domainname, sizeof(domainname),
    --											  port->hba->upn_username);
    -+		int			status;
    +@@ src/backend/libpq/auth.c: pg_SSPI_make_upn(char *accountname,
    + 	 */
    + 
    + 	samname = psprintf("%s\\%s", domainname, accountname);
     +
    -+		pgstat_report_wait_start(WAIT_EVENT_SSPI_MAKE_UPN);
    -+		status = pg_SSPI_make_upn(accountname, sizeof(accountname),
    -+								  domainname, sizeof(domainname),
    -+								  port->hba->upn_username);
    -+		pgstat_report_wait_end();
    ++	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
    + 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
    + 						NULL, &upnamesize);
    ++	pgstat_report_wait_end();
    + 
    + 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
    + 		|| upnamesize == 0)
    +@@ src/backend/libpq/auth.c: pg_SSPI_make_upn(char *accountname,
    + 	/* upnamesize includes the terminating NUL. */
    + 	upname = palloc(upnamesize);
    + 
    ++	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
    + 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
    + 						upname, &upnamesize);
    ++	pgstat_report_wait_end();
      
    - 		if (status != STATUS_OK)
    - 			/* Error already reported from pg_SSPI_make_upn */
    + 	pfree(samname);
    + 	if (res)
     @@ src/backend/libpq/auth.c: CheckPAMAuth(Port *port, const char *user, const char *password)
      		return STATUS_ERROR;
      	}
    @@ src/backend/libpq/auth.c: CheckPAMAuth(Port *port, const char *user, const char
      
      	if (retval != PAM_SUCCESS)
      	{
    -@@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
    - 	if (passwd == NULL)
    - 		return STATUS_EOF;		/* client wouldn't send password */
    +@@ src/backend/libpq/auth.c: InitializeLDAPConnection(Port *port, LDAP **ldap)
    + 			}
      
    --	if (InitializeLDAPConnection(port, &ldap) == STATUS_ERROR)
    -+	pgstat_report_wait_start(WAIT_EVENT_LDAP_INITIALIZE);
    -+	r = InitializeLDAPConnection(port, &ldap);
    -+	pgstat_report_wait_end();
    + 			/* Look up a list of LDAP server hosts and port numbers */
    +-			if (ldap_domain2hostlist(domain, &hostlist))
    ++			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
    ++			r = ldap_domain2hostlist(domain, &hostlist);
    ++			pgstat_report_wait_end();
     +
    -+	if (r == STATUS_ERROR)
    ++			if (r)
    + 			{
    + 				ereport(LOG,
    + 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
    +@@ src/backend/libpq/auth.c: InitializeLDAPConnection(Port *port, LDAP **ldap)
    + 
    + 	if (port->hba->ldaptls)
      	{
    - 		/* Error message already sent */
    - 		pfree(passwd);
    ++		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
    + #ifndef WIN32
    +-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
    ++		r = ldap_start_tls_s(*ldap, NULL, NULL);
    + #else
    +-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
    ++		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
    + #endif
    ++		pgstat_report_wait_end();
    ++
    ++		if (r != LDAP_SUCCESS)
    + 		{
    + 			ereport(LOG,
    + 					(errmsg("could not start LDAP TLS session: %s",
     @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      		 * Bind with a pre-defined username/password (if available) for
      		 * searching. If none is specified, this turns into an anonymous bind.
    @@ src/backend/libpq/auth.c: CheckLDAPAuth(Port *port)
      
      	if (r != LDAP_SUCCESS)
      	{
    -@@ src/backend/libpq/auth.c: CheckRADIUSAuth(Port *port)
    - 	identifiers = list_head(port->hba->radiusidentifiers);
    - 	foreach(server, port->hba->radiusservers)
    - 	{
    --		int			ret = PerformRadiusTransaction(lfirst(server),
    --												   lfirst(secrets),
    --												   radiusports ? lfirst(radiusports) : NULL,
    --												   identifiers ? lfirst(identifiers) : NULL,
    --												   port->user_name,
    --												   passwd);
    -+		int			ret;
    +@@ src/backend/libpq/auth.c: PerformRadiusTransaction(const char *server, const char *secret, const char *por
    + 		return STATUS_ERROR;
    + 	}
    + 
    +-	if (sendto(sock, radius_buffer, packetlength, 0,
    +-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
    ++	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
    ++	r = sendto(sock, radius_buffer, packetlength, 0,
    ++			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
    ++	pgstat_report_wait_end();
     +
    -+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_TRANSACTION);
    -+		ret = PerformRadiusTransaction(lfirst(server),
    -+									   lfirst(secrets),
    -+									   radiusports ? lfirst(radiusports) : NULL,
    -+									   identifiers ? lfirst(identifiers) : NULL,
    -+									   port->user_name,
    -+									   passwd);
    ++	if (r < 0)
    + 	{
    + 		ereport(LOG,
    + 				(errmsg("could not send RADIUS packet: %m")));
    +@@ src/backend/libpq/auth.c: PerformRadiusTransaction(const char *server, const char *secret, const char *por
    + 		FD_ZERO(&fdset);
    + 		FD_SET(sock, &fdset);
    + 
    ++		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
    + 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
     +		pgstat_report_wait_end();
    ++
    + 		if (r < 0)
    + 		{
    + 			if (errno == EINTR)
    +@@ src/backend/libpq/auth.c: PerformRadiusTransaction(const char *server, const char *secret, const char *por
    + 		 */
      
    - 		/*------
    - 		 * STATUS_OK = Login OK
    + 		addrsize = sizeof(remoteaddr);
    ++
    ++		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
    + 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
    + 								(struct sockaddr *) &remoteaddr, &addrsize);
    ++		pgstat_report_wait_end();
    ++
    + 		if (packetlength < 0)
    + 		{
    + 			ereport(LOG,
     
      ## src/backend/utils/activity/wait_event.c ##
     @@ src/backend/utils/activity/wait_event.c: static const char *pgstat_get_wait_client(WaitEventClient w);
    @@ src/backend/utils/activity/wait_event_names.txt: XACT_GROUP_UPDATE	"Waiting for
     +GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
     +LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
     +LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
    -+LDAP_INITIALIZE	"Waiting to initialize an LDAP connection."
    ++LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
     +LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
    ++LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
     +PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
     +PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
    -+RADIUS_TRANSACTION	"Waiting for a RADIUS transaction to complete."
    ++RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
    ++RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
    ++RADIUS_WAIT	"Waiting for a RADIUS server to respond."
     +SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
     +SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
     +SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
    -+SSPI_MAKE_UPN	"Waiting for Windows to translate a Kerberos UPN."
    ++SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
     +
     +ABI_compatibility:
     +
-:  ---------- > 3:  fb21328568 squash! Report external auth calls as wait events
v6-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v6-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From e755fdccf16cb4fcd3036e1209291750ffecd261 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v6 1/3] pgstat: report in earlier with STATE_STARTING

Add pgstat_bestart_pre_auth(), which reports a 'starting' state while
waiting for backend initialization and client authentication to
complete. Since we hold a transaction open for a good amount of that,
and some authentication methods call out to external systems, having a
pg_stat_activity entry helps DBAs debug when things go badly wrong.
---
 doc/src/sgml/monitoring.sgml                |  6 ++
 src/backend/utils/activity/backend_status.c | 37 +++++++++-
 src/backend/utils/adt/pgstatfuncs.c         |  3 +
 src/backend/utils/init/postinit.c           | 20 ++++-
 src/include/utils/backend_status.h          |  2 +
 src/test/authentication/Makefile            |  4 +
 src/test/authentication/meson.build         |  4 +
 src/test/authentication/t/007_pre_auth.pl   | 81 +++++++++++++++++++++
 8 files changed, 150 insertions(+), 7 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d3..81a4a95152 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index bdb3a296ca..d71d7c1b4f 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -69,6 +69,7 @@ static int	localNumBackends = 0;
 static MemoryContext backendStatusSnapContext;
 
 
+static void pgstat_bestart_internal(bool pre_auth);
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
@@ -269,6 +270,34 @@ pgstat_beinit(void)
  */
 void
 pgstat_bestart(void)
+{
+	pgstat_bestart_internal(false);
+}
+
+
+/* ----------
+ * pgstat_bestart_pre_auth() -
+ *
+ *	Like pgstat_beinit(), above, but it's designed to be called before
+ *	authentication has been performed (so we have no user or database IDs).
+ *	Called from InitPostgres.
+ *----------
+ */
+void
+pgstat_bestart_pre_auth(void)
+{
+	pgstat_bestart_internal(true);
+}
+
+
+/* ----------
+ * pgstat_bestart_internal() -
+ *
+ *	Implementation of both flavors of pgstat_bestart().
+ *----------
+ */
+static void
+pgstat_bestart_internal(bool pre_auth)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
@@ -318,9 +347,9 @@ pgstat_bestart(void)
 	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)
+	if (!pre_auth && (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;
@@ -375,7 +404,7 @@ pgstat_bestart(void)
 	lbeentry.st_gss = false;
 #endif
 
-	lbeentry.st_state = STATE_UNDEFINED;
+	lbeentry.st_state = pre_auth ? STATE_STARTING : STATE_UNDEFINED;
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f7b50e0b5a..c461bbd400 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d..adaa83e745 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -714,6 +715,21 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's going on. (A later call to
+	 * pgstat_bestart() will fill in the rest of the status.)
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_pre_auth();
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -782,9 +798,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.
@@ -882,6 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 97874300c3..8a6d573ce3 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -24,6 +24,7 @@
 typedef enum BackendState
 {
 	STATE_UNDEFINED,
+	STATE_STARTING,
 	STATE_IDLE,
 	STATE_RUNNING,
 	STATE_IDLEINTRANSACTION,
@@ -309,6 +310,7 @@ extern void BackendStatusShmemInit(void);
 
 /* Initialization functions */
 extern void pgstat_beinit(void);
+extern void pgstat_bestart_pre_auth(void);
 extern void pgstat_bestart(void);
 
 extern void pgstat_clear_backend_activity_snapshot(void);
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..d05b15de7c 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..fe4ca51873 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 0000000000..dd554462d3
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2021-2024, 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(500_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(500_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();
-- 
2.34.1

v6-0002-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v6-0002-Report-external-auth-calls-as-wait-events.patchDownload
From 3f14df0308d9bbea9ab81a3c7021bc82f3c50db6 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v6 2/3] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 81a4a95152..a148e63711 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 47e8c91606..fe4cbeb8a0 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,11 +1410,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1503,8 +1513,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1519,8 +1532,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2119,7 +2134,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2132,7 +2149,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2272,7 +2291,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2366,11 +2389,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2530,9 +2557,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2555,6 +2585,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2562,6 +2594,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2630,7 +2663,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3079,8 +3114,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3128,7 +3167,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3161,8 +3203,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d930277140..a388999b1a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b72..db8474bc85 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -161,6 +161,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 9f18a753d4..014d536441 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index fad7fc3a7e..030d1a8321 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -179,6 +179,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -187,7 +188,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

v6-0003-squash-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v6-0003-squash-Report-external-auth-calls-as-wait-events.patchDownload
From fb21328568d33ce57f6bc79c4944cea79ea9fce7 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v6 3/3] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind().
---
 src/backend/libpq/auth.c                      | 33 ++++++++++++++-----
 .../utils/activity/wait_event_names.txt       |  1 +
 2 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index fe4cbeb8a0..df38d66f48 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2226,6 +2226,7 @@ CheckBSDAuth(Port *port, char *user)
 #ifdef USE_LDAP
 
 static int	errdetail_for_ldap(LDAP *ldap);
+static void unbind(LDAP *ldap);
 
 /*
  * Initialize a connection to the LDAP server, including setting up
@@ -2383,7 +2384,7 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
-		ldap_unbind(*ldap);
+		unbind(*ldap);
 		return STATUS_ERROR;
 	}
 
@@ -2403,7 +2404,7 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
-			ldap_unbind(*ldap);
+			unbind(*ldap);
 			return STATUS_ERROR;
 		}
 	}
@@ -2547,7 +2548,7 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
-				ldap_unbind(ldap);
+				unbind(ldap);
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2571,7 +2572,7 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2604,7 +2605,7 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2626,7 +2627,7 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2645,7 +2646,7 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2673,7 +2674,7 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
-		ldap_unbind(ldap);
+		unbind(ldap);
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2682,7 +2683,7 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
-	ldap_unbind(ldap);
+	unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
 
@@ -2709,6 +2710,20 @@ errdetail_for_ldap(LDAP *ldap)
 	return 0;
 }
 
+/*
+ * ldap_unbind() is not a lightweight operation; it writes data to the socket
+ * and may need to tear down (LDAP) SASL mechanisms, depending on the LDAP
+ * implementation in use. This function just wraps the unbind with pgstat
+ * reporting in case something takes longer than expected.
+ */
+static void
+unbind(LDAP *ldap)
+{
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND);
+	ldap_unbind(ldap);
+	pgstat_report_wait_end();
+}
+
 #endif							/* USE_LDAP */
 
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index db8474bc85..00a9459ad4 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -176,6 +176,7 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND	"Waiting for an LDAP connection to be unbound and closed."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.34.1

#31Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#30)
3 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Nov 8, 2024 at 4:23 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

While I work on breaking pgstat_bestart() apart, here is a v6 which
pushes down the "coarse" wait events. No changes to 0001 yet.

v7 rewrites 0001 by splitting pgstat_bestart() into three phases.
(0002-3 are unchanged.)

1. pgstat_bestart_initial() reports STATE_STARTING, fills in the early
fields and clears out the rest.
2. pgstat_bestart_security() reports the SSL/GSS status of the
connection. This is only for backends with a valid MyProcPort; they
call it twice.
3. pgstat_bestart_final() fills in the user and database IDs, takes
the entry out of STATE_STARTING, and reports the application_name.

I was wondering if maybe I should fill in application_name before
taking the entry out of STATE_STARTING, in order to establish the rule
that "starting" pgstat entries are always partially filled, and that
DBAs can rely on their full contents once they've proceeded past it.
Thoughts?

I've added machinery to 001_ssltests.pl to make sure we see early
transport security stats prior to user authentication. This overlaps
quite a bit with the new 007_pre_auth.pl, so if we'd rather not have
the latter (as briefly discussed upthread) I can move the rest of it
over.

Thanks,
--Jacob

Attachments:

v7-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v7-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From b91a602cab8ee13a61168e1967648249e28a22e3 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v7 1/3] pgstat: report in earlier with STATE_STARTING

Split pgstat_bestart() into three phases for better observability:

1) pgstat_bestart_initial() reports a 'starting' state while waiting for
   backend initialization and client authentication to complete.  Since
   we hold a transaction open for a good amount of that, and some
   authentication methods call out to external systems, having an early
   pg_stat_activity entry helps DBAs debug when things go badly wrong.

2) pgstat_bestart_security() reports the SSL/GSS status of the
   connection.  Some backends don't call this at all; others call it
   twice, once after transport establishment and once after client
   authentication.

3) pgstat_bestart_final() reports the user and database IDs, takes the
   entry out of STATE_STARTING, and reports the application_name.
   TODO: should the order of those last two be swapped?
---
 doc/src/sgml/monitoring.sgml                |   6 +
 src/backend/postmaster/auxprocess.c         |   3 +-
 src/backend/utils/activity/backend_status.c | 207 +++++++++++++-------
 src/backend/utils/adt/pgstatfuncs.c         |   3 +
 src/backend/utils/init/postinit.c           |  42 +++-
 src/include/utils/backend_status.h          |   5 +-
 src/test/authentication/Makefile            |   4 +
 src/test/authentication/meson.build         |   4 +
 src/test/authentication/t/007_pre_auth.pl   |  81 ++++++++
 src/test/ssl/Makefile                       |   3 +-
 src/test/ssl/meson.build                    |   1 +
 src/test/ssl/t/001_ssltests.pl              |  65 ++++++
 12 files changed, 335 insertions(+), 89 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 331315f8d3..81a4a95152 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index d19174bda3..bcfd231d0d 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -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);
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index bdb3a296ca..d8f87cbc92 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -253,31 +253,18 @@ pgstat_beinit(void)
 	on_shmem_exit(pgstat_beshutdown_hook, 0);
 }
 
-
-/* ----------
- * pgstat_bestart() -
- *
- *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
- *
- *	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(), once user and database
+ * IDs have been determined.
  */
 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 +284,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 +294,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 +308,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,16 +353,135 @@ 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);
+}
+
+/*
+ * Fill in SSL and GSS information for the pgstat entry. This is separate from
+ * pgstat_bestart_initial() so that backends may call it multiple times as
+ * security details are filled in.
+ *
+ * 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);
+}
+
+/*
+ * Finalizes the startup pgstat entry by filling in the user/database IDs,
+ * clearing STATE_STARTING, and reporting the application_name.
+ *
+ * We must be inside a transaction if this isn't an aux 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);
 
 	/* Update app name to current GUC setting */
+	/* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
 	if (application_name)
 		pgstat_report_appname(application_name);
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f7b50e0b5a..c461bbd400 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -366,6 +366,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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a024b1151d..c301d0b2b7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -714,6 +715,23 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's going on.
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_initial();
+		if (MyProcPort)
+			pgstat_bestart_security();	/* fill in any SSL/GSS info too */
+
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -782,9 +800,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.
@@ -805,8 +820,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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		return;
 	}
@@ -882,6 +897,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
@@ -889,6 +905,12 @@ InitPostgres(const char *in_dbname, Oid dboid,
 			InitializeSystemUser(MyClientConnectionInfo.authn_id,
 								 hba_authname(MyClientConnectionInfo.auth_method));
 		am_superuser = superuser();
+
+		/*
+		 * Authentication may have changed SSL/GSS details for the session, so
+		 * report it again.
+		 */
+		pgstat_bestart_security();
 	}
 
 	/*
@@ -961,8 +983,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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		/* close the transaction we started above */
 		CommitTransactionCommand();
@@ -1005,7 +1027,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		 */
 		if (!bootstrap)
 		{
-			pgstat_bestart();
+			pgstat_bestart_final();
 			CommitTransactionCommand();
 		}
 		return;
@@ -1205,9 +1227,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 the PgBackendStatus array */
 	if (!bootstrap)
-		pgstat_bestart();
+		pgstat_bestart_final();
 
 	/* close the transaction we started above */
 	if (!bootstrap)
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 97874300c3..ac693b7d8b 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -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);
 
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index da0b71873a..d05b15de7c 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 8f5688dcc1..fe4ca51873 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 0000000000..bb7f61a7ca
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -0,0 +1,81 @@
+
+# Copyright (c) 2021-2024, 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();
diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile
index 6441a8047b..880fe656c7 100644
--- a/src/test/ssl/Makefile
+++ b/src/test/ssl/Makefile
@@ -10,12 +10,13 @@
 #-------------------------------------------------------------------------
 
 EXTRA_INSTALL = contrib/sslinfo
+EXTRA_INSTALL += src/test/modules/injection_points
 
 subdir = src/test/ssl
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-export OPENSSL with_ssl
+export OPENSSL enable_injection_points with_ssl
 
 # The sslfiles targets are separated into their own file due to interactions
 # with settings in Makefile.global.
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..b7827c72fa 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -6,6 +6,7 @@ tests += {
   'bd': meson.current_build_dir(),
   'tap': {
     'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
       'with_ssl': ssl_library,
       'OPENSSL': openssl.found() ? openssl.path() : '',
     },
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 131460a1fe..7d7d071614 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -7,6 +7,7 @@ use Config qw ( %Config );
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
+use Time::HiRes qw(usleep);
 
 use FindBin;
 use lib $FindBin::RealBin;
@@ -71,6 +72,23 @@ $node->start;
 my $result = $node->safe_psql('postgres', "SHOW ssl_library");
 is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
 
+my $injection_points_unavailable = '';
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	$injection_points_unavailable =
+	  'Injection points not supported by this build';
+}
+elsif (!$node->check_extension('injection_points'))
+{
+	$injection_points_unavailable =
+	  'Extension injection_points not installed';
+}
+else
+{
+	# For ease of setup, make injection_points available for all new databases.
+	$node->safe_psql('template1', 'CREATE EXTENSION injection_points');
+}
+
 $ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
 	$SERVERHOSTCIDR, 'trust');
 
@@ -554,6 +572,53 @@ command_like(
 				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,_null_,_null_,_null_\r?$}mx,
 	'pg_stat_ssl view without client certificate');
 
+# Test that pg_stat_ssl gets filled in early, prior to authentication. Requires
+# injection point support.
+SKIP:
+{
+	skip $injection_points_unavailable, 1 if $injection_points_unavailable;
+
+	# Connect to the server and inject a waitpoint.
+	my $psql =
+	  $node->background_psql('trustdb', connstr => "$common_connstr user=");
+	$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(
+		'trustdb',
+		connstr => $common_connstr,
+		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);
+	}
+
+	like(
+		$psql->query(
+			"SELECT ssl, version, cipher, bits FROM pg_stat_ssl WHERE pid = $pid"
+		),
+		qr/^t|TLSv[\d.]+|[\w-]+|\d+$/,
+		'pg_stat_ssl view is updated prior to authentication');
+
+	# Detach the waitpoint and wait for the connection to complete.
+	$psql->query_safe("SELECT injection_points_wakeup('init-pre-auth');");
+	$conn->wait_connect();
+
+	$psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
+	$psql->quit();
+	$conn->quit();
+}
+
 # Test min/max SSL protocol versions.
 $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.2",
-- 
2.34.1

v7-0002-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v7-0002-Report-external-auth-calls-as-wait-events.patchDownload
From 0cb5ab8b89d3595e32c8cd86fa49376e1a482493 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v7 2/3] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 81a4a95152..a148e63711 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 47e8c91606..fe4cbeb8a0 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -1000,6 +1001,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1011,6 +1013,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1222,6 +1225,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1231,6 +1235,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1296,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1305,6 +1312,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1402,11 +1410,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1503,8 +1513,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1519,8 +1532,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2119,7 +2134,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2132,7 +2149,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2272,7 +2291,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2366,11 +2389,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2530,9 +2557,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2555,6 +2585,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2562,6 +2594,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2630,7 +2663,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3079,8 +3114,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3128,7 +3167,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3161,8 +3203,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d930277140..a388999b1a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 16144c2b72..db8474bc85 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -161,6 +161,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 9f18a753d4..014d536441 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index fad7fc3a7e..030d1a8321 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -179,6 +179,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -187,7 +188,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

v7-0003-squash-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v7-0003-squash-Report-external-auth-calls-as-wait-events.patchDownload
From a0308928dd3c6dbb83c43f74bb3913f9bccf8596 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v7 3/3] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind().
---
 src/backend/libpq/auth.c                      | 33 ++++++++++++++-----
 .../utils/activity/wait_event_names.txt       |  1 +
 2 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index fe4cbeb8a0..df38d66f48 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2226,6 +2226,7 @@ CheckBSDAuth(Port *port, char *user)
 #ifdef USE_LDAP
 
 static int	errdetail_for_ldap(LDAP *ldap);
+static void unbind(LDAP *ldap);
 
 /*
  * Initialize a connection to the LDAP server, including setting up
@@ -2383,7 +2384,7 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
-		ldap_unbind(*ldap);
+		unbind(*ldap);
 		return STATUS_ERROR;
 	}
 
@@ -2403,7 +2404,7 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
-			ldap_unbind(*ldap);
+			unbind(*ldap);
 			return STATUS_ERROR;
 		}
 	}
@@ -2547,7 +2548,7 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
-				ldap_unbind(ldap);
+				unbind(ldap);
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2571,7 +2572,7 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2604,7 +2605,7 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2626,7 +2627,7 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2645,7 +2646,7 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
-			ldap_unbind(ldap);
+			unbind(ldap);
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2673,7 +2674,7 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
-		ldap_unbind(ldap);
+		unbind(ldap);
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2682,7 +2683,7 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
-	ldap_unbind(ldap);
+	unbind(ldap);
 	pfree(passwd);
 	pfree(fulluser);
 
@@ -2709,6 +2710,20 @@ errdetail_for_ldap(LDAP *ldap)
 	return 0;
 }
 
+/*
+ * ldap_unbind() is not a lightweight operation; it writes data to the socket
+ * and may need to tear down (LDAP) SASL mechanisms, depending on the LDAP
+ * implementation in use. This function just wraps the unbind with pgstat
+ * reporting in case something takes longer than expected.
+ */
+static void
+unbind(LDAP *ldap)
+{
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND);
+	ldap_unbind(ldap);
+	pgstat_report_wait_end();
+}
+
 #endif							/* USE_LDAP */
 
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index db8474bc85..00a9459ad4 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -176,6 +176,7 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND	"Waiting for an LDAP connection to be unbound and closed."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.34.1

#32Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#31)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Nov 11, 2024 at 03:17:44PM -0800, Jacob Champion wrote:

1. pgstat_bestart_initial() reports STATE_STARTING, fills in the early
fields and clears out the rest.
2. pgstat_bestart_security() reports the SSL/GSS status of the
connection. This is only for backends with a valid MyProcPort; they
call it twice.
3. pgstat_bestart_final() fills in the user and database IDs, takes
the entry out of STATE_STARTING, and reports the application_name.

This split is interesting, neater than the previous approches taken.

I was wondering if maybe I should fill in application_name before
taking the entry out of STATE_STARTING, in order to establish the rule
that "starting" pgstat entries are always partially filled, and that
DBAs can rely on their full contents once they've proceeded past it.
Thoughts?

/* Update app name to current GUC setting */
+ /* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
if (application_name)
pgstat_report_appname(application_name);

Historically, we've always reported the application name after
STATE_UNDEFINED is set, never the reverse. There could be potential
security implications if we were to begin reporting the application
name before the connection has fully authenticated because it would be
possible for attempted connections to show malicious data in the
catalogs, and now we're sure that the catalog data is OK to query for
other users. Let's be careful about that. I think that we should
still set that as late as possible in pgstat_bestart_final(), never at
an earlier step as this data can come from a connection
string, potentially malicious if not authenticated yet.

I've added machinery to 001_ssltests.pl to make sure we see early
transport security stats prior to user authentication. This overlaps
quite a bit with the new 007_pre_auth.pl, so if we'd rather not have
the latter (as briefly discussed upthread) I can move the rest of it
over.

+	if (!bootstrap)
+	{
+		pgstat_bestart_initial();
+		if (MyProcPort)
+			pgstat_bestart_security();	/* fill in any SSL/GSS info too */

This part of patch 0001 is giving me a very long pause. What's the
merit of filling in twice the backend entry data if we're going to
update it at the end anyway for normal backend connections? More info
for debugging, is it? It seems that removing this call does not
influence the tests you are adding, as well (or should the test
"pg_stat_ssl view is updated" be responsible for checking that?).
Perhaps this should be documented as a comment.

Anyway, allowing backends to call this routine multiple times makes me
quite uncomfortable, and it does not prevent looking at the wait event
data that may be reported with 0002, no? I think that we should only
call it once per backend and once authentication is completed, even
perhaps have a static rule to engrave this policy in stone.

With this patch, the information that we get to be able to debug a
backend entry in pg_stat_activity is st_clientaddr and
remote_hostname. If we have a backend stuck in a wait event for the
"Auth" class, would these be sufficient to provide a good user
experience? Still better than nothing as we don't know the database
and user ID used for the connection until authentication is done, I
guess, to be able to grab patterns behind authentication getting
stuck.

In patch 0002, WAIT_EVENT_SSPI_TRANSLATE_NAME is used twice for two
code paths. Perhaps these should be two separate wait events?
The events attached to ldap_unbind() make that also kind of hard to
debug. What's the code path actually impacted if we see them in
pg_stat_activity? We have nine of them with the same wait event
attached to them.

The patch needs some 2024 -> 2025 updates in its copyright notices.

At the end of [1]/messages/by-id/faesruozcdcg2aksscb3dcojy4gqjqsyaxfajkqzm4kozjowgm@nmjgrrvdax25 -- Michael, I've been reminded of this quote from Andres about
0002:
"This doesn't really seem like it's actually using wait events to
describe waits. The new wait events cover stuff like memory
allocations etc, see e.g. pg_SSPI_make_upn()."

Should we do improvements in this area with interruptions before
attempting to tackle the problem of making starting backend entries
visible in pg_stat_activity, before these complete authentication? We
use wait events for syscalls on I/O already, perhaps that does not
stand as an argument in favor of this patch on its own, but I don't
see why more events associated to external library calls as done in
0002 is a problem? Feel free to counter-argue, each code path where
an Auth event is added should be more closely looked at, for sure, but
I don't see why we should discard all of them based on this argument
if some are useful for debugging auth issues.

[1]: /messages/by-id/faesruozcdcg2aksscb3dcojy4gqjqsyaxfajkqzm4kozjowgm@nmjgrrvdax25 -- Michael
--
Michael

#33Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#32)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Feb 6, 2025 at 12:35 AM Michael Paquier <michael@paquier.xyz> wrote:

/* Update app name to current GUC setting */
+ /* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
if (application_name)
pgstat_report_appname(application_name);

Historically, we've always reported the application name after
STATE_UNDEFINED is set, never the reverse. There could be potential
security implications if we were to begin reporting the application
name before the connection has fully authenticated because it would be
possible for attempted connections to show malicious data in the
catalogs, and now we're sure that the catalog data is OK to query for
other users. Let's be careful about that. I think that we should
still set that as late as possible in pgstat_bestart_final(), never at
an earlier step as this data can come from a connection
string, potentially malicious if not authenticated yet.

I don't want to change the order of authentication and application
name, just move the application name report up above the state change
in pgstat_bestart_final(). I don't feel strongly about it, though.

+       if (!bootstrap)
+       {
+               pgstat_bestart_initial();
+               if (MyProcPort)
+                       pgstat_bestart_security();      /* fill in any SSL/GSS info too */

This part of patch 0001 is giving me a very long pause. What's the
merit of filling in twice the backend entry data if we're going to
update it at the end anyway for normal backend connections? More info
for debugging, is it?

Correct; if you already know authentication information from the
transport then it seemed nice to fill it in before doing the stuff
that hangs.

It seems that removing this call does not
influence the tests you are adding, as well (or should the test
"pg_stat_ssl view is updated" be responsible for checking that?).

The test is supposed to enforce that, but I see that it's not for some
reason. That's concerning. I'll investigate, thanks for pointing it
out.

With this patch, the information that we get to be able to debug a
backend entry in pg_stat_activity is st_clientaddr and
remote_hostname. If we have a backend stuck in a wait event for the
"Auth" class, would these be sufficient to provide a good user
experience? Still better than nothing as we don't know the database
and user ID used for the connection until authentication is done, I
guess, to be able to grab patterns behind authentication getting
stuck.

It's better than nothing, but is there a particular reason not to
trust the crypto the first time around? The SSL/GSS details are what
they are; if you don't trust them at either point one or point two
then I think we need to have a more urgent conversation about that?

In patch 0002, WAIT_EVENT_SSPI_TRANSLATE_NAME is used twice for two
code paths. Perhaps these should be two separate wait events?

That was my question from v6:

I violated the "one event name per call site" rule with
TranslateName(). The call pattern there is "call once to figure out
the buffer length, then call again to fill it in", and IMO that didn't
deserve differentiation. But if anyone objects, I'm happy to change it
(and I'd appreciate some name suggestions in that case).

So -- any name suggestions? :D (Personally, I viewed this sort of like
an unrolled loop of two. I don't know if it helps to know which call
is hanging as long as you know that TranslateName is hanging.)

The events attached to ldap_unbind() make that also kind of hard to
debug. What's the code path actually impacted if we see them in
pg_stat_activity? We have nine of them with the same wait event
attached to them.

Do we _want_ nine separate flavors of WAIT_EVENT_LDAP_UNBIND? I
figured it was enough to know that you were stuck unbinding.

The patch needs some 2024 -> 2025 updates in its copyright notices.

Will do.

At the end of [1], I've been reminded of this quote from Andres about
0002:
"This doesn't really seem like it's actually using wait events to
describe waits. The new wait events cover stuff like memory
allocations etc, see e.g. pg_SSPI_make_upn()."

Should we do improvements in this area with interruptions before
attempting to tackle the problem of making starting backend entries
visible in pg_stat_activity, before these complete authentication? We
use wait events for syscalls on I/O already, perhaps that does not
stand as an argument in favor of this patch on its own, but I don't
see why more events associated to external library calls as done in
0002 is a problem?

I had hoped that my v6 addressed Andres' concerns by pushing the
events down as far as possible (that way they can't be nested, which I
took to be the primary problem). Does something else need to be done?

Thanks,
--Jacob

#34Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#33)
4 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Feb 10, 2025 at 8:23 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

The test is supposed to enforce that, but I see that it's not for some
reason. That's concerning. I'll investigate, thanks for pointing it
out.

Bad regex escaping on my part; fixed in v8. Thanks for the report!

While debugging, I also noticed that a poorly timed autovacuum could
also show up in my new pg_stat_activity query, so I've increased the
specificity.

Do we _want_ nine separate flavors of WAIT_EVENT_LDAP_UNBIND? I
figured it was enough to know that you were stuck unbinding.

v8-0003 shows this approach. For the record, I think it's materially
worse than v7-0003. IMO it increases the cognitive load for very
little benefit and makes it more work for a newcomer to refactor the
cleanup code for those routines. I think it's enough that you can see
a separate LOG message for each failure case, if you want to know why
we're unbinding.

Thanks,
--Jacob

Attachments:

since-v7.diff.txttext/plain; charset=US-ASCII; name=since-v7.diff.txtDownload
1:  b91a602cab8 ! 1:  81a61854bdf pgstat: report in earlier with STATE_STARTING
    @@ src/backend/utils/activity/backend_status.c: pgstat_bestart(void)
     +
     +	PGSTAT_END_WRITE_ACTIVITY(beentry);
      
    + 	/* Create the backend statistics entry */
    + 	if (pgstat_tracks_backend_bktype(MyBackendType))
    + 		pgstat_create_backend(MyProcNumber);
    + 
      	/* Update app name to current GUC setting */
     +	/* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
      	if (application_name)
    @@ src/test/authentication/meson.build: tests += {
      ## src/test/authentication/t/007_pre_auth.pl (new) ##
     @@
     +
    -+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
    ++# Copyright (c) 2021-2025, PostgreSQL Global Development Group
     +
     +# Tests for connection behavior prior to authentication.
     +
    @@ src/test/ssl/t/001_ssltests.pl: command_like(
     +	while (1)
     +	{
     +		$pid = $psql->query(
    -+			"SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
    ++			"SELECT pid FROM pg_stat_activity WHERE state = 'starting' AND client_addr IS NOT NULL;");
     +		last if $pid ne "";
     +
     +		usleep(100_000);
    @@ src/test/ssl/t/001_ssltests.pl: command_like(
     +		$psql->query(
     +			"SELECT ssl, version, cipher, bits FROM pg_stat_ssl WHERE pid = $pid"
     +		),
    -+		qr/^t|TLSv[\d.]+|[\w-]+|\d+$/,
    ++		qr/^t\|TLSv[\d.]+\|[\w-]+\|\d+$/,
     +		'pg_stat_ssl view is updated prior to authentication');
     +
     +	# Detach the waitpoint and wait for the connection to complete.
2:  0cb5ab8b89d = 2:  e734e46009f Report external auth calls as wait events
3:  a0308928dd3 < -:  ----------- squash! Report external auth calls as wait events
-:  ----------- > 3:  39c7d9ce42b squash! Report external auth calls as wait events
v8-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v8-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From 81a61854bdf419d045c4b82896b8889938e41184 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v8 1/3] pgstat: report in earlier with STATE_STARTING

Split pgstat_bestart() into three phases for better observability:

1) pgstat_bestart_initial() reports a 'starting' state while waiting for
   backend initialization and client authentication to complete.  Since
   we hold a transaction open for a good amount of that, and some
   authentication methods call out to external systems, having an early
   pg_stat_activity entry helps DBAs debug when things go badly wrong.

2) pgstat_bestart_security() reports the SSL/GSS status of the
   connection.  Some backends don't call this at all; others call it
   twice, once after transport establishment and once after client
   authentication.

3) pgstat_bestart_final() reports the user and database IDs, takes the
   entry out of STATE_STARTING, and reports the application_name.
   TODO: should the order of those last two be swapped?
---
 doc/src/sgml/monitoring.sgml                |   6 +
 src/backend/postmaster/auxprocess.c         |   3 +-
 src/backend/utils/activity/backend_status.c | 207 +++++++++++++-------
 src/backend/utils/adt/pgstatfuncs.c         |   3 +
 src/backend/utils/init/postinit.c           |  42 +++-
 src/include/utils/backend_status.h          |   5 +-
 src/test/authentication/Makefile            |   4 +
 src/test/authentication/meson.build         |   4 +
 src/test/authentication/t/007_pre_auth.pl   |  81 ++++++++
 src/test/ssl/Makefile                       |   3 +-
 src/test/ssl/meson.build                    |   1 +
 src/test/ssl/t/001_ssltests.pl              |  65 ++++++
 12 files changed, 335 insertions(+), 89 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index edc2470bcf9..dcb2d77adec 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index ff366ceb0fc..4f6795f7265 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -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);
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 731342799a6..f08b41054bb 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -253,31 +253,18 @@ pgstat_beinit(void)
 	on_shmem_exit(pgstat_beshutdown_hook, 0);
 }
 
-
-/* ----------
- * pgstat_bestart() -
- *
- *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
- *
- *	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(), once user and database
+ * IDs have been determined.
  */
 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 +284,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 +294,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 +308,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,20 +353,139 @@ 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);
+}
+
+/*
+ * Fill in SSL and GSS information for the pgstat entry. This is separate from
+ * pgstat_bestart_initial() so that backends may call it multiple times as
+ * security details are filled in.
+ *
+ * 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);
+}
+
+/*
+ * Finalizes the startup pgstat entry by filling in the user/database IDs,
+ * clearing STATE_STARTING, and reporting the application_name.
+ *
+ * We must be inside a transaction if this isn't an aux 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))
 		pgstat_create_backend(MyProcNumber);
 
 	/* Update app name to current GUC setting */
+	/* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
 	if (application_name)
 		pgstat_report_appname(application_name);
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index e9096a88492..c754a31253d 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 01bb6a410cb..62364f6b767 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -58,6 +58,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"
@@ -717,6 +718,23 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 */
 	InitProcessPhase2();
 
+	/* Initialize status reporting */
+	pgstat_beinit();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's going on.
+	 */
+	if (!bootstrap)
+	{
+		pgstat_bestart_initial();
+		if (MyProcPort)
+			pgstat_bestart_security();	/* fill in any SSL/GSS info too */
+
+		INJECTION_POINT("init-pre-auth");
+	}
+
 	/*
 	 * Initialize my entry in the shared-invalidation manager's array of
 	 * per-backend data.
@@ -785,9 +803,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.
@@ -808,8 +823,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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		return;
 	}
@@ -874,6 +889,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
@@ -881,6 +897,12 @@ InitPostgres(const char *in_dbname, Oid dboid,
 			InitializeSystemUser(MyClientConnectionInfo.authn_id,
 								 hba_authname(MyClientConnectionInfo.auth_method));
 		am_superuser = superuser();
+
+		/*
+		 * Authentication may have changed SSL/GSS details for the session, so
+		 * report it again.
+		 */
+		pgstat_bestart_security();
 	}
 
 	/*
@@ -952,8 +974,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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		/* close the transaction we started above */
 		CommitTransactionCommand();
@@ -996,7 +1018,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		 */
 		if (!bootstrap)
 		{
-			pgstat_bestart();
+			pgstat_bestart_final();
 			CommitTransactionCommand();
 		}
 		return;
@@ -1196,9 +1218,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 the PgBackendStatus array */
 	if (!bootstrap)
-		pgstat_bestart();
+		pgstat_bestart_final();
 
 	/* close the transaction we started above */
 	if (!bootstrap)
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index d3d4ff6c5c9..1c9b4fe14d0 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -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);
 
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index c4022dc829b..8b5beced080 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index f6e48411c11..800b3a5ff40 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 00000000000..a638226dbaf
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -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();
diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile
index e8a1639db2d..2d800c4a254 100644
--- a/src/test/ssl/Makefile
+++ b/src/test/ssl/Makefile
@@ -10,12 +10,13 @@
 #-------------------------------------------------------------------------
 
 EXTRA_INSTALL = contrib/sslinfo
+EXTRA_INSTALL += src/test/modules/injection_points
 
 subdir = src/test/ssl
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-export OPENSSL with_ssl
+export OPENSSL enable_injection_points with_ssl
 
 # The sslfiles targets are separated into their own file due to interactions
 # with settings in Makefile.global.
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index cf8b2b9303a..5ba8bdb373d 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -6,6 +6,7 @@ tests += {
   'bd': meson.current_build_dir(),
   'tap': {
     'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
       'with_ssl': ssl_library,
       'OPENSSL': openssl.found() ? openssl.path() : '',
     },
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 5422511d4ab..4d1ab3493db 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -7,6 +7,7 @@ use Config qw ( %Config );
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
+use Time::HiRes qw(usleep);
 
 use FindBin;
 use lib $FindBin::RealBin;
@@ -71,6 +72,23 @@ $node->start;
 my $result = $node->safe_psql('postgres', "SHOW ssl_library");
 is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
 
+my $injection_points_unavailable = '';
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	$injection_points_unavailable =
+	  'Injection points not supported by this build';
+}
+elsif (!$node->check_extension('injection_points'))
+{
+	$injection_points_unavailable =
+	  'Extension injection_points not installed';
+}
+else
+{
+	# For ease of setup, make injection_points available for all new databases.
+	$node->safe_psql('template1', 'CREATE EXTENSION injection_points');
+}
+
 $ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
 	$SERVERHOSTCIDR, 'trust');
 
@@ -556,6 +574,53 @@ command_like(
 				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,_null_,_null_,_null_\r?$}mx,
 	'pg_stat_ssl view without client certificate');
 
+# Test that pg_stat_ssl gets filled in early, prior to authentication. Requires
+# injection point support.
+SKIP:
+{
+	skip $injection_points_unavailable, 1 if $injection_points_unavailable;
+
+	# Connect to the server and inject a waitpoint.
+	my $psql =
+	  $node->background_psql('trustdb', connstr => "$common_connstr user=");
+	$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(
+		'trustdb',
+		connstr => $common_connstr,
+		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' AND client_addr IS NOT NULL;");
+		last if $pid ne "";
+
+		usleep(100_000);
+	}
+
+	like(
+		$psql->query(
+			"SELECT ssl, version, cipher, bits FROM pg_stat_ssl WHERE pid = $pid"
+		),
+		qr/^t\|TLSv[\d.]+\|[\w-]+\|\d+$/,
+		'pg_stat_ssl view is updated prior to authentication');
+
+	# Detach the waitpoint and wait for the connection to complete.
+	$psql->query_safe("SELECT injection_points_wakeup('init-pre-auth');");
+	$conn->wait_connect();
+
+	$psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
+	$psql->quit();
+	$conn->quit();
+}
+
 # Test min/max SSL protocol versions.
 $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.2",
-- 
2.34.1

v8-0002-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v8-0002-Report-external-auth-calls-as-wait-events.patchDownload
From e734e46009f35e27bcfc05d8f75546eb7489131d Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v8 2/3] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index dcb2d77adec..33f67c00100 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index d6ef32cc823..f79626f019f 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -38,6 +38,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -984,6 +985,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -995,6 +997,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1206,6 +1209,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1215,6 +1219,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1280,6 +1286,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1289,6 +1296,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1386,11 +1394,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1487,8 +1497,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1503,8 +1516,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2103,7 +2118,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2116,7 +2133,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2256,7 +2275,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2350,11 +2373,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2514,9 +2541,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2539,6 +2569,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2546,6 +2578,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2614,7 +2647,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3063,8 +3098,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3112,7 +3151,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3145,8 +3187,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..6cc3e1e7c7a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f071628..a2852225614 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -162,6 +162,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index b8cb3e5a430..3d995a9e5be 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 352abc0bd42..7e671563d68 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -180,6 +180,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -188,7 +189,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

v8-0003-squash-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v8-0003-squash-Report-external-auth-calls-as-wait-events.patchDownload
From 39c7d9ce42be3f0878ee3c70b9b17c3ca96f9321 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v8 3/3] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind(). (For the record, I
do not want to implement this in this way.)
---
 src/backend/libpq/auth.c                       | 18 ++++++++++++++++++
 .../utils/activity/wait_event_names.txt        |  9 +++++++++
 2 files changed, 27 insertions(+)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index f79626f019f..e42e7daa29c 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2367,7 +2367,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SET_OPTION);
 		ldap_unbind(*ldap);
+		pgstat_report_wait_end();
 		return STATUS_ERROR;
 	}
 
@@ -2387,7 +2389,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_START_TLS);
 			ldap_unbind(*ldap);
+			pgstat_report_wait_end();
 			return STATUS_ERROR;
 		}
 	}
@@ -2531,7 +2535,9 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
+				pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_NAME_CHECK);
 				ldap_unbind(ldap);
+				pgstat_report_wait_end();
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2555,7 +2561,9 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND_FOR_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2588,7 +2596,9 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2610,7 +2620,9 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_COUNT_ENTRIES);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2629,7 +2641,9 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_GET_DN);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2657,7 +2671,9 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND);
 		ldap_unbind(ldap);
+		pgstat_report_wait_end();
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2666,7 +2682,9 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_SUCCESS);
 	ldap_unbind(ldap);
+	pgstat_report_wait_end();
 	pfree(passwd);
 	pfree(fulluser);
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a2852225614..f082756c294 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -177,6 +177,15 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND_AFTER_BIND	"Waiting for an LDAP connection to be unbound after a simple bind failed."
+LDAP_UNBIND_AFTER_BIND_FOR_SEARCH	"Waiting for an LDAP connection to be unbound after a bind for search failed."
+LDAP_UNBIND_AFTER_COUNT_ENTRIES	"Waiting for an LDAP connection to be unbound after an entry count failed."
+LDAP_UNBIND_AFTER_GET_DN	"Waiting for an LDAP connection to be unbound after ldap_get_dn failed."
+LDAP_UNBIND_AFTER_NAME_CHECK	"Waiting for an LDAP connection to be unbound after a name check failed."
+LDAP_UNBIND_AFTER_SEARCH	"Waiting for an LDAP connection to be unbound after a bind+search failed."
+LDAP_UNBIND_AFTER_SET_OPTION	"Waiting for an LDAP connection to be unbound after ldap_set_option failed."
+LDAP_UNBIND_AFTER_START_TLS	"Waiting for an LDAP connection to be unbound after ldap_start_tls_s failed."
+LDAP_UNBIND_SUCCESS	"Waiting for a successful LDAP connection to be unbound."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.34.1

#35Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#34)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Feb 10, 2025 at 11:05:32AM -0800, Jacob Champion wrote:

Bad regex escaping on my part; fixed in v8. Thanks for the report!

While debugging, I also noticed that a poorly timed autovacuum could
also show up in my new pg_stat_activity query, so I've increased the
specificity.

Now this test is able to catch the reason why it has been added.
Thanks for the fix.

Allowing pgstat_bestart_security() to be reentrant worries me.

Anyway, when it comes to the fields that would show up in the catalogs
before authentication completes, are we sure that all of them are OK
and that it would not become an open door for pushing data into the
catalogs that other users could scan?

+        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);

These three for SSL are OK, they rely on a hardcoded set of values.

+        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);

But are these three actually OK to have showing up in the catalogs
this early? This data comes from a peer certificate, that we may know
nothing about, become set at a very early stage with
secure_initialize().

+        lgssstatus.gss_auth = be_gssapi_get_auth(MyProcPort);
+        lgssstatus.gss_enc = be_gssapi_get_enc(MyProcPort);
+        lgssstatus.gss_delegation = be_gssapi_get_delegation(MyProcPort);

These three are booleans, hence OK. They are set when the connection
opens, before the first call of pgstat_bestart_security(), right?

+        if (princ)
+            strlcpy(lgssstatus.gss_princ, princ, NAMEDATALEN);

This is not going to be set, being part of pg_GSS_checkauth() that
happens later on.

As a whole we still have a gap between what could be OK, what could
not be OK, and the fact that pgstat_bestart_security() is called twice
makes that confusing. Perhaps it would be OK to document what can be
set by the first call and/or the second call, but at the end it seems
that this is going to require a split, or just to move the fields that
are potentially unsafe into the final step where we know that we're
done with authentication, leaving in the security() call the fields
that we are definitely OK to have. The boolean states from the Port
copied into the backend entries are fine. The data coming from the
peer certificate when initializing the secure connection look
problematic. We should be careful.

Daniel, perhaps you could comment about the fact of showing all these
fields in the catalogs before performing any authentication? That
worries me.

v8-0003 shows this approach. For the record, I think it's materially
worse than v7-0003. IMO it increases the cognitive load for very
little benefit and makes it more work for a newcomer to refactor the
cleanup code for those routines. I think it's enough that you can see
a separate LOG message for each failure case, if you want to know why
we're unbinding.

That's more verbose, as well. As Robert said, that makes the output
easier to debug with a 1:1 mapping between the event and a code path.
--
Michael

#36Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#35)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Feb 11, 2025 at 11:23 PM Michael Paquier <michael@paquier.xyz> wrote:

+        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);

But are these three actually OK to have showing up in the catalogs
this early? This data comes from a peer certificate, that we may know
nothing about, become set at a very early stage with
secure_initialize().

I guess I'm going to zero in on your definition of "may know nothing
about". If that is true, something is very wrong IMO.

My understanding of the backend code was that port->peer is only set
after OpenSSL has verified that the certificate has been issued by an
explicitly trusted CA. (Our verify_cb() doesn't override the internal
checks to allow failed certificates through.) That may not be enough
to authorize entry into the server, but it also shouldn't be untrusted
data. If a CA is issuing Subject data that is somehow dangerous to the
operation of the server, I think that's a security problem in and of
itself: there are clientcert HBA modes that don't validate the
Subject, but they're still going to push that data into the catalogs,
aren't they?

So if we're concerned that Subject data is dangerous at this point in
the code, I agree that my patch makes it even more dangerous and I'll
modify it -- but then I'm going to split off another thread to try to
fix that underlying issue. A user should not have to be authorized to
access the server in order for signed authentication data from the
transport layer to be considered trustworthy. Being able to monitor
those separately is important for auditability.

As a whole we still have a gap between what could be OK, what could
not be OK, and the fact that pgstat_bestart_security() is called twice
makes that confusing.

My end goal is that all of this _should_ always be OK, so calling it
once or twice or thirty times should be safe. (But again, if that's
not actually true today, I'll remove it for now.)

v8-0003 shows this approach. For the record, I think it's materially
worse than v7-0003. IMO it increases the cognitive load for very
little benefit and makes it more work for a newcomer to refactor the
cleanup code for those routines. I think it's enough that you can see
a separate LOG message for each failure case, if you want to know why
we're unbinding.

That's more verbose, as well. As Robert said, that makes the output
easier to debug with a 1:1 mapping between the event and a code path.

I agree with Robert's goal in general. I just think that following
that rule to *this* extent is counterproductive. But I won't die on
that hill; in the end, I just want to be able to see when LDAP calls
hang.

Thanks!
--Jacob

#37Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#36)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Feb 13, 2025 at 09:53:52AM -0800, Jacob Champion wrote:

I guess I'm going to zero in on your definition of "may know nothing
about". If that is true, something is very wrong IMO.

My understanding of the backend code was that port->peer is only set
after OpenSSL has verified that the certificate has been issued by an
explicitly trusted CA. (Our verify_cb() doesn't override the internal
checks to allow failed certificates through.) That may not be enough
to authorize entry into the server, but it also shouldn't be untrusted
data. If a CA is issuing Subject data that is somehow dangerous to the
operation of the server, I think that's a security problem in and of
itself: there are clientcert HBA modes that don't validate the
Subject, but they're still going to push that data into the catalogs,
aren't they?

Is that the case before we finish authentication now? I was not sure
how much of this data is set before and after finishing
authentication, tracking that this was part of the init phase of the
connection, something we do earlier than the actual authentication.
It feels like this should be documented more clearly in the patch if
pgstat_bestart_security() is allowed to be called multiple times (I
think we should not allow that). That's quite unclear now in the
patch; on HEAD everything is set after authentication completes.

So if we're concerned that Subject data is dangerous at this point in
the code, I agree that my patch makes it even more dangerous and I'll
modify it -- but then I'm going to split off another thread to try to
fix that underlying issue. A user should not have to be authorized to
access the server in order for signed authentication data from the
transport layer to be considered trustworthy. Being able to monitor
those separately is important for auditability.

Making this information visible in the catalogs for already logged in
users increases the potential of problems, and this is a sensitive
area of the code, so..

As a whole we still have a gap between what could be OK, what could
not be OK, and the fact that pgstat_bestart_security() is called twice
makes that confusing.

My end goal is that all of this _should_ always be OK, so calling it
once or twice or thirty times should be safe. (But again, if that's
not actually true today, I'll remove it for now.)

The concept of making pgstat_bestart_security() callable multiple
times relates also to the issue pointed upthread by Andres, somewhat:
allowing this pattern may lead to errors in the future regarding this
that should or should not be set this early in the authentication
process. Sounds just saner to me to do pgstat_bestart_security()
once for now once we're OK with authentication, and it does not
prevent to address your initial point of being able to know if
backends with a given PID are stuck at a certain point. At least
that's a step towards more debuggability as the backend entries are
available earlier than they are now.

Getting ready for tomatoes now, please aim freely.

I agree with Robert's goal in general. I just think that following
that rule to *this* extent is counterproductive. But I won't die on
that hill; in the end, I just want to be able to see when LDAP calls
hang.

Understood.
--
Michael

#38Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#37)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Feb 13, 2025 at 4:03 PM Michael Paquier <michael@paquier.xyz> wrote:

If a CA is issuing Subject data that is somehow dangerous to the
operation of the server, I think that's a security problem in and of
itself: there are clientcert HBA modes that don't validate the
Subject, but they're still going to push that data into the catalogs,
aren't they?

Is that the case before we finish authentication now?

No, but I still don't understand why that's relevant. My point is that
transport authentication data should be neither less trustworthy prior
to ClientAuthentication, nor more trustworthy after it, since it's
signed by the same authentication provider that you're trusting to
make the authentication decisions in the first place. (But it doesn't
seem like we're going to agree on this for now; in the meantime I'll
prepare a version of the patch that only calls
pgstat_bestart_security() once.)

At some point in the future, I would really like to clarify what
potential problems there are if we put verified Subject data into the
catalogs before ClientAuthentication completes. I think that any such
problems would continue to be problems after ClientAuthentication
completes, too.

Thanks,
--Jacob

#39Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#38)
4 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Feb 14, 2025 at 5:34 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

(But it doesn't
seem like we're going to agree on this for now; in the meantime I'll
prepare a version of the patch that only calls
pgstat_bestart_security() once.)

v9 removes the first call, and moves the second (now only) call up and
out of the if/else chain, just past client authentication. The SSL
pre-auth tests have been removed.

Thanks!
--Jacob

Attachments:

since-v8.diff.txttext/plain; charset=US-ASCII; name=since-v8.diff.txtDownload
1:  81a61854bdf ! 1:  d8de39bc076 pgstat: report in earlier with STATE_STARTING
    @@ Commit message
     
         2) pgstat_bestart_security() reports the SSL/GSS status of the
            connection.  Some backends don't call this at all; others call it
    -       twice, once after transport establishment and once after client
    -       authentication.
    +       after client authentication.
     
         3) pgstat_bestart_final() reports the user and database IDs, takes the
            entry out of STATE_STARTING, and reports the application_name.
    @@ src/backend/utils/activity/backend_status.c: pgstat_bestart(void)
     +}
     +
     +/*
    -+ * Fill in SSL and GSS information for the pgstat entry. This is separate from
    -+ * pgstat_bestart_initial() so that backends may call it multiple times as
    -+ * security details are filled in.
    ++ * Fill in SSL and GSS information for the pgstat entry.
     + *
     + * This should only be called from backends with a MyProcPort.
     + */
    @@ src/backend/utils/init/postinit.c: InitPostgres(const char *in_dbname, Oid dboid
     +	if (!bootstrap)
     +	{
     +		pgstat_bestart_initial();
    -+		if (MyProcPort)
    -+			pgstat_bestart_security();	/* fill in any SSL/GSS info too */
    -+
     +		INJECTION_POINT("init-pre-auth");
     +	}
     +
    @@ src/backend/utils/init/postinit.c: InitPostgres(const char *in_dbname, Oid dboid
      		InitializeSessionUserId(username, useroid, false);
      		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
     @@ src/backend/utils/init/postinit.c: InitPostgres(const char *in_dbname, Oid dboid,
    - 			InitializeSystemUser(MyClientConnectionInfo.authn_id,
    - 								 hba_authname(MyClientConnectionInfo.auth_method));
      		am_superuser = superuser();
    -+
    -+		/*
    -+		 * Authentication may have changed SSL/GSS details for the session, so
    -+		 * report it again.
    -+		 */
    -+		pgstat_bestart_security();
      	}
      
    ++	/* Report any SSL/GSS details for the session. */
    ++	if (MyProcPort != NULL)
    ++	{
    ++		Assert(!bootstrap);
    ++
    ++		pgstat_bestart_security();
    ++	}
    ++
      	/*
    + 	 * Binary upgrades only allowed super-user connections
    + 	 */
     @@ src/backend/utils/init/postinit.c: InitPostgres(const char *in_dbname, Oid dboid,
      		/* initialize client encoding */
      		InitializeClientEncoding();
    @@ src/test/authentication/t/007_pre_auth.pl (new)
     +$conn->quit();
     +
     +done_testing();
    -
    - ## src/test/ssl/Makefile ##
    -@@
    - #-------------------------------------------------------------------------
    - 
    - EXTRA_INSTALL = contrib/sslinfo
    -+EXTRA_INSTALL += src/test/modules/injection_points
    - 
    - subdir = src/test/ssl
    - top_builddir = ../../..
    - include $(top_builddir)/src/Makefile.global
    - 
    --export OPENSSL with_ssl
    -+export OPENSSL enable_injection_points with_ssl
    - 
    - # The sslfiles targets are separated into their own file due to interactions
    - # with settings in Makefile.global.
    -
    - ## src/test/ssl/meson.build ##
    -@@ src/test/ssl/meson.build: tests += {
    -   'bd': meson.current_build_dir(),
    -   'tap': {
    -     'env': {
    -+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
    -       'with_ssl': ssl_library,
    -       'OPENSSL': openssl.found() ? openssl.path() : '',
    -     },
    -
    - ## src/test/ssl/t/001_ssltests.pl ##
    -@@ src/test/ssl/t/001_ssltests.pl: use Config qw ( %Config );
    - use PostgreSQL::Test::Cluster;
    - use PostgreSQL::Test::Utils;
    - use Test::More;
    -+use Time::HiRes qw(usleep);
    - 
    - use FindBin;
    - use lib $FindBin::RealBin;
    -@@ src/test/ssl/t/001_ssltests.pl: $node->start;
    - my $result = $node->safe_psql('postgres', "SHOW ssl_library");
    - is($result, $ssl_server->ssl_library(), 'ssl_library parameter');
    - 
    -+my $injection_points_unavailable = '';
    -+if ($ENV{enable_injection_points} ne 'yes')
    -+{
    -+	$injection_points_unavailable =
    -+	  'Injection points not supported by this build';
    -+}
    -+elsif (!$node->check_extension('injection_points'))
    -+{
    -+	$injection_points_unavailable =
    -+	  'Extension injection_points not installed';
    -+}
    -+else
    -+{
    -+	# For ease of setup, make injection_points available for all new databases.
    -+	$node->safe_psql('template1', 'CREATE EXTENSION injection_points');
    -+}
    -+
    - $ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
    - 	$SERVERHOSTCIDR, 'trust');
    - 
    -@@ src/test/ssl/t/001_ssltests.pl: command_like(
    - 				^\d+,t,TLSv[\d.]+,[\w-]+,\d+,_null_,_null_,_null_\r?$}mx,
    - 	'pg_stat_ssl view without client certificate');
    - 
    -+# Test that pg_stat_ssl gets filled in early, prior to authentication. Requires
    -+# injection point support.
    -+SKIP:
    -+{
    -+	skip $injection_points_unavailable, 1 if $injection_points_unavailable;
    -+
    -+	# Connect to the server and inject a waitpoint.
    -+	my $psql =
    -+	  $node->background_psql('trustdb', connstr => "$common_connstr user=");
    -+	$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(
    -+		'trustdb',
    -+		connstr => $common_connstr,
    -+		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' AND client_addr IS NOT NULL;");
    -+		last if $pid ne "";
    -+
    -+		usleep(100_000);
    -+	}
    -+
    -+	like(
    -+		$psql->query(
    -+			"SELECT ssl, version, cipher, bits FROM pg_stat_ssl WHERE pid = $pid"
    -+		),
    -+		qr/^t\|TLSv[\d.]+\|[\w-]+\|\d+$/,
    -+		'pg_stat_ssl view is updated prior to authentication');
    -+
    -+	# Detach the waitpoint and wait for the connection to complete.
    -+	$psql->query_safe("SELECT injection_points_wakeup('init-pre-auth');");
    -+	$conn->wait_connect();
    -+
    -+	$psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
    -+	$psql->quit();
    -+	$conn->quit();
    -+}
    -+
    - # Test min/max SSL protocol versions.
    - $node->connect_ok(
    - 	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.2",
2:  e734e46009f = 2:  fddaedf4280 Report external auth calls as wait events
3:  39c7d9ce42b = 3:  50d14d32699 squash! Report external auth calls as wait events
v9-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchapplication/octet-stream; name=v9-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchDownload
From d8de39bc0769cce3cb0c9d3d29cd528169d0a06f Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v9 1/3] pgstat: report in earlier with STATE_STARTING

Split pgstat_bestart() into three phases for better observability:

1) pgstat_bestart_initial() reports a 'starting' state while waiting for
   backend initialization and client authentication to complete.  Since
   we hold a transaction open for a good amount of that, and some
   authentication methods call out to external systems, having an early
   pg_stat_activity entry helps DBAs debug when things go badly wrong.

2) pgstat_bestart_security() reports the SSL/GSS status of the
   connection.  Some backends don't call this at all; others call it
   after client authentication.

3) pgstat_bestart_final() reports the user and database IDs, takes the
   entry out of STATE_STARTING, and reports the application_name.
   TODO: should the order of those last two be swapped?
---
 doc/src/sgml/monitoring.sgml                |   6 +
 src/backend/postmaster/auxprocess.c         |   3 +-
 src/backend/utils/activity/backend_status.c | 205 ++++++++++++--------
 src/backend/utils/adt/pgstatfuncs.c         |   3 +
 src/backend/utils/init/postinit.c           |  41 +++-
 src/include/utils/backend_status.h          |   5 +-
 src/test/authentication/Makefile            |   4 +
 src/test/authentication/meson.build         |   4 +
 src/test/authentication/t/007_pre_auth.pl   |  81 ++++++++
 9 files changed, 264 insertions(+), 88 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 9178f1d34ef..16646f560e8 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index ff366ceb0fc..4f6795f7265 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -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);
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 5f68ef26adc..d84aea1a6b0 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -253,31 +253,18 @@ pgstat_beinit(void)
 	on_shmem_exit(pgstat_beshutdown_hook, 0);
 }
 
-
-/* ----------
- * pgstat_bestart() -
- *
- *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
- *
- *	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(), once user and database
+ * IDs have been determined.
  */
 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 +284,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 +294,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 +308,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,20 +353,137 @@ 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);
+}
+
+/*
+ * Fill in SSL and GSS information for the pgstat entry.
+ *
+ * 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);
+}
+
+/*
+ * Finalizes the startup pgstat entry by filling in the user/database IDs,
+ * clearing STATE_STARTING, and reporting the application_name.
+ *
+ * We must be inside a transaction if this isn't an aux 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))
 		pgstat_create_backend(MyProcNumber);
 
 	/* Update app name to current GUC setting */
+	/* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
 	if (application_name)
 		pgstat_report_appname(application_name);
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 68830db8633..bf972c9b67d 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 318600d6d02..6ed7024a614 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -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();
+
+	/*
+	 * This is a convenient time to sketch in a partial pgstat entry. That
+	 * way, if LWLocks or third-party authentication should happen to hang,
+	 * the DBA will still be able to see what's 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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		return;
 	}
@@ -875,6 +887,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		/* normal multiuser case */
 		Assert(MyProcPort != NULL);
+
 		PerformAuthentication(MyProcPort);
 		InitializeSessionUserId(username, useroid, false);
 		/* ensure that auth_method is actually valid, aka authn_id is not NULL */
@@ -884,6 +897,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 +974,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 the PgBackendStatus array */
+		pgstat_bestart_final();
 
 		/* close the transaction we started above */
 		CommitTransactionCommand();
@@ -997,7 +1018,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		 */
 		if (!bootstrap)
 		{
-			pgstat_bestart();
+			pgstat_bestart_final();
 			CommitTransactionCommand();
 		}
 		return;
@@ -1197,9 +1218,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 the PgBackendStatus array */
 	if (!bootstrap)
-		pgstat_bestart();
+		pgstat_bestart_final();
 
 	/* close the transaction we started above */
 	if (!bootstrap)
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index d3d4ff6c5c9..1c9b4fe14d0 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -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);
 
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index c4022dc829b..8b5beced080 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index f6e48411c11..800b3a5ff40 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 00000000000..a638226dbaf
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -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();
-- 
2.34.1

v9-0002-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v9-0002-Report-external-auth-calls-as-wait-events.patchDownload
From fddaedf4280fde31e7d495b312c24133ac0b198b Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v9 2/3] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 16646f560e8..877163fb403 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 81e2f8184e3..bd8a2a098b3 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -39,6 +39,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -990,6 +991,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1001,6 +1003,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1212,6 +1215,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1221,6 +1225,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1286,6 +1292,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1295,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1392,11 +1400,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1493,8 +1503,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1509,8 +1522,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2109,7 +2124,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2122,7 +2139,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2262,7 +2281,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2356,11 +2379,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2520,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2545,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2552,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2620,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3069,8 +3104,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3118,7 +3157,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3151,8 +3193,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..6cc3e1e7c7a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f071628..a2852225614 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -162,6 +162,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index b8cb3e5a430..3d995a9e5be 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 83228cfca29..b94930658b0 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -181,6 +181,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -189,7 +190,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

v9-0003-squash-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v9-0003-squash-Report-external-auth-calls-as-wait-events.patchDownload
From 50d14d32699bfbad2e9c891b0017535f3d475249 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v9 3/3] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind(). (For the record, I
do not want to implement this in this way.)
---
 src/backend/libpq/auth.c                       | 18 ++++++++++++++++++
 .../utils/activity/wait_event_names.txt        |  9 +++++++++
 2 files changed, 27 insertions(+)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index bd8a2a098b3..34cc145dc5e 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2373,7 +2373,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SET_OPTION);
 		ldap_unbind(*ldap);
+		pgstat_report_wait_end();
 		return STATUS_ERROR;
 	}
 
@@ -2393,7 +2395,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_START_TLS);
 			ldap_unbind(*ldap);
+			pgstat_report_wait_end();
 			return STATUS_ERROR;
 		}
 	}
@@ -2537,7 +2541,9 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
+				pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_NAME_CHECK);
 				ldap_unbind(ldap);
+				pgstat_report_wait_end();
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2561,7 +2567,9 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND_FOR_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2594,7 +2602,9 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2616,7 +2626,9 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_COUNT_ENTRIES);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2635,7 +2647,9 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_GET_DN);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2663,7 +2677,9 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND);
 		ldap_unbind(ldap);
+		pgstat_report_wait_end();
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2672,7 +2688,9 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_SUCCESS);
 	ldap_unbind(ldap);
+	pgstat_report_wait_end();
 	pfree(passwd);
 	pfree(fulluser);
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a2852225614..f082756c294 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -177,6 +177,15 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND_AFTER_BIND	"Waiting for an LDAP connection to be unbound after a simple bind failed."
+LDAP_UNBIND_AFTER_BIND_FOR_SEARCH	"Waiting for an LDAP connection to be unbound after a bind for search failed."
+LDAP_UNBIND_AFTER_COUNT_ENTRIES	"Waiting for an LDAP connection to be unbound after an entry count failed."
+LDAP_UNBIND_AFTER_GET_DN	"Waiting for an LDAP connection to be unbound after ldap_get_dn failed."
+LDAP_UNBIND_AFTER_NAME_CHECK	"Waiting for an LDAP connection to be unbound after a name check failed."
+LDAP_UNBIND_AFTER_SEARCH	"Waiting for an LDAP connection to be unbound after a bind+search failed."
+LDAP_UNBIND_AFTER_SET_OPTION	"Waiting for an LDAP connection to be unbound after ldap_set_option failed."
+LDAP_UNBIND_AFTER_START_TLS	"Waiting for an LDAP connection to be unbound after ldap_start_tls_s failed."
+LDAP_UNBIND_SUCCESS	"Waiting for a successful LDAP connection to be unbound."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.34.1

#40Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#39)
1 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Feb 28, 2025 at 12:40:13PM -0800, Jacob Champion wrote:

v9 removes the first call, and moves the second (now only) call up and
out of the if/else chain, just past client authentication. The SSL
pre-auth tests have been removed.

I have put my eyes on 0001, and this version looks sensible here, just
tweaked a bit the comments after a closer lookup and adjusted a few
things, nothing huge..

/* Update app name to current GUC setting */
+ /* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
if (application_name)
pgstat_report_appname(application_name);

This has always been set last and it's still the case in the patch, so
let's just remove that.
--
Michael

Attachments:

v10-0001-pgstat-report-in-earlier-with-STATE_STARTING.patchtext/x-diff; charset=us-asciiDownload
From 301c381261c08efdcf2b129b8150db98eb101a97 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:54:58 -0700
Subject: [PATCH v10] pgstat: report in earlier with STATE_STARTING

Split pgstat_bestart() into three phases for better observability:

1) pgstat_bestart_initial() reports a 'starting' state while waiting for
   backend initialization and client authentication to complete.  Since
   we hold a transaction open for a good amount of that, and some
   authentication methods call out to external systems, having an early
   pg_stat_activity entry helps DBAs debug when things go badly wrong.

2) pgstat_bestart_security() reports the SSL/GSS status of the
   connection.  Some backends don't call this at all; others call it
   after client authentication.

3) pgstat_bestart_final() reports the user and database IDs, takes the
   entry out of STATE_STARTING, and reports the application_name.
   TODO: should the order of those last two be swapped?
---
 src/include/utils/backend_status.h          |   5 +-
 src/backend/postmaster/auxprocess.c         |   3 +-
 src/backend/utils/activity/backend_status.c | 215 +++++++++++++-------
 src/backend/utils/adt/pgstatfuncs.c         |   3 +
 src/backend/utils/init/postinit.c           |  40 +++-
 src/test/authentication/Makefile            |   4 +
 src/test/authentication/meson.build         |   4 +
 src/test/authentication/t/007_pre_auth.pl   |  81 ++++++++
 doc/src/sgml/monitoring.sgml                |   6 +
 9 files changed, 275 insertions(+), 86 deletions(-)
 create mode 100644 src/test/authentication/t/007_pre_auth.pl

diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index d3d4ff6c5c9a..1c9b4fe14d06 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -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);
 
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index ff366ceb0fc7..4f6795f72650 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -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);
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 5f68ef26adc6..1a4ca2b179ca 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -253,31 +253,23 @@ pgstat_beinit(void)
 	on_shmem_exit(pgstat_beshutdown_hook, 0);
 }
 
-
-/* ----------
- * 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 AuxiliaryProcessMainCommon().
  *
- *	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 +289,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 +299,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 +313,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,20 +358,146 @@ 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 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))
 		pgstat_create_backend(MyProcNumber);
 
 	/* Update app name to current GUC setting */
+	/* TODO: ask the list: maybe do this before setting STATE_UNDEFINED? */
 	if (application_name)
 		pgstat_report_appname(application_name);
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 0212d8d5906b..9172e1cb9d23 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 318600d6d02e..b428a59bdd26 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -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)
diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile
index c4022dc829b6..8b5beced0806 100644
--- a/src/test/authentication/Makefile
+++ b/src/test/authentication/Makefile
@@ -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)
 
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index f6e48411c116..800b3a5ff40f 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -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
index 000000000000..a638226dbaf1
--- /dev/null
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -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();
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 9178f1d34efd..16646f560e8d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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.
-- 
2.47.2

#41Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#40)
2 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Mar 03, 2025 at 02:23:51PM +0900, Michael Paquier wrote:

This has always been set last and it's still the case in the patch, so
let's just remove that.

This first one has been now applied as c76db55c9085. Attached is the
rest to add the wait events (still need to have a closer look at this
part).
--
Michael

Attachments:

v10-0001-Report-external-auth-calls-as-wait-events.patchtext/x-diff; charset=us-asciiDownload
From 75de9ca51a61f0860a11259f3624b2eb12a4cb01 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v10 1/2] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 src/include/utils/wait_event.h                |  1 +
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/test/regress/expected/sysviews.out        |  3 +-
 doc/src/sgml/monitoring.sgml                  |  8 +++
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index b8cb3e5a4309..3d995a9e5be7 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 81e2f8184e30..bd8a2a098b39 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -39,6 +39,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -990,6 +991,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1001,6 +1003,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1212,6 +1215,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1221,6 +1225,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1286,6 +1292,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1295,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1392,11 +1400,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1493,8 +1503,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1509,8 +1522,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2109,7 +2124,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2122,7 +2139,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2262,7 +2281,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2356,11 +2379,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2520,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2545,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2552,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2620,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3069,8 +3104,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3118,7 +3157,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3151,8 +3193,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a3559..6cc3e1e7c7a0 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f0716289..a28522256140 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -162,6 +162,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 83228cfca293..b94930658b0e 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -181,6 +181,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -189,7 +190,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 16646f560e8d..877163fb4036 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
-- 
2.47.2

v10-0002-squash-Report-external-auth-calls-as-wait-events.patchtext/x-diff; charset=us-asciiDownload
From a7e18bff1aa45e1e6602581cca4d5a0efe8e5d07 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v10 2/2] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind(). (For the record, I
do not want to implement this in this way.)
---
 src/backend/libpq/auth.c                       | 18 ++++++++++++++++++
 .../utils/activity/wait_event_names.txt        |  9 +++++++++
 2 files changed, 27 insertions(+)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index bd8a2a098b39..34cc145dc5e8 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2373,7 +2373,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SET_OPTION);
 		ldap_unbind(*ldap);
+		pgstat_report_wait_end();
 		return STATUS_ERROR;
 	}
 
@@ -2393,7 +2395,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_START_TLS);
 			ldap_unbind(*ldap);
+			pgstat_report_wait_end();
 			return STATUS_ERROR;
 		}
 	}
@@ -2537,7 +2541,9 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
+				pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_NAME_CHECK);
 				ldap_unbind(ldap);
+				pgstat_report_wait_end();
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2561,7 +2567,9 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND_FOR_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2594,7 +2602,9 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2616,7 +2626,9 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_COUNT_ENTRIES);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2635,7 +2647,9 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_GET_DN);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2663,7 +2677,9 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND);
 		ldap_unbind(ldap);
+		pgstat_report_wait_end();
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2672,7 +2688,9 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_SUCCESS);
 	ldap_unbind(ldap);
+	pgstat_report_wait_end();
 	pfree(passwd);
 	pfree(fulluser);
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a28522256140..f082756c294e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -177,6 +177,15 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND_AFTER_BIND	"Waiting for an LDAP connection to be unbound after a simple bind failed."
+LDAP_UNBIND_AFTER_BIND_FOR_SEARCH	"Waiting for an LDAP connection to be unbound after a bind for search failed."
+LDAP_UNBIND_AFTER_COUNT_ENTRIES	"Waiting for an LDAP connection to be unbound after an entry count failed."
+LDAP_UNBIND_AFTER_GET_DN	"Waiting for an LDAP connection to be unbound after ldap_get_dn failed."
+LDAP_UNBIND_AFTER_NAME_CHECK	"Waiting for an LDAP connection to be unbound after a name check failed."
+LDAP_UNBIND_AFTER_SEARCH	"Waiting for an LDAP connection to be unbound after a bind+search failed."
+LDAP_UNBIND_AFTER_SET_OPTION	"Waiting for an LDAP connection to be unbound after ldap_set_option failed."
+LDAP_UNBIND_AFTER_START_TLS	"Waiting for an LDAP connection to be unbound after ldap_start_tls_s failed."
+LDAP_UNBIND_SUCCESS	"Waiting for a successful LDAP connection to be unbound."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.47.2

#42Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#41)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Mar 4, 2025 at 12:51 AM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Mar 03, 2025 at 02:23:51PM +0900, Michael Paquier wrote:

This has always been set last and it's still the case in the patch, so
let's just remove that.

This first one has been now applied as c76db55c9085.

Thanks!

Attached is the
rest to add the wait events (still need to have a closer look at this
part).

Sounds good.

--Jacob

#43Andres Freund
andres@anarazel.de
In reply to: Michael Paquier (#41)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-04 17:51:17 +0900, Michael Paquier wrote:

On Mon, Mar 03, 2025 at 02:23:51PM +0900, Michael Paquier wrote:

This has always been set last and it's still the case in the patch, so
let's just remove that.

This first one has been now applied as c76db55c9085. Attached is the
rest to add the wait events (still need to have a closer look at this
part).

This seems to trigger a bunch of CI failures, e.g.:

https://cirrus-ci.com/task/5350341408980992
https://cirrus-ci.com/task/5537391798124544
https://cirrus-ci.com/task/4657439905153024

https://api.cirrus-ci.com/v1/artifact/task/5350341408980992/testrun/build/testrun/authentication/007_pre_auth/log/regress_log_007_pre_auth

[17:47:59.698](0.000s) ok 1 - authenticating connections are recorded in pg_stat_activity
[17:47:59.698](0.000s) # issuing query 5 via background psql: SELECT injection_points_wakeup('init-pre-auth');
[17:47:59.752](0.054s) # pump_until: process terminated unexpectedly when searching for "(?^:(^|\n)background_psql: QUERY_SEPARATOR 5:\r?\n)" with stream: ""
process ended prematurely at C:/cirrus/src/test/perl/PostgreSQL/Test/Utils.pm line 439.
# Postmaster PID for node "primary" is 6084

https://api.cirrus-ci.com/v1/artifact/task/5350341408980992/testrun/build/testrun/authentication/007_pre_auth/log/007_pre_auth_primary.log
2025-03-04 17:47:59.705 GMT [7624][client backend] [007_pre_auth.pl][2/9:0] LOG: statement: SELECT injection_points_wakeup('init-pre-auth');
2025-03-04 17:47:59.705 GMT [7624][client backend] [007_pre_auth.pl][2/9:0] ERROR: could not find injection point init-pre-auth to wake up
2025-03-04 17:47:59.705 GMT [7624][client backend] [007_pre_auth.pl][2/9:0] STATEMENT: SELECT injection_points_wakeup('init-pre-auth');
2025-03-04 17:47:59.706 GMT [7624][client backend] [007_pre_auth.pl][:0] LOG: disconnection: session time: 0:00:00.333 user=SYSTEM database=postgres host=[local]

Greetings,

Andres Freund

#44Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#43)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Mar 4, 2025 at 4:10 PM Andres Freund <andres@anarazel.de> wrote:

This seems to trigger a bunch of CI failures, e.g.:

https://cirrus-ci.com/task/5350341408980992
https://cirrus-ci.com/task/5537391798124544
https://cirrus-ci.com/task/4657439905153024

Hm. All Windows.

https://api.cirrus-ci.com/v1/artifact/task/5350341408980992/testrun/build/testrun/authentication/007_pre_auth/log/007_pre_auth_primary.log
2025-03-04 17:47:59.705 GMT [7624][client backend] [007_pre_auth.pl][2/9:0] LOG: statement: SELECT injection_points_wakeup('init-pre-auth');
2025-03-04 17:47:59.705 GMT [7624][client backend] [007_pre_auth.pl][2/9:0] ERROR: could not find injection point init-pre-auth to wake up

But attaching to that injection point succeeded above, for us to have
gotten to this point... Does that error message indicate that the
point itself doesn't exist, or that nothing is currently waiting?

--Jacob

#45Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#44)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Mar 4, 2025 at 4:26 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

But attaching to that injection point succeeded above, for us to have
gotten to this point... Does that error message indicate that the
point itself doesn't exist, or that nothing is currently waiting?

Looks like the latter. With the following diff I can reproduce locally:

    --- a/src/backend/utils/init/postinit.c
    +++ b/src/backend/utils/init/postinit.c
    @@ -730,6 +730,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
            if (!bootstrap)
            {
                    pgstat_bestart_initial();
    +               pg_usleep(1000000);
                    INJECTION_POINT("init-pre-auth");
            }

So I've misunderstood the API. I should have added a background
version of $node->wait_for_event(), or similar.

I'll work on a fix, but it probably won't be fast since I need to
learn more about the injection points architecture. The test may need
to be disabled, or the patch backed out, depending on how painful the
flake is for everybody.

Thanks, and sorry,
--Jacob

#46Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#45)
1 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Mar 04, 2025 at 04:53:14PM -0800, Jacob Champion wrote:

I'll work on a fix, but it probably won't be fast since I need to
learn more about the injection points architecture. The test may need
to be disabled, or the patch backed out, depending on how painful the
flake is for everybody.

Oops, missed these failures.. So we have a race condition where we
are trying to wake up a point that's not waiting yet, because there is
a small window between the moment when the backend entry is marked as
"starting" and the injection point wait.

What this is telling us is that we should change the query scanning
pg_stat_activity for a PID of a backend in 'starting' state so as we
also check the wait_event init-pre-auth, as this is reported when
using injection point waits. The attached should be enough to take
care of this race condition.
--
Michael

Attachments:

0001-Fix-race-condition-in-pre-auth-test.patchtext/x-diff; charset=us-asciiDownload
From d5c976dc995bf09c4868e0b07d4652dabcaec888 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 5 Mar 2025 13:30:43 +0900
Subject: [PATCH] Fix race condition in pre-auth test

---
 src/test/authentication/t/007_pre_auth.pl | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index a638226dbaf1..90aaea4b5a64 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -43,12 +43,14 @@ $psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
 # authentication. Use the $psql connection handle for server interaction.
 my $conn = $node->background_psql('postgres', wait => 0);
 
-# Wait for the connection to show up.
+# Wait for the connection to show up in pg_stat_activity, with the wait_event
+# of the injection point.
 my $pid;
 while (1)
 {
 	$pid = $psql->query(
-		"SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
+		qq{SELECT pid FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
 	last if $pid ne "";
 
 	usleep(100_000);
-- 
2.47.2

#47Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#46)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Tue, Mar 4, 2025 at 8:45 PM Michael Paquier <michael@paquier.xyz> wrote:

What this is telling us is that we should change the query scanning
pg_stat_activity for a PID of a backend in 'starting' state so as we
also check the wait_event init-pre-auth, as this is reported when
using injection point waits. The attached should be enough to take
care of this race condition.

That's a lot easier than the rabbit hole I was running down; thank you.

- "SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
+ qq{SELECT pid FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});

I had intended for this part of the patch to also wait for client
backends only (see v8-0001, 001_ssltests.pl), but I must have
misapplied it. (The ssltests change was lost completely when that file
was dropped from the set.) So while we're at it, should we add a
`backend_type = 'client backend'` filter to stop that from flaking in
the future? That would further align this query with the
wait_for_event() implementation.

Thanks,
--Jacob

#48Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#47)
2 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Mar 5, 2025 at 5:47 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

So while we're at it, should we add a
`backend_type = 'client backend'` filter to stop that from flaking in
the future? That would further align this query with the
wait_for_event() implementation.

More concretely: here's a squashable patchset with that suggestion,
for the CI to chew on.

--Jacob

Attachments:

0001-Fix-race-condition-in-pre-auth-test.patchapplication/octet-stream; name=0001-Fix-race-condition-in-pre-auth-test.patchDownload
From efc9fc3b3993601e9611131f229fbcaf4daa46f1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 5 Mar 2025 13:30:43 +0900
Subject: [PATCH 1/2] Fix race condition in pre-auth test

---
 src/test/authentication/t/007_pre_auth.pl | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index a638226dbaf..90aaea4b5a6 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -43,12 +43,14 @@ $psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
 # authentication. Use the $psql connection handle for server interaction.
 my $conn = $node->background_psql('postgres', wait => 0);
 
-# Wait for the connection to show up.
+# Wait for the connection to show up in pg_stat_activity, with the wait_event
+# of the injection point.
 my $pid;
 while (1)
 {
 	$pid = $psql->query(
-		"SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
+		qq{SELECT pid FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
 	last if $pid ne "";
 
 	usleep(100_000);
-- 
2.34.1

0002-squash-Fix-race-condition-in-pre-auth-test.patchapplication/octet-stream; name=0002-squash-Fix-race-condition-in-pre-auth-test.patchDownload
From 6ef54e90ec5eeb92ec8bd857f6d6f35578f058b9 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Wed, 5 Mar 2025 08:13:22 -0800
Subject: [PATCH 2/2] squash! Fix race condition in pre-auth test

Wait only for client backends.
---
 src/test/authentication/t/007_pre_auth.pl | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index 90aaea4b5a6..12e40dc722c 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -50,7 +50,9 @@ while (1)
 {
 	$pid = $psql->query(
 		qq{SELECT pid FROM pg_stat_activity
-  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
+  WHERE backend_type = 'client backend'
+    AND state = 'starting'
+    AND wait_event = 'init-pre-auth';});
 	last if $pid ne "";
 
 	usleep(100_000);
-- 
2.34.1

#49Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#48)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-05 08:16:45 -0800, Jacob Champion wrote:

From efc9fc3b3993601e9611131f229fbcaf4daa46f1 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 5 Mar 2025 13:30:43 +0900
Subject: [PATCH 1/2] Fix race condition in pre-auth test

---
src/test/authentication/t/007_pre_auth.pl | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index a638226dbaf..90aaea4b5a6 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -43,12 +43,14 @@ $psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
# authentication. Use the $psql connection handle for server interaction.
my $conn = $node->background_psql('postgres', wait => 0);
-# Wait for the connection to show up.
+# Wait for the connection to show up in pg_stat_activity, with the wait_event
+# of the injection point.
my $pid;
while (1)
{
$pid = $psql->query(
-		"SELECT pid FROM pg_stat_activity WHERE state = 'starting';");
+		qq{SELECT pid FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
last if $pid ne "";

Unrelated to the change in this patch, but tests really shouldn't use while(1)
loops without a termination condition. If something is wrong, the test will
hang indefinitely, instead of timing out. On the buildfarm that can take out
an animal if it hasn't configured a timeout (with autoconf at least, meson
terminates tests after a timeout).

I guess you can't use poll_query_until() here, but in that case you should
copy some of the timeout logic. Or, perhaps better, add a poll_query_until()
to BackgroundPsql.pm.

Greetings,

Andres Freund

#50Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#49)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Mar 5, 2025 at 9:28 AM Andres Freund <andres@anarazel.de> wrote:

Unrelated to the change in this patch, but tests really shouldn't use while(1)
loops without a termination condition. If something is wrong, the test will
hang indefinitely, instead of timing out. On the buildfarm that can take out
an animal if it hasn't configured a timeout (with autoconf at least, meson
terminates tests after a timeout).

With the current patchset, if I pull the PG_TEST_TIMEOUT_DEFAULT down
low, and modify the backend so that either one of the two conditions
never completes, the tests still stop due to BackgroundPsql's session
timeout. This is true for Meson and Autoconf. Is there a different
situation where I can't rely on that?

Thanks!
--Jacob

#51Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#50)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-05 16:19:04 -0800, Jacob Champion wrote:

On Wed, Mar 5, 2025 at 9:28 AM Andres Freund <andres@anarazel.de> wrote:

Unrelated to the change in this patch, but tests really shouldn't use while(1)
loops without a termination condition. If something is wrong, the test will
hang indefinitely, instead of timing out. On the buildfarm that can take out
an animal if it hasn't configured a timeout (with autoconf at least, meson
terminates tests after a timeout).

With the current patchset, if I pull the PG_TEST_TIMEOUT_DEFAULT down
low, and modify the backend so that either one of the two conditions
never completes, the tests still stop due to BackgroundPsql's session
timeout. This is true for Meson and Autoconf. Is there a different
situation where I can't rely on that?

Oh, I see. I missed that it's relying on the timeout and that the timeout
isn't reset in the loop. Sorry for the noise.

FWIW, I still don't like open-coded poll loops, as I'd really like our tests
to use some smarter retry/backoff logic than a single hardcoded
usleep(100_000).

The first few iterations that's too long, commonly the state isn't reached
in the first iteration, but will be within a millisecond or two. Waiting
100ms is obviously way too long.

Once we've slept for 10+ seconds without reaching the target, sleeping for
100us is way too short a sleep and just wastes CPU cycles. A decent portion
of the CPU time when running under valgrind is wasted just due to way too
tight retry loops.

That's harder to do if we have many places polling.

But anyway, I digress, that's really not related to your change.

Greetings,

Andres Freund

#52Michael Paquier
michael@paquier.xyz
In reply to: Andres Freund (#51)
1 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Mar 05, 2025 at 08:55:55PM -0500, Andres Freund wrote:

Once we've slept for 10+ seconds without reaching the target, sleeping for
100us is way too short a sleep and just wastes CPU cycles. A decent portion
of the CPU time when running under valgrind is wasted just due to way too
tight retry loops.

That's harder to do if we have many places polling.

But anyway, I digress, that's really not related to your change.

Please let me agree with your previous argument, then. While looking
at the test when reviewing the patch a couple of days ago, I was also
wondering why we could not have a poll_query_until() in BackgroundPsql
and gave up on the idea.

Honestly, I don't see a reason not to introduce that, like in the
attached. BackgroundPsql->query() does all the job already, and it is
possible to rely on $PostgreSQL::Test::Utils::timeout_default in the
loops, so that's simple, and it makes the test a bit easier to parse.
--
Michael

Attachments:

v2-0001-Fix-race-condition-in-pre-auth-test.patchtext/x-diff; charset=us-asciiDownload
From 8caea1d434aa1cbd6f1da777b89dd4895a88b44f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 6 Mar 2025 13:06:05 +0900
Subject: [PATCH v2] Fix race condition in pre-auth test

---
 src/test/authentication/t/007_pre_auth.pl     | 36 ++++++---------
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    | 44 +++++++++++++++++++
 2 files changed, 57 insertions(+), 23 deletions(-)

diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index a638226dbaf1..fb4241cac4ab 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -43,36 +43,26 @@ $psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
 # 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 "";
+# Wait for the connection to show up in pg_stat_activity, with the wait_event
+# of the injection point.
+my $res = $psql->poll_query_until(
+	qq{SELECT count(pid) > 0 FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
+ok($res, 'authenticating connections are recorded in pg_stat_activity');
 
-	usleep(100_000);
-}
-
-note "backend $pid is authenticating";
-ok(1, 'authenticating connections are recorded in pg_stat_activity');
+# Get the PID of the backend waiting, for the next checks.
+my $pid = $psql->query(
+	qq{SELECT pid FROM pg_stat_activity
+  WHERE state = 'starting' and wait_event = 'init-pre-auth';});
 
 # 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');
+$res = $psql->poll_query_until(
+	qq{SELECT state FROM pg_stat_activity WHERE pid = $pid;}, 'idle');
+ok($res, 'authenticated connections reach idle state in pg_stat_activity');
 
 $psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
 $psql->quit();
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index c611a61cf4e6..83ecf8b3e720 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -61,6 +61,7 @@ use Config;
 use IPC::Run;
 use PostgreSQL::Test::Utils qw(pump_until);
 use Test::More;
+use Time::HiRes qw(usleep);
 
 =pod
 
@@ -172,6 +173,49 @@ sub wait_connect
 	die "psql startup timed out" if $self->{timeout}->is_expired;
 }
 
+
+=pod
+
+=item $session->poll_query_until($query [, $expected ])
+
+Run B<$query> repeatedly, until it returns the B<$expected> result
+('t', or SQL boolean true, by default).
+Continues polling if B<query> returns an error result.
+
+Times out after $PostgreSQL::Test::Utils::timeout_default seconds.
+
+Returns 1 if successful, 0 if timed out.
+
+=cut
+
+sub poll_query_until
+{
+	my ($self, $query, $expected) = @_;
+
+	$expected = 't' unless defined($expected);    # default value
+
+	my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default;
+	my $attempts = 0;
+
+	while ($attempts < $max_attempts)
+	{
+		my $ret = $self->query($query);
+
+		if ($ret eq $expected)
+		{
+			return 1;
+		}
+
+		# Wait 0.1 second before retrying.
+		usleep(100_000);
+		$attempts++;
+	}
+
+	# Give up.  The output of the last attempt is logged by query(),
+	# so no need to do anything here.
+	return 0;
+}
+
 =pod
 
 =item $session->quit
-- 
2.47.2

#53Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#52)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Mar 5, 2025 at 8:08 PM Michael Paquier <michael@paquier.xyz> wrote:

Honestly, I don't see a reason not to introduce that, like in the
attached.

This new code races against the session timeout. I see this on timer expiration:

[14:19:55.224](0.000s) # issuing query 34 via background psql:
SELECT state FROM pg_stat_activity WHERE pid = ;
[14:19:55.228](0.004s) # pump_until: process terminated
unexpectedly when searching for "(?^:(^|\n)background_psql:
QUERY_SEPARATOR 34:\r?\n)" with stream: ""
process ended prematurely at
/home/jacob/src/postgres/src/test/perl/PostgreSQL/Test/Utils.pm line
439.

Which makes it seem like some sort of crash, IMO. I don't find that as
easily debuggable as the previous log message, which was

[14:21:33.104](0.001s) # issuing query 32 via background psql:
SELECT pid FROM pg_stat_activity
# WHERE state = 'starting' and wait_event = 'init-pre-auth';
IPC::Run: timeout on timer #1 at
/home/jacob/perl5/lib/perl5/IPC/Run.pm line 3007.

+ WHERE state = 'starting' and wait_event = 'init-pre-auth';});

Did you have thoughts on expanding the check to backend_type [1]/messages/by-id/CAOYmi+nxNCQcTQE-tQ7Lwpe4cYc1u-yxwEe5kt2AVN+DXXVVbQ@mail.gmail.com?

+ # Give up.  The output of the last attempt is logged by query(),
+ # so no need to do anything here.
+ return 0;

One of my primary complaints about the poll_query_until()
implementation is that "giving up" in this case means continuing to
run pieces of the test that have no business running after a timeout,
and increasing the log noise after a failure. I'm not sure how loudly
to complain in this particular case, since I know we use it
elsewhere...

Thanks!
--Jacob

[1]: /messages/by-id/CAOYmi+nxNCQcTQE-tQ7Lwpe4cYc1u-yxwEe5kt2AVN+DXXVVbQ@mail.gmail.com

#54Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#53)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Mar 06, 2025 at 02:25:07PM -0800, Jacob Champion wrote:

On Wed, Mar 5, 2025 at 8:08 PM Michael Paquier <michael@paquier.xyz> wrote:

+ WHERE state = 'starting' and wait_event = 'init-pre-auth';});

Did you have thoughts on expanding the check to backend_type [1]?

+ # Give up.  The output of the last attempt is logged by query(),
+ # so no need to do anything here.
+ return 0;

One of my primary complaints about the poll_query_until()
implementation is that "giving up" in this case means continuing to
run pieces of the test that have no business running after a timeout,
and increasing the log noise after a failure. I'm not sure how loudly
to complain in this particular case, since I know we use it
elsewhere...

Indeed. The existing poll_query_until() is a bit more reliable in
terms of error handling, even with a very low PG_TEST_TIMEOUT_DEFAULT.

A second thing that was bugging me on a second lookup this morning is
how we should handle error cases. A background psql process depends
on what the caller defines for ON_ERROR_STOP. In the case of this
test, we're OK to fail immediately because we expect the queries to
always work. I'm not sure if this is fine by default, especially if
callers of this routine expect to have the same properties as
poll_query_until() in Cluster.pm. They would not, because a
BackgroundPsql is an entire different object, except if given options
when staring psql to act like that.

I have applied the simplest patch for now, to silence the failures in
the CI, and included your suggestion to add a check on the
backend_type for the extra safety it offers.

I'd like the addition of the poll_query_until() in the long-term, but
I'm really not sure if the semantics would be right this way under a
background psql. In the auth 007 test, they would be OK, but it could
be surprising if we have other callers that begin relying on it.
--
Michael

#55Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Michael Paquier (#54)
2 attachment(s)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Mar 6, 2025 at 3:15 PM Michael Paquier <michael@paquier.xyz> wrote:

I have applied the simplest patch for now, to silence the failures in
the CI, and included your suggestion to add a check on the
backend_type for the extra safety it offers.

Thanks! Initial CI run looks green, so that's a good start.

I've reattached the wait event patches, to get the cfbot back to where it was.

I'd like the addition of the poll_query_until() in the long-term, but
I'm really not sure if the semantics would be right this way under a
background psql. In the auth 007 test, they would be OK, but it could
be surprising if we have other callers that begin relying on it.

Yeah, that API is definitely subtle.

--Jacob

Attachments:

v11-0001-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v11-0001-Report-external-auth-calls-as-wait-events.patchDownload
From 0dc8e12758a1e7bdde7e5e4e60570b8b4f75d240 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 3 May 2024 15:58:23 -0700
Subject: [PATCH v11 1/2] Report external auth calls as wait events

Introduce a new "Auth" wait class for various external authentication
systems, to make it obvious what's going wrong if one of those systems
hangs. Each new wait event is unique in order to more easily pinpoint
problematic locations in the code.

Discussion: https://postgr.es/m/CAOYmi%2B%3D60deN20WDyCoHCiecgivJxr%3D98s7s7-C8SkXwrCfHXg%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                  |  8 +++
 src/backend/libpq/auth.c                      | 56 +++++++++++++++++--
 src/backend/utils/activity/wait_event.c       | 11 ++++
 .../utils/activity/wait_event_names.txt       | 27 +++++++++
 src/include/utils/wait_event.h                |  1 +
 src/test/regress/expected/sysviews.out        |  3 +-
 6 files changed, 100 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 16646f560e8..877163fb403 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1045,6 +1045,14 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
        see <xref linkend="wait-event-activity-table"/>.
       </entry>
      </row>
+     <row>
+      <entry><literal>Auth</literal></entry>
+      <entry>The server process is waiting for an external system to
+       authenticate and/or authorize the client connection.
+       <literal>wait_event</literal> will identify the specific wait point;
+       see <xref linkend="wait-event-auth-table"/>.
+      </entry>
+     </row>
      <row>
       <entry><literal>BufferPin</literal></entry>
       <entry>The server process is waiting for exclusive access to
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 81e2f8184e3..bd8a2a098b3 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -39,6 +39,7 @@
 #include "replication/walsender.h"
 #include "storage/ipc.h"
 #include "utils/memutils.h"
+#include "utils/wait_event.h"
 
 /*----------------------------------------------------------------
  * Global authentication functions
@@ -990,6 +991,7 @@ pg_GSS_recvauth(Port *port)
 		elog(DEBUG4, "processing received GSS token of length %u",
 			 (unsigned int) gbuf.length);
 
+		pgstat_report_wait_start(WAIT_EVENT_GSSAPI_ACCEPT_SEC_CONTEXT);
 		maj_stat = gss_accept_sec_context(&min_stat,
 										  &port->gss->ctx,
 										  port->gss->cred,
@@ -1001,6 +1003,7 @@ pg_GSS_recvauth(Port *port)
 										  &gflags,
 										  NULL,
 										  pg_gss_accept_delegation ? &delegated_creds : NULL);
+		pgstat_report_wait_end();
 
 		/* gbuf no longer used */
 		pfree(buf.data);
@@ -1212,6 +1215,7 @@ pg_SSPI_recvauth(Port *port)
 	/*
 	 * Acquire a handle to the server credentials.
 	 */
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_ACQUIRE_CREDENTIALS_HANDLE);
 	r = AcquireCredentialsHandle(NULL,
 								 "negotiate",
 								 SECPKG_CRED_INBOUND,
@@ -1221,6 +1225,8 @@ pg_SSPI_recvauth(Port *port)
 								 NULL,
 								 &sspicred,
 								 &expiry);
+	pgstat_report_wait_end();
+
 	if (r != SEC_E_OK)
 		pg_SSPI_error(ERROR, _("could not acquire SSPI credentials"), r);
 
@@ -1286,6 +1292,7 @@ pg_SSPI_recvauth(Port *port)
 		elog(DEBUG4, "processing received SSPI token of length %u",
 			 (unsigned int) buf.len);
 
+		pgstat_report_wait_start(WAIT_EVENT_SSPI_ACCEPT_SECURITY_CONTEXT);
 		r = AcceptSecurityContext(&sspicred,
 								  sspictx,
 								  &inbuf,
@@ -1295,6 +1302,7 @@ pg_SSPI_recvauth(Port *port)
 								  &outbuf,
 								  &contextattr,
 								  NULL);
+		pgstat_report_wait_end();
 
 		/* input buffer no longer used */
 		pfree(buf.data);
@@ -1392,11 +1400,13 @@ pg_SSPI_recvauth(Port *port)
 
 	CloseHandle(token);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_LOOKUP_ACCOUNT_SID);
 	if (!LookupAccountSid(NULL, tokenuser->User.Sid, accountname, &accountnamesize,
 						  domainname, &domainnamesize, &accountnameuse))
 		ereport(ERROR,
 				(errmsg_internal("could not look up account SID: error code %lu",
 								 GetLastError())));
+	pgstat_report_wait_end();
 
 	free(tokenuser);
 
@@ -1493,8 +1503,11 @@ pg_SSPI_make_upn(char *accountname,
 	 */
 
 	samname = psprintf("%s\\%s", domainname, accountname);
+
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						NULL, &upnamesize);
+	pgstat_report_wait_end();
 
 	if ((!res && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
 		|| upnamesize == 0)
@@ -1509,8 +1522,10 @@ pg_SSPI_make_upn(char *accountname,
 	/* upnamesize includes the terminating NUL. */
 	upname = palloc(upnamesize);
 
+	pgstat_report_wait_start(WAIT_EVENT_SSPI_TRANSLATE_NAME);
 	res = TranslateName(samname, NameSamCompatible, NameUserPrincipal,
 						upname, &upnamesize);
+	pgstat_report_wait_end();
 
 	pfree(samname);
 	if (res)
@@ -2109,7 +2124,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_AUTHENTICATE);
 	retval = pam_authenticate(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2122,7 +2139,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password)
 		return pam_no_password ? STATUS_EOF : STATUS_ERROR;
 	}
 
+	pgstat_report_wait_start(WAIT_EVENT_PAM_ACCT_MGMT);
 	retval = pam_acct_mgmt(pamh, 0);
+	pgstat_report_wait_end();
 
 	if (retval != PAM_SUCCESS)
 	{
@@ -2262,7 +2281,11 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 			}
 
 			/* Look up a list of LDAP server hosts and port numbers */
-			if (ldap_domain2hostlist(domain, &hostlist))
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_HOST_LOOKUP);
+			r = ldap_domain2hostlist(domain, &hostlist);
+			pgstat_report_wait_end();
+
+			if (r)
 			{
 				ereport(LOG,
 						(errmsg("LDAP authentication could not find DNS SRV records for \"%s\"",
@@ -2356,11 +2379,15 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 
 	if (port->hba->ldaptls)
 	{
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_START_TLS);
 #ifndef WIN32
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL);
 #else
-		if ((r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL)) != LDAP_SUCCESS)
+		r = ldap_start_tls_s(*ldap, NULL, NULL, NULL, NULL);
 #endif
+		pgstat_report_wait_end();
+
+		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
 					(errmsg("could not start LDAP TLS session: %s",
@@ -2520,9 +2547,12 @@ CheckLDAPAuth(Port *port)
 		 * Bind with a pre-defined username/password (if available) for
 		 * searching. If none is specified, this turns into an anonymous bind.
 		 */
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND_FOR_SEARCH);
 		r = ldap_simple_bind_s(ldap,
 							   port->hba->ldapbinddn ? port->hba->ldapbinddn : "",
 							   port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : "");
+		pgstat_report_wait_end();
+
 		if (r != LDAP_SUCCESS)
 		{
 			ereport(LOG,
@@ -2545,6 +2575,8 @@ CheckLDAPAuth(Port *port)
 			filter = psprintf("(uid=%s)", port->user_name);
 
 		search_message = NULL;
+
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_SEARCH);
 		r = ldap_search_s(ldap,
 						  port->hba->ldapbasedn,
 						  port->hba->ldapscope,
@@ -2552,6 +2584,7 @@ CheckLDAPAuth(Port *port)
 						  attributes,
 						  0,
 						  &search_message);
+		pgstat_report_wait_end();
 
 		if (r != LDAP_SUCCESS)
 		{
@@ -2620,7 +2653,9 @@ CheckLDAPAuth(Port *port)
 							port->user_name,
 							port->hba->ldapsuffix ? port->hba->ldapsuffix : "");
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_BIND);
 	r = ldap_simple_bind_s(ldap, fulluser, passwd);
+	pgstat_report_wait_end();
 
 	if (r != LDAP_SUCCESS)
 	{
@@ -3069,8 +3104,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		return STATUS_ERROR;
 	}
 
-	if (sendto(sock, radius_buffer, packetlength, 0,
-			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0)
+	pgstat_report_wait_start(WAIT_EVENT_RADIUS_SENDTO);
+	r = sendto(sock, radius_buffer, packetlength, 0,
+			   serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen);
+	pgstat_report_wait_end();
+
+	if (r < 0)
 	{
 		ereport(LOG,
 				(errmsg("could not send RADIUS packet: %m")));
@@ -3118,7 +3157,10 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		FD_ZERO(&fdset);
 		FD_SET(sock, &fdset);
 
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_WAIT);
 		r = select(sock + 1, &fdset, NULL, NULL, &timeout);
+		pgstat_report_wait_end();
+
 		if (r < 0)
 		{
 			if (errno == EINTR)
@@ -3151,8 +3193,12 @@ PerformRadiusTransaction(const char *server, const char *secret, const char *por
 		 */
 
 		addrsize = sizeof(remoteaddr);
+
+		pgstat_report_wait_start(WAIT_EVENT_RADIUS_RECVFROM);
 		packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0,
 								(struct sockaddr *) &remoteaddr, &addrsize);
+		pgstat_report_wait_end();
+
 		if (packetlength < 0)
 		{
 			ereport(LOG,
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..6cc3e1e7c7a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -34,6 +34,7 @@ static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
+static const char *pgstat_get_wait_auth(WaitEventAuth w);
 
 
 static uint32 local_my_wait_event_info;
@@ -413,6 +414,9 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 		case PG_WAIT_INJECTIONPOINT:
 			event_type = "InjectionPoint";
 			break;
+		case PG_WAIT_AUTH:
+			event_type = "Auth";
+			break;
 		default:
 			event_type = "???";
 			break;
@@ -495,6 +499,13 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_io(w);
 				break;
 			}
+		case PG_WAIT_AUTH:
+			{
+				WaitEventAuth w = (WaitEventAuth) wait_event_info;
+
+				event_name = pgstat_get_wait_auth(w);
+				break;
+			}
 		default:
 			event_name = "unknown wait event";
 			break;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index e199f071628..a2852225614 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -162,6 +162,33 @@ XACT_GROUP_UPDATE	"Waiting for the group leader to update transaction status at
 
 ABI_compatibility:
 
+#
+# Wait Events - Auth
+#
+# Use this category when a process is waiting for a third party to
+# authenticate/authorize the user.
+#
+
+Section: ClassName - WaitEventAuth
+
+GSSAPI_ACCEPT_SEC_CONTEXT	"Waiting for a response from a Kerberos server via GSSAPI."
+LDAP_BIND	"Waiting for an LDAP bind operation to authenticate the user."
+LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory."
+LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
+LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
+LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
+PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
+RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
+RADIUS_SENDTO	"Waiting for a <function>sendto</function> call during a RADIUS transaction."
+RADIUS_WAIT	"Waiting for a RADIUS server to respond."
+SSPI_ACCEPT_SECURITY_CONTEXT	"Waiting for a Windows security provider to accept the client's SSPI token."
+SSPI_ACQUIRE_CREDENTIALS_HANDLE	"Waiting for a Windows security provider to acquire server credentials for SSPI."
+SSPI_LOOKUP_ACCOUNT_SID	"Waiting for Windows to find the user's security identifier."
+SSPI_TRANSLATE_NAME	"Waiting for Windows to translate a Kerberos UPN."
+
+ABI_compatibility:
+
 #
 # Wait Events - Timeout
 #
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index b8cb3e5a430..3d995a9e5be 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -25,6 +25,7 @@
 #define PG_WAIT_TIMEOUT				0x09000000U
 #define PG_WAIT_IO					0x0A000000U
 #define PG_WAIT_INJECTIONPOINT		0x0B000000U
+#define PG_WAIT_AUTH				0x0C000000U
 
 /* enums for wait events */
 #include "utils/wait_event_types.h"
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 83228cfca29..b94930658b0 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -181,6 +181,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
    type    | ok 
 -----------+----
  Activity  | t
+ Auth      | t
  BufferPin | t
  Client    | t
  Extension | t
@@ -189,7 +190,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events
  LWLock    | t
  Lock      | t
  Timeout   | t
-(9 rows)
+(10 rows)
 
 -- Test that the pg_timezone_names and pg_timezone_abbrevs views are
 -- more-or-less working.  We can't test their contents in any great detail
-- 
2.34.1

v11-0002-squash-Report-external-auth-calls-as-wait-events.patchapplication/octet-stream; name=v11-0002-squash-Report-external-auth-calls-as-wait-events.patchDownload
From 15dd9dfcded95024f6e148d4a2a216ebb3e7ed1b Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 8 Nov 2024 14:19:26 -0800
Subject: [PATCH v11 2/2] squash! Report external auth calls as wait events

Add a wait event around all calls to ldap_unbind(). (For the record, I
do not want to implement this in this way.)
---
 src/backend/libpq/auth.c                       | 18 ++++++++++++++++++
 .../utils/activity/wait_event_names.txt        |  9 +++++++++
 2 files changed, 27 insertions(+)

diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index bd8a2a098b3..34cc145dc5e 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2373,7 +2373,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 				(errmsg("could not set LDAP protocol version: %s",
 						ldap_err2string(r)),
 				 errdetail_for_ldap(*ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SET_OPTION);
 		ldap_unbind(*ldap);
+		pgstat_report_wait_end();
 		return STATUS_ERROR;
 	}
 
@@ -2393,7 +2395,9 @@ InitializeLDAPConnection(Port *port, LDAP **ldap)
 					(errmsg("could not start LDAP TLS session: %s",
 							ldap_err2string(r)),
 					 errdetail_for_ldap(*ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_START_TLS);
 			ldap_unbind(*ldap);
+			pgstat_report_wait_end();
 			return STATUS_ERROR;
 		}
 	}
@@ -2537,7 +2541,9 @@ CheckLDAPAuth(Port *port)
 			{
 				ereport(LOG,
 						(errmsg("invalid character in user name for LDAP authentication")));
+				pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_NAME_CHECK);
 				ldap_unbind(ldap);
+				pgstat_report_wait_end();
 				pfree(passwd);
 				return STATUS_ERROR;
 			}
@@ -2561,7 +2567,9 @@ CheckLDAPAuth(Port *port)
 							server_name,
 							ldap_err2string(r)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND_FOR_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			return STATUS_ERROR;
 		}
@@ -2594,7 +2602,9 @@ CheckLDAPAuth(Port *port)
 					 errdetail_for_ldap(ldap)));
 			if (search_message != NULL)
 				ldap_msgfree(search_message);
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_SEARCH);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			return STATUS_ERROR;
@@ -2616,7 +2626,9 @@ CheckLDAPAuth(Port *port)
 										  count,
 										  filter, server_name, count)));
 
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_COUNT_ENTRIES);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2635,7 +2647,9 @@ CheckLDAPAuth(Port *port)
 							filter, server_name,
 							ldap_err2string(error)),
 					 errdetail_for_ldap(ldap)));
+			pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_GET_DN);
 			ldap_unbind(ldap);
+			pgstat_report_wait_end();
 			pfree(passwd);
 			pfree(filter);
 			ldap_msgfree(search_message);
@@ -2663,7 +2677,9 @@ CheckLDAPAuth(Port *port)
 				(errmsg("LDAP login failed for user \"%s\" on server \"%s\": %s",
 						fulluser, server_name, ldap_err2string(r)),
 				 errdetail_for_ldap(ldap)));
+		pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_AFTER_BIND);
 		ldap_unbind(ldap);
+		pgstat_report_wait_end();
 		pfree(passwd);
 		pfree(fulluser);
 		return STATUS_ERROR;
@@ -2672,7 +2688,9 @@ CheckLDAPAuth(Port *port)
 	/* Save the original bind DN as the authenticated identity. */
 	set_authn_id(port, fulluser);
 
+	pgstat_report_wait_start(WAIT_EVENT_LDAP_UNBIND_SUCCESS);
 	ldap_unbind(ldap);
+	pgstat_report_wait_end();
 	pfree(passwd);
 	pfree(fulluser);
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index a2852225614..f082756c294 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -177,6 +177,15 @@ LDAP_BIND_FOR_SEARCH	"Waiting for an LDAP bind operation to search the directory
 LDAP_HOST_LOOKUP	"Waiting for DNS lookup of LDAP servers."
 LDAP_SEARCH	"Waiting for an LDAP search operation to complete."
 LDAP_START_TLS	"Waiting for an LDAP StartTLS exchange."
+LDAP_UNBIND_AFTER_BIND	"Waiting for an LDAP connection to be unbound after a simple bind failed."
+LDAP_UNBIND_AFTER_BIND_FOR_SEARCH	"Waiting for an LDAP connection to be unbound after a bind for search failed."
+LDAP_UNBIND_AFTER_COUNT_ENTRIES	"Waiting for an LDAP connection to be unbound after an entry count failed."
+LDAP_UNBIND_AFTER_GET_DN	"Waiting for an LDAP connection to be unbound after ldap_get_dn failed."
+LDAP_UNBIND_AFTER_NAME_CHECK	"Waiting for an LDAP connection to be unbound after a name check failed."
+LDAP_UNBIND_AFTER_SEARCH	"Waiting for an LDAP connection to be unbound after a bind+search failed."
+LDAP_UNBIND_AFTER_SET_OPTION	"Waiting for an LDAP connection to be unbound after ldap_set_option failed."
+LDAP_UNBIND_AFTER_START_TLS	"Waiting for an LDAP connection to be unbound after ldap_start_tls_s failed."
+LDAP_UNBIND_SUCCESS	"Waiting for a successful LDAP connection to be unbound."
 PAM_ACCT_MGMT	"Waiting for the PAM service to validate the user account."
 PAM_AUTHENTICATE	"Waiting for the PAM service to authenticate the user."
 RADIUS_RECVFROM	"Waiting for a <function>recvfrom</function> call during a RADIUS transaction."
-- 
2.34.1

#56Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#55)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-06 15:39:44 -0800, Jacob Champion wrote:

I've reattached the wait event patches, to get the cfbot back to where it was.

FWIW, I continue to think that this is a misuse of wait events. We shouldn't
use them as a poor man's general purpose tracing framework.

Greetings,

Andres Freund

#57Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#56)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Mar 7, 2025 at 8:38 AM Andres Freund <andres@anarazel.de> wrote:

FWIW, I continue to think that this is a misuse of wait events. We shouldn't
use them as a poor man's general purpose tracing framework.

Well, okay. That's frustrating.

If I return to the original design, but replace all of the high-level
wait events with calls to pgstat_report_activity(), does that work?

--Jacob

#58Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#57)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-07 09:03:18 -0800, Jacob Champion wrote:

On Fri, Mar 7, 2025 at 8:38 AM Andres Freund <andres@anarazel.de> wrote:

FWIW, I continue to think that this is a misuse of wait events. We shouldn't
use them as a poor man's general purpose tracing framework.

Well, okay. That's frustrating.

I should have clarified - there are a few that I think are ok, basically the
places where we wrap syscalls, e.g. around the sendto, select and recvfrom in
PerformRadiusTransaction().

OTOH that code is effectively completely broken. Doing a blocking select() is
just a no-go, the code isn't interruptible, breaking authentication
timeout. And using select() means that we theoretically could crash due to an
fd that's above FD_SETSIZE.

Most of the other places I'm not on board with, that's wrapping large amounts
of code in a wait event, which pretty much means we're not waiting.

I think some of the wrapped calls into library code might actually call back
into our code (to receive/send data), and our code then will use wait events
around lower level operations done as part of that.

Which pretty much explains my main issue with this - either the code can't
wait in those function calls, in which case it's wrong to use wait events, or
the code is flat out broken.

It's also IMO quite wrong to do something that can throw an error inside a
wait event, because that means that the wait event will still be reported
during error recovery. Probably not the only place doing so, but it's still
wrong.

If I return to the original design, but replace all of the high-level
wait events with calls to pgstat_report_activity(), does that work?

It'd be less wrong.

But I really doubt that it's a good idea to encode all kinds of function calls
happening during authentication into something SQL visible. Why stop with
these functions and not just do that for *all* functions in postgres? I mean
it'd not work and slow everything down, but how do you define that line?

Greetings,

Andres Freund

#59Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#58)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Mar 7, 2025 at 9:25 AM Andres Freund <andres@anarazel.de> wrote:

I should have clarified - there are a few that I think are ok, basically the
places where we wrap syscalls, e.g. around the sendto, select and recvfrom in
PerformRadiusTransaction().

Okay.

OTOH that code is effectively completely broken. Doing a blocking select() is
just a no-go, the code isn't interruptible, breaking authentication
timeout. And using select() means that we theoretically could crash due to an
fd that's above FD_SETSIZE.

I think we're in agreement here; I'm just trying to improve things
incrementally. If someone actually hits the broken case, I think it'd
be helpful for them to see it.

I think some of the wrapped calls into library code might actually call back
into our code (to receive/send data), and our code then will use wait events
around lower level operations done as part of that.

That would be a problem, agreed, but I didn't think I'd wrapped any
callback APIs. (Admittedly I have little experience with the SSPI
stuff.) But looking at the wrapped calls in the patch... which are you
suspicious of?

It's also IMO quite wrong to do something that can throw an error inside a
wait event, because that means that the wait event will still be reported
during error recovery.

Hm, okay. I can change that for the LookupAccountSid case.

Probably not the only place doing so, but it's still
wrong.

It's definitely not the only place. :D

Why stop with
these functions and not just do that for *all* functions in postgres? I mean
it'd not work and slow everything down,

(That seems like a good reason not to do it for all functions in
Postgres, no? I hope the slope is not all that slippery in practice.)

but how do you define that line?

Cost/benefit. In this case, authentication hanging in an unknown place
in PAM and LDAP has caused tangible support problems. I suspect I'd
have gotten complaints if I only focused on those two places, though,
so I expanded it to the other blocking calls I could see.

Thanks,
--Jacob

#60Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#59)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Fri, Mar 7, 2025 at 10:28 AM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I think some of the wrapped calls into library code might actually call back
into our code (to receive/send data), and our code then will use wait events
around lower level operations done as part of that.

That would be a problem, agreed, but I didn't think I'd wrapped any
callback APIs. (Admittedly I have little experience with the SSPI
stuff.) But looking at the wrapped calls in the patch... which are you
suspicious of?

I missed PAM_CONV, sorry. I'm worried about the sendAuthRequest()
being done there; it doesn't seem safe to potentially ereport(ERROR)
and longjmp through a PAM call stack? But I'll switch those over to
something safe or else drop that part of the patch.

Thanks,
--Jacob

#61Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#60)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Wed, Mar 12, 2025 at 3:16 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I missed PAM_CONV, sorry. I'm worried about the sendAuthRequest()
being done there; it doesn't seem safe to potentially ereport(ERROR)
and longjmp through a PAM call stack? But I'll switch those over to
something safe or else drop that part of the patch.

PAM aside... Michael, what's your level of enthusiasm for the rest of
this patch? I was confidently, embarrassingly wrong about how
CheckPAMAuth worked, and it makes me think I need to put this down and
take a completely new crack at it in 19.

Thanks,
--Jacob

#62Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#61)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

Hi,

On 2025-03-13 09:23:10 -0700, Jacob Champion wrote:

On Wed, Mar 12, 2025 at 3:16 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I missed PAM_CONV, sorry. I'm worried about the sendAuthRequest()
being done there; it doesn't seem safe to potentially ereport(ERROR)
and longjmp through a PAM call stack?

That indeed doesn't seem safe.

I am wondering if PAM is so fundamentally incompatible with handling
interrupts / a non-blocking interface that we have little choice but to
eventually remove it...

PAM aside... Michael, what's your level of enthusiasm for the rest of this
patch? I was confidently, embarrassingly wrong about how CheckPAMAuth
worked, and it makes me think I need to put this down and take a completely
new crack at it in 19.

FWIW, I continue to think that it's better to invest in making more auth
methods non-blocking, rather than adding wait events for code that could maybe
sometimes wait on different things internally.

Greetings,

Andres Freund

#63Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#62)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Mar 13, 2025 at 9:56 AM Andres Freund <andres@anarazel.de> wrote:

I am wondering if PAM is so fundamentally incompatible with handling
interrupts / a non-blocking interface that we have little choice but to
eventually remove it...

Given the choice between a usually-working PAM module with known
architectural flaws, and not having PAM at all, I think many users
would rather continue using what's working for them.

FWIW, I continue to think that it's better to invest in making more auth
methods non-blocking, rather than adding wait events for code that could maybe
sometimes wait on different things internally.

I think we disagree on the either/or nature of that. If I can get
proof that a certain thing is causing bugs in the wild, then I have
ammunition to fix that thing. Right now there is no visibility, and my
interest in rewriting old authentication methods without bug reports
to motivate that work is pretty low. I'm not willing to sign up for
that at the moment.

(But I do really appreciate the review. I'm just feeling crispy about
the overall result...)

Thanks,
--Jacob

#64Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#63)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On 2025-03-13 10:29:49 -0700, Jacob Champion wrote:

On Thu, Mar 13, 2025 at 9:56 AM Andres Freund <andres@anarazel.de> wrote:

I am wondering if PAM is so fundamentally incompatible with handling
interrupts / a non-blocking interface that we have little choice but to
eventually remove it...

Given the choice between a usually-working PAM module with known
architectural flaws, and not having PAM at all, I think many users
would rather continue using what's working for them.

authentication_timeout currently doesn't reliably work while in some auth
methods, nor does pg_terminate_backend() etc. That's IMO is rather bad from a
DOSability perspective.

The fact that some auth methods are broken like that has had a sizable
negative impact on postgres for a long time. Not just when those methods are
used, but also architecturally.

It's e.g. one of the main reasons we need the ugly escalating logic in
postmaster shutdowns to send SIGQUITs and then SIGKILL after a while, because
we don't have a reliable way of terminating backends normally. This used to
be way worse because historically postgres considered it sane (why, I have no
idea) to ereport() in timeout functions, which then occasionally lead to
backends stuck in malloc locks etc.

FWIW, I continue to think that it's better to invest in making more auth
methods non-blocking, rather than adding wait events for code that could maybe
sometimes wait on different things internally.

I think we disagree on the either/or nature of that. If I can get
proof that a certain thing is causing bugs in the wild, then I have
ammunition to fix that thing.

FWIW, I've have repeatedly seen production issues due to authentication
timeout not working for some auth methods.

It's not hard to see why - e.g. a non-resonsive radius server just leaves the
backend hanging in select(). Even though it would get interrupted by signals,
we'll just retry without even checking interrupts / timeouts :(.

Right now there is no visibility, and my interest in rewriting old
authentication methods without bug reports to motivate that work is pretty
low. I'm not willing to sign up for that at the moment.

Fair enough.

(But I do really appreciate the review. I'm just feeling crispy about
the overall result...)

Also fair enough :)

Greetings,

Andres Freund

#65Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#64)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Thu, Mar 13, 2025 at 10:56 AM Andres Freund <andres@anarazel.de> wrote:

Given the choice between a usually-working PAM module with known
architectural flaws, and not having PAM at all, I think many users
would rather continue using what's working for them.

authentication_timeout currently doesn't reliably work while in some auth
methods, nor does pg_terminate_backend() etc. That's IMO is rather bad from a
DOSability perspective.

The fact that some auth methods are broken like that has had a sizable
negative impact on postgres for a long time. Not just when those methods are
used, but also architecturally.

Right -- I just don't think end users are going to factor that into
their choice of authentication method. If IT tells you "use this PAM
module", then... that's it.

If we remove PAM, maybe they change authentication methods... or maybe
they just don't ever upgrade Postgres again. My money's on the latter.

--

I looked into switching over to pgstat_report_activity(), but that
wasn't designed to be called in the middle of backend initialization.
It would take more work to make those calls safe/sane when `st_state
== STATE_STARTING`. I plan to mark this patchset as Withdrawn for now.

Thanks all!
--Jacob

#66Michael Paquier
michael@paquier.xyz
In reply to: Jacob Champion (#65)
Re: [PATCH] pg_stat_activity: make slow/hanging authentication more visible

On Mon, Mar 17, 2025 at 10:22:47AM -0700, Jacob Champion wrote:

I looked into switching over to pgstat_report_activity(), but that
wasn't designed to be called in the middle of backend initialization.
It would take more work to make those calls safe/sane when `st_state
== STATE_STARTING`. I plan to mark this patchset as Withdrawn for now.

Okay, fine by me. I had the impression that it would have been
possible to salvage some of the wait event states, but at least the
starting state showing up in pg_stat_activity will be able to provide
some information, so it's better than none. Unfortunately, I don't
have any room until the feature freeze for that.

Outside the stat report activity calls, I've been wondering if we
should add some dynamic tracking of which hba/ident entry a backend
PID is working with. For example, if we knew the file and the entry
line number, we would know on which auth method this backend is
bumping into. That maybe of course limited if someone modifies and
reloads the HBA file while a backend is stuck. Now, these files are
mostly static, and we have system views that provide the contents of
the ident and HBA files as SQL, so with a JOIN..
--
Michael