Skip to content

Commit ff03f94

Browse files
h4x3rotabclaude
andcommitted
docs(consul-postgres-ha): tfvars credential-trap fix + ICE auth-race bug writeup
Two findings from live verification of the declarative-services refactor: 1. README's PHALA_CLOUD_API_KEY snippet silently followed `~/.phala-cloud/credentials.json`'s `current_profile`, so the deploy billed whichever account was last `phala auth login`-ed into. Replace with an explicit `export PHALA_CLOUD_API_KEY=phak_...` instruction so the user picks a specific token. Caught when a verification run deployed onto an unrelated profile. 2. mesh-conn's "fresh auth supersedes consumed" cancellation (commit cc5a12d) is symmetric — when both peers restart in close succession, each side keeps cancelling the other's in-flight dial as fresher auth arrives, and the 5-second back-off doesn't break the symmetry. Observed in live testing: multiple worker↔coordinator pairs flapping >7 minutes without converging, blocking Consul RPC and replica pg_basebackup. Adds an "ICE auth-race convergence bug" section to ROBUSTNESS.md walking the symptom, the diagnosis, and three possible fix shapes (asymmetric back-off keyed on peer-id lexicographic order is the cheapest experiment). Promotes it to top of the Layer 1 recommended-fixes list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d84aee commit ff03f94

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

consul-postgres-ha/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ inside the app.
5454

5555
Prerequisites:
5656

