← 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 →