fix(uats): never block on the feature-count groupBy — cold cache froze UAT selector
The ~30s groupBy over 9.7M GisFeature rows ran synchronously on the first /api/eterra/uats call after every redeploy (in-memory cache), freezing the UAT autocomplete right when users reload post-deploy. Counts only feed the decorative 'N local' badge — return the (possibly empty) cache immediately and refresh in the background, single-flight so concurrent cold requests don't stack 30s queries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,41 +15,46 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
const gCache = globalThis as {
|
const gCache = globalThis as {
|
||||||
__featureCountCache?: { map: Map<string, number>; ts: number };
|
__featureCountCache?: { map: Map<string, number>; ts: number };
|
||||||
|
__featureCountInFlight?: Promise<void> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getCachedFeatureCounts(): Promise<Map<string, number>> {
|
// NEVER block the response on the counts query — the groupBy takes ~30s
|
||||||
|
// on 9.7M GisFeature rows and the in-memory cache dies on every redeploy,
|
||||||
|
// which used to freeze the UAT selector for the first ~30s after a deploy
|
||||||
|
// (2026-06-04 incident). Counts only feed the decorative "N local" badge;
|
||||||
|
// a cold start simply renders the badge as absent until the first refresh
|
||||||
|
// lands. Single-flight so concurrent cold requests don't pile up 30s
|
||||||
|
// queries on the DB.
|
||||||
|
function getCachedFeatureCounts(): Map<string, number> {
|
||||||
const TTL = 5 * 60 * 1000; // 5 minutes
|
const TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
const now = Date.now();
|
const cached = gCache.__featureCountCache;
|
||||||
|
|
||||||
if (gCache.__featureCountCache && now - gCache.__featureCountCache.ts < TTL) {
|
if (!cached || Date.now() - cached.ts >= TTL) {
|
||||||
return gCache.__featureCountCache.map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run in background if cache exists but expired (return stale, refresh async)
|
|
||||||
if (gCache.__featureCountCache) {
|
|
||||||
void refreshFeatureCounts();
|
void refreshFeatureCounts();
|
||||||
return gCache.__featureCountCache.map;
|
|
||||||
}
|
}
|
||||||
|
return cached?.map ?? new Map();
|
||||||
// First call: must wait
|
|
||||||
return refreshFeatureCounts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshFeatureCounts(): Promise<Map<string, number>> {
|
function refreshFeatureCounts(): Promise<void> {
|
||||||
try {
|
if (gCache.__featureCountInFlight) return gCache.__featureCountInFlight;
|
||||||
const groups = await prisma.gisFeature.groupBy({
|
gCache.__featureCountInFlight = (async () => {
|
||||||
by: ["siruta"],
|
try {
|
||||||
_count: { id: true },
|
const groups = await prisma.gisFeature.groupBy({
|
||||||
});
|
by: ["siruta"],
|
||||||
const map = new Map<string, number>();
|
_count: { id: true },
|
||||||
for (const g of groups) {
|
});
|
||||||
map.set(g.siruta, g._count.id);
|
const map = new Map<string, number>();
|
||||||
|
for (const g of groups) {
|
||||||
|
map.set(g.siruta, g._count.id);
|
||||||
|
}
|
||||||
|
gCache.__featureCountCache = { map, ts: Date.now() };
|
||||||
|
} catch {
|
||||||
|
// keep whatever cache we had; next request retries
|
||||||
|
} finally {
|
||||||
|
gCache.__featureCountInFlight = null;
|
||||||
}
|
}
|
||||||
gCache.__featureCountCache = { map, ts: Date.now() };
|
})();
|
||||||
return map;
|
return gCache.__featureCountInFlight;
|
||||||
} catch {
|
|
||||||
return gCache.__featureCountCache?.map ?? new Map();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -171,10 +176,9 @@ export async function GET() {
|
|||||||
select: { siruta: true, name: true, county: true, workspacePk: true },
|
select: { siruta: true, name: true, county: true, workspacePk: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Feature counts: use in-memory cache (refreshed every 5 min)
|
// Feature counts: in-memory cache, refreshed in the background (never
|
||||||
// The groupBy query is expensive (~25s without cache) but the data
|
// awaited — see getCachedFeatureCounts). Cold cache → badge absent.
|
||||||
// changes rarely (only when sync jobs run)
|
const featureCounts = getCachedFeatureCounts();
|
||||||
const featureCounts = await getCachedFeatureCounts();
|
|
||||||
|
|
||||||
const uats: UatResponse[] = rows.map((r) => ({
|
const uats: UatResponse[] = rows.map((r) => ({
|
||||||
siruta: r.siruta,
|
siruta: r.siruta,
|
||||||
|
|||||||
Reference in New Issue
Block a user