97 lines
2.7 KiB
Go
97 lines
2.7 KiB
Go
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")
|
|
}
|