Skip to content

A bundled PostgreSQL pod restart leaves ate-api-server NotReady and failing for 2 minutes: pgx dials the stale pod IP with no connect timeout #2995

Open

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 main at 4b4fbb2 (2026-10-20), ate-setup deploy ate-system defaults: bundled PostgreSQL StatefulSet behind the headless Service postgres (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 min

Timeline (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.go poolConfig sets no ConnConfig.ConnectTimeout and the DSN carries no connect_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's store check shares the same pool configuration, so /readyz stays 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), not Unavailable. 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.ConnectTimeout of a few seconds, or connect_timeout in the DSN) so a stale address fails fast and the pool re-resolves.
  • Classify store connectivity failures as Unavailable so 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

  1. aditya-shantanu commented on Oct 20, 2026

    @aditya-shantanu
    CollaboratorAuthor

    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 Internal while the actor's workflow is mid-flight:

    2026-10-20T20:23:22Z  workflow failed at step FinalizeRunning: while updating actor: updating actor benchmark/sb-2a96af47-...: failed to connect to `user=substrate_readwrite_user database=atepg`: hostname resolving error: lookup postgres.ate-system.svc on 34.118.224.10:53: no such host
    2026-10-20T20:23:49Z  workflow failed at step AssignWorker: while updating actor: getting actor benchmark/sb-df270af2-... for update: failed to connect to ...: hostname resolving error: lookup postgres.ate-system.svc on 34.118.224.10:53: no such host
    2026-10-20T20:23:12Z  getting actor benchmark/sb-154cfbb8-...: failed to connect to ...: hostname resolving error: ...  (GetActor; the router surfaced it as HTTP 500 "error resuming actor")
    

    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.svc on every new connection, so any resolver blip has the same user-visible result as the stale-IP case above, and the error is still classified Internal rather than Unavailable.

  2. ericcurtin commented on Oct 24, 2026

    @ericcurtin
    Contributor

    I'll take the connect timeout part of this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions