Prod'da PL24 prefetch tamamen durmuştu: günlük bütçe 600/600 dolu
görünürken gün boyunca SIFIR katalog isteği ve SIFIR yeni kategori
üretiliyordu (ölçüm 2026-09-20).
İki hata birlikte kapalı bir döngü kuruyordu:
1. Günlük bütçe `process()` içinde düşülüyor, iş-saati (ve cooldown)
kapısı ise her handler'ın başında duruyordu. Pencere dışında uyanan
bir iş önce bütçeden bir birim yiyor, sonra `time-window` fırlatıp
hiçbir iş yapmadan erteleniyordu.
2. Bütçesi biten kaynak "bir sonraki UTC gece yarısı"na erteleniyordu.
PREFETCH_PL24_START=9 ile bu an 03:00 Europe/Istanbul'a denk gelir —
pencere açılmadan altı saat önce. Uyanan iş yine pencereye takılıyor,
yine erteleniyor; taze günlük bütçe daha pencere açılmadan bu boş
uyanmalarla tükeniyordu.
Düzeltme:
- cooldown + iş-saati kapıları `process()` içinde, bütçe düşülmeden
ÖNCE çalışıyor; handler'lardaki kopyaları kaldırıldı.
- `alignToWindow()` eklendi: bütçe ertelemesi pencerenin içine
hizalanıyor. Pencere tanımlı değilse (varsayılan 0–24) no-op.
- `currentIstanbulHour` artık `istanbulHourAt`'e deleg ediyor ve h24
döngüsünün gece yarısı için ürettiği "24" değeri `% 24` ile
normalleniyor (aksi halde saat hiçbir pencereye düşmez).
8 yeni regresyon testi; api paketi 602 test geçiyor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tek global 20/dk limiter tüm kaynakları paylaştırıyordu → yavaş pcat PL24/emex'i
açlığa düşürüyordu. Artık her kaynak Redis fixed-window ile BAĞIMSIZ kısılıyor
(SOURCE_RATE_MAX: pl24=20, emex=20, pcat=8; env PREFETCH_RATE_PL24/_EMEX/_PCAT).
Global WORKER_RATE_MAX güvenlik tavanı (20→60). RateLimitError yeni cause
"source-rate" → process() per-job defer eder (emex/pcat PL24'ü beklemez); cooldown
hâlâ tüm worker'ı duraklatır. PL24 default'u 20'de KALDI (ban riski; yüksek-rate
Faz 4 rotating proxy ile). +spec: under/over ceiling + unknown-source unlimited.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PL24 prefetch artık 09:00-18:00 İstanbul penceresine bağlı değil; backfill
backlog'unun ~%96.7'si PL24 children olduğundan 9 saatlik pencere drenajı
boğuyordu. Pencere env ile geri daraltılabilir (PREFETCH_PL24_START/_END).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dev.sase.tr was running the daily lifecycle-email cron (trial-ending,
win-back) against sase_dev, which is a seeded copy of the prod DB —
real customers were getting duplicate mails (one from prod, one from
dev). Mirror the isCatalogBackfillEnabled() gate so the lifecycle cron
only registers on the canonical prod host (COOLIFY_FQDN=sase.tr or
BETTER_AUTH_URL=https://sase.tr). LIFECYCLE_EMAIL_ENABLED env can force-
enable for staging testing. The else branch removes any stale scheduler
from Redis so a previously-registered cron stops firing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After Tier 1 removed the self-throttle, the 5 jobs/min limiter + concurrency 1
became the bottleneck. Make throughput env-tunable so prod can ramp while
watching the fail rate:
- concurrency 1 -> 3 (PREFETCH_CONCURRENCY): parallelises emex/pl24 so a slow
parts-catalogs job no longer head-of-line-blocks the queue.
- rate ceiling 5 -> 20 jobs/min (PREFETCH_RATE_MAX).
- parts-catalogs: drop the pathological cumulative index*20s enqueue delay (the
Nth leaf of a vehicle waited N*20s); keep one per-job pace (PCAT_PACE_MS,
default 15s, 0 to disable).
- PL24 09-18 scrape window now env-tunable (PREFETCH_PL24_START / _END).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The worker's own upstream fetches called touchActivity(), setting the
prefetch:activity:<source> cooldown key (TTL 300s) that checkCooldown then
honoured — so after each fetch the worker paused itself for up to ~5 minutes
(re-checking every 60s, ~5 empty cycles per key). At ~1 fetch / 5 min the
3387-job backlog needed ~6 days to drain.
- Wrap each worker job in an AsyncLocalStorage backfill context; touchActivity
skips the cooldown key when invoked from the worker, so the cooldown reflects
only real user requests (worker yields to users, never to itself).
- Cooldown TTL 300s -> 90s (a request 5 min ago isn't "active").
- checkCooldown pauses for the key's actual remaining TTL (one wait) instead of
a fixed 60s re-check loop.
No extra upstream load — only removes the worker's self-imposed idle time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backfill was sweeping only 20 vehicles/hour and skipping pcat outside
09:00–19:00 Istanbul, leaving the catalog backlog (115 pl24 + 63 pcat +
27 emex zero-parts vehicles) crawling forward at ~528 parts/day and the
biggest user vehicle (Toyota Corolla 2026) unchanged across 24h.
Two related changes — both unblocked by the Redis-persisted warm JWT pool
(aa4d055 + dcf7e06) which keeps pcat captures alive 24/7 instead of
needing the 09:00–19:00 office-hours assumption:
* isWithinTimeWindow: parts-catalogs no longer gated — the warm pool +
Redis hydration cover the cold-start case the old office-hours rule
was working around. PL24 keeps its 09:00–18:00 window because the
upstream rate-limit is still tighter outside it.
* BACKFILL_BATCH_SIZE 20 → 40 — twice as many vehicles per wave, still
protected by MAX_BACKLOG=1000 self-throttle and per-source cooldown
(prefetch:activity:<source>) so live-user traffic still gets priority.
Combined effect: pcat goes from ~10h/day to 24h/day, batch doubles —
roughly 3× backfill throughput. Worst-case proxy spend tracked by the
DataImpulse daily cap; if a wave saturates upstream the rate-limit
handler (c7e59b9) defers without burning attempts.
Per-job moveToDelayed for cooldown livelocked the worker: the activity key is
refreshed by every user request, so 60s later the deferred job comes back, key
is still set, defers again. Last 3h on prod logged ~1800 deferrals against 12
real inits and one completion every ~6 min.
Tag RateLimitError with a `cause`. checkCooldown throws "cooldown"; the worker
now calls `this.worker.rateLimit(delayMs)` and throws Worker.RateLimitError() —
the whole queue waits once instead of cycling every job. checkTimeWindow throws
"time-window"; that branch keeps the existing job.moveToDelayed (per-job) so
EMEX (no scrape window) keeps flowing while PL24/pcat jobs sleep till 09:00.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dev.sase.tr (staging) and sase.tr (prod) BOTH run NODE_ENV=production with
SEPARATE databases, so the previous NODE_ENV check would have let the hourly
backfill sweep run against the dev DB too. Gate on the canonical prod host
instead (COOLIFY_FQDN / BETTER_AUTH_URL), with an explicit
CATALOG_BACKFILL_ENABLED override. Default off for any unknown host.
New isCatalogBackfillEnabled() helper used by both the cron registration and
processBackfillScan; dev redeploy now removes the stale scheduler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
prefetch was reactive-only (on decode) and processInit read top-level
categories straight from DB, so a vehicle decoded but never viewed got
no catalog. Add a production-only sweep so no decoded vehicle is left
without catalog data.
- processInit self-seeds top categories via getCategoryTree when DB has
none (fetches+inserts top groups from PL24/PSA/EMEX), closing the
never-viewed gap for both reactive and backfill paths
- new backfill-scan job + hourly cron: phase 1 queues decoded vehicles
with zero parts, phase 2 rolling createdAt-cursor rescan of all decoded
vehicles (prefetch-init is idempotent → gap-fills partial ones)
- guardrails: skip wave if queue backlog > 1000, per-source cooldown,
business-hours window (isWithinTimeWindow), batch <=20, in-flight guard
- PRODUCTION ONLY: gated on NODE_ENV both at cron registration and in
processBackfillScan; dev has a separate DB and must not scrape
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integrate external parts catalogs API with auth service, add BullMQ-based
catalog prefetch worker for background data caching, expand vehicles service
with shared vehicle support, and add vehicle select modal to frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>