返回 DeepSeek-Reasonix
filelock_test.go
根目录 / internal / filelock / filelock_test.go
1 package filelock
2
3 import (
4 "context"
5 "errors"
6 "path/filepath"
7 "testing"
8 "time"
9 )
10
11 func TestTryAcquireModeSharedIsNonBlocking(t *testing.T) {
12 path := filepath.Join(t.TempDir(), "state.lock")
13 first, err := TryAcquireMode(path, ModeShared)
14 if err != nil {
15 t.Fatal(err)
16 }
17 second, err := TryAcquireMode(path, ModeShared)
18 if err != nil {
19 first()
20 t.Fatal(err)
21 }
22 first()
23 second()
24 }
25
26 func TestExplicitLocalKeySerializesDifferentAccessPaths(t *testing.T) {
27 dir := t.TempDir()
28 first, err := TryAcquireModeWithKey(filepath.Join(dir, "first.lock"), "shared-identity", ModeExclusive)
29 if err != nil {
30 t.Fatal(err)
31 }
32 defer first()
33 if release, err := TryAcquireModeWithKey(filepath.Join(dir, "second.lock"), "shared-identity", ModeExclusive); !errors.Is(err, ErrHeld) {
34 if release != nil {
35 release()
36 }
37 t.Fatalf("second acquire error = %v, want ErrHeld", err)
38 }
39 }
40
41 func TestWaitingWriterBlocksNewLocalReaders(t *testing.T) {
42 path := filepath.Join(t.TempDir(), "state.lock")
43 reader, err := AcquireMode(context.Background(), path, ModeShared)
44 if err != nil {
45 t.Fatal(err)
46 }
47 writerAcquired := make(chan func(), 1)
48 go func() {
49 release, acquireErr := Acquire(context.Background(), path)
50 if acquireErr == nil {
51 writerAcquired <- release
52 }
53 }()
54 deadline := time.After(2 * time.Second)
55 for {
56 release, tryErr := TryAcquireMode(path, ModeShared)
57 if errors.Is(tryErr, ErrHeld) {
58 break
59 }
60 if tryErr != nil {
61 t.Fatal(tryErr)
62 }
63 release()
64 select {
65 case <-deadline:
66 t.Fatal("new readers continued to bypass the waiting writer")
67 default:
68 }
69 }
70 reader()
71 select {
72 case release := <-writerAcquired:
73 release()
74 case <-time.After(2 * time.Second):
75 t.Fatal("waiting writer did not acquire after reader release")
76 }
77 }
78
79 func TestAcquireHonorsDeadlineAndRecoversAfterRelease(t *testing.T) {
80 path := filepath.Join(t.TempDir(), "state.lock")
81 release, err := Acquire(context.Background(), path)
82 if err != nil {
83 t.Fatalf("first acquire: %v", err)
84 }
85
86 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
87 defer cancel()
88 if _, err := Acquire(ctx, path); !errors.Is(err, context.DeadlineExceeded) {
89 t.Fatalf("contended acquire error = %v, want deadline exceeded", err)
90 }
91
92 release()
93 secondRelease, err := Acquire(context.Background(), path)
94 if err != nil {
95 t.Fatalf("acquire after release: %v", err)
96 }
97 secondRelease()
98 }
99
100 func TestAcquireWithExternalTimeoutBoundsOnlyFileLockRetries(t *testing.T) {
101 path := filepath.Join(t.TempDir(), "state.lock")
102 releaseExternal, err := tryLockFile(path)
103 if err != nil {
104 t.Fatalf("hold external file lock: %v", err)
105 }
106 defer releaseExternal()
107
108 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
109 defer cancel()
110 started := time.Now()
111 _, err = AcquireWithExternalTimeout(ctx, path, 60*time.Millisecond)
112 elapsed := time.Since(started)
113 if !errors.Is(err, context.DeadlineExceeded) {
114 t.Fatalf("external acquire error = %v, want deadline exceeded", err)
115 }
116 if elapsed >= time.Second {
117 t.Fatalf("external acquire waited %v, want the short external budget", elapsed)
118 }
119 }
120
121 func TestAcquireWithExternalTimeoutRejectsInvalidBudget(t *testing.T) {
122 path := filepath.Join(t.TempDir(), "state.lock")
123 if _, err := AcquireWithExternalTimeout(context.Background(), path, 0); err == nil {
124 t.Fatal("zero external timeout should be rejected")
125 }
126 }
127
128 func TestAcquireSharedAllowsConcurrentReaders(t *testing.T) {
129 path := filepath.Join(t.TempDir(), "state.lock")
130 first, err := AcquireMode(context.Background(), path, ModeShared)
131 if err != nil {
132 t.Fatal(err)
133 }
134 second, err := AcquireMode(context.Background(), path, ModeShared)
135 if err != nil {
136 first()
137 t.Fatal(err)
138 }
139 first()
140 second()
141 }
142
143 func TestAcquireSharedConflictsWithExclusive(t *testing.T) {
144 path := filepath.Join(t.TempDir(), "state.lock")
145 shared, err := AcquireMode(context.Background(), path, ModeShared)
146 if err != nil {
147 t.Fatal(err)
148 }
149 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
150 defer cancel()
151 if _, err := Acquire(ctx, path); !errors.Is(err, context.DeadlineExceeded) {
152 shared()
153 t.Fatalf("exclusive vs shared error = %v, want deadline exceeded", err)
154 }
155 shared()
156 exclusive, err := Acquire(context.Background(), path)
157 if err != nil {
158 t.Fatal(err)
159 }
160 exclusive()
161 }
162
163 func TestAcquireZeroValueRemainsExclusive(t *testing.T) {
164 path := filepath.Join(t.TempDir(), "state.lock")
165 first, err := AcquireMode(context.Background(), path, ModeExclusive)
166 if err != nil {
167 t.Fatal(err)
168 }
169 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
170 defer cancel()
171 if _, err := Acquire(ctx, path); !errors.Is(err, context.DeadlineExceeded) {
172 first()
173 t.Fatalf("zero-value exclusive error = %v", err)
174 }
175 first()
176 }
177
178 func TestLocalRegistryReclaimsReleasedEntries(t *testing.T) {
179 before := RegistrySizeForTest()
180 path := filepath.Join(t.TempDir(), "ephemeral.lock")
181 release, err := Acquire(context.Background(), path)
182 if err != nil {
183 t.Fatal(err)
184 }
185 if RegistrySizeForTest() <= before {
186 t.Fatal("registry should grow while lock is held")
187 }
188 release()
189 if got := RegistrySizeForTest(); got != before {
190 t.Fatalf("registry size after release = %d, want %d (reclaimed)", got, before)
191 }
192
193 // Re-acquire still works after reclaim.
194 release2, err := Acquire(context.Background(), path)
195 if err != nil {
196 t.Fatal(err)
197 }
198 release2()
199 if got := RegistrySizeForTest(); got != before {
200 t.Fatalf("registry size after second cycle = %d, want %d", got, before)
201 }
202 }
203
203 lines GO