issue 1858: remove action lines older than 365 days each night (1858.5)
ADR 0517 §9: the action log keeps a line one year; the removal runs at start and each night at 03:00 (Europe/Brussels) and says how many it removed. Handling goes with its entry by the schema's cascade.
This commit is contained in:
@@ -74,6 +74,7 @@ func main() {
|
||||
b := &Bundle{Env: envFromOS(), Store: store.get}
|
||||
go hearLoop(store.get, stdio.Subscribe, time.Sleep)
|
||||
go batchLoop(store.get)
|
||||
go removeEachNight(store.get, time.Now, time.Sleep, logf)
|
||||
if err := stdio.ServeOn("", b.Tools(), os.Stdin, os.Stdout); err != nil {
|
||||
logf("[mesh-mail-reader] %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
// The nightly removal (novox/hq ADR 0517 §9, issue 1858): the action log keeps a line 365 days; each night at
|
||||
// 03:00 (Europe/Brussels) the older ones are removed, and the journal says how many. Handling and reminders
|
||||
// need no removal of their own: they go with their entry (ADR 0517 §10, the schema's on delete cascade).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ActionsKeptFor is how long an action line is kept.
|
||||
const ActionsKeptFor = 365 * 24 * time.Hour
|
||||
|
||||
// RemoveOldActions removes the lines older than ActionsKeptFor, answering how many.
|
||||
func RemoveOldActions(ctx context.Context, store Store, now time.Time, logf func(string, ...any)) (int64, error) {
|
||||
removed, err := store.RemoveActionsBefore(ctx, now.Add(-ActionsKeptFor))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logf("[mesh-mail-reader] the nightly removal: %d action lines older than 365 days removed", removed)
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
// nextNight is the next 03:00 in the operator's zone after now.
|
||||
func nextNight(now time.Time) time.Time {
|
||||
local := now.In(Brussels)
|
||||
night := time.Date(local.Year(), local.Month(), local.Day(), 3, 0, 0, 0, Brussels)
|
||||
if !night.After(now) {
|
||||
night = time.Date(local.Year(), local.Month(), local.Day()+1, 3, 0, 0, 0, Brussels)
|
||||
}
|
||||
return night
|
||||
}
|
||||
|
||||
// removeEachNight runs the removal at once, then each night, forever; a failure is said and tried again the
|
||||
// next night.
|
||||
func removeEachNight(storeFor func(context.Context) (Store, error), now func() time.Time, sleep func(time.Duration), logf func(string, ...any)) {
|
||||
for {
|
||||
ctx := context.Background()
|
||||
store, err := storeFor(ctx)
|
||||
if err == nil {
|
||||
_, err = RemoveOldActions(ctx, store, now(), logf)
|
||||
}
|
||||
if err != nil {
|
||||
logf("[mesh-mail-reader] the nightly removal failed: %v; it tries again the next night", err)
|
||||
}
|
||||
sleep(nextNight(now()).Sub(now()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
// Issue 1858/AC7 (novox/hq ADR 0517 §9): each night, the action lines older than 365 days are removed.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 1858/AC7: the removal drops the lines older than 365 days, and keeps the rest.
|
||||
func TestTheNightlyRemovalDropsLinesOlderThan365Days(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
now := brusselsAt(2026, 10, 22, 3, 0)
|
||||
for _, age := range []time.Duration{366 * 24 * time.Hour, 365*24*time.Hour + time.Minute, 365*24*time.Hour - time.Hour, time.Hour} {
|
||||
_ = store.Record(ctx, ActionLine{What: ActPageOpened, Mode: "work", At: now.Add(-age)})
|
||||
}
|
||||
removed, err := RemoveOldActions(ctx, store, now, noopLogf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left := lines(t, store)
|
||||
if removed != 2 || len(left) != 2 {
|
||||
t.Fatalf("removed %d, %d left; want 2 removed (older than 365 days) and 2 kept", removed, len(left))
|
||||
}
|
||||
for _, l := range left {
|
||||
if now.Sub(l.At) > ActionsKeptFor {
|
||||
t.Fatalf("kept a line of %s, older than 365 days", l.At)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1858/AC7: the removal runs each night at 03:00 in the operator's zone: the next one is tonight's, or
|
||||
// tomorrow's once tonight's has passed.
|
||||
func TestTheRemovalRunsEachNight(t *testing.T) {
|
||||
for _, tc := range []struct{ now, want time.Time }{
|
||||
{brusselsAt(2026, 10, 22, 1, 0), brusselsAt(2026, 10, 22, 3, 0)},
|
||||
{brusselsAt(2026, 10, 22, 3, 0), brusselsAt(2026, 10, 23, 3, 0)},
|
||||
{brusselsAt(2026, 10, 24, 22, 0), brusselsAt(2026, 10, 25, 3, 0)}, // the night the clocks go back
|
||||
} {
|
||||
if got := nextNight(tc.now); !got.Equal(tc.want) {
|
||||
t.Fatalf("nextNight(%s) = %s, want %s", tc.now, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user