feat: log-tailer offset persistence + in-flight L2 rate-limit accounting (code-review.md § Phase 3)

- logtail: persist the read position (offset + fingerprint of the log's
  first 512 bytes) in a new logtail_state table (migration 0003) and
  resume from it on start, so delivery lines written while the panel was
  down are parsed instead of skipped and their send-log rows no longer
  stay "queued" forever. Fingerprint mismatch (rotated/recreated while
  down) reads the file from the start — re-parsing is idempotent; a
  first-ever start with nothing stored still begins at end-of-file.
  Writes are throttled to one per 5s, forced on rotation and shutdown.

- milter: count messages that passed the level-2 check but have not
  reached the send log yet (internal/milter/inflight.go), so concurrent
  SMTP sessions cannot each spend the same last slot. A literal
  count+insert transaction, as the review suggested, is not possible:
  the count happens at MAIL FROM and the insert at end-of-message.
  Reservations are released after the insert, on ABORT, and after a
  10-minute TTL — a client that drops mid-transaction must not be able
  to hold a slot, since the limiter is fail-open by design.

Docs: architecture.md (log tailer, persistence, L2 counting),
security.md and roadmap.md (restart gap closed, container recreate
remains), CHANGELOG, progress.md, code-review.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 17:12:37 +03:00
parent 0093878eea
commit a92d583053
15 changed files with 683 additions and 40 deletions
+6 -5
View File
@@ -60,11 +60,12 @@ CHANGELOG `[Unreleased]/Security`, а разделы B.1–B.3 и C.4 вырез
(`docs/logo` уже отсутствует как каталог — критерию «либо содержит файлы, либо
отсутствует» удовлетворяет; отдельного действия не требуется.)
**Send-log vs `mail.log` (опционально).** После рестарта панели или пересоздания
контейнера строки send-log могут навсегда остаться `queued`log-tailer не
дочитывает пропущенный хвост, `mail.log` не в `/data`. Rename-ротация (B.2)
это не лечит. Кандидаты, если станет больно: persist позиции чтения, volume для
лога, сверка зависших строк через `postqueue`. As-built и принятый риск:
**Send-log vs `mail.log` (частично закрыто).** Persist позиции чтения сделан
(Фаза 3 [code-review.md](code-review.md), таблица `logtail_state`): после
рестарта панели log-tailer дочитывает пропущенный хвост. Остаётся пересоздание
контейнера — `mail.log` не в `/data` и теряется вместе с ним, такие строки
навсегда останутся `queued`. Кандидаты, если станет больно: volume для лога,
сверка зависших строк через `postqueue`. As-built и принятый риск:
[architecture.md](architecture.md) § Log tailer, [security.md](security.md).
---