Бэкенд-надёжность: единый docker-exec с таймаутом, retention+индексы, permanent SMTP errors#85
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Третий заход — бэкенд-надёжность. Три коммита, вся группа тестируемая.
#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 до
maxAttempts—accounts × 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