Skip to content

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-адресу.

Решение ​

  1. Классы ошибок. Новый машинный код internal (HTTP 500, без Retry-After) для неклассифицированных ошибок. Классификатор classifyFailure в packages/infrastructure (адаптер особенностей pg/Node): ENOSPC/EDQUOT и SQLSTATE 53100 — 507 capacity_exceeded; SQLSTATE класса 08, 57P01–57P03, 53300, 57014, 40001, 40P01, ECONNREFUSED/ECONNRESET/ETIMEDOUT/EPIPE/EHOSTUNREACH/ENETUNREACH/EAI_AGAIN и точные тексты транспортных ошибок pg без кода — 503 unavailable; AbortError/TimeoutError — 503 unavailable. ArkvoryError сохраняет код, ошибки клиента Fastify — 400 без Retry-After. Клиент получает только фиксированное сообщение; журнал — errorName, errno, sqlstate под тем же requestId, без текста ошибки. Worker записывает в задачу тот же класс (internal, capacity_exceeded, unavailable); повторы задачи по-прежнему ограничены пятью попытками. SDK повторяет только 408/429/502/503/504 и сетевые сбои.
  2. Диагностика запуска. API пишет в stderr JSON startup.failed, worker — worker.unavailable, с reason и идентификаторами причины. reason берёт текст только у ArkvoryError и простого Error собственной проверки (имя переменной, не значение), прогоняя его через редактор URL/паролей/длинных токенов; ошибки библиотек и системы сводятся к обобщённой причине и SQLSTATE/errno.
  3. Access log. ARKVORY_ACCESS_LOG (по умолчанию true): одна строка http.access на закрытие ответа, включая прерванные передачи: requestId, method, шаблон route, status, durationMs, bytesSent (дельта сокета), principal, clientIp, completed. Query string и заголовки не пишутся; успешные публичные probes пропускаются. Используется тот же bounded неблокирующий DiagnosticLogger.
  4. Здоровье. Новая публичная операция GET /health/status (getReadinessStatus, аддитивное изменение /api/v1-контракта): {"status":"ready"} 200 или {"status":"unavailable"|"draining"} 503 без подробностей. Проверка БД/хранилища single-flight и кешируется на 1 с, чтобы публичные запросы не умножали нагрузку на БД. /health/* не входит в бюджет ARKVORY_MAX_REQUESTS; /health/ready по-прежнему требует credential.
  5. Graceful drain. SIGTERM/SIGINT включает drain: readiness — 503 draining; новые запросы (кроме health) и ожидающие в очереди передачи — 503 busy, Retry-After: 2, Connection: close; принятые запросы завершаются в течение ARKVORY_DRAIN_TIMEOUT_MS (по умолчанию 30 с, максимум 1 ч). Затем app.close() прерывает оставшиеся передачи и закрывает соединения. Жёсткий срок выхода — окно drain + 120 с. Второй сигнал пропускает остаток окна; потеря владения хранилищем завершает процесс без drain.
  6. Параметры ресурсов. ARKVORY_DATABASE_POOL_SIZE (4–200, по умолчанию 10), ARKVORY_MAX_REQUESTS (по умолчанию 128, строго больше суммы лимитов загрузок и скачиваний; без явного значения при больших лимитах — сумма + 64, чтобы обновление не ломало запуск), ARKVORY_STORAGE_RESERVE_BYTES (по умолчанию 1 GiB, читают API и worker).
  7. 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.

Arkvory · ProAnimaStudio