alert: wire engine into ws hello + MarkJobFinished + offline sweep

- ws.HandlerDeps gains an AlertEngine *alert.Engine field; populated
  from http.Deps.AlertEngine (nil until G1 constructs the engine)
- runAgentLoop calls NotifyHostOnline after MarkHostHello succeeds
- dispatchAgentMessage MsgJobFinished case calls NotifyJobFinished,
  looking up the job Kind via Store.GetJob before notifying
- store.MarkHostsOfflineStaleReturnIDs added: SELECT+UPDATE in one
  transaction, returns the IDs that flipped to offline
- offline sweeper in cmd/server/main.go switched to the new variant;
  TODO(G1) comment marks where NotifyHostOffline calls will land
This commit is contained in:
2026-05-04 19:54:39 +01:00
parent 5e655d756d
commit c710743231
4 changed files with 78 additions and 2 deletions
+49
View File
@@ -110,6 +110,55 @@ func (s *Store) MarkHostsOfflineStale(ctx context.Context, cutoff time.Time) (in
return n, nil
}
// MarkHostsOfflineStaleReturnIDs flips any host that hasn't been seen
// since before `cutoff` from 'online' to 'offline' and returns the IDs
// of every host that was flipped. Uses a single transaction.
func (s *Store) MarkHostsOfflineStaleReturnIDs(ctx context.Context, cutoff time.Time) ([]string, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("store: begin tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
cutoffStr := cutoff.UTC().Format(time.RFC3339Nano)
rows, err := tx.QueryContext(ctx,
`SELECT id FROM hosts
WHERE status = 'online'
AND (last_seen_at IS NULL OR last_seen_at < ?)`,
cutoffStr)
if err != nil {
return nil, fmt.Errorf("store: select stale hosts: %w", err)
}
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
_ = rows.Close()
return nil, fmt.Errorf("store: scan stale host id: %w", err)
}
ids = append(ids, id)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("store: iterate stale hosts: %w", err)
}
_ = rows.Close()
if len(ids) > 0 {
if _, err := tx.ExecContext(ctx,
`UPDATE hosts SET status = 'offline'
WHERE status = 'online'
AND (last_seen_at IS NULL OR last_seen_at < ?)`,
cutoffStr); err != nil {
return nil, fmt.Errorf("store: mark offline: %w", err)
}
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("store: commit: %w", err)
}
return ids, nil
}
// ListHosts returns every host. Phase 1 callers fit a small fleet in
// memory; pagination lands when it matters.
func (s *Store) ListHosts(ctx context.Context) ([]Host, error) {