mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(api): wait for Postgres and Redis at startup instead of crash-looping (#537)
Briefly retry Postgres and Redis connectivity at boot (waitForService, DB_STARTUP_TIMEOUT_MS) so an ordered-but-not-yet-ready dependency recovers cleanly instead of crash-looping.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { waitForService } from "../../apps/api/src/lib/wait-for-service.js";
|
||||
|
||||
// Deterministic fake clock: `sleep` advances virtual time and `now` reads it,
|
||||
// so we can assert retry timing without real timers slowing the suite down.
|
||||
function fakeClock() {
|
||||
let ms = 0;
|
||||
return {
|
||||
now: () => ms,
|
||||
sleep: async (delta: number) => {
|
||||
ms += delta;
|
||||
},
|
||||
elapsed: () => ms,
|
||||
};
|
||||
}
|
||||
|
||||
describe("waitForService", () => {
|
||||
it("resolves immediately when the probe succeeds on the first attempt", async () => {
|
||||
const clock = fakeClock();
|
||||
let attempts = 0;
|
||||
await waitForService(
|
||||
async () => {
|
||||
attempts++;
|
||||
},
|
||||
{ timeoutMs: 30_000, intervalMs: 1_000, now: clock.now, sleep: clock.sleep },
|
||||
);
|
||||
expect(attempts).toBe(1);
|
||||
expect(clock.elapsed()).toBe(0); // never slept
|
||||
});
|
||||
|
||||
it("retries until the probe succeeds, then resolves", async () => {
|
||||
const clock = fakeClock();
|
||||
let attempts = 0;
|
||||
await waitForService(
|
||||
async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) throw new Error("not ready");
|
||||
},
|
||||
{ timeoutMs: 30_000, intervalMs: 1_000, now: clock.now, sleep: clock.sleep },
|
||||
);
|
||||
expect(attempts).toBe(3);
|
||||
expect(clock.elapsed()).toBe(2_000); // slept twice between three attempts
|
||||
});
|
||||
|
||||
it("rejects with the last probe error once the timeout elapses", async () => {
|
||||
const clock = fakeClock();
|
||||
let attempts = 0;
|
||||
await expect(
|
||||
waitForService(
|
||||
async () => {
|
||||
attempts++;
|
||||
throw new Error(`refused #${attempts}`);
|
||||
},
|
||||
{ timeoutMs: 3_000, intervalMs: 1_000, now: clock.now, sleep: clock.sleep },
|
||||
),
|
||||
).rejects.toThrow("refused #4"); // attempts fire at t=0,1000,2000,3000
|
||||
expect(attempts).toBe(4);
|
||||
});
|
||||
|
||||
it("reports each failed attempt through the onRetry callback", async () => {
|
||||
const clock = fakeClock();
|
||||
let attempts = 0;
|
||||
const retried: number[] = [];
|
||||
await waitForService(
|
||||
async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) throw new Error("not ready");
|
||||
},
|
||||
{
|
||||
timeoutMs: 30_000,
|
||||
intervalMs: 1_000,
|
||||
now: clock.now,
|
||||
sleep: clock.sleep,
|
||||
onRetry: (attempt) => retried.push(attempt),
|
||||
},
|
||||
);
|
||||
expect(retried).toEqual([1, 2]); // two failures before the third-attempt success
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user