Skip to content

fix(database): retry failed PostgreSQL logins and report them as Unavailable - #3550

Open
danielle-mustillo wants to merge 4 commits into
mainfrom
danielle-mustillo/db-connect-retry
Open

danielle-mustillo wants to merge 4 commits into
mainfrom
danielle-mustillo/db-connect-retry

Conversation

@danielle-mustillo

@danielle-mustillo danielle-mustillo commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Why

When the controller can't log in to PostgreSQL, for example during a restart, a failover, or a serverless database resuming from a pause, every API call fails at once with Internal "internal server error". Nothing is logged, and clients can't tell that a retry would work.

What changed

  • Retry (second commit). Store calls retry a failed login with backoff for up to database.pool.connectRetryTimeout (default 90s, 0s turns it off). No SQL has been sent when a login fails, so a retry never repeats a statement. The caller's context still ends the retry.
  • Error code (first commit). A failed login returns Unavailable "database unavailable", and the cause is logged. A login PostgreSQL rejects (28P01, 28000, 3D000, 42501) stays Internal and isn't retried. A database failure during runtime auth returns Unavailable instead of Unauthenticated.
  • A login's TCP dial stops after 10 s, so a host that never answers can't hold one attempt for 2 minutes.
  • helm/README.md explains the retry window and how client timeouts cut it short. The web UI waits 30 s.

Scope

The two commits are separate on purpose. The first one (error code and log) is useful without the retry.

Blast radius

This touches every store call. A call that used to fail at once can now wait up to 90 s before it fails. Set connectRetryTimeout: 0s to get the old behavior back.

Known limits

  • A proxy in front of PostgreSQL that replaces the server's error with a generic one hides a wrong password. The controller then retries it for the full window and reports Unavailable.
  • A query that fails after login, such as one killed by a failover (57P01), isn't retried. Runtime auth still reports it as Unauthenticated.

Verification

  • Unit tests put a proxy in front of a real PostgreSQL that drops, holds or rejects logins.
  • Real Aurora Serverless v2 resuming from a pause, behind an IAM auth proxy that cuts each login at 15 s. Each attempt was the first request after CloudWatch showed 0 capacity. With the default 90s window, 2 of 2 session creates succeeded in 18.4 s and 20.4 s, after one retry each. With 0s, 2 of 2 failed with Unavailable at 15.3 s.
  • Kind cluster, database cut off with a NetworkPolicy, runtime auth: before, Unauthenticated after 30 s with no cause logged. After, Unavailable after 20.5 s (20 s window) with the cause logged.
  • Kind cluster, login role set to NOLOGIN: Internal in 0.3 s after one attempt, with SQLSTATE 28000 logged.
  • Tests that fail on this branch (TestSessionExpiration*, TestEgressPolicyCatalog, TestSandboxTemplateCatalog) fail the same way on main.

@github-actions github-actions Bot added bug Something isn't working and removed bug Something isn't working labels Nov 6, 2026
@danielle-mustillo
danielle-mustillo marked this pull request as ready for review November 6, 2026 04:33
@danielle-mustillo
danielle-mustillo requested a review from a team as a code owner November 6, 2026 04:33
@danielle-mustillo
danielle-mustillo force-pushed the danielle-mustillo/db-connect-retry branch from 1c1825b to 50fd57d Compare November 6, 2026 16:38
When the controller could not log in to PostgreSQL at runtime, the RPC
returned Internal "internal server error" and the cause never reached
the logs.

- A *pgconn.ConnectError in the error chain now maps to Unavailable
  "database unavailable", so clients know to retry. Internal errors log
  their cause.
- A login PostgreSQL rejects (28P01, 28000, 3D000, 42501) is a config
  error, not an outage, so it stays Internal.
- A database failure during runtime auth returns Unavailable instead of
  Unauthenticated "invalid credentials".

Signed-off-by: Danielle Mustillo <danielle.mustillo@solo.io>
The store's pool makes one login attempt per acquire. When PostgreSQL is
briefly unreachable, for example during a restart or failover, a store
call fails at once even though the next login would succeed.

