| index a6445bb6d..da7abf4a5 100644 |
| --- a/internal/analytics/meter_test.go |
| +++ b/internal/analytics/meter_test.go |
| @@ -1,6 +1,7 @@ |
| package analytics |
| |
| import ( |
| + "path/filepath" |
| "testing" |
| "time" |
| ) |
| @@ -77,6 +78,9 @@ func TestMeterCountsAndDrains(t *testing.T) { |
| if e := d.Events.Fold(); e["signup"] != 1 || len(e) != 1 { |
| t.Fatalf("events = %v", e) |
| } |
| + if d.Dropped.A.Malformed != 1 { |
| + t.Fatalf("bad event name must count Dropped.A.Malformed, not vanish: %+v", d.Dropped) |
| + } |
| if len(m.Drain()) != 0 { |
| t.Fatal("second drain must be empty") |
| } |
| @@ -88,60 +92,109 @@ func TestMeterCountsAndDrains(t *testing.T) { |
| } |
| } |
| |
| -// TestMeterRequeueSurvivesOnce pins down the exact requeue semantics: |
| -// |
| -// 1. Requeue(first) after a failed flush: first's delta is held in |
| -// pending — nothing was pending before, so nothing is dropped. |
| -// 2. A normal Drain (no Requeue in between, so pending got emptied by |
| -// the Drain call itself) returns the held delta ahead of the |
| -// newly-current one, each keeping its own Seq — the re-queued delta |
| -// keeps the seq it was drained with (so a replay that actually landed |
| -// is a no-op at the day file, via Day.Merge's seq gate), while the |
| -// current delta gets a fresh, higher seq. |
| -// 3. Requeue(second) holds both of second's deltas in pending. Because |
| -// Drain had just cleared pending, this call finds nothing already |
| -// there and drops nothing yet. |
| -// 4. A second Requeue call — this time with a freshly built delta, |
| -// fired with NO Drain in between — finds second's two deltas still |
| -// sitting in pending from step 3. That whole older window is dropped; |
| -// its count (2) is added to Dropped.Flush on the newest delta in the |
| -// incoming batch, and only the incoming batch is kept in pending. |
| -// 5. A Drain() with nothing newly observed returns exactly that one |
| -// surviving, Dropped.Flush-stamped delta: the older window is gone. |
| +// TestMeterRequeueSurvivesOnce exercises the production caller's actual |
| +// sequence — Drain, flush, Requeue on failure, repeat — where pending is |
| +// always empty at the moment Requeue is called (the prior Drain just |
| +// cleared it). The "already re-queued once" state has to live on the |
| +// delta itself, not on pending's emptiness, or nothing is ever dropped |
| +// and pending grows without bound across repeated failures. Each delta |
| +// may survive exactly one failed flush: a second failure for the same |
| +// delta drops it and counts the drop (once the delta is marked, a second |
| +// Requeue drops it) on the next delta that reaches the app's bucket — |
| +// bounded forever at two windows per app no matter how many flushes in a |
| +// row fail. |
| func TestMeterRequeueSurvivesOnce(t *testing.T) { |
| now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) |
| m := NewMeter(t.TempDir(), "box1", func() time.Time { return now }) |
| k := AppKey{Acct: "bab", App: "web"} |
| |
| m.Observe(obs("GET", 200, "text/html")) |
| - first := m.Drain() |
| - m.Requeue(first) // flush failed |
| + d1 := m.Drain() |
| + m.Requeue(d1) // flush failed: d1's delta is marked and held |
| + |
| + m.Observe(obs("GET", 200, "text/html")) |
| + d2 := m.Drain() // = d1's delta (marked, own seq) + the new one (fresh seq) |
| + if ds := d2[k]; len(ds) != 2 || ds[0].Seq >= ds[1].Seq { |
| + t.Fatalf("d2 must be the requeued delta then the new one, in seq order: %+v", ds) |
| + } |
| + m.Requeue(d2) // failed again: d1's delta is already marked, so it drops; d2's new delta is marked and held |
| |
| m.Observe(obs("GET", 200, "text/html")) |
| - second := m.Drain() |
| - ds := second[k] |
| - if len(ds) != 2 || ds[0].Seq >= ds[1].Seq { |
| - t.Fatalf("requeued delta must come back first with its own seq: %+v", ds) |
| + d3 := m.Drain()[k] |
| + if len(d3) != 2 { |
| + t.Fatalf("d1's window must be gone, leaving d2's surviving delta plus the new one: %+v", d3) |
| + } |
| + if d3[0].Dropped.Flush != 1 { |
| + t.Fatalf("the drop must be reported on the delta that reaches the bucket: %+v", d3) |
| + } |
| + if d3[0].Seq != d2[k][1].Seq { |
| + t.Fatalf("the surviving delta must keep the seq it had in d2, not be re-stamped: got %d, want %d", d3[0].Seq, d2[k][1].Seq) |
| } |
| - if ds[0].Seq != first[k][0].Seq { |
| - t.Fatalf("requeued delta must keep its original seq: got %d, want %d", ds[0].Seq, first[k][0].Seq) |
| + if d3[1].Seq <= d3[0].Seq { |
| + t.Fatalf("the new delta must get a fresh, higher seq: %+v", d3) |
| + } |
| + |
| + // A third failure in a row: bounded forever at two windows per app. |
| + m.Requeue(map[AppKey][]Delta{k: d3}) |
| + m.Observe(obs("GET", 200, "text/html")) |
| + d4 := m.Drain()[k] |
| + if len(d4) != 2 || d4[0].Dropped.Flush != 1 { |
| + t.Fatalf("repeated failures must stay bounded at two windows, each drop reported once: %+v", d4) |
| + } |
| +} |
| + |
| +// TestMeterTrackMalformedCountsDropped: an invalid event name must not |
| +// vanish silently — it counts against the app's current-day |
| +// Dropped.A.Malformed. |
| +func TestMeterTrackMalformedCountsDropped(t *testing.T) { |
| + now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) |
| + m := NewMeter(t.TempDir(), "box1", func() time.Time { return now }) |
| + m.Track("bab", "web", "not a valid name!", now) |
| + m.Track("bab", "web", "also/not valid", now) |
| + d := m.Drain()[AppKey{Acct: "bab", App: "web"}][0] |
| + if d.Dropped.A.Malformed != 2 { |
| + t.Fatalf("Dropped.A.Malformed = %d, want 2", d.Dropped.A.Malformed) |
| } |
| +} |
| |
| - m.Requeue(second) // failed again: pending was empty, so nothing drops yet |
| +// TestMeterSaltFailureIsThrottled: a persistently failing DaySalt must not |
| +// be retried on every request — pageviews still count, but visitors stay |
| +// uncounted while it fails, without blocking or panicking on the |
| +// serving path. |
| +func TestMeterSaltFailureIsThrottled(t *testing.T) { |
| + now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) |
| + saltDir := filepath.Join(t.TempDir(), "missing", "nested") // DaySalt can't create its file here |
| + m := NewMeter(saltDir, "box1", func() time.Time { return now }) |
| + m.Observe(obs("GET", 200, "text/html")) // salt lookup fails |
| + m.Observe(obs("GET", 200, "text/html")) // within the backoff window: not retried |
| + now = now.Add(31 * time.Second) |
| + m.Observe(obs("GET", 200, "text/html")) // backoff elapsed: retried (and fails again, saltDir still missing) |
| + d := m.Drain()[AppKey{Acct: "bab", App: "web"}][0] |
| + if d.Hours["09"].PV != 3 { |
| + t.Fatalf("pageviews must still count while the salt fails: %+v", d.Hours) |
| + } |
| + if c := d.Visitors.Count(); c != 0 { |
| + t.Fatalf("visitors must stay uncounted while the salt fails, got %.2f", c) |
| + } |
| +} |
| |
| - // A further failure before any successful Drain: pending still holds |
| - // second's two deltas from the call above, so this Requeue drops that |
| - // whole older window onto the newest delta of the incoming batch. |
| - handBuilt := Delta{Date: "2026-09-07", Box: "box1", Seq: 999, |
| - Hours: map[string]HourRow{}, Visitors: &HLL{}, Pages: NewTopK(), Referers: NewTopK(), Events: NewTopK()} |
| - m.Requeue(map[AppKey][]Delta{k: {handBuilt}}) |
| +// TestMeterRestartedOnlyOnFirstDelta: Restarted marks the very first |
| +// delta created after the meter starts, not every app-day's first |
| +// delta. |
| +func TestMeterRestartedOnlyOnFirstDelta(t *testing.T) { |
| + now := time.Date(2026, 9, 7, 9, 30, 0, 0, time.UTC) |
| + m := NewMeter(t.TempDir(), "box1", func() time.Time { return now }) |
| + m.Observe(obs("GET", 200, "text/html")) // first delta created since start: bab/web |
| + o := obs("GET", 200, "text/html") |
| + o.Acct, o.App = "other", "app2" |
| + m.Observe(o) // a different app's first delta of the day, but not the meter's first since start |
| |
| - third := m.Drain()[k] |
| - if len(third) != 1 || third[0].Dropped.Flush != 2 { |
| - t.Fatalf("requeue-while-pending must drop the older window and count both its deltas: %+v", third) |
| + out := m.Drain() |
| + if !out[AppKey{Acct: "bab", App: "web"}][0].Restarted { |
| + t.Fatal("the very first delta created after start must carry Restarted") |
| } |
| - if third[0].Seq != handBuilt.Seq { |
| - t.Fatalf("surviving window must be the most recently re-queued one: %+v", third) |
| + if out[AppKey{Acct: "other", App: "app2"}][0].Restarted { |
| + t.Fatal("only the meter's first delta since start should carry Restarted, not every app's") |
| } |
| } |
| |