checker: keep 1 report per hour after the first day
Some checks are pending
continuous-integration/drone/push Build is running

Insert an hourly tier between the full-detail window and the daily
bucket so users still get sub-day resolution for the first week:

  0..1 day  -> all
  1..7 days -> 1 per hour
  7..30     -> 2 per day
  ...
This commit is contained in:
nemunaire 2026-04-08 12:02:04 +07:00
commit b416cb5688
2 changed files with 46 additions and 8 deletions

View file

@ -37,7 +37,8 @@ import (
//
// age window | kept
// ------------------------- | ------------------------------------------
// 0 .. 7 days | every execution
// 0 .. 1 day | every execution
// 1 .. 7 days | up to 1 execution per hour per (checker,target)
// 7 .. 30 days | up to 2 executions per day per (checker,target)
// 30 .. D/2 days | up to 1 execution per week per (checker,target)
// D/2 .. D days | up to 1 execution per month per (checker,target)
@ -52,7 +53,11 @@ type RetentionPolicy struct {
// FullDetailDays: every execution kept under this age.
FullDetailDays int
// DailyBucketDays: between FullDetailDays and DailyBucketDays, keep
// HourlyBucketDays: between FullDetailDays and HourlyBucketDays, keep
// PerHourKept executions per UTC hour per (checker,target).
HourlyBucketDays int
PerHourKept int
// DailyBucketDays: between HourlyBucketDays and DailyBucketDays, keep
// PerDayKept executions per UTC day per (checker,target).
DailyBucketDays int
PerDayKept int
@ -73,7 +78,9 @@ func DefaultRetentionPolicy(retentionDays int) RetentionPolicy {
}
return RetentionPolicy{
RetentionDays: retentionDays,
FullDetailDays: 7,
FullDetailDays: 1,
HourlyBucketDays: 7,
PerHourKept: 1,
DailyBucketDays: 30,
PerDayKept: 2,
WeeklyBucketDays: max(retentionDays/2, 31),
@ -104,6 +111,7 @@ func (p RetentionPolicy) Decide(executions []*happydns.Execution, now time.Time)
hardCutoff := now.AddDate(0, 0, -p.RetentionDays)
fullCutoff := now.AddDate(0, 0, -p.FullDetailDays)
hourlyCutoff := now.AddDate(0, 0, -p.HourlyBucketDays)
dailyCutoff := now.AddDate(0, 0, -p.DailyBucketDays)
weeklyCutoff := now.AddDate(0, 0, -p.WeeklyBucketDays)
@ -113,6 +121,7 @@ func (p RetentionPolicy) Decide(executions []*happydns.Execution, now time.Time)
return group[i].StartedAt.After(group[j].StartedAt)
})
hourBuckets := map[string]int{}
dayBuckets := map[string]int{}
weekBuckets := map[string]int{}
monthBuckets := map[string]int{}
@ -125,6 +134,14 @@ func (p RetentionPolicy) Decide(executions []*happydns.Execution, now time.Time)
case !t.Before(fullCutoff):
// 0 .. FullDetailDays — keep everything.
keep = append(keep, e.Id)
case !t.Before(hourlyCutoff):
k := t.UTC().Format("2006-01-02T15")
if hourBuckets[k] < p.PerHourKept {
hourBuckets[k]++
keep = append(keep, e.Id)
} else {
drop = append(drop, e.Id)
}
case !t.Before(dailyCutoff):
k := t.UTC().Format("2006-01-02")
if dayBuckets[k] < p.PerDayKept {

View file

@ -50,17 +50,38 @@ func TestDecide_FullDetailWindow(t *testing.T) {
now := time.Date(2026, 4, 8, 12, 0, 0, 0, time.UTC)
p := DefaultRetentionPolicy(365)
// 20 executions in the first 20 minutes — all inside 0..1 day window.
var execs []*happydns.Execution
for i := 0; i < 50; i++ {
execs = append(execs, mkExec(fmt.Sprintf("e%d", i), time.Duration(i)*time.Hour, now))
for i := 0; i < 20; i++ {
execs = append(execs, mkExec(fmt.Sprintf("e%d", i), time.Duration(i)*time.Minute, now))
}
keep, drop := p.Decide(execs, now)
if len(drop) != 0 {
t.Fatalf("expected no drops in <7d window, got %d", len(drop))
t.Fatalf("expected no drops in <1d window, got %d", len(drop))
}
if len(keep) != 50 {
t.Fatalf("expected 50 keeps, got %d", len(keep))
if len(keep) != 20 {
t.Fatalf("expected 20 keeps, got %d", len(keep))
}
}
func TestDecide_HourlyBucket(t *testing.T) {
now := time.Date(2026, 4, 8, 12, 0, 0, 0, time.UTC)
p := DefaultRetentionPolicy(365)
// 6 executions in the same hour ~3 days ago (inside hourly window).
var execs []*happydns.Execution
base := 3*24*time.Hour + 30*time.Minute
for i := 0; i < 6; i++ {
execs = append(execs, mkExec(fmt.Sprintf("e%d", i), base+time.Duration(i)*time.Minute, now))
}
keep, drop := p.Decide(execs, now)
if len(keep) != p.PerHourKept {
t.Fatalf("expected %d keeps in hourly bucket, got %d", p.PerHourKept, len(keep))
}
if len(drop) != 6-p.PerHourKept {
t.Fatalf("expected %d drops, got %d", 6-p.PerHourKept, len(drop))
}
}