logtail: the deferred counter remembers a place in the log, not a second (stage 2b)
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
The counter passed over, while catching up after a start, every warning no later than the last one counted - by time; mail.log stamps whole seconds, so a second warning of that second, later in the file, was lost. - The tailer gives each line its place (Place: the file's fingerprint and the offset just past the line): follow tracks the bytes the reader hands over, readRotated likewise in mail.log.1. Run hands tr.line to the counter. - store.AddSenderCheckDeferred(at, n, place) counts, moves the last-deferral mark (never back - an upsert WHERE) and saves the place (settings sender_check_counted_place) in one transaction. - Catching up, a warning in the file of the last one counted is passed over by place (end <= counted end); in another file - a rotated log read again - by time, as before. The fingerprint carries the inode, so the place stays true across a rotation. Tests: two warnings of one second, an unclean stop between them, all lines read again - each counted once; the store saves the place with the count and keeps the latest deferral time. Existing counter tests pass a zero place (time rule). CHANGELOG [Unreleased], audit plan line ticked, roadmap 24/62. Evidence: go vet ./... clean; go test ./internal/store, ./internal/logtail ok (but for the Windows-only test); the server run follows before the push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,9 @@ const (
|
||||
// settingSenderCheckDeferredAt is when a message was last deferred because
|
||||
// the check did not answer (RFC3339 UTC).
|
||||
settingSenderCheckDeferredAt = "sender_check_deferred_at"
|
||||
// settingSenderCheckCountedPlace is the place in mail.log of the last
|
||||
// warning counted ("<fingerprint>@<offset>"), saved with the count.
|
||||
settingSenderCheckCountedPlace = "sender_check_counted_place"
|
||||
)
|
||||
|
||||
// senderCheckKeepDays is how long a day of the statistics is kept.
|
||||
@@ -80,33 +83,55 @@ func (s *Store) AddSenderCheckDown(at time.Time, d time.Duration) error {
|
||||
}
|
||||
|
||||
// AddSenderCheckDeferred counts, on the day of at, n messages deferred because
|
||||
// the check did not answer, and remembers at as the last such moment.
|
||||
func (s *Store) AddSenderCheckDeferred(at time.Time, n int64) error {
|
||||
// the check did not answer, remembers at as the last such moment, and saves
|
||||
// place - where in mail.log the counted line ends - in the same transaction:
|
||||
// a count without its place, or the other way round, would be counted again
|
||||
// or lost at the next start (audit 2026-10-12, stage 2b).
|
||||
func (s *Store) AddSenderCheckDeferred(at time.Time, n int64, place string) error {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO sender_check_days (day, deferred) VALUES (?, ?)
|
||||
ON CONFLICT (day) DO UPDATE SET deferred = deferred + excluded.deferred`,
|
||||
at.UTC().Format(dayLayout), n,
|
||||
)
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("sender check deferred: %w", err)
|
||||
}
|
||||
// Lines are read in order, but a re-read log must not move the mark back.
|
||||
last, err := s.GetSetting(settingSenderCheckDeferredAt)
|
||||
if err != nil {
|
||||
return err
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO sender_check_days (day, deferred) VALUES (?, ?)
|
||||
ON CONFLICT (day) DO UPDATE SET deferred = deferred + excluded.deferred`,
|
||||
at.UTC().Format(dayLayout), n,
|
||||
); err != nil {
|
||||
return fmt.Errorf("sender check deferred: %w", err)
|
||||
}
|
||||
// To the fraction of a second the log gives, and of a fixed width so that
|
||||
// the strings compare as the times do.
|
||||
// the strings compare as the times do; a re-read log must not move the
|
||||
// mark back.
|
||||
stamp := at.UTC().Format("2006-01-02T15:04:05.000000Z")
|
||||
if stamp > last {
|
||||
return s.SetSetting(settingSenderCheckDeferredAt, stamp)
|
||||
setting := func(key, value, cond string) error {
|
||||
_, err := tx.Exec(`INSERT INTO settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT (key) DO UPDATE SET value = excluded.value`+cond, key, value)
|
||||
return err
|
||||
}
|
||||
if err := setting(settingSenderCheckDeferredAt, stamp, " WHERE excluded.value > settings.value"); err != nil {
|
||||
return fmt.Errorf("sender check deferred: %w", err)
|
||||
}
|
||||
if place != "" {
|
||||
if err := setting(settingSenderCheckCountedPlace, place, ""); err != nil {
|
||||
return fmt.Errorf("sender check deferred: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("sender check deferred: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SenderCheckCountedPlace is the place in mail.log of the last warning
|
||||
// counted; "" before any was.
|
||||
func (s *Store) SenderCheckCountedPlace() (string, error) {
|
||||
return s.GetSetting(settingSenderCheckCountedPlace)
|
||||
}
|
||||
|
||||
// SenderCheckDeferredAt is when a message was last counted as deferred; zero
|
||||
// if none was.
|
||||
func (s *Store) SenderCheckDeferredAt() (time.Time, error) {
|
||||
|
||||
@@ -42,12 +42,12 @@ func TestSenderCheckStats(t *testing.T) {
|
||||
must(st.AddSenderCheckDown(now, 3*time.Second))
|
||||
must(st.AddSenderCheckDown(now, 1500*time.Millisecond)) // whole seconds: one
|
||||
must(st.AddSenderCheckDown(now, 400*time.Millisecond)) // nothing
|
||||
must(st.AddSenderCheckDeferred(now.Add(-time.Hour), 2))
|
||||
must(st.AddSenderCheckDeferred(now.Add(-time.Hour), 2, ""))
|
||||
must(st.AddSenderCheckDown(day(10), 30*time.Second))
|
||||
must(st.AddSenderCheckDeferred(day(10), 1))
|
||||
must(st.AddSenderCheckDeferred(day(10), 1, ""))
|
||||
must(st.AddSenderCheckDown(day(29), time.Minute))
|
||||
must(st.AddSenderCheckDown(day(30), time.Hour)) // the 31st day back: the year's only
|
||||
must(st.AddSenderCheckDeferred(day(200), 5))
|
||||
must(st.AddSenderCheckDeferred(day(200), 5, ""))
|
||||
must(st.AddSenderCheckDown(day(365), 24*time.Hour)) // a year and a day: nobody's
|
||||
|
||||
got, err = st.SenderCheckStats(now)
|
||||
@@ -112,3 +112,22 @@ func TestRefusedRowKeepsItsReason(t *testing.T) {
|
||||
t.Errorf("GetSendLog = %+v, %v", one, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The place in the log is saved with the count, and the moment of the last
|
||||
// deferral does not move back when a log is read again.
|
||||
func TestSenderCheckDeferredSavesItsPlace(t *testing.T) {
|
||||
st := openTestStore(t)
|
||||
later := time.Date(2026, 10, 14, 12, 0, 1, 0, time.UTC)
|
||||
if err := st.AddSenderCheckDeferred(later, 1, "abc:1.2@100"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.AddSenderCheckDeferred(later.Add(-time.Hour), 1, "abc:1.2@200"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p, err := st.SenderCheckCountedPlace(); err != nil || p != "abc:1.2@200" {
|
||||
t.Fatalf("place = %q, %v", p, err)
|
||||
}
|
||||
if at, err := st.SenderCheckDeferredAt(); err != nil || !at.Equal(later) {
|
||||
t.Fatalf("last deferral = %s, %v; want %s", at, err, later)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user