hub v0.63.0 — system-initiated immediacy: wire poke/bump at every mutation site that lacked one
The immediate-sync arc covered only operator-initiated desired-state changes; system-initiated mutations bumped the generation silently, so a freshly onboarded box waited a full agent tick for state the hub had already minted (observed live at slice-C onboarding). Wire the existing, live-proven notifiers into every system site on the correct plane — call-site wiring only, no new mechanism. Agent plane (poke.Notifier): - web/pbsdr.go: PBSDRAutoProvision (the observed lag), ReissuePBSDR (also lifts the pbsdrheal reconciler escalation, zero reconciler changes), handlePBSDRReissue — each pokes AFTER the successful SetHostDesired, never on a blocked/error path. - api: new nil-safe Poker seam (PokeHost/PokeAllHosts + SetPoker); handleAdminSetDesiredState pokes the target host; handleAdminSetOperatorPeer fires PokeAllHosts only when the fleet generation bump succeeded (fire-after-commit). - main.go: one poke.Notifier now feeds both planes (SetPoke + SetPoker). Controller plane (intent.Hub.Bump): - api/reissueOnReenroll: one nil-guarded bump so a long-polling controller wakes in seconds instead of on the 15-min cycle. Deliberate non-sites (unchanged): WG register (undeliverable pre-tunnel — the agent fast-tick SECONDARY owns it), WG delete (transport removed), pbsdrheal Restage (no generation bump → the 60s ticker is the pickup path). internal/pbsdrheal byte-unchanged. Tests: 10 non-hollow tests (web async channel-synchronized fake sender; api synchronous fake Poker) with explicit zero-count negatives; representative red-proofs per group (A/B/C/D) run-fail-restored. Green: go build/vet/test all pass.
This commit is contained in:
@@ -35,6 +35,16 @@ type LatestVersionProvider interface {
|
||||
LatestVersion() string
|
||||
}
|
||||
|
||||
// Poker is the agent-plane immediate-sync seam (v0.63.0): satisfied by *poke.Notifier. A system-
|
||||
// initiated desired-state write here (admin-set, operator-peer bump) fires a contentless, fire-and-
|
||||
// forget nudge so the box ticks in seconds. nil = poke disabled — mutations still persist; the box
|
||||
// picks them up on its next ≤15-min cycle. Both methods are safe to call on a nil *poke.Notifier,
|
||||
// but every call site still guards with `if h.poker != nil` (the field itself may be nil).
|
||||
type Poker interface {
|
||||
PokeHost(hostID string)
|
||||
PokeAllHosts()
|
||||
}
|
||||
|
||||
// Handler handles API endpoints for report ingest and customer queries.
|
||||
type Handler struct {
|
||||
store *store.Store
|
||||
@@ -83,6 +93,12 @@ type Handler struct {
|
||||
// notifier that GET /api/v1/wait long-polls against. nil = wait endpoint returns 503 (the box
|
||||
// falls back to the 15-min cycle). Shared with the web server, whose intent handlers Bump it.
|
||||
intentHub *intent.Hub
|
||||
|
||||
// poker (v0.63.0, Direction-2a agent-plane immediate-sync) fires a fire-and-forget nudge after a
|
||||
// system-initiated HOST desired-state write (admin-set desired-state, operator-peer bump) so the
|
||||
// box ticks in seconds instead of ≤15 min. Shared with the web server (same *poke.Notifier). nil
|
||||
// = poke disabled (a no-op; the report cycle still reconciles).
|
||||
poker Poker
|
||||
}
|
||||
|
||||
// SetClaimEngine wires the customer-claim code engine (nil-safe everywhere it is used).
|
||||
@@ -112,6 +128,12 @@ func (h *Handler) SetIntentHub(hub *intent.Hub) {
|
||||
h.intentHub = hub
|
||||
}
|
||||
|
||||
// SetPoker wires the agent-plane immediate-sync notifier (v0.63.0; nil-safe — an unset poker makes
|
||||
// the admin desired-state writes fire no nudge, and the box picks the change up on its next cycle).
|
||||
func (h *Handler) SetPoker(p Poker) {
|
||||
h.poker = p
|
||||
}
|
||||
|
||||
// New creates a new API handler.
|
||||
func New(store *store.Store, apiKey, resendAPIKey, fromEmail string, templateProvider ConfigTemplateProvider, logger *log.Logger) *Handler {
|
||||
return &Handler{
|
||||
@@ -1051,6 +1073,15 @@ func (h *Handler) reissueOnReenroll(cc *store.CustomerConfig) {
|
||||
h.logger.Printf("[WARN] offsite re-issue on re-enroll for %s failed: %v", cc.CustomerID, err)
|
||||
}
|
||||
}
|
||||
// Direction-2 (v0.63.0): wake a long-polling controller so the re-staged claim code / offsite
|
||||
// password ride the next ACK in seconds, not on the 15-min cycle. Both legs above are
|
||||
// best-effort; an over-bump costs one cheap wake. (On the clean-slate path the controller
|
||||
// usually does not exist yet — its startup fetch covers that shape; a bump landing during a
|
||||
// fresh controller's FIRST hold is recorded as baseline without firing — the known open
|
||||
// observation, fixed later by carrying intent_gen in the report ACK. Out of scope here.)
|
||||
if h.intentHub != nil {
|
||||
h.intentHub.Bump(cc.CustomerID)
|
||||
}
|
||||
}
|
||||
|
||||
// escrowUploadRequest is the agent→hub wire shape for the OPAQUE PBS recovery-code escrow blob
|
||||
@@ -1396,6 +1427,9 @@ func (h *Handler) handleAdminSetDesiredState(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
h.logger.Printf("[INFO] admin-set desired-state for host %s (generation now %d, %d bytes)", pathHostID, gen, len(body))
|
||||
if h.poker != nil {
|
||||
h.poker.PokeHost(pathHostID) // agent-plane immediate-sync (Direction-2a): generation bumped → nudge the box now
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok", "generation": gen})
|
||||
|
||||
Reference in New Issue
Block a user