Subject: Fix pg_stat_wal_receiver to show CONNECTING status

From: Chao Li <[email-masked]>
Date: Tue, 19 May 2026 13:55:14 +0800

Hi,

I just tested "Add WALRCV_CONNECTING state to the WAL receiver” and found an issue.

Commit a36164e74 added the feature, and the commit message says:
```
...
the WAL receiver is ready to stream changes. This change is useful for
monitoring purposes, especially in environments with a high latency
where a connection could take some time to be established, giving some
room between the [re]start phase and the streaming activity.
```

However, I failed to see the CONNECTING status. To simulate a high-latency primary connection, I shut down the real primary server and created a fake socket server:
```
chaol@ChaodeMacBook-Air ~ % perl -MIO::Socket::INET -e '
  $s = IO::Socket::INET->new(
    LocalAddr => "127.0.0.1",
    LocalPort => 5432,
    Listen => 1,
    ReuseAddr => 1
  ) or die $!;
  $c = $s->accept;
  sleep 600;
'
```

Then pg_stat_wal_receiver only shows an empty result:
```
evantest=# SELECT * FROM pg_stat_wal_receiver;
 pid | status | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli | last_msg_send_time | last_msg_receipt_time | latest_end_lsn | latest_end_time | slot_name | sender_host | sender_port | conninfo
-----+--------+-------------------+-------------------+-------------+-------------+--------------+--------------------+-----------------------+----------------+-----------------+-----------+-------------+-------------+----------
(0 rows)
```

I also tried restarting the standby server, and the result was the same.

The problem is that pg_stat_wal_receiver is gated by WalRcv->ready_to_display, and when the status is CONNECTING, WalRcv->ready_to_display is false.

Given that the original commit message explicitly mentions “monitoring purposes”, I think hiding this status during the connecting phase is a bug. I tried to fix it by showing only the PID and CONNECTING status when WalRcv->ready_to_display is false, like this:
```
evantest=# SELECT * FROM pg_stat_wal_receiver;
 pid  |   status   | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli | last_msg_send_time | last_msg_receipt_time | latest_end_lsn | latest_end_time | slot_name | sender_host | sender_port | conninfo
------+------------+-------------------+-------------------+-------------+-------------+--------------+--------------------+-----------------------+----------------+-----------------+-----------+-------------+-------------+----------
 3256 | connecting |                   |                   |             |             |              |                    |                       |                |                 |           |             |             |
(1 row)
```

See the attached patch for details.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

---

From: Michael Paquier <[email-masked]>
Date: Tue, 19 May 2026 22:55:39 +0900

On Tue, May 19, 2026 at 01:55:14PM +0800, Chao Li wrote:
> The problem is that pg_stat_wal_receiver is gated by
> WalRcv->ready_to_display, and when the status is CONNECTING,
> WalRcv->ready_to_display is false.

Initially, I was thinking that the walrcv_connect() delay would not be
that important to track in this context, but you are right that this
stands for improvement before the release.

@@ -1474,21 +1474,10 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS)
-	if (pid == 0 || !ready_to_display)
+	/* No WAL receiver, just return a tuple with NULL values */
+	if (pid == 0)
 		PG_RETURN_NULL();

This suggestion is making the SQL function call feebler, IMO,
impacting the readability around ready_to_display that we want to act
as a gate to the data provided in the view.  This flag is important to
check at an early state of the function call, and I don't really want
to change that.  A better thing to do would be to split into two steps
how the WAL receiver data is filled between the walrcv_connect() call:
1) Before the call, reset all the connection-related fields because
they are not relevant before the connection to the remote is
completed, set ready_for_display to true to make the connecting state
visible in the view.   The connection information does not matter
anyway here: we cannot be sure which point we are connected to until
the connection is fully established.
2) After the call, fill in the connection-related fields.

This means taking twice the WAL receiver spinlock instead of once,
which is not going to matter in practice as the latency of the
connection attempt is much larger than that.

