feat(latest): record one poll pass per exit with skip reason and outcome counts

Every way a Lane pass can end now writes exactly one durable row: a skip
value naming the exit (paused, refusing, sidecar-down, no-fetcher,
due-query, asleep, eligible-count, nothing-eligible, or empty for the
loop), five outcome counts from the classification the Series read already
makes, and carry-forward of the previous pass's figures exactly when the
pass's own gap is zero. Refusal is durable through the poll_lanes row, so
a restart does not re-probe a Site inside its backoff. Retention is 14
days. The mid-loop browser-unreachable return writes an empty skip by
design: a tenth value is not invented here. (#141)
This commit is contained in:
2026-08-21 18:48:30 +07:00
parent e0b9063d9e
commit 503fb49d0a
4 changed files with 686 additions and 43 deletions
+10 -8
View File
@@ -1140,18 +1140,20 @@ func (s *Store) PausedLanes() ([]LanePause, error) {
return out, rows.Err()
}
// LanePausedUntil reads a Site's pause stamp. A missing state row is the
// default unpaused state.
func (s *Store) LanePausedUntil(site string) (int64, error) {
var until int64
err := s.db.QueryRow(`SELECT paused_until FROM poll_lanes WHERE site = $1`, site).Scan(&until)
// LaneGates reads a Site's pause and refusal stamps in one row read — the
// top-of-pass gate the poller uses (issue #141). A missing state row is the
// default: unpaused and not refusing.
func (s *Store) LaneGates(site string) (pausedUntil, refuseUntil int64, err error) {
err = s.db.QueryRow(
`SELECT paused_until, refuse_until FROM poll_lanes WHERE site = $1`, site).
Scan(&pausedUntil, &refuseUntil)
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
return 0, 0, nil
}
if err != nil {
return 0, fmt.Errorf("lane pause %s: %w", site, err)
return 0, 0, fmt.Errorf("lane gates %s: %w", site, err)
}
return until, nil
return pausedUntil, refuseUntil, nil
}
// DueForLatestCheck returns one Site's series whose server-side