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:
1 parent
db73409e8f
commit
864208238f
34 files changed
+1570
-72
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user