D47 / R-1269 (P2): a new install never writes into an earlier install's backup — every install has an identity (install_id), the unit records it, every unit writer asks first; another install's unit is set aside whole (backups/set-aside/<app>/<stamp>, a D25 kept copy, „Visszaállítás” + „Eltávolítás” on the restore page); a move that fails refuses the write. Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
admin committed 2026-10-25 09:27:48 +01:00
1 parent e1613aea8d
commit 02daebe9c5
30 files changed
+1990 -17

No files matched your search

+20 -2
View File
@@ -100,7 +100,10 @@ func (m *Manager) SetTier2Notifier(fn func(stackName, destLabel string, dur time
//
// v0.275.0 (R-696): CaptureRecoveryUnit is the capture AFTER a data run (it follows the legs in
// RunAppBackupNow). The periodic refresh calls captureRecoveryUnit(…, false), which never moves the
// unit's data time and never rewrites the definition away from the data it belongs to.
// unit's data time and never rewrites the definition away from the data it belongs to — for the SAME install
// (the pins below). A unit of ANOTHER install (a reinstall after „remove, keep backups") is never written at
// all: guardUnitWrite sets it aside whole first (D47, R-1269 — measured: the refresh wrote the new install's
// password beside the old data). Pinned by TestD47_* (unit_owner_test.go).
func (m *Manager) CaptureRecoveryUnit(stackName string) error {
return m.captureRecoveryUnit(stackName, true)
}
@@ -118,6 +121,11 @@ func (m *Manager) captureRecoveryUnit(stackName string, dataRun bool) error {
return fmt.Errorf("cannot determine absolute drive path for %s", stackName)
}
nsRoot := m.namespaceRoot(drivePath)
// D47 (R-1269): the unit of another install is set aside whole before this install writes its own — or this
// capture writes nothing. Before the in-memory build, so the frozen/current checks below read THIS install's unit.
if err := m.guardUnitWrite(stackName, nsRoot, "recovery-unit capture"); err != nil {
return err
}
// Build the captured config CONTENT in memory (no writes yet) so we can checksum-compare.
type capFile struct {
@@ -463,6 +471,14 @@ func (m *Manager) captureAllRecoveryUnits(dataRun bool) {
if m.isHeld(stack.Name) {
continue
}
// D47: a restore of this app is reading its unit (and may be bringing back another install's) — the refresh
// waits for it; the restore's own end decides whose unit the app writes next.
if !dataRun && m.restoreRunningFor(stack.Name) {
if m.isDebug() {
m.logger.Printf("[DEBUG] [backup] recovery-unit refresh skipped for %s — a restore of it runs", stack.Name)
}
continue
}
// R-645 (09 §3 decision 142): the unit of an app that is not running its pinned version stays
// as it is — the measured case wrote a just-failed definition over the good unit within seconds
// of a hand-lifted hold. Said at WARN on a data run; the periodic refresh repeats it only at DEBUG.
@@ -506,6 +522,8 @@ type strippedAppYaml struct {
// SetupGate (v0.314.0, R-1057): the gate at this backup; absent for an app with no gate, so an ungated app's
// unit bytes are unchanged (the checksum-skip guard).
SetupGate *GateState `yaml:"setup_gate,omitempty"`
// InstallID (D47): the install this unit's data belongs to — the unit's owner. Absent in a unit written before D47.
InstallID string `yaml:"install_id,omitempty"`
}
// buildUnitAppYaml renders the unit's app.yaml as bytes: the non-secret env PLUS the portable secret
@@ -524,7 +542,7 @@ func buildUnitAppYaml(info RecoveryInfo) []byte {
for k, v := range info.PortableSecrets {
env[k] = v
}
body, err := yaml.Marshal(strippedAppYaml{Deployed: true, Env: env, SetupGate: info.SetupGate})
body, err := yaml.Marshal(strippedAppYaml{Deployed: true, Env: env, SetupGate: info.SetupGate, InstallID: info.InstallID})
if err != nil {
body = []byte("deployed: true\nenv: {}\n")
}