v0.203.0: the box collects what the hub staged for it (R-218 consume half) + R-220's message
gates / gates (push) Successful in 10s
gates / gates (push) Successful in 10s
R-218's declaration half shipped in v0.201.0 and works. Its consume half never existed. Reconcile ran exactly twice per process — at start-up and when the recovery screen drives it — and BOTH fire before the hub has anything staged, because the hub stages in RESPONSE to the declaration those runs precede. Measured on the R-201 re-walk: unlock reconcile 11:43:07, hub staged 11:44:57 saying 'next cycle', a full report cycle ran 11:55:46, still unconsumed at 12:06. A guest command line applied it in 18 seconds — everything correct except the trigger. Bridge.RetryIfDeclared re-runs the SAME reconcile on a 5-minute tick, driven from the box's own published declaration (OffboxReportStatus().State) — the very statement the hub acts on, so the two cannot disagree. Poll, not an ACK flag, decided on the promise: the no-target message says 'amint megvannak' (no deadline) and the card says 'within a day'. Five minutes is inside both by a wide margin and needs no hub change. It stops by construction — a healthy box does no work and logs nothing — and the settle gate is deliberately kept via ReconcileWhenSettled. The marker was investigated and left alone: applied_marker lives in the guest's DataDir, which a rebuild destroys, so it cannot suppress a legitimate re-run. R-220's customer half: the refusal no longer tells the customer to choose from a list that may be empty. It names the rebuild, points at the Meghajtók page, and promises no outcome. Red-proofs: remove the retry -> credential uncollected (the dead end reproduced); drop the stop condition -> a healthy box hammers the hub; call Reconcile instead of ReconcileWhenSettled -> settle gate bypassed; restore the old sentence -> the impossible action returns. 28 packages ok, vet clean, all controller gates OK.
This commit is contained in:
@@ -756,6 +756,55 @@ func main() {
|
||||
return err
|
||||
})
|
||||
|
||||
// ── R-218, THE CONSUME HALF (v0.203.0) ────────────────────────────────────────────────
|
||||
//
|
||||
// The declaration half shipped in v0.201.0 and works: a stranded box says
|
||||
// `offsite.state=needs_credential`, and the hub's `offsiteheal` re-stages the one-time secret
|
||||
// and logs *"the box re-consumes on its next cycle"*.
|
||||
//
|
||||
// **THERE WAS NO NEXT CYCLE.** `Reconcile` ran exactly twice in a process's life: once at
|
||||
// start-up (the goroutine above, whose own comment says "retries on next config refresh/
|
||||
// restart") and once when the recovery screen drives it (R-219). Both fire BEFORE the hub has
|
||||
// anything staged, because the hub only stages in response to the declaration those runs
|
||||
// precede.
|
||||
//
|
||||
// Measured on the R-201 re-walk, 2026-08-06: unlock reconcile 11:43:07 · hub staged 11:44:57
|
||||
// saying "next cycle" · a full report cycle ran 11:55:46 · still unconsumed at 12:06. A guest
|
||||
// command line moved it in **18 seconds**, which proves the credential, the target and the key
|
||||
// were all fine and only the trigger was missing.
|
||||
//
|
||||
// So: re-run the SAME reconcile, on a tick, for exactly as long as the box itself says it needs
|
||||
// a credential. The signal is the box's own `OffboxReportStatus().State` — the very statement
|
||||
// the hub acts on, so the two can never disagree about whether a retry is wanted.
|
||||
//
|
||||
// WHY A POLL AND NOT AN ACK FLAG: the customer-facing text promises "amint megvannak" (no
|
||||
// deadline) and the backups card promises "within a day"; a 5-minute tick is inside both by a
|
||||
// wide margin, and it needs no hub change. If either promise ever tightens to minutes, revisit.
|
||||
//
|
||||
// IT STOPS BY CONSTRUCTION: the instant a target exists `OffboxReportStatus` stops returning
|
||||
// the declared state, so a healthy box does no work and makes no noise. The settle gate is
|
||||
// deliberately kept — `ReconcileWhenSettled` waits for floor knowledge exactly as at start-up.
|
||||
sched.Every("offsite-credential-retry", 5*time.Minute, func(ctx context.Context) error {
|
||||
if offsiteBridge == nil {
|
||||
return nil // off-site not configured for this customer
|
||||
}
|
||||
attempted, err := offsiteBridge.RetryIfDeclared(ctx, func() bool {
|
||||
st := backupMgr.OffboxReportStatus()
|
||||
return st != nil && st.State == backup.OffsiteStateNeedsCredential
|
||||
})
|
||||
if !attempted {
|
||||
return nil // a target exists (or we never needed one) — no work, and no log line
|
||||
}
|
||||
if err != nil {
|
||||
// NOT a job failure: the box still declares, so the next tick tries again. Logged at
|
||||
// WARN because a credential that never arrives is exactly what this exists to surface.
|
||||
logger.Printf("[WARN] [offsite-apply] credential retry: %v (the box still declares a need; retrying)", err)
|
||||
return nil
|
||||
}
|
||||
logger.Printf("[INFO] [offsite-apply] credential retry: the staged credential was collected and the tier applied")
|
||||
return nil
|
||||
})
|
||||
|
||||
// Cache refresh: every 5 minutes. Recompute the effective window each pass so the cached
|
||||
// "next DB dump" follows a runtime window change (the UI save also refreshes immediately).
|
||||
sched.Every("backup-cache", 5*time.Minute, func(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user