What do you think about the attached, then?
--
Michael
From 3c381a90b1270fdd3f1b01e8eefb85f1ac4af3d8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email-masked]>
Date: Tue, 19 May 2026 22:52:38 +0900
Subject: [PATCH v2] Improve pg_stat_wal_receiver for CONNECTING status

Commit a36164e7465 added a CONNECTING status for the WAL receiver, but
pg_stat_wal_receiver returned no information while the connection to the
primary was attempted, limiting the usability of the feature in
high-latency environments where the connection attempt to the primary
could take time.

This commit improves the report of the status by splitting the way the
shared memory state of the WAL receiver is filled before and after the
connection to the primary is attempted:
- Before the attempt, reset all the connection fields, switch
ready_to_display to true.
- After the attempt, fill in the connection fields.

This change means two spinlock acquisitions instead of one, but at least
monitoring tools can know about the connection attempt before its
completion, enlarging the usability of the feature.

Reported-by: Chao Li <[email-masked]>
Author: Michael Paquier <[email-masked]>
Discussion: https://postgr.es/m/XXX
---
 src/backend/replication/walreceiver.c | 24 ++++++++++++++++--------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c
index 07eac07b9ce4..d19317703c1f 100644
--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -267,6 +267,20 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len)
 	/* Unblock signals (they were blocked when the postmaster forked us) */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Switch the WAL receiver state as ready for display before doing a
+	 * connection attempt, so as its connecting state is visible before
+	 * attempting to contact the primary server.  Note that this resets the
+	 * original conninfo, sender_port and sender_host, for security.  These
+	 * fields are filled once the connection is fully established.
+	 */
+	SpinLockAcquire(&walrcv->mutex);
+	memset(walrcv->conninfo, 0, MAXCONNINFO);
+	memset(walrcv->sender_host, 0, NI_MAXHOST);
+	walrcv->sender_port = 0;
+	walrcv->ready_to_display = true;
+	SpinLockRelease(&walrcv->mutex);
+
 	/* Establish the connection to the primary for XLOG streaming */
 	appname = cluster_name[0] ? cluster_name : "walreceiver";
 	wrconn = walrcv_connect(conninfo, true, false, false, appname, &err);
@@ -277,23 +291,17 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len)
 						appname, err)));
 
 	/*
-	 * Save user-visible connection string.  This clobbers the original
-	 * conninfo, for security. Also save host and port of the sender server
-	 * this walreceiver is connected to.
+	 * Save user-visible connection string, now that the connection has been
+	 * achieved.
 	 */
 	tmp_conninfo = walrcv_get_conninfo(wrconn);
 	walrcv_get_senderinfo(wrconn, &sender_host, &sender_port);
 	SpinLockAcquire(&walrcv->mutex);
-	memset(walrcv->conninfo, 0, MAXCONNINFO);
 	if (tmp_conninfo)
 		strlcpy(walrcv->conninfo, tmp_conninfo, MAXCONNINFO);
-
-	memset(walrcv->sender_host, 0, NI_MAXHOST);
 	if (sender_host)
 		strlcpy(walrcv->sender_host, sender_host, NI_MAXHOST);
-
 	walrcv->sender_port = sender_port;
-	walrcv->ready_to_display = true;
 	SpinLockRelease(&walrcv->mutex);
 
 	if (tmp_conninfo)
-- 
2.54.0

---

From: Chao Li <[email-masked]>
Date: Wed, 20 May 2026 09:47:40 +0800

> On May 19, 2026, at 21:55, Michael Paquier <[email-masked]> wrote:
> 
> <v2-0001-Improve-pg_stat_wal_receiver-for-CONNECTING-statu.patch>

Hi Micheal,

Thanks for your patch.

I just read v2, and it is actually the first solution I tried. The reason I gave up on that approach and switched to the implementation in v1 is that it may wrongly report last_msg_send_time, last_msg_receipt_time, and latest_end_time. See my test with v2:

