bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// Gate: the one background hashing job survives a file whose read never
|
||||
// returns (a network mount that stopped answering). Each file gets a bound;
|
||||
// past it the job moves on to the other folders, reads nothing more from that
|
||||
// folder until the stuck read returns, and keeps that read's digest when it
|
||||
// does.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func awaitHash(t *testing.T, p, want, why string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if fi, err := os.Lstat(p); err == nil && cachedHash(p, fi) == want {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatal(why)
|
||||
}
|
||||
|
||||
func TestAHashReadThatNeverReturnsHoldsUpNoOtherFolder(t *testing.T) {
|
||||
const bound = 200 * time.Millisecond
|
||||
hashMu.Lock()
|
||||
prevBound := hashJobTimeout
|
||||
hashJobTimeout = bound
|
||||
hashMu.Unlock()
|
||||
t.Cleanup(func() { hashMu.Lock(); hashJobTimeout = prevBound; hashMu.Unlock() })
|
||||
quiet := opMaskLog
|
||||
opMaskLog = func(string, ...any) {}
|
||||
t.Cleanup(func() { opMaskLog = quiet })
|
||||
|
||||
dead, live := t.TempDir(), t.TempDir()
|
||||
deadFile := filepath.Join(dead, fixtureData)
|
||||
deadOther := filepath.Join(dead, "20260101T000000Z.tar.zst.age")
|
||||
liveFile := filepath.Join(live, fixtureData)
|
||||
for p, b := range map[string]string{deadFile: "stuck", deadOther: "stuck too", liveFile: "answers"} {
|
||||
if err := os.WriteFile(p, []byte(b), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
deadReads := 0
|
||||
gate := make(chan struct{})
|
||||
var once sync.Once
|
||||
release := func() { once.Do(func() { close(gate) }) }
|
||||
prev := hashFileFn
|
||||
hashFileFn = func(p string) (string, int64, error) {
|
||||
if filepath.Dir(p) == dead {
|
||||
mu.Lock()
|
||||
deadReads++
|
||||
mu.Unlock()
|
||||
<-gate
|
||||
}
|
||||
return hashFile(p)
|
||||
}
|
||||
t.Cleanup(func() { release(); hashFileFn = prev })
|
||||
reads := func() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return deadReads
|
||||
}
|
||||
|
||||
requestHash(deadFile)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for reads() == 0 {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("the hashing job never started")
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
requestHash(liveFile)
|
||||
awaitHash(t, liveFile, sha([]byte("answers")),
|
||||
"a read that never returned held up the hashing of every other folder")
|
||||
|
||||
// While the read is stuck, nothing more is read from that folder.
|
||||
requestHash(deadOther)
|
||||
time.Sleep(3 * bound)
|
||||
if n := reads(); n != 1 {
|
||||
t.Fatalf("the folder whose read is stuck was read %d times", n)
|
||||
}
|
||||
|
||||
// When it returns, its digest is kept, and the folder is read again.
|
||||
release()
|
||||
awaitHash(t, deadFile, sha([]byte("stuck")), "the stuck read's digest was not kept when it returned")
|
||||
requestHash(deadOther)
|
||||
awaitHash(t, deadOther, sha([]byte("stuck too")), "the folder was not read again once it answered")
|
||||
}
|
||||
Reference in New Issue
Block a user