Repository navigation
Description
Summary
Deleting the bundled postgres-0 pod (it was back and Running 2/2 within 20 s) left both ate-api-server replicas NotReady and failing every store call for about 2 minutes. The cause is visible in the logs: ate-api-server kept dialing the old pod IP and each attempt only failed after the OS TCP connect timeout, because the pgx pool has no connect timeout configured. Clients saw codes.Internal for the whole window.
Environment
- Substrate
mainat 4b4fbb2 (2026-10-20),ate-setup deploy ate-systemdefaults: bundled PostgreSQL StatefulSet behind the headless Servicepostgres(clusterIP: None), 2 ate-api-server replicas. - GKE 1.36.4, 3x n2-standard-8. Light steady load: 20 glutton users, async suspend (pause) and resume, about 12 requests/s through the ateapi gRPC API.
Reproduction
kubectl -n ate-system delete pod postgres-0 --grace-period=0 --force
watch kubectl -n ate-system get pods -l app=ate-api-server # READY 1/2 on both replicas for ~2 minTimeline (Cloud Logging, ate-api-server container, UTC)
| Time | What |
|---|---|
| 19:25:38 | postgres-0 force deleted. New pod created 19:25:39 with pod IP 10.0.129.10 (old: 10.0.129.7). |
| 19:25:30 to 19:25:49 | 271 worker outbox poll failed warnings: dial to 10.0.129.7:5432 refused, then FATAL: the database system is shutting down. RPC handlers return getting actor ...: failed to connect ... dial error: ... connection refused. |
| ~19:26:00 | postgres-0 is Running 2/2. |
| 19:25:50 to 19:27:40 | No successful store operation in either replica. Only listing actors ... context canceled (callers giving up). Readiness probe fails continuously: 25 consecutive 503s on one replica, 64 on the other (period 2 s). |
| 19:27:40 | worker outbox polling has failed persist: failed to connect ... 10.0.129.7:5432 ... dial error: timeout: context deadline exceeded. The dial to the stale IP finally times out. |
| 19:27:45 | Normal operation resumes on both replicas. |
Client side (Locust/boomer over the ateapi Service): ResumeActor 314 failures out of 552 in the window, all code = Internal desc = internal server error: getting actor <name>: failed to connect ... or ... workflow failed at step FinalizeRunning: getting actor ...; one call blocked for 127 s. One response carried rpc error: code = Code(4000) desc = Internal Server Error inside the Internal text (the authorization store client's error).
Actors: 20 of 22 ended SUSPENDED, no CRASHED, no stuck transitional state; the invariant checker was clean two minutes later.
Analysis
- The StatefulSet pod comes back with a different IP behind a headless Service. Connections resolved to the old address have to fail before the pool reconnects.
cmd/ateapi/internal/store/atepg/atepg.gopoolConfigsets noConnConfig.ConnectTimeoutand the DSN carries noconnect_timeout, so a dial to a dead address runs for the Linux SYN retry budget (about 127 s, which matches 19:25:40 to 19:27:40 exactly). The readiness probe'sstorecheck shares the same pool configuration, so/readyzstays 503 for the same period and the whole control plane is unavailable for 2 minutes after a 20 second database restart.- The error reaches clients as
Internal(non-retriable for the router's parking logic and for kubectl-ate), notUnavailable. Support OCI seccomp profiles inside gVisor sandboxes #2277 covers hiding the raw pgx text; it does not change the code or the recovery time.
What would help
- Set a short connect timeout on the pgx pool (
ConnConfig.ConnectTimeoutof a few seconds, orconnect_timeoutin the DSN) so a stale address fails fast and the pool re-resolves. - Classify store connectivity failures as
Unavailableso callers and the router retry instead of failing hard. - Document that a bundled PostgreSQL restart is expected to cause a brief outage and how long.
Evidence
Cloud Logging export of the window (690 entries), readiness probe events, Locust stats and the invariant checker output are saved from the run and available on request.
Activity
aditya-shantanu commentedon Oct 20, 2026
Same failure class, different trigger: with kube-dns scaled to 0 for 2 minutes (chaos test on main 4b4fbb2, 20 users of pause/resume load), new pgx connections fail at name resolution and the error leaks to clients as
Internalwhile the actor's workflow is mid-flight:Pooled connections that were already open kept working, so only a handful of calls failed (fail ratio about 1%), no readiness flap, no restart, and both actors ended up SUSPENDED once DNS was back. The point for this issue: pgx resolves
postgres.ate-system.svcon every new connection, so any resolver blip has the same user-visible result as the stale-IP case above, and the error is still classifiedInternalrather thanUnavailable.ericcurtin commentedon Oct 24, 2026
I'll take the connect timeout part of this.