Skip to content

[Bug] Observer/follower keeps forwarding statements to old (degraded) master after master failover, clients receive errors for minutes #67297

Description

@bosswnx

Search before asking

  • I had searched in the issues and found no similar issues.

Version

Doris 3.0.8 (also verified against master branch code)

What's Wrong?

After a master FE failover (old master lost leadership due to OOM/GC pause but the process stayed alive and kept serving its MySQL/thrift ports), a non-master FE (observer/follower) with a lagging journal replay keeps forwarding statements to the old master, and clients continuously receive:

ERROR 1105 (HY000): errCode = 2, detailMessage = The statement has been forwarded to master FE(<old master ip>) and failed to execute because Master FE is not ready. You may need to check FE's status

The error persists until the observer's journal replay catches up (or the observer is restarted), because:

  1. The forward target is Env.getMasterHost()/getMasterRpcPort() (MasterOpExecutorFEOpExecutor), which is backed by the in-memory masterInfo field.
  2. masterInfo is only updated by replaying the OP_MASTER_INFO_CHANGE journal entry written by the new master (EditLog.javaEnv.setMaster()), or by loading an image at startup. There is no active master re-discovery on the forward path.
  3. Meanwhile the old master process is still listening on its ports, so the forwarded thrift call succeeds at the transport level — it reaches StmtExecutor on the old master, which (as a proxy-forwarded statement, isProxy=true) detects it cannot find a valid master and throws the error above (StmtExecutor.executeByNereids/executeByLegacy, the isProxy branch) instead of the sender learning "this node is not master, retry elsewhere".
  4. LB health checks (TCP-level) keep routing client connections to the degraded old master as well, compounding the issue.

Additionally, for FORWARD_WITH_SYNC statements (e.g. CREATE USER), the old master's error response still carries a maxJournalId, so the observer blocks in JournalObservable.waitOn() (up to query_timeout * 1.2, default ~18 min) before surfacing anything — the client just hangs.

What You Expected?

After a master failover, the non-master FE should stop forwarding statements to the old (degraded) master quickly — regardless of its journal replay lag. Either:

  • the forwarded request should be rejected early by the old master with a NOT_MASTER-style response (so the sender can learn the real master and retry), or
  • the sender (FEOpExecutor) should re-discover the current master on forward failure/NOT_MASTER and retry against the real master,

so that statements executed via the observer succeed (or fail fast with a clear, actionable error) instead of failing with "Master FE is not ready" (or hanging in journal-sync wait) for the whole meta_delay_toleration_second window.

How to Reproduce?

Reproduced deterministically on Doris 3.0.8 with 4 FEs in docker (fe1=master, fe2/fe3=followers, fe4=observer). nsenter-based iptables is used to simulate the degraded states (equivalent to what an OOM/GC-pause produces):

  1. Freeze the observer's journal replay: block fe4 → fe2/fe3 on the bdbje port (9010), so fe4 cannot replay the new master's journals (its masterInfo stays = fe1).
  2. Kill fe1 (docker kill, simulating the OOM death of the old master). fe2 is elected as the new master.
  3. Restart fe1 with its bdbje isolated (block 9010 toward fe2/fe3/fe4). fe1 comes back "alive but degraded": MySQL (9030) and thrift (9020) ports still serve, but it is no longer the master and cannot rejoin/catch up — exactly the state of an OOM-recovered old master that lost the election.
  4. Execute a forward-to-master statement via the observer (fe4):
$ mysql -h <fe4> -P 9030 -u root -e 'ADMIN SET FRONTEND CONFIG ("label_keep_max_second" = "259200")'
ERROR 1105 (HY000) at line 1: errCode = 2, detailMessage = The statement has been forwarded to master FE(172.20.80.2) and failed to execute because Master FE is not ready. You may need to check FE's status

(The IP in the message is the old master's own IP — the error is thrown by the old master itself.)

Evidence from the logs:

  • Observer (fe4) keeps forwarding to the old (dead/degraded) master after the switch:
    [fe4 log] MasterOpExecutor.forward: forward to master FE TNetworkAddress(hostname:172.20.80.2, port:9020)
    
  • Old master (fe1) throws the error on the proxy-forwarded statement:
    [fe1 log] StmtExecutor.executeByLegacy:1042 execute Exception.
    org.apache.doris.common.UserException: errCode = 2, detailMessage = The statement has been forwarded to master FE(172.20.80.2) and failed to execute because Master FE is not ready. You may need to check FE's status
    

Notes:

  • With a FORWARD_WITH_SYNC statement (CREATE USER), the client instead hangs in journal-sync wait on the observer for up to ~18 minutes before failing.
  • The recovery observed in production (rolling restart of the observer) matches this analysis: a restart forces an image load + full journal replay, refreshing masterInfo to the new master.
  • meta_delay_toleration_second (default 300s) eventually marks the observer not-ready, which changes the failure mode but does not fix the stale-forward window (5 minutes of broken/hanging statements even in the best case).

Anything Else?

Related prior work:

Suggested fix directions (for discussion):

  1. In FrontendServiceImpl.forward(): when the receiving FE is not the master, return a NOT_MASTER-style response immediately (with the real master address as a hint, similar to [feature](thrift) Add FE thrift rpc redirect master address #25371) instead of executing the statement deep into StmtExecutor and throwing "Master FE is not ready".
  2. In FEOpExecutor.forward(): on receiving NOT_MASTER (or on forward failure), re-discover the current master (e.g. via Env.getHaProtocol().getLeader() which asks bdbje directly and does not depend on journal replay) and retry once against the real master.

This combination would close the stale-forward window regardless of journal replay lag, and also prevents the journal-wait hang caused by the old master's stale maxJournalId in error responses.

Full reproduction scripts and timelines available if needed.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions