carlos / platform Public

edge: geoip alert reason codes; expiry keeps the alert until off or a fresh publish (geoip spec §10.4, codex R11-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9c54868b995db3444fd5df1116c47dc9c4ba7390 parent 67e010e
2 files changed, +181 −6
  • internal/edge/geoipmirror.go +82 −4
  • internal/edge/geoipmirror_test.go +99 −2
diff --git a/internal/edge/geoipmirror.go b/internal/edge/geoipmirror.go
index 535684185..7cee66a56 100644
--- a/internal/edge/geoipmirror.go
+++ b/internal/edge/geoipmirror.go
@@ -57,9 +57,20 @@ type geoMirror struct {
skipSHA map[string]string // sha -> why (rejected | expired); cleared whenever the observed pointer changes
pointerSHA string // the sha the last readable pointer named; "" for none
syncPending bool // the last `current` rename is not known durable: keep the previous generation
- lastErr string // last poll's error category, "" when healthy
- failingAt time.Time
- lastFailLog time.Time
+
+ // The alert's inputs (geoip spec §10.4). The pointer's metadata is
+ // kept after the reader is dropped, so an expired pointer still reads
+ // as expired by the clock before the console marks it (codex R11-1).
+ pointerPresent bool
+ pointerType string
+ pointerBuild time.Time
+ pointerMarked bool
+ wantSince time.Time // a live pointer has named a sha not loaded here since then
+ feedFailing bool
+ feedBox string
+ lastErr string // last poll's error category, "" when healthy
+ failingAt time.Time
+ lastFailLog time.Time
}
func newGeoMirror(store release.Store, dir string, sw *geo.Swappable) *geoMirror {
@@ -242,6 +253,7 @@ func (m *geoMirror) poll(ctx context.Context, recheck bool) error {
if err := os.MkdirAll(m.dir, 0o755); err != nil {
return m.fail("local directory unusable", err)
}
+ m.readFeedStatus(ctx)
raw, err := m.readPointer(ctx)
var p geoip.Pointer
if err == nil {
@@ -259,6 +271,7 @@ func (m *geoMirror) poll(ctx context.Context, recheck bool) error {
}
}
m.pointerSHA, m.skipSHA = "", map[string]string{}
+ m.pointerPresent, m.pointerType, m.pointerBuild, m.pointerMarked, m.wantSince = false, "", time.Time{}, false, time.Time{}
m.clearLocked()
m.lastErr = ""
m.genMu.Unlock()
@@ -271,6 +284,15 @@ func (m *geoMirror) poll(ctx context.Context, recheck bool) error {
return m.fail("pointer invalid", err)
}
m.genMu.Lock()
+ m.pointerPresent, m.pointerType, m.pointerBuild, m.pointerMarked = true, p.DatabaseType, p.Build(), p.Expired
+ switch {
+ case p.Expired || geoip.Expired(p.DatabaseType, p.Build(), m.now()):
+ m.wantSince = time.Time{}
+ case p.SHA256 == m.loadedSHA:
+ m.wantSince = time.Time{}
+ case m.wantSince.IsZero():
+ m.wantSince = m.now()
+ }
if p.SHA256 != m.pointerSHA {
// The pointer moved (to anything, including back to an sha seen
// before): every earlier rejection is void (plan review R1-8).
@@ -421,7 +443,7 @@ func (m *geoMirror) install(tmp string, p geoip.Pointer) error {
} else {
m.cleanLocked(p.SHA256, false)
}
- m.lastErr, m.failingAt = "", time.Time{}
+ m.lastErr, m.failingAt, m.wantSince = "", time.Time{}, time.Time{}
log.Printf("edge: geoip database %s (%s, built %s) loaded", short(p.SHA256), db.Source(), db.BuildEpoch().Format("2006-01-02"))
return nil
}
@@ -461,6 +483,53 @@ func (m *geoMirror) failLocked(category string, err error) error {
return fmt.Errorf("%s: %w", category, err)
}
+// readFeedStatus refreshes the feed reason's inputs, best effort: a
+// missing or unreadable feed.json means no feed reason.
+func (m *geoMirror) readFeedStatus(ctx context.Context) {
+ ctx, cancel := context.WithTimeout(ctx, geoPointerReadTimeout)
+ defer cancel()
+ failing, box := false, ""
+ if raw, err := readCapped(ctx, m.store, geoip.FeedKey, geoip.MaxPointerBytes); err == nil {
+ if st, err := geoip.ParseFeedStatus(raw); err == nil {
+ failing, box = st.Failing(), st.Box
+ }
+ }
+ m.genMu.Lock()
+ m.feedFailing, m.feedBox = failing, box
+ m.genMu.Unlock()
+}
+
+// Alert reason codes (geoip spec §10.4), the geoip rule's reading.
+const (
+ GeoAlertNone = 0
+ GeoAlertStale = 1 // the loaded database is over 45 days old
+ GeoAlertExpiring = 2 // a GeoLite2 database within 9 days of D1
+ GeoAlertExpired = 3 // the pointer is expired, marked or by the clock
+ GeoAlertNotLoaded = 4 // a live pointer not loaded here for over an hour
+ GeoAlertFeed = 5 // the feed has failed 7 times in a row (the console's box only)
+)
+
+// AlertCode is the geoip rule's reading for this box: the most severe
+// reason that holds (3, 4, 2, 1, then 5), or 0.
+func (m *geoMirror) AlertCode(now time.Time, box string) float64 {
+ m.genMu.Lock()
+ defer m.genMu.Unlock()
+ expired := m.pointerPresent && (m.pointerMarked || geoip.Expired(m.pointerType, m.pointerBuild, now))
+ switch {
+ case expired:
+ return GeoAlertExpired
+ case m.pointerPresent && !m.wantSince.IsZero() && now.Sub(m.wantSince) > time.Hour:
+ return GeoAlertNotLoaded
+ case m.loadedSHA != "" && strings.HasPrefix(m.loadedType, "GeoLite2-") && now.Sub(m.loadedBuild) >= geoip.GeoLiteWarn:
+ return GeoAlertExpiring
+ case m.loadedSHA != "" && now.Sub(m.loadedBuild) > geoip.StaleAfter:
+ return GeoAlertStale
+ case m.feedFailing && box != "" && m.feedBox == box:
+ return GeoAlertFeed
+ }
+ return GeoAlertNone
+}
+
// readPointer is one bounded pointer read.
func (m *geoMirror) readPointer(ctx context.Context) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, geoPointerReadTimeout)
@@ -532,3 +601,12 @@ func (e *Edge) GeoIPState() GeoIPState {
}
return GeoIPState{Mode: e.geoMode}
}
+
+// GeoIPAlertCode is the geoip rule's reading for this box (spec §10.4);
+// 0 on an edge with no mirror.
+func (e *Edge) GeoIPAlertCode(now time.Time) float64 {
+ if e.geoMirror == nil {
+ return GeoAlertNone
+ }
+ return e.geoMirror.AlertCode(now, e.box)
+}
diff --git a/internal/edge/geoipmirror_test.go b/internal/edge/geoipmirror_test.go
index c8fed921d..4e09507d3 100644
--- a/internal/edge/geoipmirror_test.go
+++ b/internal/edge/geoipmirror_test.go
@@ -32,8 +32,8 @@ func publishFixture(t *testing.T, s release.Store, body []byte, dbType string, b
sum := sha256.Sum256(body)
sha := hex.EncodeToString(sum[:])
ctx := context.Background()
- if _, err := s.Put(ctx, geoip.BlobKey(sha), body, ""); err != nil {
- t.Fatal(err)
+ if _, err := s.Put(ctx, geoip.BlobKey(sha), body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) {
+ t.Fatal(err) // an existing blob is the same bytes: content-addressed
}
p := geoip.Pointer{V: 1, SHA256: sha, Key: geoip.BlobKey(sha), Size: int64(len(body)),
DatabaseType: dbType, BuildEpoch: build.Unix(), Origin: "manual"}
@@ -338,3 +338,100 @@ func TestMirrorNewerPublishDuringADownloadInstallsTheNewer(t *testing.T) {
t.Fatalf("loaded %s, want the newer %s", st.SHA256, newer.SHA256)
}
}
+
+// Plan 2 Task 4 / spec §10.4: the geoip rule's reason codes.
+func TestMirrorAlertCodes(t *testing.T) {
+ now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
+ m, s, _ := newTestMirror(t)
+ m.now = func() time.Time { return now }
+ ctx := context.Background()
+
+ if c := m.AlertCode(now, "box"); c != GeoAlertNone {
+ t.Fatalf("nothing published = %v", c)
+ }
+ publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
+ m.Poll(ctx)
+ // The fixture was built 2026-02-04: more than 45 days before now.
+ if c := m.AlertCode(now, "box"); c != GeoAlertStale {
+ t.Fatalf("an old database = %v, want stale", c)
+ }
+ if c := m.AlertCode(time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC), "box"); c != GeoAlertNone {
+ t.Fatalf("a fresh database = %v", c)
+ }
+ // GeoLite2 at 22 days: expiring wins over stale's absence.
+ m.genMu.Lock()
+ m.loadedType, m.loadedBuild = "GeoLite2-City", now.Add(-22*24*time.Hour)
+ m.genMu.Unlock()
+ if c := m.AlertCode(now, "box"); c != GeoAlertExpiring {
+ t.Fatalf("GeoLite2 at 22 days = %v, want expiring", c)
+ }
+}
+
+// Codex R11-1: an expired pointer keeps the alert firing after the reader
+// is dropped and before the console marks the bucket; off clears it.
+func TestMirrorAlertSurvivesExpiryUntilOff(t *testing.T) {
+ now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
+ m, s, _ := newTestMirror(t)
+ m.now = func() time.Time { return now }
+ ctx := context.Background()
+ publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour)) // unmarked, past D1
+ m.Poll(ctx)
+ if c := m.AlertCode(now, "box"); c != GeoAlertExpired {
+ t.Fatalf("expired, unmarked = %v, want expired", c)
+ }
+ // A restart with the same unmarked pointer: still expired.
+ m2 := newGeoMirror(s, m.dir, geo.NewSwappable())
+ m2.now = m.now
+ m2.Boot()
+ m2.Poll(ctx)
+ if c := m2.AlertCode(now, "box"); c != GeoAlertExpired {
+ t.Fatalf("after restart = %v, want expired", c)
+ }
+ // A fresh publish clears it.
+ publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", now)
+ m.Poll(ctx)
+ if c := m.AlertCode(time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC), "box"); c == GeoAlertExpired {
+ t.Fatal("a fresh publish left the expired reason")
+ }
+ // Expired again, then off: off clears every pointer reason.
+ publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour))
+ m.Poll(ctx)
+ s.Delete(ctx, geoip.PointerKey)
+ m.Poll(ctx)
+ if c := m.AlertCode(now, "box"); c != GeoAlertNone {
+ t.Fatalf("after off = %v, want none", c)
+ }
+}
+
+func TestMirrorAlertNotLoadedAfterAnHour(t *testing.T) {
+ now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
+ m, s, _ := newTestMirror(t)
+ clock := now
+ m.now = func() time.Time { return clock }
+ p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
+ os.WriteFile(filepath.Join(testMirrorStoreDir[s], p.Key), []byte("corrupt"), 0o644)
+ m.Poll(context.Background())
+ if c := m.AlertCode(clock.Add(59*time.Minute), "box"); c != GeoAlertNone {
+ t.Fatalf("not loaded for 59 minutes = %v", c)
+ }
+ if c := m.AlertCode(clock.Add(61*time.Minute), "box"); c != GeoAlertNotLoaded {
+ t.Fatalf("not loaded for 61 minutes = %v, want not loaded", c)
+ }
+}
+
+func TestMirrorAlertFeedReasonOnlyOnTheConsoleBox(t *testing.T) {
+ now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
+ m, s, _ := newTestMirror(t)
+ m.now = func() time.Time { return now }
+ raw, _ := json.Marshal(geoip.FeedStatus{V: 1, Feed: "dbip-city-lite", Box: "console-box", ConsecutiveNotFound: 7})
+ if _, err := s.Put(context.Background(), geoip.FeedKey, raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ m.Poll(context.Background()) // no pointer at all: a fresh deployment
+ if c := m.AlertCode(now, "console-box"); c != GeoAlertFeed {
+ t.Fatalf("console box = %v, want feed", c)
+ }
+ if c := m.AlertCode(now, "other-box"); c != GeoAlertNone {
+ t.Fatalf("another box = %v, want none", c)
+ }
+}