Files
selfpost/internal/store/deliverylog_test.go
T
mixandClaude Opus 5.5 ffefa9d6df delivery log: measure the sweep before deciding its fate (audit H3, 0007)
Migration 0007 (DDL only): delivery_log_days, a row per UTC day, kept a
year, shown nowhere - read off a copy of the database after a month by the
decision rule written in the audit plan.

- reconcile_closed: the sweep adds the rows it actually closed.
- reconcile_contradicted: store.UpdateStatus, in one transaction, counts the
  rows with reason "reconcile" a delivery line now speaks of, writes the
  log's status and clears the reason - so a re-read line counts once.
- tailer_blind_seconds: follow() tracks stretches in which the file is gone
  or reopening fails (logtail.blindness); stored when a stretch ends and
  every minute while it lasts; under 3 s not counted (the rotation's tick
  between rename and create loses nothing).
- queued_over_lifetime: every sweep overwrites the day's level with the rows
  queued past LongestLifetime + MaximalBackoff (a message Postfix still
  works on stays queued until its first retry after the lifetime).
- retentionLoop prunes days older than a year.

Tests: store (sums per day, the level overwritten, under a second nothing,
prune; a contradicted sweep counted once and its reason cleared; count of
queued rows); logtail (the sweep counts what it closed; countStuck records
the level; blindness - a one-tick gap not counted, 75 s stored as 60 + 15
from where the last part ended). Schema tests at user_version 7.

schema-migrations.md: chain head 7 (it still said 5 since 2.4.0), row 7.
CHANGELOG [Unreleased], audit plan line ticked, roadmap 10/61.
Evidence: go vet ./... clean; go test ./internal/store ok, ./internal/logtail
ok but for TestFollowTailsAndRotates (Windows); the server run of the stage
follows before the push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:20:44 +03:00

70 lines
2.0 KiB
Go

package store
import (
"testing"
"time"
)
// The counters add up per UTC day, the stuck level is overwritten rather than
// added, nothing is written for nothing, and a year is kept.
func TestDeliveryLogDays(t *testing.T) {
st := openTestStore(t)
day1 := time.Date(2026, 10, 14, 23, 59, 0, 0, time.UTC)
day2 := day1.Add(2 * time.Minute)
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(st.AddReconcileClosed(day1, 2))
must(st.AddReconcileClosed(day1, 3))
must(st.AddReconcileClosed(day1, 0))
must(st.AddTailerBlind(day1, 1500*time.Millisecond))
must(st.AddTailerBlind(day1, 900*time.Millisecond)) // under a second: nothing
must(st.SetQueuedOverLifetime(day1, 4))
must(st.SetQueuedOverLifetime(day1, 1))
must(st.AddTailerBlind(day2, time.Minute))
days, err := st.DeliveryLogDays()
must(err)
want := []DeliveryLogDay{
{Day: "2026-10-14", Closed: 5, BlindSeconds: 1, Stuck: 1},
{Day: "2026-10-15", BlindSeconds: 60},
}
if len(days) != len(want) {
t.Fatalf("days = %+v, want %+v", days, want)
}
for i := range want {
if days[i] != want[i] {
t.Errorf("day %d = %+v, want %+v", i, days[i], want[i])
}
}
must(st.PruneDeliveryLogDays(day2.AddDate(1, 0, 1)))
days, err = st.DeliveryLogDays()
must(err)
if len(days) != 1 || days[0].Day != "2026-10-15" {
t.Fatalf("after pruning a year and a day later: %+v, want 2026-10-15 alone", days)
}
}
func TestCountQueuedOlderThan(t *testing.T) {
st := openTestStore(t)
for _, e := range []SendLogEntry{{QueueID: "Q1", To: "a@example.net"}, {QueueID: "Q2", To: "b@example.net"}} {
if err := st.InsertQueued(e); err != nil {
t.Fatal(err)
}
}
if _, err := st.UpdateStatus("Q2", "b@example.net", StatusSent); err != nil {
t.Fatal(err)
}
if n, err := st.CountQueuedOlderThan(time.Now().Add(time.Hour)); err != nil || n != 1 {
t.Fatalf("queued rows before an hour from now = %d, %v; want 1", n, err)
}
if n, err := st.CountQueuedOlderThan(time.Now().Add(-time.Hour)); err != nil || n != 0 {
t.Fatalf("queued rows before an hour ago = %d, %v; want 0", n, err)
}
}