Show egress/ingress levels in the registry; freeze checks at the verdict

Registry: the "last result" column now also shows, per level (egress,
ingress), how many of the recorded checks of the latest cycle succeeded, split
by check family (tcp-22 and tcp-443 are both "tcp"). One grouped query per
chunk of addresses; new fields last_cycle_id, egress, ingress in
GET /admin/registry; the dashboard renders them under the verdict.

Verdict integrity (migration 0010):
- the prober is handed an address once per site and attempt, not on every
  poll, so results are no longer overwritten by later probe rounds;
- UpsertCheckIfOpen refuses writes once the address is aggregating or has its
  verdict, or for an older attempt; senders get {"ok":true,"ignored":N} and a
  result_dropped event is recorded;
- the checking window counts from checking_started_at, not from assigned_at;
- checks.recorded_at (server clock) and checks.after_verdict (flag for rows
  written after the verdict in existing data);
- the verdict rule is a pure function (computeVerdict) and the aggregated
  event carries the egress/ingress check counts.

Rebuilt bin/control-api and bin/admin-dashboard to match. Plans and summaries
are in docs/changes; README, API, USAGE, DASHBOARD and DIAGRAMS are updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-03 17:59:52 +03:00
1 parent db73409e8f
commit 864208238f
34 files changed
+1570 -72

No files matched your search

+126
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"time"
)
@@ -56,6 +57,28 @@ type RegistrySummary struct {
LastCheckedAt *time.Time
InQueue bool
CurrentState string
// LastCycleID is the newest cycle_id with recorded checks (0 if none).
// Egress and Ingress count that cycle's recorded checks per level.
LastCycleID int
Egress LevelResult
Ingress LevelResult
}
// TypeStat counts the recorded checks of one check family (see CheckFamily)
// within a level: Total checks, OK of them successful.
type TypeStat struct {
Type string
Total int
OK int
}
// LevelResult is the "OK of Total" rollup of one level (egress or ingress) of
// a cycle, with the same counts split by check family, sorted by Type.
type LevelResult struct {
Total int
OK int
ByType []TypeStat
}
// ListRegistry returns every address ever submitted, newest first-seen
@@ -87,6 +110,9 @@ func (d *DB) ListRegistry(ctx context.Context) ([]RegistrySummary, error) {
}
out[i] = s
}
if err := d.fillRegistryLevels(ctx, out); err != nil {
return nil, err
}
return out, nil
}
@@ -165,6 +191,9 @@ func (d *DB) ListRegistryPage(ctx context.Context, f RegistryFilter, limit, offs
}
out[i] = s
}
if err := d.fillRegistryLevels(ctx, out); err != nil {
return nil, 0, err
}
return out, total, nil
}
@@ -186,6 +215,11 @@ func (d *DB) GetRegistryByAddress(ctx context.Context, address string) (*Registr
if err := d.fillRegistrySummary(ctx, s); err != nil {
return nil, err
}
one := []RegistrySummary{*s}
if err := d.fillRegistryLevels(ctx, one); err != nil {
return nil, err
}
*s = one[0]
return s, nil
}
@@ -253,6 +287,98 @@ func (d *DB) fillRegistrySummary(ctx context.Context, s *RegistrySummary) error
return nil
}
// registryLevelsChunk bounds the number of registry ids per query, well under
// SQLite's bound-variable limit.
const registryLevelsChunk = 500
// registryLevelsQuery is the grouped query of fillRegistryLevels for n
// registry ids: per address, the counts of its newest cycle by source and
// check type.
func registryLevelsQuery(n int) string {
return `
SELECT c.registry_id, m.cid, c.source, c.check_type, COUNT(*), COALESCE(SUM(c.success), 0)
FROM checks c
JOIN (SELECT registry_id, MAX(cycle_id) AS cid FROM checks
WHERE registry_id IN (` + strings.TrimSuffix(strings.Repeat("?,", n), ",") + `)
GROUP BY registry_id) m
ON m.registry_id = c.registry_id AND m.cid = c.cycle_id
GROUP BY c.registry_id, m.cid, c.source, c.check_type
`
}
// fillRegistryLevels sets LastCycleID, Egress and Ingress on every summary in
// sums: the recorded checks of each address's newest cycle (the same cycle
// whose time is LastCheckedAt), counted per level and per check family. It
// runs one grouped query per chunk of addresses, not one per address, over
// idx_checks_registry_cycle. Checks whose source is neither egress nor an
// inbound site are not counted. The counts follow the recorded rows only, so
// they can differ from LastResult, which also treats missing results as
// failures.
func (d *DB) fillRegistryLevels(ctx context.Context, sums []RegistrySummary) error {
pos := make(map[int64]int, len(sums))
for i := range sums {
pos[sums[i].ID] = i
}
type key struct {
id int64
level, family string
}
for start := 0; start < len(sums); start += registryLevelsChunk {
end := min(start+registryLevelsChunk, len(sums))
args := make([]any, 0, end-start)
for _, s := range sums[start:end] {
args = append(args, s.ID)
}
rows, err := d.QueryContext(ctx, registryLevelsQuery(len(args)), args...)
if err != nil {
return err
}
stats := map[key]*TypeStat{}
for rows.Next() {
var id int64
var cid, total, ok int
var source, checkType string
if err := rows.Scan(&id, &cid, &source, &checkType, &total, &ok); err != nil {
rows.Close()
return err
}
sums[pos[id]].LastCycleID = cid
level := CheckLevel(source)
if level == "" {
continue
}
k := key{id, level, CheckFamily(checkType)}
st := stats[k]
if st == nil {
st = &TypeStat{Type: k.family}
stats[k] = st
}
st.Total += total
st.OK += ok
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
for k, st := range stats {
lr := &sums[pos[k.id]].Egress
if k.level == LevelIngress {
lr = &sums[pos[k.id]].Ingress
}
lr.Total += st.Total
lr.OK += st.OK
lr.ByType = append(lr.ByType, *st)
}
}
for i := range sums {
for _, lr := range []*LevelResult{&sums[i].Egress, &sums[i].Ingress} {
sort.Slice(lr.ByType, func(a, b int) bool { return lr.ByType[a].Type < lr.ByType[b].Type })
}
}
return nil
}
// lastCycleResultFromChecks classifies the most recent cycle recorded for
// registryID directly from its checks rows: pass if every recorded check
// succeeded, fail if every one failed, partial on a mix. Returns "" if no