```
evantest=# SELECT * FROM pg_stat_wal_receiver;
  pid  |   status   | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time        | slot_name | sender_host | sender_port | conninfo
-------+------------+-------------------+-------------------+-------------+-------------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------+-----------+-------------+-------------+----------
 83930 | connecting | 0/03000000        |                 1 | 0/03000000  | 0/03000000  |            1 | 2026-05-20 09:24:09.121679+08 | 2026-05-20 09:24:09.121679+08 |                | 2026-05-20 09:24:09.121679+08 |           |             |             |
(1 row)

evantest=# \c
You are now connected to database "evantest" as user "chaol".
evantest=# SELECT * FROM pg_stat_wal_receiver;
  pid  |   status   | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time        | slot_name | sender_host | sender_port | conninfo
-------+------------+-------------------+-------------------+-------------+-------------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------+-----------+-------------+-------------+----------
 84709 | connecting | 0/03000000        |                 1 | 0/03000000  | 0/03000000  |            1 | 2026-05-20 09:27:37.407117+08 | 2026-05-20 09:27:37.407117+08 |                | 2026-05-20 09:27:37.407117+08 |           |             |             |
(1 row)

evantest=# \c
You are now connected to database "evantest" as user "chaol".
evantest=# SELECT * FROM pg_stat_wal_receiver;
  pid  |   status   | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time        | slot_name | sender_host | sender_port | conninfo
-------+------------+-------------------+-------------------+-------------+-------------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------+-----------+-------------+-------------+----------
 84805 | connecting | 0/03000000        |                 1 | 0/03000000  | 0/03000000  |            1 | 2026-05-20 09:28:03.251298+08 | 2026-05-20 09:28:03.251298+08 |                | 2026-05-20 09:28:03.251298+08 |           |             |             |
(1 row)
```

As shown above, every time I restarted the standby server, last_msg_send_time, last_msg_receipt_time, and latest_end_time were updated to the standby server start time. But in this test, the standby was connecting to a fake primary, so no WAL receiver message had been sent or received.

I tried to avoid more complicated changes, so I ended up with the v1 approach. I think it's okay to leave the other columns NULL while the receiver is still connecting, because at that point the only reliable information available is the receiver process's PID and status.

For v1, maybe we could clarify the meaning of ready_to_display with a comment. It seems to be intended to indicate that the connection-related information, such as LSNs and timestamps, is ready to display. In that sense, pid and status don't need to be gated by it.

Best regards,

---

From: Michael Paquier <[email-masked]>
Date: Wed, 20 May 2026 13:10:33 +0900

On Wed, May 20, 2026 at 09:47:40AM +0800, Chao Li wrote:
> I just read v2, and it is actually the first solution I tried. The
> reason I gave up on that approach and switched to the implementation
> in v1 is that it may wrongly report last_msg_send_time,
> last_msg_receipt_time, and latest_end_time.

As of the code, we have the following at the top of WalReceiverMain()
before the first connection attempt:
    /* Initialise to a sanish value */
    now = GetCurrentTimestamp();
    walrcv->lastMsgSendTime =
       walrcv->lastMsgReceiptTime = walrcv->latestWalEndTime = now;

And the state of v2 is actually fine, because we finish by reporting
in the SQL calls values that represent the state the WAL receiver is 
initialized at based on what the code does.  It would be IMO an issue
to hide this information, as they can offer hits about the moment when
we've begun a connection.
--
Michael

---

From: Chao Li <[email-masked]>
Date: Wed, 20 May 2026 15:53:38 +0800

> On May 20, 2026, at 12:10, Michael Paquier <[email-masked]> wrote:
> 

Was that okay because walrcv->ready_to_display was false, so the sane initial value would not show up through pg_stat_wal_receiver?

> we've begun a connection.

With v2, slot_name, sender_host, sender_port, and conninfo are already left NULL while the receiver is in CONNECTING state. I feel we don't have to show the timestamp fields either. Since the columns are named last_msg_send_time and last_msg_receipt_time, users may naturally interpret them as the last time a message was sent to or received from
the primary. If we show the standby server start time in those columns, I am afraid that could be confusing.

