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 {
|
||||
__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 now = Date.now();
|
||||
const cached = gCache.__featureCountCache;
|
||||
|
||||
if (gCache.__featureCountCache && now - gCache.__featureCountCache.ts < TTL) {
|
||||
return gCache.__featureCountCache.map;
|
||||
}
|
||||
|
||||
// Run in background if cache exists but expired (return stale, refresh async)
|
||||
if (gCache.__featureCountCache) {
|
||||
if (!cached || Date.now() - cached.ts >= TTL) {
|
||||
void refreshFeatureCounts();
|
||||
return gCache.__featureCountCache.map;
|
||||
}
|
||||
|
||||
// First call: must wait
|
||||
return refreshFeatureCounts();
|
||||
return cached?.map ?? new Map();
|
||||
}
|
||||
|
||||
async function refreshFeatureCounts(): Promise<Map<string, number>> {
|
||||
try {
|
||||
const groups = await prisma.gisFeature.groupBy({
|
||||
by: ["siruta"],
|
||||
_count: { id: true },
|
||||
});
|
||||
const map = new Map<string, number>();
|
||||
for (const g of groups) {
|
||||
map.set(g.siruta, g._count.id);
|
||||
function refreshFeatureCounts(): Promise<void> {
|
||||
if (gCache.__featureCountInFlight) return gCache.__featureCountInFlight;
|
||||
gCache.__featureCountInFlight = (async () => {
|
||||
try {
|
||||
const groups = await prisma.gisFeature.groupBy({
|
||||
by: ["siruta"],
|
||||
_count: { id: true },
|
||||
});
|
||||
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;
|
||||
} catch {
|
||||
return gCache.__featureCountCache?.map ?? new Map();
|
||||
}
|
||||
})();
|
||||
return gCache.__featureCountInFlight;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -171,10 +176,9 @@ export async function GET() {
|
||||
select: { siruta: true, name: true, county: true, workspacePk: true },
|
||||
});
|
||||
|
||||
// Feature counts: use in-memory cache (refreshed every 5 min)
|
||||
// The groupBy query is expensive (~25s without cache) but the data
|
||||
// changes rarely (only when sync jobs run)
|
||||
const featureCounts = await getCachedFeatureCounts();
|
||||
// Feature counts: in-memory cache, refreshed in the background (never
|
||||
// awaited — see getCachedFeatureCounts). Cold cache → badge absent.
|
||||
const featureCounts = getCachedFeatureCounts();
|
||||
|
||||
const uats: UatResponse[] = rows.map((r) => ({
|
||||
siruta: r.siruta,
|
||||
|
||||
Reference in New Issue
Block a user