[bug][ai-chat][infra] AI-провайдер рвёт соединение pre-response (ECONNRESET) на /ai-chat/stream — retry (#175) восстанавливает, но бюджет из 2 попыток был исчерпан + латентно… #310
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Симптом
В проде на
POST /api/ai-chat/streamапстрим LLM-провайдера рвёт соединение до ответа (ECONNRESET, pre-response), после чего срабатывает pre-response retry (#175) и запрос восстанавливается. Из серверного лога (server@0.94.1, 2026-07-03T14:13, UA Safari 17.6):call#1(attempt 0) иcall#2(attempt 1) первого шага упали сECONNRESET,call#3(attempt 2) прошёл. Ответ пользователю в итоге пришёл.Что это на самом деле
Это ожидаемая телеметрия работающего механизма #175, а не краш:
ECONNRESETвходит вRETRYABLE_CONNECT_CODES,isRetryableConnectError()→true.withPreResponseRetryпереигрывает pre-response сброс на свежем соединении (poisoned keep-alive socket destroys → retry лендится на новый коннект).createInstrumentedFetch) намеренно логирует каждую попытку, включая восстановленные сбросы — поэтому WRN-строки видны именно тогда, когда фикс работает.Код:
apps/server/src/integrations/ai/ai-streaming-fetch.ts—PRE_RESPONSE_CONNECT_RETRIES = 2(стр. 43),RETRYABLE_CONNECT_CODES(стр. 46),withPreResponseRetry(стр. 178), дефолтыDEFAULT_STREAM_KEEPALIVE_MS = 10_000(стр. 36),DEFAULT_STREAM_TIMEOUT_MS = 900_000(стр. 19).apps/server/src/integrations/ai/ai-provider-http.ts—createInstrumentedFetch(телеметрияcall# … PRE-RESPONSE FAILED … reqBytes/idleSincePrevCall).Почему это всё-таки стоит issue (severity: medium)
PRE_RESPONSE_CONNECT_RETRIES = 2→ всего 3 попытки; здесь упали 2 из 3, восстановились на последней разрешённой. Ещё один сброс — и весь turn падает с ошибкой у пользователя. Запаса нет.firstChunkLatency=10068ms,headersAfter7663–14519ms: из-за сбросов + повторов первый чанк приходит через ~10с, ответы ощутимо тормозят.Гипотезы / направления диагностики
Телеметрия специально несёт
reqBytesиidleSincePrevCall, чтобы это разбирать:keepAliveTimeout/keepAliveMaxTimeout=AI_STREAM_KEEPALIVE_MS(по умолчанию 10s). Если провайдер/мидлбокс убивает idle-сокеты раньше, чем undici их ресайклит, повторное использование протухшего сокета даёт pre-response reset. Стоит проверить корреляцию сбросов сidleSincePrevCall.reqBytes=48064(~48KB накопленного контекста) на упавших попытках — проверить лимиты на теле/заголовках и поведение при больших POST у reverse-proxy передdocs.vvzvlad.xyzи у самого провайдера.proxy_read_timeout, буферизация, HTTP/1.1 keep-alive к апстриму) или сам эндпоинт провайдера.call#1упал сразу (первый коннект,idleSincePrevCall=n/a) — это скорее не idle-протухание, а cold-connect/большое тело/флап апстрима.Что попробовать
AI_STREAM_KEEPALIVE_MS, чтобы undici ресайклил keep-alive сокеты заранее (до того, как их прибьёт апстрим/мидлбокс), и замерить, уходят ли сбросы.PRE_RESPONSE_CONNECT_RETRIES(напр. 3–4) и/или jittered backoff (сейчас линейный150 * (attempt+1)ms), чтобы был запас на серию сбросов.Acceptance / DoD
idleSincePrevCall/reqBytes).firstChunkLatencyв норме на «холодном» первом шаге.AI_STREAM_KEEPALIVE_MS/PRE_RESPONSE_CONNECT_RETRIES(и, при необходимости, таймауты прокси) задокументированы в.env.example.Окружение
POST /api/ai-chat/stream,context=AiService:provider-httpПримечание: механизм ретраев/таймаутов относится к #175 — этот issue про наблюдаемый в проде флап апстрима и запас прочности/тюнинг, а не про регресс самого механизма.
Закрываю: issue заведён по ошибке не для того пункта. Нужный баг (500 на
/ai-chat/bound-chat, slugId в UUID-колонку) вынесен в #312. Флап ECONNRESET — это штатная телеметрия механизма #175, отдельный issue сейчас не требовался. При необходимости легко переоткрыть.