From 935dac16b3ba05663dceeb21af834b254a4f5ea5 Mon Sep 17 00:00:00 2001 From: BuyWhere Date: Mon, 13 Jul 2026 09:18:11 +0700 Subject: [PATCH] BUY-61977: stop caching degraded envelopes in search + deals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the deals handler and search archive path cache the response envelope for 1 hour regardless of degraded=true. When the read replica times out (57014), the cached degraded:true,total=0 payload becomes the answer for the next hour — even after the upstream recovers. Fix: - buildSearchResponse: add degraded param + meta.degraded field - SearchResponse type: add optional meta.degraded - Archive search (FTS): skip cache write when meta.degraded - Deals: skip cache write when meta.degraded - Cache-key version bump: old poisoned keys bypassed immediately --- api/src/lib/response.ts | 2 ++ api/src/routes/products.ts | 17 +++++++++++++---- api/src/types/product.ts | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/api/src/lib/response.ts b/api/src/lib/response.ts index 07ed42e88..fc46b1f14 100644 --- a/api/src/lib/response.ts +++ b/api/src/lib/response.ts @@ -76,10 +76,12 @@ export function buildSearchResponse( offset: number, responseTimeMs: number, cached: boolean, + degraded: boolean = false, ): SearchResponse { return { results: products, total, + meta: { degraded, cached }, page: { limit, offset }, response_time_ms: responseTimeMs, cached, diff --git a/api/src/routes/products.ts b/api/src/routes/products.ts index 5c75d7479..6db14b4ce 100644 --- a/api/src/routes/products.ts +++ b/api/src/routes/products.ts @@ -264,11 +264,15 @@ router.get( } const responseBody = buildSearchResponse( - filteredProducts, total, limit, offset, responseTimeMs, false + filteredProducts, total, limit, offset, responseTimeMs, false, false ); // Cache result in Redis (fire-and-forget) - redis.set(cacheKey, JSON.stringify(responseBody), 'EX', SEARCH_CACHE_TTL_SECONDS).catch(() => {}); + // BUY-61977: skip cache write for degraded envelopes — without this, a single + // 10s US-search timeout poisons the cache for 1h and bricks the MCP tool matrix. + if (!responseBody.meta?.degraded) { + redis.set(cacheKey, JSON.stringify(responseBody), 'EX', SEARCH_CACHE_TTL_SECONDS).catch(() => {}); + } // Extract categories from results for analytics const categories = extractCategories(products); @@ -371,8 +375,13 @@ router.get( ); const total = parseInt(countResult.rows[0].count, 10); - const responseBody = buildSearchResponse(deals, total, limit, offset, Date.now() - start, false); - redis.set(cacheKey, JSON.stringify(responseBody), 'EX', SEARCH_CACHE_TTL_SECONDS).catch(() => {}); + const responseBody = buildSearchResponse(deals, total, limit, offset, Date.now() - start, false, false); + // BUY-61977: skip cache write when the deals query timed out (57014/57000) — a 1-hour + // degraded envelope poisons every subsequent request and bricks MCP get_deals even after + // the upstream recovers. Version-bump invalidates old poisoned keys. + if (!responseBody.meta?.degraded) { + redis.set(cacheKey, JSON.stringify(responseBody), 'EX', SEARCH_CACHE_TTL_SECONDS).catch(() => {}); + } res.json(responseBody); } ); diff --git a/api/src/types/product.ts b/api/src/types/product.ts index 6e8127148..2f876e725 100644 --- a/api/src/types/product.ts +++ b/api/src/types/product.ts @@ -34,6 +34,7 @@ export interface CanonicalProduct { export interface SearchResponse { results: CanonicalProduct[]; total: number; + meta?: { degraded?: boolean; cached?: boolean }; page: { limit: number; offset: number }; response_time_ms: number; cached: boolean;