1package app
2
3import (
4 "context"
5 "errors"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/bnema/gordon/internal/boundaries/out"
11)
12
13func TestGCBarrierExcludesExclusiveDuringShared(t *testing.T) {
14 barrier := newGCBarrier()
15 ctx := context.Background()
16
17 shared, err := barrier.AcquireShared(ctx)
18 if err != nil {
19 t.Fatalf("AcquireShared: %v", err)
20 }
21
22 // An exclusive lease must wait until the last shared holder is
23 // done, so a prune cannot plan against a half-acquired resource.
24 waitCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
25 defer cancel()
26 if _, err := barrier.AcquireExclusive(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
27 t.Fatalf("exclusive lease during a shared lease returned %v, want deadline exceeded", err)
28 }
29
30 exclusive, err := acquireExclusiveAfter(barrier, shared)
31 if err != nil {
32 t.Fatalf("exclusive lease after release: %v", err)
33 }
34 exclusive.Release()
35}
36
37// acquireExclusiveAfter releases shared, then takes the exclusive lease
38// that must now be grantable.
39func acquireExclusiveAfter(barrier *gcBarrier, shared out.GCLease) (out.GCLease, error) {
40 shared.Release()
41 return barrier.AcquireExclusive(context.Background())
42}
43
44func TestGCBarrierExcludesSharedDuringExclusive(t *testing.T) {
45 barrier := newGCBarrier()
46 ctx := context.Background()
47
48 exclusive, err := barrier.AcquireExclusive(ctx)
49 if err != nil {
50 t.Fatalf("AcquireExclusive: %v", err)
51 }
52 defer exclusive.Release()
53
54 waitCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
55 defer cancel()
56 if _, err := barrier.AcquireShared(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
57 t.Fatalf("shared lease during an exclusive lease returned %v, want deadline exceeded", err)
58 }
59
60 // A second exclusive lease also waits.
61 if _, err := barrier.AcquireExclusive(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
62 t.Fatalf("second exclusive lease returned %v, want deadline exceeded", err)
63 }
64
65 exclusive.Release()
66 shared, err := barrier.AcquireShared(ctx)
67 if err != nil {
68 t.Fatalf("shared lease after release: %v", err)
69 }
70 shared.Release()
71}
72
73func TestGCBarrierCancellation(t *testing.T) {
74 barrier := newGCBarrier()
75 held, err := barrier.AcquireExclusive(context.Background())
76 if err != nil {
77 t.Fatalf("AcquireExclusive: %v", err)
78 }
79 defer held.Release()
80
81 ctx, cancel := context.WithCancel(context.Background())
82 cancel()
83 if _, err := barrier.AcquireShared(ctx); !errors.Is(err, context.Canceled) {
84 t.Fatalf("canceled acquisition returned %v, want context.Canceled", err)
85 }
86
87 // A waiter canceled while blocked returns instead of wedging
88 // shutdown.
89 waitCtx, cancelWait := context.WithCancel(context.Background())
90 done := make(chan error, 1)
91 go func() {
92 _, err := barrier.AcquireShared(waitCtx)
93 done <- err
94 }()
95 cancelWait()
96 select {
97 case err := <-done:
98 if !errors.Is(err, context.Canceled) {
99 t.Fatalf("blocked waiter returned %v, want context.Canceled", err)
100 }
101 case <-time.After(2 * time.Second):
102 t.Fatal("canceled waiter did not return")
103 }
104}
105
106func TestGCBarrierDeterministicHandoff(t *testing.T) {
107 barrier := newGCBarrier()
108 ctx := context.Background()
109
110 // Simulate a workload mutation holding the lease across resource
111 // acquisition and durable publication.
112 shared, err := barrier.AcquireShared(ctx)
113 if err != nil {
114 t.Fatalf("AcquireShared: %v", err)
115 }
116
117 acquired := make(chan struct{})
118 releaseExclusive := make(chan struct{})
119 go func() {
120 exclusive, err := barrier.AcquireExclusive(ctx)
121 if err != nil {
122 close(acquired)
123 return
124 }
125 close(acquired)
126 <-releaseExclusive
127 exclusive.Release()
128 }()
129
130 // The exclusive lease must not be granted while the shared lease is
131 // held, no matter how the goroutine is scheduled.
132 select {
133 case <-acquired:
134 t.Fatal("exclusive lease granted while a shared lease was held")
135 case <-time.After(50 * time.Millisecond):
136 }
137
138 shared.Release()
139 select {
140 case <-acquired:
141 case <-time.After(2 * time.Second):
142 t.Fatal("exclusive lease was never granted after the shared lease was released")
143 }
144
145 // Once the exclusive lease is held, a new shared lease waits.
146 waitCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
147 defer cancel()
148 if _, err := barrier.AcquireShared(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
149 t.Fatalf("shared lease during handoff returned %v, want deadline exceeded", err)
150 }
151
152 close(releaseExclusive)
153 // The barrier must drain back to idle.
154 deadline := time.Now().Add(2 * time.Second)
155 for {
156 lease, err := barrier.AcquireExclusive(context.Background())
157 if err == nil {
158 lease.Release()
159 break
160 }
161 if time.Now().After(deadline) {
162 t.Fatal("barrier never returned to idle")
163 }
164 time.Sleep(time.Millisecond)
165 }
166}
167
168func TestGCBarrierConcurrentSharedHolders(t *testing.T) {
169 barrier := newGCBarrier()
170 ctx := context.Background()
171
172 const holders = 16
173 var wg sync.WaitGroup
174 start := make(chan struct{})
175 for range holders {
176 wg.Add(1)
177 go func() {
178 defer wg.Done()
179 <-start
180 lease, err := barrier.AcquireShared(ctx)
181 if err != nil {
182 t.Errorf("AcquireShared: %v", err)
183 return
184 }
185 time.Sleep(time.Millisecond)
186 lease.Release()
187 }()
188 }
189 close(start)
190
191 // While any shared holder may be active, an exclusive lease must
192 // never be granted concurrently with one.
193 exclusiveDone := make(chan struct{})
194 go func() {
195 defer close(exclusiveDone)
196 for range 200 {
197 lease, err := barrier.AcquireExclusive(ctx)
198 if err != nil {
199 t.Errorf("AcquireExclusive: %v", err)
200 return
201 }
202 barrier.mu.Lock()
203 shared := barrier.shared
204 holding := barrier.exclusive
205 barrier.mu.Unlock()
206 if !holding || shared != 0 {
207 t.Errorf("exclusive lease held with %d shared holders (exclusive=%v)", shared, holding)
208 }
209 lease.Release()
210 }
211 }()
212
213 wg.Wait()
214 <-exclusiveDone
215}
216
217func TestGCBarrierLeaseReleaseIsIdempotent(t *testing.T) {
218 barrier := newGCBarrier()
219 lease, err := barrier.AcquireShared(context.Background())
220 if err != nil {
221 t.Fatalf("AcquireShared: %v", err)
222 }
223 lease.Release()
224 lease.Release()
225
226 barrier.mu.Lock()
227 shared := barrier.shared
228 barrier.mu.Unlock()
229 if shared != 0 {
230 t.Fatalf("shared count after double release = %d, want 0", shared)
231 }
232
233 exclusive, err := barrier.AcquireExclusive(context.Background())
234 if err != nil {
235 t.Fatalf("AcquireExclusive after double release: %v", err)
236 }
237 exclusive.Release()
238}