test(server): tolerate transient DB restart during test teardown#659
test(server): tolerate transient DB restart during test teardown#659rongxin-liu wants to merge 1 commit into
Conversation
Under heavy concurrent migration load the TimescaleDB test container occasionally crashes and auto-restarts within ~1-2s, briefly entering recovery mode (SQLSTATE 57P03). The per-test-database setup path already retries across this blip, but the t.Cleanup teardown dropped the database on a single unguarded admin connection, so any test whose DROP DATABASE landed in the recovery window failed with "error dropping test database" despite passing its assertions -- cascading across every test tearing down at that moment. Mirror the setup path's resilience in teardown: dropTestDatabase retries the DROP across transient server errors, waiting for the server to come back before each retry. The happy path stays at a single admin connection (the readiness ping only runs before a retry) so teardown adds no connection pressure that could itself provoke a crash. waitForServerReady now takes an explicit context because cleanup functions must use context.Background() -- t.Context() is canceled before they run.
🔐 Codex Security Review
Review SummaryOverall Risk: NONE FindingsNo reportable security, correctness, or reliability findings in the scoped diff. Notes
Generated by Codex Security Review | |
There was a problem hiding this comment.
Pull request overview
This PR hardens the Go server test DB harness (server/internal/testutil) to reduce CI flakes when the TimescaleDB container transiently restarts during highly concurrent migrations, by making per-test database teardown (t.Cleanup) retry DROP DATABASE across transient “server not ready” windows.
Changes:
- Route
t.Cleanupteardown through a newdropTestDatabase/tryDropTestDatabasepath with transient-retry + backoff. - Generalize
waitForServerReadyto accept an explicitcontext.Context(setup usest.Context(), teardown usescontext.Background()). - Keep the “happy path” to a single admin connection, only pinging readiness before retries.
| if !isTransientServerError(lastErr.Error()) || attempt == migrationMaxRetries { | ||
| break | ||
| } | ||
|
|
||
| t.Logf("drop test database failed transiently (attempt %d/%d), retrying: %v", attempt, migrationMaxRetries, lastErr) | ||
| // Wait for the server to come back before retrying, then back off. | ||
| waitForServerReady(ctx, t, adminConfig) | ||
| time.Sleep(time.Duration(attempt) * migrationRetryBaseDelay) |
| deadline := time.Now().Add(serverReadyTimeout) | ||
| for { | ||
| err := pingAdminDatabase(t.Context(), adminConfig) | ||
| err := pingAdminDatabase(ctx, adminConfig) | ||
| if err == nil { | ||
| return | ||
| } |
Reviewable diff: +72/-25 across 1 file (excludes generated, test, and story files).
Summary
The server test suite intermittently fails
Server Checks / Testwhen the TimescaleDB test container briefly crashes and auto-restarts under heavy concurrent-migration load (SQLSTATE57P03, "database system is in recovery mode"). The per-test-database setup path already retries across this blip; this change extends the same resilience to thet.Cleanupteardown, which previously dropped each test database on a single unguarded admin connection and failed any test whoseDROP DATABASEhappened to land in the ~1–2s recovery window — even though the test's own assertions had passed. One such restart cascades into several unrelated "error dropping test database" failures across whatever tests are tearing down at that moment.Observed on PR #658's run:
TestCollectionStore_ListCollections_SortsByIssueCountandTestServeHTTP_SelfMonitoringNoActiveOrgsFallsBackToUnscopedboth failed only at teardown, in the same 2-second window, with57P03.How it works
Every DB-backed test gets its own database via
testutil.GetTestDB, which creates the database, migrates it, and registers at.Cleanupthat drops it. The teardown now routes throughdropTestDatabase, which mirrors the existingcreateTestDatabaseretry loop:DROP DATABASEon a fresh admin connection.isTransientServerError), wait for the server to accept queries again (waitForServerReady) and retry with backoff.The happy path keeps teardown at a single admin connection — the readiness ping only runs before a retry, so normal teardowns add no extra connections (which would only worsen the very load that triggers the crash).
waitForServerReadynow takes an explicitcontext.Contextbecause cleanup functions must usecontext.Background():t.Context()is canceled beforet.Cleanupruns.Diagram
flowchart TD A["t.Cleanup fires"] --> B["conn.Close()"] B --> C["dropTestDatabase"] C --> D["tryDropTestDatabase: terminate + DROP DATABASE"] D -->|"success"| E["done"] D -->|"transient error (57P03 etc.)"| F{"attempts left?"} F -->|"yes"| G["waitForServerReady + backoff"] G --> D F -->|"no"| H["assert.NoError fails"] D -->|"non-transient error"| HAreas of the code involved
server/internal/testutil/database_setup.godropTestDatabase/tryDropTestDatabase;t.Cleanupnow retries the DROP across transient restarts;waitForServerReadygained an explicitctxparamKey technical decisions & trade-offs
isTransientServerError/waitForServerReadymachinery as setup, keeping the two paths symmetric — chosen over a bespoke teardown retry.createTestDatabase): avoids doubling admin connections on every teardown, which would add connection pressure to the exact scenario that provokes the crash.context.Background()in cleanup (with a scoped//nolint:usetesting): required becauset.Context()is already canceled once cleanup runs.Testing & validation
go build,go vet, andgolangci-lint runon./internal/testutil/...all pass.DROPcoinciding with a container restart), so it is not deterministically reproducible locally; correctness rests on teardown now sharing the setup path's already-proven retry behavior. Not covered: forcing an actual mid-teardown crash in a unit test.