But I think it might be useful to show the *_lsn and *_tli values in CONNECTING state if they are available.

Best regards,

---

From: Michael Paquier <[email-masked]>
Date: Thu, 21 May 2026 05:43:52 +0900

On Wed, May 20, 2026 at 03:53:38PM +0800, Chao Li wrote:
> With v2, slot_name, sender_host, sender_port, and conninfo are
> already left NULL while the receiver is in CONNECTING state. I feel
> we don't have to show the timestamp fields either. Since the columns
> are named last_msg_send_time and last_msg_receipt_time, users may
> naturally interpret them as the last time a message was sent to or
> received from
> the primary. If we show the standby server start time in those
> columns, I am afraid that could be confusing.
> 

The original reason why ready_to_display has been introduced is this
one, where we wanted to have a strict control over the connection
information across multiple calls of pg_stat_get_wal_receiver():
https://www.postgresql.org/message-id/[email-masked]

With v2, ready_to_display is still able to do the job it is defined
for.  This does not need to apply on the time fields, so IMO showing
them to the values they are initialized is not a big deal, and they
can actually be useful to know even in the early stage of connection
as they reveal the state of the code.  

Note also that the time values could still show up based on their
initial values at the early connection stage, even after completing
walrcv_connect() and after ready_to_display is switched to true, so
it's not like these values are that confusing: we just expose them a
bit more at an earlier stage of the connection attempt process.  As a
whole v2 is fine, and addresses your issue.
--
Michael

---

From: Chao Li <[email-masked]>
Date: Thu, 21 May 2026 07:06:33 +0800

> On May 21, 2026, at 04:43, Michael Paquier <[email-masked]> wrote:
> 

Thanks for the detailed explanation.

Now I see that, based on the original discussion you pointed out, as long as v2 clears conninfo before setting ready_to_display to true, it is okay to do that earlier while the state is still CONNECTING. On that point, I’m good with v2.

I’m still not fully convinced about displaying the *_time fields, but I don’t have a stronger argument either, so I’m fine with that. Maybe we can add a brief description in the doc like the attached diff?

Overall, v2 looks good to me now.

Best regards,

---

From: Chao Li <[email-masked]>
Date: Thu, 21 May 2026 15:20:13 +0800

> On May 21, 2026, at 07:06, Chao Li <[email-masked]> wrote:
> 
> 
> 
> <nocfbot_monitoring.sgml.diff>

I spent more time here, and found that it is still possible to leak conninfo in the WAL receiver reuse path:

* WalRcvWaitForStartPosition() sets the state to WALRCV_WAITING.
* Then RequestXLogStreaming() copies raw conninfo into walrcv->conninfo and sets the state to WALRCV_RESTARTING.
* WalRcvWaitForStartPosition() then moves the state to WALRCV_CONNECTING, but this path does not clear walrcv->conninfo again.

The attached nocfbot_test.diff demonstrates the leak.

Initially I thought we could also set ready_to_display to false when setting the state to WALRCV_WAITING in WalRcvWaitForStartPosition(), and set it back to true when switching back to WALRCV_CONNECTING. However, that would make the WALRCV_WAITING and WALRCV_RESTARTING states invisible in pg_stat_wal_receiver.

I ended up with a solution that copies the primary connection info to walrcv->conninfo only when RequestXLogStreaming() is switching to WALRCV_STARTING. In the WALRCV_WAITING reuse path, the WAL receiver keeps using the existing wrconn, so it does not need raw conninfo to be copied into shared memory again. See the attached nocfbot_walreceiverfuncs.c.diff.

With that change, the new test passes. I also ran "make check-world" successfully.

Best regards,

---

From: Michael Paquier <[email-masked]>
Date: Thu, 21 May 2026 21:08:54 +0900

On Thu, May 21, 2026 at 03:20:13PM +0800, Chao Li wrote:
> * Then RequestXLogStreaming() copies raw conninfo into
> * walrcv->conninfo and sets the state to WALRCV_RESTARTING.
> * WalRcvWaitForStartPosition() then moves the state to
> * WALRCV_CONNECTING, but this path does not clear walrcv->conninfo
> * again.
> 

