fix(cache): stop serving stale site-settings defaults after deploy
The site-settings loader kept an in-process map forever after a Redis miss and promoted DEFAULTS (no logo/theme) to Redis on any DB error, so a build that started before the DB was reachable stuck the site on the preset logo and default theme until a manual reload or full restart. - Redis miss now reloads from the database instead of the stale in-process map - a DB failure returns defaults only as an in-process last resort and never writes them to Redis, so the shared cache can't be poisoned by a transient error at startup - regression tests: DB re-read on Redis miss after cache expiry, defaults never promoted to Redis, recovery from transient DB failure
This commit is contained in:
1 parent
9dc9d1fa4b
commit
35f66d879f
2 files changed
+81
-19
No files matched your search
@@ -1,6 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { selectFrom } = vi.hoisted(() => ({ selectFrom: vi.fn() }));
|
||||
const { selectFrom, redisMock } = vi.hoisted(() => ({
|
||||
selectFrom: vi.fn(),
|
||||
redisMock: {
|
||||
get: vi.fn(),
|
||||
setex: vi.fn(),
|
||||
del: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
select: () => ({
|
||||
@@ -15,10 +23,18 @@ vi.mock("@/lib/db", () => ({
|
||||
WebsiteSetting: { key: "WebsiteSetting.key", value: "WebsiteSetting.value" },
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/redis", () => ({ redis: redisMock }));
|
||||
|
||||
import { siteSettings } from "./site-settings";
|
||||
|
||||
beforeEach(async () => {
|
||||
selectFrom.mockReset();
|
||||
redisMock.get.mockReset();
|
||||
redisMock.setex.mockReset();
|
||||
redisMock.del.mockReset();
|
||||
redisMock.get.mockResolvedValue(null);
|
||||
redisMock.setex.mockResolvedValue(undefined);
|
||||
redisMock.del.mockResolvedValue(undefined);
|
||||
await siteSettings.reload();
|
||||
});
|
||||
|
||||
@@ -53,4 +69,33 @@ describe("siteSettings", () => {
|
||||
await siteSettings.get("hotel_name");
|
||||
expect(selectFrom).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("re-reads the database on a Redis miss instead of serving stale cache", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
selectFrom.mockResolvedValue([{ key: "hotel_name", value: "First" }]);
|
||||
expect(await siteSettings.get("hotel_name")).toBe("First");
|
||||
|
||||
selectFrom.mockResolvedValue([{ key: "hotel_name", value: "Second" }]);
|
||||
vi.setSystemTime(Date.now() + 2 * 60_000); // expire the in-memory cache
|
||||
expect(await siteSettings.get("hotel_name")).toBe("Second");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not promote defaults to Redis when the database fails", async () => {
|
||||
selectFrom.mockRejectedValue(new Error("db down"));
|
||||
expect(await siteSettings.get("hotel_name", "fallback")).toBe("fallback");
|
||||
expect(redisMock.setex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("recovers from a transient database failure on the next load", async () => {
|
||||
selectFrom.mockRejectedValueOnce(new Error("db down"));
|
||||
expect(await siteSettings.get("hotel_name", "fallback")).toBe("fallback");
|
||||
|
||||
selectFrom.mockResolvedValueOnce([{ key: "hotel_name", value: "Live" }]);
|
||||
expect(await siteSettings.get("hotel_name")).toBe("Live");
|
||||
expect(redisMock.setex).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user