Repository navigation
fix(etcd): identify the member by peer URL, refuse a duplicate ETCD_NAME, stop the EOF log spew - #154
Merged
Conversation
…AME, stop the EOF log spew The entrypoint found "self" in `member list` by name. A service running with another member's ETCD_NAME (onyx-staging: service etcd-3 with ETCD_NAME=etcd-2) matched the voting etcd-2 instead of its own learner entry, so promotion logged "Already a voting member", wrote the bootstrap marker, and the node stayed a learner for weeks: three members, two voters, no fault tolerance. - Find self by the member ID `member add` printed, else by the advertised peer URL; by name only when no peer URL is configured. Log the key used. - Refuse to join when a voting member already holds ETCD_NAME at another peer URL: explicit message, ComponentError telemetry, exit without retry. - remove_stale_self and move_leader_away no longer match by name. - The monitor confirms on every start that this node's own entry votes and promotes it if not; the marker waits for that on a learner join. A restarted member keeps retrying (slowly after the cap) instead of exiting. - Throttle etcd's gRPC gateway "ERROR: Failed to send request: EOF" lines in the stderr tee to one per minute with a repeat count.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
On onyx-staging/staging, the service
etcd-3runs withETCD_NAME=etcd-2. Its logs showAdding self as learner node=etcd-2,Joining as learner cluster=etcd-1=...,etcd-2=http://etcd-3.railway.internal:2380, and etcd'spublished local member ... Name:etcd-2 ... local-member-id a1331e7952d5ee8a. The realetcd-2service also publishesName:etcd-2(membereae04cfdcb9f0b03). So two members share a name, and their peer URLs differ.The entrypoint found "self" in
member listby name (is_learner,get_my_member_id). With two members namedetcd-2, both lookups matched the votingetcd-2, sopromote_selflogged "Already a voting member", the monitor wrote the bootstrap marker, and the node has stayed a learner since 2026-09-05. At boot on 2026-10-06 the raft config showed 2 voters and 1 learner (this node). That morning's join logs show it re-adding itself as a learner about 20 times in 30 minutes.remove_stale_selfalso matchedname == my_name || peer_url == my_peer_urland removed the first match, which with a shared name can be the other member's entry.The cluster had three members but only two voters, so it had no fault tolerance. On 2026-10-13 11:42Z one voter (etcd-2, placed cross-region) timed out and ReadIndex failed. Patroni lost the leader lock, and its etcd client rotated to the learner. The learner rejects watches with
etcdserver: rpc not supported for learner, logged about 1.8M times a day per Postgres member. The Postgres members have been replicas with no primary for more than 27 hours. Two more clusters of the same customer (onyx-prod/prod, myso-staging) show the same learner error.Separately, the etcd service logs
2026/10/13 11:42:27 ERROR: Failed to send request: EOFtwice every ~50 ms: about 7M lines a day on one member and about 20M across the three clusters. This line comes from etcd itself, not from this repo. etcd's gRPC gateway (ETCD_ENABLE_GRPC_GATEWAY=true, the HTTP/JSON API Patroni's etcd3 client uses) handles the bidirectional Watch and LeaseKeepAlive streams inapi/etcdserverpb/gw/rpc.pb.gw.go(v3.6.6,request_Watch_Watch_0/request_Lease_LeaseKeepAlive_0). Whenstream.Sendfails, it callsgrpclog.Errorf("Failed to send request: %v", err). etcd points grpclog's default logger at stderr (grpclog.NewLoggerV2(io.Discard, os.Stderr, os.Stderr)inserver/embed/config_logging.go), which explains the Gologtimestamp and theERROR:prefix. Each time the server ends a stream the client is still writing to, the gateway logs one line. Patroni reopening rejected watches against the learner produces this rate. The env varGRPC_GO_LOG_SEVERITY_LEVELcannot silence it because etcd installs its own grpclog logger.What changed
find_selflocates this node's entry by the member ID thatetcdctl member add --learnerprinted, when the node added itself in this run. Otherwise it uses the advertised peer URL fromETCD_INITIAL_ADVERTISE_PEER_URLS. etcd refuses a duplicate peer URL, so the peer URL is unique. Name matching remains only as a fallback when no peer URL is configured. If a peer URL is configured but absent from the list, the node is not a member, whatever other member shares its name.promote_self,remove_stale_self, the "already a member" check inadd_self_to_cluster, the cluster string, andmove_leader_away(which excluded self by name) all use this lookup now. Each log line records which key matched.ETCD_NAMEat a different peer URL, the node sends aComponentErrorand exits without retrying (a configuration error no retry fixes). The message reads:ETCD_NAME is etcd-2 but the voting member etcd-2 already exists at http://etcd-2.railway.internal:2380; this service advertises http://etcd-3.railway.internal:2380. ... To recover: set this service's ETCD_NAME to its own name (etcd-3) and redeploy.It does not auto-rename. A running member that already shares its name (the onyx case) is not killed. It logs and reports a one-time warning with the same fix and keeps working, because it now identifies itself by peer URL.EtcdPromotionFailedonce.ERROR: Failed to send request:andERROR: Failed to terminate client stream:. Each distinct message is written at most once a minute, followed by a line with the number of suppressed repeats. Every other line passes through untouched. The trigger itself (Patroni hammering a learner) goes away with the promotion fix.How it was tested
cargo test --locked -p etcd-entrypoint -p common: etcd-entrypoint 48 passed, common 14 passed, 0 failed. New tests:member addwins; a stale ID falls back to the peer URLmember addoutput parsing (ID andETCD_INITIAL_CLUSTER)cargo fmt --all --checkpasses.cargo clippy -p etcd-entrypoint --all-targetsreports one warning,for_kv_mapinget_voting_member_endpoint, which also exists onmainand is untouched here. These changes were not exercised against a live cluster.