Skip to content

gpstate: fix false "Acting as Primary" report for hot standby mirrors - #1743

Open
jangjang0401 wants to merge 4 commits into
apache:mainfrom
jangjang0401:fix/gpstate-hot-standby-false-positive
Open

jangjang0401 wants to merge 4 commits into
apache:mainfrom
jangjang0401:fix/gpstate-hot-standby-false-positive

Conversation

@jangjang0401

Copy link
Copy Markdown
Contributor

With hot_standby=on, mirrors accept SQL connections and return PQPING_OK
from pg_isready. The previous code unconditionally mapped PQPING_OK +
role=mirror to "Acting as Primary", causing gpstate -s to show a spurious
warning on every mirror.

Fix this in clsSystemState.__buildGpStateData() by cross-checking
pg_stat_replication on the primary: if the mirror has an active WAL
receiver connection in streaming or catchup state, it is a legitimate
hot standby and the status is corrected to "Up". Only fall through to
"Acting as Primary" when no such replication connection exists, meaning
the segment truly promoted itself to primary.

This approach reuses the existing primary connection already established
by _add_replication_info(), so no additional database connections are
required. _add_replication_info() is updated to return the raw
replication state string to make this information available to the
caller.

What does this PR do?

Fixes a false positive in gpstate -s where every mirror segment is
reported as "Acting as Primary" when hot_standby=on is enabled.

Root cause:

  • With hot_standby=on, mirrors accept read-only SQL connections and
    pg_isready returns PQPING_OK(0) instead of PQPING_MIRROR_READY(64).
  • The legacy logic in gpgetstatususingtransition.py assumed
    PQPING_OK + role=mirror could only mean "the mirror was promoted to
    primary", which is no longer true under hot standby.

Fix:

  • In clsSystemState.__buildGpStateData(), cross-check the mirror's
    status against pg_stat_replication (already queried on the primary
    by _add_replication_info()).
  • If the mirror has an active WAL receiver connection (streaming or
    catchup), it is a legitimate hot standby → corrected to "Up".
  • If no such replication connection exists, the mirror genuinely
    promoted itself → "Acting as Primary" is preserved.

Type of Change

  • Bug fix (non-breaking change)
  • New feature (non-breaking change)
  • Breaking change (fix or feature with breaking changes)
  • Documentation update

Breaking Changes

None. The fix only changes how the status string is derived in
gpstate; cluster configuration, replication behavior, and segment
behavior are unchanged.

Test Plan

Tested on a 5-VM cluster (1 coordinator + 1 standby coordinator + 3
segment hosts, 6 primary / 6 mirror segments) with hot_standby=on:

Before the fix:

  • Every mirror reported Segment status = Acting as Primary
  • gpstate -s produced spurious warnings on every mirror
  • gp_segment_configuration, replication state, and gpstate -m all
    showed the cluster as healthy — only the gpstate -s output was wrong

After the fix:

  • Every mirror reports Segment status = Up
  • No "Acting as Primary" entries appear on healthy mirrors
  • No additional database connections are opened

Impact

Performance:
No additional database connections. The fix consumes data that
_add_replication_info() already collects from the primary; only the
return value of that function is changed.

User-facing changes:
gpstate -s no longer reports false "Acting as Primary" warnings for
healthy hot-standby mirrors. Genuine promotion is still detected and
reported as before.

Dependencies:
None.

Checklist

Additional Context

An alternative approach considered was to call pg_is_in_recovery()
directly on each mirror inside _get_segment_status() in
gpgetstatususingtransition.py. That approach was rejected because:

  1. It opens one additional DB connection per mirror on every gpstate
    invocation, which is undesirable in unhealthy-cluster scenarios
    where gpstate is run most frequently.
  2. Mirror-side recovery state is a weaker source of truth than the
    primary's view of its replication connections.

The pg_stat_replication approach reuses an existing connection and is
authoritative from the primary's perspective.

@leborchuk leborchuk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Another approach could be to check recovery.conf for each database instance. However, we cannot do this using SQL. We must connect to each host and run a shell command. After that, we must check if the database is up and the replication slot is active. We should postpone checking recovery. conf until the current approach is no longer sufficient.

@jangjang0401

Copy link
Copy Markdown
Contributor Author

LGTM. Another approach could be to check recovery.conf for each database instance. However, we cannot do this using SQL. We must connect to each host and run a shell command. After that, we must check if the database is up and the replication slot is active. We should postpone checking recovery. conf until the current approach is no longer sufficient.

Thank you for the review and for sharing the alternative approach.
I agree with your suggestion to postpone the standby.signal check
until the current approach is no longer sufficient.

@MisterRaindrop

Copy link
Copy Markdown
Contributor

Thanks for the fix, @jangjang0401. I reproduced the problem and verified the patch on a demo cluster (3 primaries + 3 mirrors, built from this branch in the Rocky 8 build image).

With hot_standby=on, a mirror that is still in recovery (pg_is_in_recovery() = t) answers pg_isready with 0 (PQPING_OK), which is the cause you describe.

Scenario main this PR
hot_standby=off mirrors Up –
hot_standby=on, mirrors streaming all 3 mirrors Acting as Primary, with warnings all 3 mirrors Up, no warnings
hot_standby=on, one mirror promoted with pg_ctl promote before FTS noticed – only that mirror is Acting as Primary; the other two are Up

So healthy hot standbys are no longer flagged, and a mirror that has really been promoted still is. The only consumer of the status string is clsSystemState.py, so correcting it there doesn't leave another caller behind.

I pushed one commit on top (a54731c) so this can move forward:

  • unit tests in test_unit_gpstate.py for the value _add_replication_info() now returns: the walreceiver state for streaming / catchup / startup, and None when the primary is down, can't be queried, or doesn't show exactly one standby connection. All 28 tests in the file pass; the state test fails against main ('streaming' != None).
  • one sentence in the comment beside the check: a hot standby whose WAL receiver hasn't reached streaming yet (e.g. still in startup) is reported as Acting as Primary until it does.

Please squash when merging, since the branch carries a merge from main.

@MisterRaindrop
MisterRaindrop enabled auto-merge (squash) October 22, 2026 08:27
@Alena0704

Copy link
Copy Markdown
Collaborator

There is still a false positive when a hot standby loses its WAL receiver connection. With hot_standby=on, the mirror can remain in recovery and accept read-only connections while pg_stat_replication on the primary has no corresponding row. _add_replication_info() then returns None, so this check leaves the status as “Acting as Primary”, even though no promotion occurred.
With AI assistance, I reproduced this in an isolation2 test by pausing FTS and temporarily pointing the mirror’s primary_conninfo at an unreachable endpoint:

  • pg_is_in_recovery() remains true.
  • pg_isready returns 0.
  • gpstate -s reports “Acting as Primary”.
    An active replication connection confirms that the mirror is a standby, but its absence does not prove promotion. Could we fall back to checking pg_is_in_recovery() on the mirror when replication state is unavailable?

I've attached the patch with isolation test - isolation2-gpstate.patch.

@MisterRaindrop

Copy link
Copy Markdown
Contributor

@Alena0704 You're right, thanks — the absence of a replication row says nothing about promotion. I've disabled auto-merge and pushed two commits:

  • c593594: when a mirror reads as "Acting as Primary" and the primary shows no streaming/catchup connection for it, gpstate now asks the mirror with pg_is_in_recovery() (utility mode, 10 s connect timeout). Only a mirror that answers and is out of recovery stays "Acting as Primary"; one that can't be asked does too, as before. The extra connection is made only on that path, so a healthy cluster pays nothing for it. Unit tests cover the new helper.
  • 1f15966: your isolation2 test, committed under your name.

Run on a demo cluster (Rocky 8, 3 primaries + 3 mirrors), make -C src/test/isolation2 installcheck-hot-standby:

clsSystemState.py hot_standby/gpstate
main fails: streaming and disconnected both reported as Acting as Primary
this PR before the fix fails: disconnected reported as Acting as Primary
with the fix passes; all 8 tests in the schedule pass

