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

+29 -8
View File
@@ -15,20 +15,41 @@ import (
// per-address cycle rather than the ip_queue row's attempt_number, so it
// survives that row being deleted and the address later resubmitted.
func (d *DB) UpsertCheck(ctx context.Context, c Check) error {
_, err := d.ExecContext(ctx, `
_, err := d.UpsertCheckIfOpen(ctx, c)
return err
}
// UpsertCheckIfOpen is UpsertCheck for the live write path. It stores the
// check only while the address can still take results: the queue row exists,
// c.AttemptNumber is its current attempt, and it has not reached the
// aggregating state or a terminal one. Once the verdict is being computed the
// checks are frozen, so the verdict always matches the stored rows and a late
// probe of an already released floating IP cannot change them. It returns
// false (and writes nothing) when the result was dropped. The state test and
// the write are one statement, so they cannot interleave with SetAggregating.
func (d *DB) UpsertCheckIfOpen(ctx context.Context, c Check) (bool, error) {
now := timeToDB(Now())
res, err := d.ExecContext(ctx, `
INSERT INTO checks (registry_id, cycle_id, ip_id, ip_address, attempt_number, validator_id,
source, check_type, target, success, latency_ms, detail, checked_at, created_at)
VALUES ((SELECT registry_id FROM ip_queue WHERE id=?), (SELECT cycle_id FROM ip_queue WHERE id=?),
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
source, check_type, target, success, latency_ms, detail, checked_at, created_at, recorded_at)
SELECT q.registry_id, q.cycle_id, q.id, ?, q.attempt_number, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
FROM ip_queue q
WHERE q.id=? AND q.attempt_number=? AND q.state NOT IN (?, ?, ?, ?)
ON CONFLICT(registry_id, cycle_id, source, check_type, target) DO UPDATE SET
validator_id=excluded.validator_id,
success=excluded.success,
latency_ms=excluded.latency_ms,
detail=excluded.detail,
checked_at=excluded.checked_at
`, c.IPID, c.IPID, c.IPID, c.IPAddress, c.AttemptNumber, c.ValidatorID, c.Source, c.CheckType, c.Target,
c.Success, c.LatencyMS, c.Detail, timeToDB(c.CheckedAt), timeToDB(Now()))
return err
checked_at=excluded.checked_at,
recorded_at=excluded.recorded_at
`, c.IPAddress, c.ValidatorID, c.Source, c.CheckType, c.Target,
c.Success, c.LatencyMS, c.Detail, timeToDB(c.CheckedAt), now, now,
c.IPID, c.AttemptNumber, IPAggregating, IPDone, IPFailed, IPOccupied)
if err != nil {
return false, err
}
n, err := res.RowsAffected()
return n > 0, err
}
const checksSelect = `