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>
70 lines
2.0 KiB
Go
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)
|
|
}
|
|
}
|