A mirror really promoted with pg_ctl promote (before FTS reacts) is still reported as Acting as Primary, with the other two Up. All 30 tests in test_unit_gpstate.py pass.

Could you take another look when you have a moment? Auto-merge stays off until you have.

@jangjang0401

Copy link
Copy Markdown
Contributor Author

Thank you so much, @MisterRaindrop , for taking the time to implement the pg_is_in_recovery() fallback, and @Alena0704 , for catching the disconnected case and kindly providing the isolation2 test. I really appreciate your help in moving this PR forward. I've gone through the new commits, and they look good to me.

@tuhaihe

tuhaihe commented Oct 23, 2026

Copy link
Copy Markdown
Member

Here is one behave test PR for the gpMgmt/ file changes: #1639. Welcome to leave your comments if you're available.

@Alena0704

Copy link
Copy Markdown
Collaborator

I think I have found another problem - when the WAL connection is absent, a failed pg_is_in_recovery() query is treated as False, causing gpstate to report “Acting as Primary” without evidence of promotion.
The test injects a SQL error while pg_isready still succeeds and verifies that gpstate reports “Unknown” with a warning.

I attached the patch with the test.
pr1743-isolation.patch

@Alena0704

Copy link
Copy Markdown
Collaborator

I think I have found another problem - when the WAL connection is absent, a failed pg_is_in_recovery() query is treated as False, causing gpstate to report “Acting as Primary” without evidence of promotion. The test injects a SQL error while pg_isready still succeeds and verifies that gpstate reports “Unknown” with a warning.

I attached the patch with the test. pr1743-isolation.patch

I missed your explanation above, so I don't consider it a blocker for merging this PR.

@Alena0704 Alena0704 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

jangjang0401 and others added 4 commits October 26, 2026 11:00
With hot_standby=on, mirrors accept SQL connections and return
PQPING_OK from pg_isready. The previous code unconditionally mapped
PQPING_OK + role=mirror to "Acting as Primary", causing gpstate -s
to show a spurious warning on every mirror.

Fix this in clsSystemState.__buildGpStateData() by cross-checking
pg_stat_replication on the primary: if the mirror has an active WAL
receiver connection in streaming or catchup state, it is a legitimate
hot standby and the status is corrected to "Up". Only fall through to
"Acting as Primary" when no such replication connection exists,
meaning the segment truly promoted itself to primary.

This approach reuses the existing primary connection already established
by _add_replication_info(), so no additional database connections are
required. _add_replication_info() is updated to return the raw
replication state string to make this information available to the
caller.
The hot standby check in __buildGpStateData() relies on
_add_replication_info() returning the walreceiver's state from
pg_stat_replication, and None whenever it has none to give.  Cover both:
the state is passed through for streaming, catchup and startup, and None
comes back when the primary is down, cannot be queried, or does not show
exactly one standby connection.

Also say in the comment beside the check that a hot standby whose WAL
receiver has not reached streaming yet is still reported as "Acting as
Primary" until it does.
…AL connection

An active WAL receiver connection on the primary proves that a mirror
answering PQPING_OK is a hot standby, but its absence proves nothing: a
receiver that is starting up, or whose connection has dropped, leaves a
mirror that is still in recovery and still accepts read-only
connections.  Such a mirror was reported as "Acting as Primary" without
having been promoted.

When there is no streaming or catchup connection, ask the mirror itself
with pg_is_in_recovery().  Only a mirror that answers and is out of
recovery is still reported as "Acting as Primary"; one that cannot be
asked is too, as before.  The extra connection is made only for a
mirror already reported as "Acting as Primary" without replication, so
a healthy cluster pays nothing for it.

Reported by Alena Rybakina.
Two cases in the hot_standby schedule.  A streaming hot standby must be
reported as "Up", and so must one whose WAL connection has been cut
while FTS probes are skipped: it is still in recovery and accepting
connections, and has not been promoted.  primary_conninfo and FTS are
restored even when the assertion fails.
@tuhaihe
tuhaihe force-pushed the fix/gpstate-hot-standby-false-positive branch from aa883eb to ba34328 Compare October 26, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants