R-241 part 3: abandoning starts a 14-day countdown that ends the question
Until now "set aside" renamed the remote store and touched neither the escrow
nor the key, so the hub went on holding a sealed package for a key the box no
longer used. Shape (c) compares those two, finds them different, and offers
recovery - correctly, and for ever. A customer who had already said "I do not
want the old data" would be asked again at every login.
The operator's ruling is that the answer is NOT a "they decided" flag: fix the
state, do not remember that it is wrong. So the decision starts a countdown,
at the end of which the set-aside store and the sealed package that protects
it are removed TOGETHER. Afterwards shape (c) has nothing to compare and the
offer falls silent on its own - because the state is right, not because
something remembers it once was not.
THE GRACE IS REAL. The recovery offer stays reachable for the whole 14 days;
that is the change-of-mind path, and a grace in which recovery is impossible
would be decorative.
BOTH HALVES OR NEITHER. Removing only the store leaves a package that opens
nothing; removing only the package leaves ciphertext nobody can ever decrypt.
The two cannot be atomic across two machines, so it is a two-phase commit:
delete the store, record a durable marker, and keep DECLARING
offsite.abandon_purge_requested until the hub's ACK stops reporting a
superseded package. A crash between the halves re-declares on the next sweep;
it never leaves the pair half-removed and silent.
HUB HALF - SEC 8.2 ANSWERED: yes, the hub was needed, and only for this.
store.PurgeSupersededEscrowForCustomer is the one place R-198's retention is
ever undone, and it never touches host_escrow (the package covering the key
the box uses now). The handler acts on the DECLARATION, never an inference,
and is placed immediately BEFORE the ACK is built - so
GetEscrowStatusForCustomer reads the effect and the SAME response closes the
box's two-phase commit. No second round-trip and no window where the box
thinks it is still owed. felhom-agent was NOT touched.
The countdown starts in ResetOrphanedRepo, NOT in the shared helper: the
helper is also the unclaimed auto-reset path, where nobody decided anything,
and an as-delivered box tidying a stranger's leftover store must not get a
customer's deletion clock. Pinned by a test.
Cancellation is wired into the recovery unlock, BEFORE the tier-up and the
listing - those can fail, and a countdown surviving a successful unlock
because a later step errored would delete the history the customer just
proved they can open.
The sweep is a Daily job at 05:10, not on the backup leg: it must run on a box
whose tier is not configured for runs. Quiet by construction on every box with
no countdown, and that silence is asserted.
Tests (all clock-injected; SEC 7.4 forbids shortening a live timer):
Scenario E (aside + package kept + countdown + offer still reachable, and
NOTHING deleted), Scenario F (both halves, the declaration repeating, the
close-out), Scenario G (cancel, path still nameable, no later deletion),
plus: not closed out while the package remains, a transport failure leaves the
countdown due and retrying, the no-op sweep issues zero remote commands, and
the unclaimed auto-reset starts no countdown.
RED-PROOFS, each with the mutation confirmed present in the file first:
F1) store deletion skipped -> Scenario F FAILS (no rm issued)
F2) declaration dropped from the report -> Scenario F FAILS (the hub is
never asked; the package would outlive the store for ever)
G) CancelAbandon made a no-op -> Scenario G FAILS (uncancellable countdown)
Green: controller and hub both build, vet and test clean; controller gates OK.
NOTHING WAS DELETED ANYWHERE - the terminal step has only ever run against
in-test fakes.
This commit is contained in:
@@ -677,7 +677,17 @@ func main() {
|
||||
RecordPresence: sett.SetHubEscrowIdentityPresent,
|
||||
// v0.201.0 (R-222): whether the hub is ALSO keeping an EARLIER sealed package, so the
|
||||
// recovery screen can name that situation instead of blaming the customer's typing.
|
||||
RecordSuperseded: sett.SetHubEscrowSuperseded,
|
||||
// v0.206.0 (R-241): the superseded fact ALSO closes out an abandonment. When the hub stops
|
||||
// reporting a superseded package, the sealed package that protected the deleted set-aside
|
||||
// history is gone too — both halves are away and the question is finished (Scenario F).
|
||||
// Chained onto the existing recorder rather than added as a second ACK consumer, for the
|
||||
// reason RecordPresence's own comment gives.
|
||||
RecordSuperseded: func(present bool, at string) error {
|
||||
if backupMgr != nil {
|
||||
backupMgr.ClearAbandonPurgeIfConfirmed(present)
|
||||
}
|
||||
return sett.SetHubEscrowSuperseded(present, at)
|
||||
},
|
||||
// v0.206.0 (R-241): the hash the hub's package COVERS. This is the fact the recovery
|
||||
// screen's shape (c) reads, and the comparison against it was already being computed here
|
||||
// on every ACK and discarded. Wired at the same point as the two above, deliberately —
|
||||
@@ -937,6 +947,12 @@ func main() {
|
||||
case "offbox_repo_reset":
|
||||
notifier.PushEvent("offbox_repo_reset", "info",
|
||||
"A távoli mentési tároló visszaállítva: a régi előzmény félretéve (nem törölve), és egy üres, új tároló jött létre a mostani kulccsal.", map[string]string{"renamed_to": renamedTo})
|
||||
case "offbox_abandon_completed":
|
||||
// R-241: the ONLY event in the product that reports a customer's off-site history
|
||||
// being deleted. It is fired after the deletion, not before — the operator wants to
|
||||
// know it happened, and a pre-announcement that then fails would be worse than silence.
|
||||
notifier.PushEvent("offbox_abandon_completed", "info",
|
||||
"A korábbi távoli mentések a türelmi idő lejártával törlésre kerültek, az ügyfél döntése alapján. A hozzájuk tartozó lezárt helyreállítási csomag eltávolítását is kértük.", map[string]string{"deleted_path": renamedTo})
|
||||
}
|
||||
})
|
||||
sched.Daily("offbox-backup", offboxLeg, func(ctx context.Context) error {
|
||||
@@ -946,6 +962,26 @@ func main() {
|
||||
}
|
||||
return backupMgr.RunOffboxBackup(ctx)
|
||||
})
|
||||
// R-241 — the abandonment terminal step. DAILY and not on the backup leg, deliberately: it must
|
||||
// run on a box whose off-site tier is NOT configured for runs (an abandoning box may be sitting
|
||||
// with escrow pending), and tying it to the backup leg would make the deletion depend on a
|
||||
// condition that has nothing to do with it.
|
||||
//
|
||||
// It is quiet by construction: on every box with no countdown it returns immediately and logs
|
||||
// nothing, which is asserted (TestR241_Sweep_QuietWhenNothingDue).
|
||||
sched.Daily("offsite-abandon-sweep", "05:10", func(ctx context.Context) error {
|
||||
deleted, err := backupMgr.AbandonSweep(ctx)
|
||||
if err != nil {
|
||||
// NOT a job failure: the countdown stays due and tomorrow's sweep retries. A transport
|
||||
// blip must never silently abandon the abandonment.
|
||||
logger.Printf("[WARN] [offbox] abandonment sweep: %v (the countdown stays due and retries)", err)
|
||||
return nil
|
||||
}
|
||||
if deleted {
|
||||
logger.Printf("[INFO] [offbox] abandonment sweep: the set-aside history was deleted this cycle")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Metrics prune — daily at 04:00
|
||||
|
||||
Reference in New Issue
Block a user