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
@@ -32,15 +32,15 @@ class SiteSettings {
|
||||
// not each hammer Redis/DB (cache-stampede protection).
|
||||
private inFlight: Promise<Map<string, string>> | null = null;
|
||||
|
||||
private async loadFromDb(): Promise<Map<string, string>> {
|
||||
private async loadFromDb(): Promise<Map<string, string> | null> {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
|
||||
.from(WebsiteSetting);
|
||||
return new Map(rows.map((r) => [r.key, r.value]));
|
||||
} catch {
|
||||
logger.warn("Failed to load site settings from database, using defaults");
|
||||
return new Map(Object.entries(DEFAULTS));
|
||||
logger.warn("Failed to load site settings from database");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,26 +59,43 @@ class SiteSettings {
|
||||
}
|
||||
}
|
||||
|
||||
// Expired but usable fallback — keeps the site up if both Redis and DB fail.
|
||||
if (this.cache !== null) return this.cache.map;
|
||||
|
||||
// Redis miss/unavailable → the database is authoritative. Reloading here
|
||||
// (instead of serving the in-process map) is what picks up changes made
|
||||
// by other instances after the Redis TTL expires.
|
||||
const map = await this.loadFromDb();
|
||||
this.cache = { map, expiresAt: Date.now() + MEMORY_TTL_MS };
|
||||
if (map) {
|
||||
this.cache = { map, expiresAt: Date.now() + MEMORY_TTL_MS };
|
||||
|
||||
if (redis) {
|
||||
try {
|
||||
const obj = Object.fromEntries(map.entries());
|
||||
await redis.setex(
|
||||
REDIS_CACHE_KEY,
|
||||
Math.ceil(CACHE_TTL_MS / 1000),
|
||||
JSON.stringify(obj),
|
||||
);
|
||||
} catch {
|
||||
logger.warn("Failed to write site settings to Redis cache");
|
||||
if (redis) {
|
||||
try {
|
||||
const obj = Object.fromEntries(map.entries());
|
||||
await redis.setex(
|
||||
REDIS_CACHE_KEY,
|
||||
Math.ceil(CACHE_TTL_MS / 1000),
|
||||
JSON.stringify(obj),
|
||||
);
|
||||
} catch {
|
||||
logger.warn("Failed to write site settings to Redis cache");
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
return map;
|
||||
// DB unavailable → keep serving the last known-good map from memory.
|
||||
if (this.cache !== null) {
|
||||
logger.warn("Site settings: database unreachable, serving stale cache");
|
||||
return this.cache.map;
|
||||
}
|
||||
|
||||
// Nothing cached in this process and the DB is down → last resort. These
|
||||
// defaults are kept in-memory only and never written to Redis, so a
|
||||
// momentary DB failure at startup can't poison the shared cache and
|
||||
// downgrade every instance to the default/preset branding.
|
||||
logger.warn(
|
||||
"Site settings: database unreachable and no cached copy, using defaults",
|
||||
);
|
||||
return new Map(Object.entries(DEFAULTS));
|
||||
}
|
||||
|
||||
private async load(): Promise<Map<string, string>> {
|
||||
|
||||
Reference in new issue
Block a user