R-1271 (P2): „use my kept data” never takes a copy dated in the future — a copy more than 10 minutes past the box's clock never counts as the newest (the newest true date wins, tie local); alone it is still offered, said and not recommended. Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
2026-10-25 09:44:56 +01:00
parent 2d71270858
commit bd9f4f7b74
13 changed files with 278 additions and 32 deletions
+65 -13
View File
@@ -44,6 +44,9 @@ type KeptCopy struct {
// SnapshotID and UnitPath name the off-site copy (Tier 3 only): the snapshot and the unit's path in it.
SnapshotID string
UnitPath string
// Future (R-1271): the copy's date lies past this box's clock (+ KeptClockSkew) — chosen only when it is the only
// copy, and the question then says so and does not recommend it.
Future bool
}
// KeptTierOffsite is KeptCopy.Tier for the off-site copy.
@@ -112,11 +115,10 @@ func (m *Manager) KeptDBCopy(app, drive string) (KeptCopy, bool) {
if app == "" || m.isStackDeployed(app) {
return KeptCopy{}, false
}
var best KeptCopy
found := false
var cands []KeptCopy
consider := func(c KeptCopy, ok bool) {
if ok && (!found || c.Time.After(best.Time)) {
best, found = c, true
if ok {
cands = append(cands, c)
}
}
if u, ok := m.RemovedAppUnitFor(app); ok {
@@ -127,7 +129,7 @@ func (m *Manager) KeptDBCopy(app, drive string) (KeptCopy, bool) {
for _, d := range m.Tier2MirrorDirsForApp(app) {
consider(m.KeptCopyAt(tier2UnitDir(d), drive, 2))
}
return best, found
return keptPick(time.Now(), cands...) // R-1271: the newest TRUE date; own unit before the second drive on a tie
}
// KeptOffsiteCopies is the off-site copy kept files of each app could be loaded from: the app's newest
@@ -174,21 +176,71 @@ func (m *Manager) KeptOffsiteCopy(ctx context.Context, app string) (KeptCopy, bo
return c, ok
}
// KeptNewer chooses between a local copy and the off-site copy: the off-site one only when it is NEWER or
// the only one (a tie goes local — no download).
func KeptNewer(local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) {
if ook && (!lok || off.Time.After(local.Time)) {
return off, true
// KeptNewerAt chooses between a local copy and the off-site copy at now: the off-site one only when it is NEWER by a
// true date, or the only one (a tie goes local — no download). R-1271: a future-dated copy never counts as newer.
func KeptNewerAt(now time.Time, local KeptCopy, lok bool, off KeptCopy, ook bool) (KeptCopy, bool) {
var cands []KeptCopy
if lok {
cands = append(cands, local)
}
return local, lok
if ook {
cands = append(cands, off)
}
return keptPick(now, cands...)
}
// KeptBestCopy is the copy „Use my kept data" and Load use for app's kept files on drive: the newest
// usable local copy (KeptDBCopy), or the off-site copy when it is newer or the only one.
// usable local copy (KeptDBCopy), or the off-site copy when it is newer by a true date or the only one.
func (m *Manager) KeptBestCopy(ctx context.Context, app, drive string) (KeptCopy, bool) {
local, lok := m.KeptDBCopy(app, drive)
off, ook := m.KeptOffsiteCopy(ctx, app)
return KeptNewer(local, lok, off, ook)
return KeptNewerAt(time.Now(), local, lok, off, ook)
}
// ── R-1271 (brief 2026-10-25 Part 4): a copy dated in the future is never „the newest" ────────────────────────────
//
// MEASURED (catalog sweep 2026-10-24, Tester4: Paperless-ngx, Nextcloud, Immich): the off-site store held copies of the
// 2026-10-20 rehearsal box, whose clock had been set to 2026-10-28. The off-site copy's date is the snapshot time that
// OTHER box wrote — this box recorded no data time to cap it (offsiteDataTime) — so „newer than every local copy" took
// it over the family's own copy of the same evening, and „Use my kept data" loaded it. The `07` §6.5 „taken of another
// drive" check (KeptCopyAt) compares the unit's HDD_PATH, a mount PATH with the same name on every box: a copy of
// another box's /mnt/felhom-drives/hdd_1 matches this box's. It is a path check, not a physical identity, and it stays
// one — loading the old box's data on a new box is what off-site exists for.
//
// THE RULE. A copy dated more than KeptClockSkew past now is FUTURE-DATED and never counts as the newest: the newest
// copy with a true date wins (a tie goes to the earlier candidate — local before off-site, own unit before the second
// drive). A future-dated copy is chosen only when no other exists; it is then marked Future, and the question says its
// date is in the future and does not recommend it. Pinned by kept_pick_test.go.
// KeptClockSkew is how far a copy's date may lie past this box's clock and still count as a true date: NTP keeps a box
// within a second; ten minutes covers a box whose clock synced late after a power cut.
const KeptClockSkew = 10 * time.Minute
// KeptFuture reports whether c's date lies more than KeptClockSkew past now.
func KeptFuture(c KeptCopy, now time.Time) bool { return c.Time.After(now.Add(KeptClockSkew)) }
// keptPick: the newest candidate with a true date (the first one on a tie); only when every candidate is
// future-dated, the first one, marked Future.
func keptPick(now time.Time, cands ...KeptCopy) (KeptCopy, bool) {
var best KeptCopy
found := false
for _, c := range cands {
if KeptFuture(c, now) {
continue
}
if !found || c.Time.After(best.Time) {
best, found = c, true
}
}
if found {
return best, true
}
if len(cands) == 0 {
return KeptCopy{}, false
}
c := cands[0]
c.Future = true
return c, true
}
// Refusals of an off-site load, born as bundle keys. Each is raised BEFORE prepare and before anything
@@ -0,0 +1,66 @@
package backup
import (
"testing"
"time"
)
// R-1271 (brief 2026-10-25 Part 4) — a copy dated in the future is never „the newest". The measured case (catalog sweep
// 2026-10-24): the rehearsal box's off-site copy dated 2026-10-28 07:39 beat the family's own copy of 2026-10-24 19:17
// on a box whose clock said 2026-10-24.
// COMPANION RED-PROOF (REPORT.md): with the old KeptNewer (off-site when its date is after the local one's),
// TestR1271_AFutureOffsiteCopyNeverBeatsTheOwnCopy fails — the off-site copy is chosen.
var r1271Now = time.Date(2026, 10, 24, 19, 22, 0, 0, time.UTC)
func TestR1271_AFutureOffsiteCopyNeverBeatsTheOwnCopy(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: time.Date(2026, 10, 24, 19, 17, 0, 0, time.UTC)}
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 28, 6, 39, 0, 0, time.UTC), SnapshotID: "abba81ba"}
c, ok := KeptNewerAt(r1271Now, local, true, off, true)
if !ok || c.Tier != 1 || c.Future {
t.Fatalf("the household's own copy must win over a future-dated off-site one, got %+v", c)
}
}
func TestR1271_AFutureCopyAloneIsOfferedButMarked(t *testing.T) {
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 28, 6, 39, 0, 0, time.UTC), SnapshotID: "abba81ba"}
c, ok := KeptNewerAt(r1271Now, KeptCopy{}, false, off, true)
if !ok || c.Tier != KeptTierOffsite || !c.Future {
t.Fatalf("a future-dated copy alone is offered and marked Future (not recommended), got %+v ok=%v", c, ok)
}
}
func TestR1271_AnOlderOffsiteCopyAloneIsChosenAsBefore(t *testing.T) {
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 20, 4, 15, 0, 0, time.UTC), SnapshotID: "s1"}
c, ok := KeptNewerAt(r1271Now, KeptCopy{}, false, off, true)
if !ok || c.Tier != KeptTierOffsite || c.Future {
t.Fatalf("a normal older off-site copy alone is chosen as before, got %+v", c)
}
}
// The other direction of the old rule still holds: a NEWER off-site copy with a true date beats an older local one.
func TestR1271_ANewerTrueOffsiteCopyStillWins(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: time.Date(2026, 10, 20, 4, 0, 0, 0, time.UTC)}
off := KeptCopy{Tier: KeptTierOffsite, Time: time.Date(2026, 10, 24, 4, 0, 0, 0, time.UTC), SnapshotID: "s2"}
if c, _ := KeptNewerAt(r1271Now, local, true, off, true); c.Tier != KeptTierOffsite {
t.Fatalf("a newer off-site copy with a true date wins, got %+v", c)
}
// A tie goes local (no download).
off.Time = local.Time
if c, _ := KeptNewerAt(r1271Now, local, true, off, true); c.Tier != 1 {
t.Fatalf("a tie goes local, got %+v", c)
}
}
// The skew allowance: a few minutes ahead is a true date; beyond KeptClockSkew it is not.
func TestR1271_TheClockSkewAllowance(t *testing.T) {
local := KeptCopy{UnitDir: "/u", Tier: 1, Time: r1271Now.Add(-time.Hour)}
near := KeptCopy{Tier: KeptTierOffsite, Time: r1271Now.Add(KeptClockSkew - time.Minute), SnapshotID: "n"}
if c, _ := KeptNewerAt(r1271Now, local, true, near, true); c.Tier != KeptTierOffsite {
t.Fatalf("a copy within the skew is a true date and newer, got %+v", c)
}
far := KeptCopy{Tier: KeptTierOffsite, Time: r1271Now.Add(KeptClockSkew + time.Minute), SnapshotID: "f"}
if c, _ := KeptNewerAt(r1271Now, local, true, far, true); c.Tier != 1 {
t.Fatalf("a copy beyond the skew is future-dated and never the newest, got %+v", c)
}
}