v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The unit's data files are stamped with the versions that wrote them; the capture keeps the definition the data belongs to; a restore never starts data under another version's definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's definition); every tier's time is its data's; the conversion-copy release needs a dump on the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept view joins the folder's owning group, language switch resyncs (R-691); a restore-generated login is not shown as the password (R-694). Red-proofs in felhom.eu/documentation/audits/version-travel-2026-09-26/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-695 (v0.275.0) — the file-browser sync is single-flight: callers that queued behind a running sync
|
||||
// are covered by ONE sync that read the state after they asked, not by one restart each; a caller whose
|
||||
// request came after a sync began reading is never dropped.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): remove the `fbDoneGen >= mine` early return — five queued callers
|
||||
// then run five syncs.
|
||||
func TestR695_QueuedSyncsCoalesceIntoOne(t *testing.T) {
|
||||
s := &Server{logger: log.New(io.Discard, "", 0)}
|
||||
var runs int32
|
||||
release := make(chan struct{})
|
||||
first := true
|
||||
var fmu sync.Mutex
|
||||
s.syncFileBrowserHook = func() {
|
||||
atomic.AddInt32(&runs, 1)
|
||||
fmu.Lock()
|
||||
f := first
|
||||
first = false
|
||||
fmu.Unlock()
|
||||
if f {
|
||||
<-release // the first sync is slow: everything below queues behind it
|
||||
}
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); s.SyncFileBrowserMounts() }()
|
||||
time.Sleep(50 * time.Millisecond) // the first sync holds the lock
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); s.SyncFileBrowserMounts() }()
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond) // all five are queued
|
||||
close(release)
|
||||
wg.Wait()
|
||||
if got := atomic.LoadInt32(&runs); got != 2 {
|
||||
t.Fatalf("%d syncs ran for 6 requests (1 running + 5 queued) — want 2: the running one and ONE that re-reads for the queue", got)
|
||||
}
|
||||
// A request after everything finished is never dropped.
|
||||
s.SyncFileBrowserMounts()
|
||||
if got := atomic.LoadInt32(&runs); got != 3 {
|
||||
t.Fatalf("a new request after the queue drained ran %d syncs in total, want 3", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user