File is missing, but I get it.  This is a legit bug from what I can
see, that also affects all the stable branches, not only HEAD.

> Initially I thought we could also set ready_to_display to false when
> setting the state to WALRCV_WAITING in WalRcvWaitForStartPosition(),
> and set it back to true when switching back to
> WALRCV_CONNECTING. However, that would make the WALRCV_WAITING and
> WALRCV_RESTARTING states invisible in pg_stat_wal_receiver.

Nah, we should not do that.  We want to track the waiting and
restarting states in the view.

> I ended up with a solution that copies the primary connection info
> to walrcv->conninfo only when RequestXLogStreaming() is switching to
> WALRCV_STARTING. In the WALRCV_WAITING reuse path, the WAL receiver
> keeps using the existing wrconn, so it does not need raw conninfo to
> be copied into shared memory again. See the attached
> nocfbot_walreceiverfuncs.c.diff.

Ah, yeah.  This solution to this problem makes sense.  We should not
clobber conninfo either in this case, or we'd lose the
user-displayable string returned by walrcv_get_conninfo() (conninfo
cannot be NULL based on the in-core callers of RequestXLogStreaming()
AFAIK, but who knows for things out there).  As mentioned above, this
is a different issue than the visibility of the connection information
while we are connecting, and it should be backpatched.  Would you like
to send a patch?
--
Michael

---

From: Chao Li <[email-masked]>
Date: Thu, 21 May 2026 20:29:35 +0800

> On May 21, 2026, at 20:08, Michael Paquier <[email-masked]> wrote:
> 

Sorry for missing the attachments. Please take a look first. It’s late here, I can spend more time tomorrow.

Best regards,

---

From: Chao Li <[email-masked]>
Date: Fri, 22 May 2026 10:06:33 +0800

> On May 21, 2026, at 20:29, Chao Li <[email-masked]> wrote:
> 
> 
> 
> <nocfbot_test.diff><nocfbot_walreceiverfuncs.c.diff>

Here comes the patch set:

* v3-0001 is the exactly same as v2-0001
* In v3-0002, the change in walreceiverfuncs.c is the same as the previous diff, and I tuned the test change a little bit.

Best regards,

---

From: Michael Paquier <[email-masked]>
Date: Sat, 23 May 2026 08:23:00 +0900

On Fri, May 22, 2026 at 10:06:33AM +0800, Chao Li wrote:
> * In v3-0002, the change in walreceiverfuncs.c is the same as the
> * previous diff, and I tuned the test change a little bit.

Okay, applied the first one on HEAD, and backpatched the second one.
--
Michael

---

From: Chao Li <[email-masked]>
Date: Sat, 23 May 2026 07:55:39 +0800

> On May 23, 2026, at 07:23, Michael Paquier <[email-masked]> wrote:
> 

Thank you very much for applying the patch and still working hard during the PGConf.

Best regards,

---

From: Xuneng Zhou <[email-masked]>
Date: Sat, 23 May 2026 08:51:41 -0700

> Thank you very much for applying the patch and still working hard during the PGConf.

Thanks for dealing with this and sorry for being late in the game.
I'll do a post-commit review while I'm on the flight.

-- 
Regards,
Xuneng Zhou

---

From: Xuneng Zhou <[email-masked]>
Date: Sat, 23 May 2026 20:51:40 -0700

> I'll do a post-commit review while I'm on the flight.

I agree with Michael's point that displaying partial columns gated by
the flag ready-to-display is not very ideal. Showing two columns in
connecting status inconsistently in some scenarios implies something
broken under the hood.[1] It may require extra documentation effort to
dispel the confusion for the user.

As for the timeline/lsn and timestamp values, they may still be useful
operationally because they reveal when the WAL receiver entered this
part of the code. Although they are not meant to be used and
interpreted in this way... So there's some risk for misinterpretation,
but they precedes the changes.

[1] https://www.postgresql.org/message-id/[email-masked]