57-
- A Phala Cloud account with API credentials at `~/.phala-cloud/credentials.json`.
57+
- A Phala Cloud API key for the account you want this cluster billed to.
58+
Get one from the Phala dashboard (Settings → API Keys) or use the
59+
one already in your `~/.phala-cloud/credentials.json` profile if
60+
you've used `phala` CLI before — but **pick a specific token**,
61+
don't let it follow whatever profile is currently selected.
5862
- A Linux box with a public IP for the external coordinator (coturn + signaling).
5963
- The four container images (`mesh-sidecar`, `patroni`, `webdemo`,
6064
`signaling`) either already published to GHCR (via the CI workflow
@@ -66,9 +70,11 @@ cd consul-postgres-ha/cluster-example
6670
cp terraform.tfvars.example terraform.tfvars
6771
$EDITOR terraform.tfvars # set gateway_domain, image refs, external_*
6872

69-
export PHALA_CLOUD_API_KEY=$(python3 -c "
70-
import json; d=json.load(open('$HOME/.phala-cloud/credentials.json'))
71-
print(d['profiles'][d['current_profile']]['token'])")
73+
# Set this explicitly to a specific account's token. Do NOT auto-pull
74+
# from credentials.json's `current_profile` — that follows whichever
75+
# account you last `phala auth login`-ed into, and a wrong-account
76+
# deploy is silent until you spot it in the dashboard.
77+
export PHALA_CLOUD_API_KEY=phak_...
7278

7379
terraform init
7480
terraform apply -parallelism=1 # phala-cloud#247 needs serial creates

consul-postgres-ha/ROBUSTNESS.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,80 @@ whose direct path goes down can't fail over to TURN until it's back".
9797
| Resource exhaustion (many TCP streams) | QUIC per-session limits kick in (256 streams default); new TCP streams to that peer fail. UDP and existing TCP unaffected. | bump `AcceptBacklog` / `MaxIncomingStreams` if it ever hits us at scale. |
9898
| Head-of-line blocking | A big TCP write on one stream briefly delays a UDP datagram or another TCP stream. Imperceptible at Consul scale. | None needed today. If a future workload becomes jitter-sensitive, split into two ICE conns per pair (UDP-only + TCP-only). |
9999

100+
### The ICE auth-race convergence bug — the one real open issue
101+
102+
`dialICE` both publishes its local auth (ufrag+pwd) via the signalling
103+
broker and waits on the remote auth from the broker. On a peer
104+
restart or fresh bring-up, both sides republish in close succession.
105+
mesh-conn's protection against consuming a stale auth is to compare
106+
the most-recently-seen remote auth against the one it's currently
107+
dialing against; if a fresher auth arrives mid-dial, the in-flight
108+
attempt is cancelled and `runPeerLink` retries after 5 s.
109+
110+
What goes wrong is that **both peers do this symmetrically**:
111+
112+
```
113+
peer-A consumed=X publishes auth Y → dials with peer-B's X
114+
peer-B sees A's Y, supersedes its X-bound dial, publishes new auth Z
115+
peer-A sees B's Z, supersedes its Y-bound dial
116+
peer-B sees A's next auth, supersedes again
117+
118+
```
119+
120+
The 5 s back-off doesn't break the symmetry, so the loop never
121+
converges. Observed in live testing on 2026-05-13: multiple worker↔
122+
coordinator and worker↔worker pairs flapping for 7+ minutes without
123+
stabilizing, even with `MESH_CONN_RELAY_ONLY=1` set (so it's not a
124+
NAT-direct-path issue). Per-peer log signature:
125+
126+
```
127+
[<peer>] ice state: Checking
128+
[<peer>] fresh auth (ufrag=Y) supersedes consumed (ufrag=X) — aborting attempt
129+
[<peer>] link failed: ice: connecting canceled by caller — retrying in 5s
130+
[<peer>] ice state: Closed
131+
```
132+
133+
The previous commit message acknowledged this: `cc5a12d fix(consul-
134+
postgres-ha): partial — cancel in-flight ICE dial on fresher peer
135+
auth`. The cancellation logic is correct; the convergence isn't.
136+
137+
Practical impact: when this happens, the affected peer pair never
138+
forms a working link, which means the Consul agent on one side
139+
can't reach the server tier (Raft RPC on `:8300`), Envoy can't fetch
140+
its xDS config, and any cross-peer Connect call through that pair
141+
fails. Patroni leader election still works (Consul KV is on the
142+
working pairs), but replica `pg_basebackup` fails because the
143+
replica's local Envoy has no endpoint for `postgres-master`.
144+
145+
**Possible fix shapes**:
146+
147+
- **Asymmetric back-off keyed on peer ID lexicographic order.** The
148+
side with the *higher* peer-id waits 5 s before redialing; the
149+
*lower* side dials immediately. Breaks the symmetry; one side
150+
always wins the auth race within one cycle.
151+
- **Don't supersede if the new auth came from a poll happening
152+
during the in-flight dial.** Today's logic supersedes on any newer
153+
auth; a tighter rule would only supersede if the auth changed for
154+
a reason other than "the remote peer also just restarted." Harder
155+
to define; the asymmetric back-off is simpler.
156+
- **A "settling" window after publishing auth.** Don't act on a
157+
remote auth that arrives in the first ~1 s after we publish ours;
158+
the remote is probably reacting to our publish, not to a peer
159+
restart we should respect.
160+
161+
The first of these is the cheapest experiment. Needs a focused
162+
mesh-conn debugging session with `MESH_CONN_DEBUG_ICE=1` traces from
163+
a fresh repro, looking at which side initiates each supersession.
164+
100165
### Recommended fixes
101166

102-
1. **Set a QUIC read deadline on the UDP-stream pumps**, so if a
167+
1. **The ICE auth-race convergence bug above** — top priority. Blocks
168+
end-to-end live verification of every recent refactor.
169+
2. **Set a QUIC read deadline on the UDP-stream pumps**, so if a
103170
stream silently stalls (QUIC keep-alive happens at session
104171
level, not stream level), the pump returns and `runPeerLink`
105172
restarts.
106-
2. **Tune QUIC `MaxStreamWindowSize`** if we ever need higher
173+
3. **Tune QUIC `MaxStreamWindowSize`** if we ever need higher
107174
throughput; default is 256 KB which is fine for now.
108175

109176
### Already shipped

0 commit comments

Comments
 (0)