返回 DeepSeek-Reasonix
directory_ownership_test.go
根目录 / internal / session / directory_ownership_test.go
1 package session
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 filelock "reasonix/internal/identitylock"
11 )
12
13 func writerProbeFixture(t *testing.T) string {
14 t.Helper()
15 dir := filepath.Join(t.TempDir(), "identity")
16 if err := os.MkdirAll(dir, 0o700); err != nil {
17 t.Fatal(err)
18 }
19 if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`{}`), 0o600); err != nil {
20 t.Fatal(err)
21 }
22 return dir
23 }
24
25 func resetWriterHooks(t *testing.T) {
26 t.Helper()
27 backoff := writerProbeCollisionBackoff
28 t.Cleanup(func() {
29 writerProbeHoldHookForTest = nil
30 writerAcquireEnterHookForTest = nil
31 writerAcquireRetryHookForTest = nil
32 writerProbeCollisionBackoff = backoff
33 })
34 }
35
36 // A lease this process holds is answered from the ownership registry: the
37 // probe must not take the lock file at all, so it cannot race the holder's own
38 // re-acquire or any other in-process acquirer.
39 func TestProbeWriterHeldAnswersFromInProcessOwnership(t *testing.T) {
40 resetWriterHooks(t)
41 dir := writerProbeFixture(t)
42 writerProbeHoldHookForTest = func() { t.Fatal("probe touched the lock file for a lease this process holds") }
43 release, err := acquireSessionWriter(dir)
44 if err != nil {
45 t.Fatal(err)
46 }
47 if !ProbeWriterHeld(dir) {
48 t.Fatal("probe reported an in-process writer lease as free")
49 }
50 release()
51 writerProbeHoldHookForTest = nil
52 if ProbeWriterHeld(dir) {
53 t.Fatal("probe still reports the released lease as held")
54 }
55 }
56
57 // Probing a session no writer ever opened must not manufacture writer.lock.
58 func TestProbeWriterHeldDoesNotCreateLockFile(t *testing.T) {
59 dir := writerProbeFixture(t)
60 if ProbeWriterHeld(dir) {
61 t.Fatal("never-opened session reported as held")
62 }
63 if _, err := os.Stat(filepath.Join(dir, "writer.lock")); !os.IsNotExist(err) {
64 t.Fatalf("probe created writer.lock: %v", err)
65 }
66 }
67
68 // An in-process acquirer and an in-process probe are mutually exclusive: an
69 // acquire that arrives while the probe holds its transient lock waits for the
70 // probe to leave and then succeeds, instead of failing with ErrHeld against a
71 // holder that was never a writer.
72 func TestAcquireSessionWriterNeverCollidesWithInProcessProbe(t *testing.T) {
73 resetWriterHooks(t)
74 dir := writerProbeFixture(t)
75 // Materialize writer.lock so the probe reaches the lock path.
76 if release, err := acquireSessionWriter(dir); err != nil {
77 t.Fatal(err)
78 } else {
79 release()
80 }
81 probeInside := make(chan struct{})
82 releaseProbe := make(chan struct{})
83 writerProbeHoldHookForTest = func() {
84 close(probeInside)
85 <-releaseProbe
86 }
87 acquirerContending := make(chan struct{})
88 writerAcquireEnterHookForTest = func() { close(acquirerContending) }
89 retries := 0
90 writerAcquireRetryHookForTest = func(int) { retries++ }
91
92 probeResult := make(chan bool, 1)
93 go func() { probeResult <- ProbeWriterHeld(dir) }()
94 <-probeInside
95
96 type acquired struct {
97 release func()
98 err error
99 }
100 acquireResult := make(chan acquired, 1)
101 go func() {
102 release, err := acquireSessionWriter(dir)
103 acquireResult <- acquired{release: release, err: err}
104 }()
105 // The acquirer is about to contend while the probe still holds the lock;
106 // only now does the probe leave. Any ErrHeld from here on would be the
107 // collision this protocol rules out.
108 <-acquirerContending
109 close(releaseProbe)
110
111 if held := <-probeResult; held {
112 t.Fatal("probe reported a free session as held")
113 }
114 result := <-acquireResult
115 if result.err != nil {
116 t.Fatalf("acquire collided with the in-process probe: %v", result.err)
117 }
118 result.release()
119 if retries != 0 {
120 t.Fatalf("acquire needed %d retries against an in-process probe; the mutex must make that impossible", retries)
121 }
122 }
123
124 // A probe in another process holds writer.lock for an instant. Simulate it
125 // with a bare identity lock (invisible to the ownership registry) that is
126 // released after the acquirer's first failed attempt: the acquirer retries and
127 // succeeds instead of reporting the session as owned.
128 func TestAcquireSessionWriterRetriesForeignProbeCollision(t *testing.T) {
129 resetWriterHooks(t)
130 writerProbeCollisionBackoff = time.Millisecond
131 dir := writerProbeFixture(t)
132 foreign, err := filelock.TryAcquire(filepath.Join(dir, "writer.lock"))
133 if err != nil {
134 t.Fatal(err)
135 }
136 attempts := 0
137 writerAcquireRetryHookForTest = func(attempt int) {
138 attempts++
139 if attempt == 0 {
140 foreign()
141 }
142 }
143 release, err := acquireSessionWriter(dir)
144 if err != nil {
145 t.Fatalf("acquire failed against a transient foreign hold: %v", err)
146 }
147 release()
148 if attempts != 1 {
149 t.Fatalf("acquire retried %d times, want exactly 1 after the transient hold cleared", attempts)
150 }
151 }
152
153 // A real foreign owner is still refused, after exactly the documented number
154 // of retries, so a genuine ErrWriterOwned stays prompt and bounded.
155 func TestAcquireSessionWriterRefusesForeignOwnerAfterBoundedRetries(t *testing.T) {
156 resetWriterHooks(t)
157 writerProbeCollisionBackoff = time.Millisecond
158 dir := writerProbeFixture(t)
159 foreign, err := filelock.TryAcquire(filepath.Join(dir, "writer.lock"))
160 if err != nil {
161 t.Fatal(err)
162 }
163 defer foreign()
164 attempts := 0
165 writerAcquireRetryHookForTest = func(int) { attempts++ }
166 if _, err := acquireSessionWriter(dir); !errors.Is(err, filelock.ErrHeld) {
167 t.Fatalf("acquire against a live foreign owner = %v, want ErrHeld", err)
168 }
169 if attempts != writerProbeCollisionRetries {
170 t.Fatalf("acquire retried %d times, want %d", attempts, writerProbeCollisionRetries)
171 }
172 if !ProbeWriterHeld(dir) {
173 t.Fatal("probe reported a foreign-held session as free")
174 }
175 }
176
176 lines GO