fix(ai): устойчивость pre-response ECONNRESET — бюджет ретраев, jittered backoff, keep-alive (#310) #317
Reference in New Issue
Block a user
Delete Branch "fix/310-econnreset-tuning"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Тюнинг устойчивости AI-стриминга к pre-response
ECONNRESET(issue #310, закрыт — это не диагностика «кто рвёт», а запас прочности, как договорились). В проде #175-ретрай восстанавливал сброс, но из 3 попыток было сожжено 2 — запаса нет, ещё один сброс уронил бы turn.AI_STREAM_PRE_RESPONSE_RETRIES(0= без ретрая; пусто/мусор → дефолт 4).150*(attempt+1)→ capped exponential + full jitter (preResponseBackoffMs, чистый инъектируемый хелпер): база 150ms, ×2 на попытку, cap 2000ms, задержка = random в[0, capped]. Убирает синхронный retry-storm и размазывает реконнекты по окну сбросов.AI_STREAM_KEEPALIVE_MSсохранён..env.exampleдокументирует обе ручки.Таймаут (900s),
RETRYABLE_CONNECT_CODESи инструментирование не тронуты.relates to #310
How verified
apps/serverjestai-streaming-fetch.spec.ts→ 20 passed. Неваккуумно: границы backoff (rand=0→0,rand=1→верх 150/300/600/1200/2000;attempt0,rand0.5→75вместо старых 150); env-парс (default/override/0/negative/NaN/empty); интеграционные хиты (retries=2 → 3 попытки,=4 → 5). Падало бы на старом коде.tsc --noEmit/ eslint по затронутому файлу — чисто.Если это не заглушит 500/WRN в логах — переоткрывай, копнём проксю/апстрим (там уже нужен доступ к проду).
Checklist
AI_STREAM_KEEPALIVE_MS/AI_STREAM_PRE_RESPONSE_RETRIESв.env.exampleРевью — #317 (ai: устойчивость pre-response ECONNRESET retry, #310), round 1, head
808a5c70, base developScope: реальная дельта — ТОЛЬКО
808a5c70поверх смердженного #305 (родитель36b35395); 3 файла (.env.example +18 доки, ai-streaming-fetch.ts +78, .spec +119).Вердикт: PASS — три скоординированных изменения устойчивости корректны (retry идемпотентен/bounded, backoff bounded), объективка зелёная, регрессий нет. Готово к мержу.
Полный веер (stability, regressions, test-coverage, coherence, conventions, documentation). Объективка запущена мной (детач
808a5c70): serverjest ai-streaming-fetch.spec→ 20 passed; tsc (в jest ts-компиляции) чисто.Подтверждено по коду + прогоны
baseFetchРЕДЖЕКТИТ (undicifetch()реджектит строго ДО резолва Response) → started/streaming-Response не реплеится → идемпотентно, нет дубль-эффекта/двойного счёта у провайдера. Цикл BOUNDED (attempt>=maxRetries → throw; maxRetries = finite floored env-значение), завершается за ≤maxRetries+1.abort(init.signal.aborted) короткозамыкает ПЕРЕД backoff (нет retry-storm на отмену).isRetryableConnectError-gate не менялся (HTTP-500-с-телом резолвит Response → не ретраится; только connection-level pre-response коды).preResponseBackoffMs(a,rand)=rand()*min(150*2^a, 2000)(capped-exp + FULL jitter, anti-thundering-herd): rand=0→0, rand=1→cap, mean=cap/2, ∈[0,cap];2**a→Infinity клампитсяMath.minДО умножения (нет overflow).preResponseConnectRetries(): пустая/whitespace→default 4 (НЕ 0), «0»→0 (single attempt/off), invalid/negative→default, valid→floor. Правильно НЕ переиспользуетpositiveEnv(тот бы отверг валидный 0). empty-vs-0 различие load-bearing и покрыто тестом.withPreResponseRetry(instrument(createStreamingFetch()))(ai.service:59) — контрактtypeof fetchнеизменен. УдалённыйPRE_RESPONSE_CONNECT_RETRIES=2— ноль остаточных ссылок. Новые экспорты без коллизий.requests===MAX_ATTEMPTSпиннит off-by-one И infinite-loop через hang→timeout), honours-raised-budget→5, NOT-retry-aborted→0. Изоляция env через beforeEach/afterEach._MS-суффикс верно,rand-инъекция чистая, паттерн env-config консистентен.(служебное: коррекция маркера — в прошлом маркере round-1 PASS я опечатался в укороченном sha; ниже полный. Код не менялся, вердикт прежний: PASS.)