0048. Эксплуатационная наблюдаемость, классы ошибок и graceful drain
Статус: accepted. Дата: 2026-10-01.
Контекст
Аудит эксплуатации API и worker выявил:
- любая непредвиденная ошибка превращалась в
503 unavailableсRetry-After: дефекты маскировались под временную недоступность, клиенты повторяли запрос без конца; - при отказе запуска API писал только «startup failed», worker —
worker.unavailable; причина (неверная переменная, недоступная БД, чужое хранилище) не попадала в журнал; - не было access log: по
requestIdнельзя было восстановить маршрут, статус, длительность и принципала; /health/readyтребовал Bearer и занимал общий бюджет 128 запросов, поэтому под нагрузкой скачиваний узел выпадал из балансировщика;forceCloseConnections: trueобрывал все передачи при SIGTERM и обновлении;- размер пула БД (5), бюджет запросов (128) и резерв свободного места (256 MiB) были зашиты в код;
- проверка готовности в
apps/deployобращалась к127.0.0.1даже приARKVORY_HOST, привязанном к LAN-адресу.
Решение
- Классы ошибок. Новый машинный код
internal(HTTP 500, без Retry-After) для неклассифицированных ошибок. КлассификаторclassifyFailureвpackages/infrastructure(адаптер особенностей pg/Node):ENOSPC/EDQUOTи SQLSTATE 53100 — 507capacity_exceeded; SQLSTATE класса 08, 57P01–57P03, 53300, 57014, 40001, 40P01,ECONNREFUSED/ECONNRESET/ETIMEDOUT/EPIPE/EHOSTUNREACH/ENETUNREACH/EAI_AGAINи точные тексты транспортных ошибок pg без кода — 503unavailable;AbortError/TimeoutError— 503unavailable.ArkvoryErrorсохраняет код, ошибки клиента Fastify — 400 без Retry-After. Клиент получает только фиксированное сообщение; журнал —errorName,errno,sqlstateпод тем жеrequestId, без текста ошибки. Worker записывает в задачу тот же класс (internal,capacity_exceeded,unavailable); повторы задачи по-прежнему ограничены пятью попытками. SDK повторяет только 408/429/502/503/504 и сетевые сбои. - Диагностика запуска. API пишет в stderr JSON
startup.failed, worker —worker.unavailable, сreasonи идентификаторами причины.reasonберёт текст только уArkvoryErrorи простогоErrorсобственной проверки (имя переменной, не значение), прогоняя его через редактор URL/паролей/длинных токенов; ошибки библиотек и системы сводятся к обобщённой причине и SQLSTATE/errno. - Access log.
ARKVORY_ACCESS_LOG(по умолчаниюtrue): одна строкаhttp.accessна закрытие ответа, включая прерванные передачи:requestId,method, шаблонroute,status,durationMs,bytesSent(дельта сокета),principal,clientIp,completed. Query string и заголовки не пишутся; успешные публичные probes пропускаются. Используется тот же bounded неблокирующийDiagnosticLogger. - Здоровье. Новая публичная операция
GET /health/status(getReadinessStatus, аддитивное изменение/api/v1-контракта):{"status":"ready"}200 или{"status":"unavailable"|"draining"}503 без подробностей. Проверка БД/хранилища single-flight и кешируется на 1 с, чтобы публичные запросы не умножали нагрузку на БД./health/*не входит в бюджетARKVORY_MAX_REQUESTS;/health/readyпо-прежнему требует credential. - Graceful drain. SIGTERM/SIGINT включает drain: readiness — 503
draining; новые запросы (кроме health) и ожидающие в очереди передачи — 503busy,Retry-After: 2,Connection: close; принятые запросы завершаются в течениеARKVORY_DRAIN_TIMEOUT_MS(по умолчанию 30 с, максимум 1 ч). Затемapp.close()прерывает оставшиеся передачи и закрывает соединения. Жёсткий срок выхода — окно drain + 120 с. Второй сигнал пропускает остаток окна; потеря владения хранилищем завершает процесс без drain. - Параметры ресурсов.
ARKVORY_DATABASE_POOL_SIZE(4–200, по умолчанию 10),ARKVORY_MAX_REQUESTS(по умолчанию 128, строго больше суммы лимитов загрузок и скачиваний; без явного значения при больших лимитах — сумма + 64, чтобы обновление не ломало запуск),ARKVORY_STORAGE_RESERVE_BYTES(по умолчанию 1 GiB, читают API и worker). - Deploy. Проверка готовности и создание владельца обращаются к
ARKVORY_HOST; для0.0.0.0/::и в режиме compose — к loopback.
Альтернативы
- Оставить 503 для всего непредвиденного: проще для клиентов, но скрывает дефекты и создаёт бесконечные повторы.
- Классифицировать ошибки pg в каждом адаптере, оборачивая их в
ArkvoryError: точнее, но требует правок во всех запросах; единый классификатор на границе HTTP покрывает и будущие адаптеры. forceCloseConnections: 'idle'без собственного состояния drain: Fastify ждал бы все запросы без предела и не отказывал бы новым передачам до закрытия.- Публичный
/health/ready: раскрыл бы роль, счётчики очередей и полосы; отдельный минимальный ресурс безопаснее.
Последствия
- Клиенты, которые раньше повторяли любой 503, больше не получают Retry-After для дефектов сервера; это намеренное изменение поведения. Интеграционные тесты, моделирующие ошибку триггера БД, теперь ожидают 500
internal. - Drain эффективен, только если окно меньше таймаута остановки супервизора (120 с у systemd, WinSW и compose) и балансировщик опрашивает
/health/statusчаще окна. - Access log увеличивает объём stdout пропорционально числу запросов; при медленном приёмнике строки отбрасываются со счётчиком.
clientIp— адрес TCP-соединения; если соединение пришло от адреса изARKVORY_TRUSTED_PROXIES(IP/CIDR), Fastify берёт ближайший недоверенный адрес изX-Forwarded-For. Без этой настройки за reverse proxy это адрес proxy. Поля и уровни журнала, корреляция и метрики уточнены в ADR 0052.- Удалённый туннель
arkvoryctlпо-прежнему обращается к127.0.0.1:8080на сервере.
Проверка
tests/operability.test.mjs: классификация ошибок (включая наличие текстов в установленных pg/pg-pool), HTTP-коды и Retry-After, отсутствие секретов в ответах и журнале, access log,RequestDrain, drain очереди допуска, кешReadinessProbe, валидация параметров.tests/startup-diagnostics.test.mjs: дочерние API и worker с неверной конфигурацией и недоступной БД печатают причину и не печатают пароль и URL.tests/integration/operability.test.mjs: health при исчерпанном бюджете запросов; скачивание завершается во время drain, новые запросы и readiness получают 503; превышение окна drain закрывает передачу.tests/deployment-health.test.mjs: выбор адреса для LAN/wildcard и проверка готовности по настроенному адресу;tests/sdk-transfer.test.mjs: SDK не повторяет 500.