perf(vehicles): dedup, abort budget, circuit breaker for VIN decode

Production p99 was 18min for failed decodes and 15min for PL24 successes;
same VIN could trigger N parallel 10+ min decodes. Three layered fixes:

- In-flight dedup via Redis SETNX (vin🔒*); concurrent same-VIN requests
  poll the resolve cache instead of re-firing the upstream chain.
- 24h positive cache (vin:resolve:*) and 6h negative cache
  (vin:resolve:neg:*); previously 5min positive / no negative.
- 25s hard abort budget via AbortController; PCAT gets the signal natively
  (AbortSignal.any), PL24/EMEX wrapped with raceWithSignal at the boundary.
  Aborted decodes don't poison the negative cache.
- PCAT/EMEX real race: first definitive single-result wins; the slower
  source is skipped (previously PCAT was always awaited first).
- PL24 circuit breaker: 3 consecutive failures opens a 30s cooldown
  (pl24:cb:cooldown_until); successes reset the counter.
- Stage-level timings in query_logs.timings (jsonb): pcat/emex/pl24/
  lock_wait/cache_hit/aborted. Failed source now logged as "none" or
  "aborted" instead of misleading "corgi".

Verified locally with 3 parallel decodes of a fresh VIN: 1 real decode
(3.59s), 2 lock-waits (3.53s) sharing the result, 4th request 23ms cache
hit. Previously this would have been 3 separate 10+ min PL24 decodes.

Migration 0002 adds query_logs.timings jsonb (NULL default). Must be
applied manually before deploy (deploy.sh does not run db:push).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-05-12 11:31:25 +00:00
parent 189d5a9656
commit c4f6cc1caf
9 changed files with 5518 additions and 36 deletions

View File

@@ -27,6 +27,7 @@ interface QueryLogItem {
success: boolean;
errorMessage: string | null;
responseTimeMs: number | null;
timings: Record<string, number> | null;
createdAt: string;
}
@@ -185,12 +186,20 @@ function AdminAnalyticsPage() {
<div className="text-right">
{log.responseTimeMs !== null ? (
<span
title={
log.timings
? Object.entries(log.timings)
.map(([k, v]) => `${k}: ${v}ms`)
.join("\n")
: undefined
}
className={
log.responseTimeMs > 5000
(log.responseTimeMs > 5000
? "text-red-500"
: log.responseTimeMs > 2000
? "text-amber-500"
: "text-green-500"
: "text-green-500") +
(log.timings ? " cursor-help underline decoration-dotted" : "")
}
>
{log.responseTimeMs}ms