[object Object]

← back to Dw Pairs Well

pairs: PAIRS_CLIP_DEFAULT env flag (default OFF) blends CLIP for all callers; add circuit-breaker + 1.5s timeout on visual-search so an unreachable service never hangs customer requests

ce3fda2a0acf93391fb2981bf71323e4e5346323 · 2026-07-13 00:00:40 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit ce3fda2a0acf93391fb2981bf71323e4e5346323
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 13 00:00:40 2026 -0700

    pairs: PAIRS_CLIP_DEFAULT env flag (default OFF) blends CLIP for all callers; add circuit-breaker + 1.5s timeout on visual-search so an unreachable service never hangs customer requests
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 33 ++++++++++++++++++++++++++++++---
 1 file changed, 30 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index dbbdc66..6263ac0 100644
--- a/server.js
+++ b/server.js
@@ -20,6 +20,21 @@ const CORS_ORIGINS = (process.env.CORS_ORIGIN || '*')
 // falls back to the tag engine, so this is a pure enhancement.
 const VISUAL_SEARCH_URL = process.env.VISUAL_SEARCH_URL || 'http://127.0.0.1:9914';
 
+// CLIP blend can be turned ON for ALL /api/pairs callers (not just ?engine=clip) by
+// setting PAIRS_CLIP_DEFAULT=1. Defaults OFF so shipping this is a no-op in prod until
+// the visual-search service is confirmed reachable from the serving host. ?engine=tag
+// is an explicit per-request override that force-disables the blend even when default-on.
+const PAIRS_CLIP_DEFAULT = /^(1|true|on)$/i.test(process.env.PAIRS_CLIP_DEFAULT || '');
+// Latency guardrail: a short per-call timeout + a circuit-breaker so an unreachable/slow
+// visual-search service never adds more than one slow call per cooldown window to customer
+// traffic. On any failure we "trip" and skip the service for CLIP_COOLDOWN_MS.
+const CLIP_TIMEOUT_MS  = Number(process.env.CLIP_TIMEOUT_MS  || 1500);
+const CLIP_COOLDOWN_MS = Number(process.env.CLIP_COOLDOWN_MS || 30000);
+let _clipDownUntil = 0;
+function clipCircuitOpen() { return Date.now() < _clipDownUntil; }
+function clipTrip()  { _clipDownUntil = Date.now() + CLIP_COOLDOWN_MS; }
+function clipReset() { _clipDownUntil = 0; }
+
 if (!DATABASE_URL) {
   console.error('FATAL: DATABASE_URL not set — copy .env.example to .env');
   process.exit(1);
@@ -270,16 +285,18 @@ async function loadRowsByDwSkus(skus) {
 // down, source SKU never embedded, empty result) → caller falls back to the tag engine.
 async function clipSimilarTop(source, k) {
   if (!source.dw_sku) return null;
+  if (clipCircuitOpen()) return null;                   // service recently failed → skip, no latency hit
   try {
     const body = { dw_sku: source.dw_sku, hex: source.dominant_hex || '', k };
     const r = await fetch(`${VISUAL_SEARCH_URL}/similar`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify(body),
-      signal: AbortSignal.timeout(4000)
+      signal: AbortSignal.timeout(CLIP_TIMEOUT_MS)
     });
     if (!r.ok) return null;                              // 404 = no stored vector for this sku
     const d = await r.json();
+    clipReset();                                        // reachable → close the breaker
     if (!d || !d.ok || !Array.isArray(d.results) || d.results.length === 0) return null;
 
     const skus = d.results.map(x => x.dw_sku).filter(Boolean);
@@ -303,6 +320,7 @@ async function clipSimilarTop(source, k) {
       };
     });
   } catch (e) {
+    clipTrip();                                         // unreachable/slow → open the breaker
     console.error('clipSimilarTop fallback →', e.message);
     return null;
   }
@@ -328,6 +346,7 @@ function clipPairBonus(cos) {
 // the pure tag+palette scorer, never blocks the response.
 async function clipPairwiseMap(source, candidates) {
   if (!source.dw_sku) return {};
+  if (clipCircuitOpen()) return {};                     // service recently failed → skip, no latency hit
   try {
     const skus = candidates.map(c => c.dw_sku).filter(Boolean);
     if (!skus.length) return {};
@@ -335,12 +354,14 @@ async function clipPairwiseMap(source, candidates) {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ dw_sku: source.dw_sku, candidates: skus }),
-      signal: AbortSignal.timeout(4000)
+      signal: AbortSignal.timeout(CLIP_TIMEOUT_MS)
     });
     if (!r.ok) return {};
     const d = await r.json();
+    clipReset();                                        // reachable → close the breaker
     return (d && d.ok && d.cosines) ? d.cosines : {};
   } catch (e) {
+    clipTrip();                                         // unreachable/slow → open the breaker
     console.error('clipPairwiseMap fallback →', e.message);
     return {};
   }
@@ -1081,7 +1102,13 @@ app.get('/api/pairs', async (req, res) => {
     // makes the blend a no-op — the response never depends on the service being up.
     let usedEngine = 'tag';
     let clipMap = null;
-    if (req.query.engine === 'clip') {
+    // Blend CLIP when explicitly requested (?engine=clip) OR when PAIRS_CLIP_DEFAULT is on
+    // for all callers — but ?engine=tag always force-disables. clipPairwiseMap returns {}
+    // on any miss (service down / circuit open), so the blend degrades to tag+palette and
+    // the response never depends on the service being up.
+    const wantClip = req.query.engine === 'clip'
+      || (PAIRS_CLIP_DEFAULT && req.query.engine !== 'tag');
+    if (wantClip) {
       clipMap = await clipPairwiseMap(source, candidates);
       if (Object.keys(clipMap).length) usedEngine = 'clip-blend';
       else clipMap = null;

← f189739 auto-save: 2026-07-12T23:51:20 (1 files) — server.js  ·  back to Dw Pairs Well  ·  pairs: trip CLIP circuit-breaker on 5xx (service alive but e b2207f5 →