Skip to content

Бэкенд-надёжность: единый docker-exec с таймаутом, retention+индексы, permanent SMTP errors#85

Merged
friench merged 3 commits into
mainfrom
backend/reliability
Jul 8, 2026
Merged

Бэкенд-надёжность: единый docker-exec с таймаутом, retention+индексы, permanent SMTP errors#85
friench merged 3 commits into
mainfrom
backend/reliability

Conversation

@friench

@friench friench commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Третий заход — бэкенд-надёжность. Три коммита, вся группа тестируемая.

#72 — единый docker-exec с таймаутом

Пять почти одинаковых блоков exec → demux → buffer → inspect (dms-client ×2, engine, ops, migrator, nginx-reloader) свёрнуты в один src/lib/docker-exec.ts. Ни у одного не было таймаута — зависший doveadm/postqueue/nginx держал worker-tick или HTTP-запрос бесконечно. Общий хелпер добавляет дефолтный дедлайн 30с (destroy стрима по таймауту); мигратор его отключает (timeoutMs: 0, doveadm backup легитимно долгий и крутится в серийном воркере). Хелпер не бросает на non-zero exit — политику решает каждый вызывающий (dms/nginx бросают, engine/ops → null, migrator хранит лог). Транспорт впервые покрыт unit-тестом (stdout, non-zero, stdin, combined, timeout) — раньше не было ни одного на все пять копий.

#73 — retention + индекс

Retention чистил только send_jobs/webhook_deliveries, поэтому росли вечно: bounce_events (у каждой строки текстовый diagnostic) и migration_jobs (колонка log = полный вывод dsync). RetentionService теперь удаляет и bounce-события старше окна, и терминальные (done/failed) миграции. Две новые индекса (миграция 0020): send_jobs.message_id — сканировался на каждом bounce-ingest через findByMessageId — и bounce_events.created_at (под DESC-список и новый prune).

#74 — перманентные SMTP-ошибки

5xx-ответ (плохой получатель, policy-reject, message-too-big) обрабатывался как транзиентный: mailer перебирал все аккаунты, затем очередь перепланировала с backoff до maxAttemptsaccounts × maxAttempts полных SMTP-сессий на заведомо провальную отправку. Теперь responseCode >= 500 классифицируется как перманентный → типизированный PermanentSendError бросается сразу (без ретрая в аккаунте и без failover), а SendJobService дед-леттерит его мгновенно, игнорируя остаток попыток. 4xx остаётся транзиентным (greylisting → ретрай/failover). Плюс 'All SMTP accounts failed' больше не теряет причину — дописывает сообщение последнего аккаунта и кладёт его в cause, так что send_jobs.lastError диагностируем.

Проверки

typecheck · lint · format:check · backend 560 ✓ (+16 новых: docker-exec 7, permanent-send/queue, retention расширен) · миграция 0020 применяется во всех интеграционных сьютах.

Closes #72
Closes #73
Closes #74

🤖 Generated with Claude Code

friench and others added 3 commits July 7, 2026 23:19
Five near-identical exec-demux-buffer-inspect blocks (dms-client x2, engine,
ops, migrator, nginx reloader) collapse into one lib/docker-exec.ts. None of
them had a timeout, so a hung doveadm/postqueue/nginx could pin a worker tick or
HTTP request forever; the shared helper adds a 30s default deadline (destroys the
stream on timeout) that the migrator opts out of (timeoutMs:0) since a full
doveadm backup is legitimately long and runs in the serial worker.

The helper never throws on a non-zero exit — each caller keeps its own policy
(dms/nginx throw, engine/ops return null, migrator keeps the log). The transport
now has a unit test (stdout, non-zero, stdin, combined streams, timeout) — it
had none before across the five copies.

Closes #72

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s.message_id

Retention only pruned send_jobs and webhook_deliveries, so two tables grew
forever: bounce_events (each row carries diagnostic text) and migration_jobs
(the log column holds full dsync output). RetentionService now also deletes
bounce events older than the window and terminal (done/failed) migration jobs.

Adds two indexes (migration 0020): send_jobs.message_id — scanned on every bounce
ingest via findByMessageId — and bounce_events.created_at, backing both the
DESC list and the new prune.

Closes #73

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A 5xx SMTP reply (bad recipient, policy reject, message too big) was treated
like a transient failure: the mailer failed over to every remaining account and
then the queue rescheduled with backoff up to maxAttempts — accounts x
maxAttempts full SMTP conversations for a send that can never succeed. The
mailer now classifies a >=500 responseCode as permanent, throws a typed
PermanentSendError immediately (no within-account retry, no failover), and
SendJobService dead-letters it at once regardless of attempts left. 4xx replies
stay transient and keep the retry/failover path.

Also stop discarding the underlying error: 'All SMTP accounts failed' now
appends the last account's message and carries it as the error `cause`, so
send_jobs lastError is diagnosable instead of a constant string.

Closes #74

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@friench friench merged commit 38f3a38 into main Jul 8, 2026
1 check passed
@friench friench deleted the backend/reliability branch July 8, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment