package notes import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "github.com/stretchr/testify/require" ) // lockChild runs inside a re-executed test binary: it takes the note's edit // lock N times and, while holding it, creates an O_EXCL marker. Finding the // marker already there means another process holds the "same" lock: mutual // exclusion failed. It prints the overlap count. func lockChild(brainDir string, rounds int) { t, err := Resolve(brainDir, "wiki/w/facts/n.md", ScopeNotes) if err != nil { fmt.Println("resolve:", err) os.Exit(2) } marker := filepath.Join(brainDir, "marker") overlaps := 0 for i := 0; i < rounds; i++ { unlock, err := flockFile(t) if err != nil { fmt.Println("lock:", err) os.Exit(2) } f, err := os.OpenFile(marker, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if errors.Is(err, os.ErrExist) { overlaps++ } else if err == nil { _ = f.Close() _ = os.Remove(marker) } unlock() } fmt.Println(overlaps) os.Exit(0) } func TestMain(m *testing.M) { if dir := os.Getenv("NOTES_LOCK_CHILD"); dir != "" { n, _ := strconv.Atoi(os.Getenv("NOTES_LOCK_ROUNDS")) lockChild(dir, n) } os.Exit(m.Run()) } // hyperguild#192: the edit lock must exclude other processes (two ingestion // pods share the hostPath brain during a RollingUpdate). Unlock removes the // lock file, so without re-checking that the locked file is still the one at // the path, a waiter and the next caller each held a lock (measured on main: // hundreds of overlaps in 2000 rounds). func TestEditLockExcludesOtherProcesses(t *testing.T) { brain := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki", "w", "facts"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(brain, "wiki", "w", "facts", "n.md"), []byte("x"), 0o644)) const procs, rounds = 3, 400 cmds := make([]*exec.Cmd, procs) outs := make([]*strings.Builder, procs) for i := range cmds { outs[i] = &strings.Builder{} cmds[i] = exec.Command(os.Args[0], "-test.run=^$") cmds[i].Env = append(os.Environ(), "NOTES_LOCK_CHILD="+brain, "NOTES_LOCK_ROUNDS="+strconv.Itoa(rounds)) cmds[i].Stdout, cmds[i].Stderr = outs[i], outs[i] require.NoError(t, cmds[i].Start()) } total := 0 for i, c := range cmds { require.NoError(t, c.Wait(), "child %d: %s", i, outs[i].String()) n, err := strconv.Atoi(strings.TrimSpace(outs[i].String())) require.NoError(t, err, "child %d output: %q", i, outs[i].String()) total += n } require.Zero(t, total, "processes held the edit lock at the same time") }