- Client.db is a retryingPool with Exec, Query and BeginTx. It retries a
  failed login (database.IsUnavailable) with backoff, 0.5 s doubling to
  5 s. A login fails before any SQL is sent, so no statement repeats.
- database.pool.connectRetryTimeout sets how long new attempts may start,
  90s by default, 0s to turn the retry off. The caller's context still
  ends the retry.
- Each login's TCP dial stops after 10 s when the URL sets no
  connect_timeout, so one hung dial cannot outlast the retry window.
- helm/README.md explains the retry window and how client timeouts,
  such as the web UI's 30 s, cut it short.

Signed-off-by: Danielle Mustillo <danielle.mustillo@solo.io>
@danielle-mustillo
danielle-mustillo force-pushed the danielle-mustillo/db-connect-retry branch from 50fd57d to 55fbb98 Compare November 6, 2026 16:40
@EItanya
EItanya requested review from iplay88keys and a balanced review from Copilot November 6, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The retry loop can start attempts after its time budget expires, while rejected database logins bypass logging and incorrectly return Unauthenticated instead of Internal.

2 open issues
1 file not reviewed
  • go/api/gen/kagent/config/v1/config.pb.go: Generated file
What changed in this PR

This PR improves how the controller handles PostgreSQL login failures, adding configurable retries and clearer gRPC errors without retrying statements that have already run.

Changes:

  • Adds login retries with backoff, a default 90-second retry window, and a 10-second default TCP dial timeout.
  • Maps transient login failures to Unavailable and logs server-side error causes.
  • Adds configuration validation, failure-path tests, and Helm documentation.
File Description
proto/​kagent/​config/​v1/​config.proto Defines and validates the retry timeout.
helm/​README.md Explains retries and client timeout interactions.
helm/​kagent/​values.yaml Adds a commented retry configuration example.
go/​core/​pkg/​postgres/​pool.go Bounds TCP dialing when no connection timeout is configured.
go/​core/​pkg/​config/​load_test.go Tests retry timeout validation and defaults.
go/​core/​pkg/​config/​helm_test.go Tests Helm defaults and disabling retries.
go/​core/​pkg/​config/​defaults.go Sets the default retry window to 90 seconds.
go/​core/​pkg/​app/​app.go Passes retry configuration into the store.
go/​core/​internal/​grpcserver/​interceptors.go Maps database failures and logs error causes.
go/​core/​internal/​grpcserver/​interceptors_test.go Tests error mapping, logging, and runtime authentication failures.
go/​core/​internal/​dbtest/​dbtest.go Adds a fake server that rejects PostgreSQL logins.
go/​core/​internal/​database/​sql.go Wraps pool operations with login retries.
go/​core/​internal/​database/​session_deletion_test.go Adapts client reconstruction to the pool wrapper.
go/​core/​internal/​database/​push_notifications_test.go Uses the shared pool for direct test queries.
go/​core/​internal/​database/​errors.go Classifies retryable login failures.
go/​core/​internal/​database/​errors_test.go Tests login failure classification.
go/​core/​internal/​database/​connect_retry_test.go Tests recovery, retry limits, and statement safety.
go/​core/​internal/​database/​client_postgres.go Adds the retry option and integrates the pool wrapper.
go/​api/​gen/​kagent/​config/​v1/​config.pb.go Regenerates protobuf bindings for the retry setting.

Review effort: Balanced

Comment thread go/core/internal/database/sql.go Resolved
@blacksmith-sh

This comment has been minimized.

danielle-mustillo and others added 2 commits November 6, 2026 14:02
A rejected controller login to PostgreSQL during actor authentication
returned Unauthenticated "invalid credentials" and logged nothing. Every
database login error now goes through the error mapper, so a rejected
login returns Internal and logs its cause.

Signed-off-by: Danielle Mustillo <danielle.mustillo@solo.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants