1package out_test
2
3import (
4 "context"
5 "errors"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/bnema/gordon/internal/boundaries/out"
11 "github.com/bnema/gordon/internal/boundaries/out/mocks"
12)
13
14// The generated mocks must always satisfy their boundary ports.
15var (
16 _ out.PruneProtectionStore = (*mocks.MockPruneProtectionStore)(nil)
17 _ out.PruneRuntime = (*mocks.MockPruneRuntime)(nil)
18 _ out.GCBarrier = (*mocks.MockGCBarrier)(nil)
19)
20
21// referenceBarrier is the minimal shape any GCBarrier implementation
22// must have: shared leases exclude exclusive leases, exclusive leases
23// exclude everything, and a blocked acquisition honors context
24// cancellation instead of wedging shutdown.
25type referenceBarrier struct {
26 mu sync.Mutex
27 shared int
28 exclusive bool
29 // released is closed whenever a lease is released so waiters retry.
30 released chan struct{}
31}
32
33func newReferenceBarrier() *referenceBarrier {
34 return &referenceBarrier{released: make(chan struct{})}
35}
36
37type referenceLease struct {
38 barrier *referenceBarrier
39 exclusive bool
40 once sync.Once
41}
42
43func (l *referenceLease) Release() {
44 l.once.Do(func() {
45 l.barrier.mu.Lock()
46 if l.exclusive {
47 l.barrier.exclusive = false
48 } else {
49 l.barrier.shared--
50 }
51 close(l.barrier.released)
52 l.barrier.released = make(chan struct{})
53 l.barrier.mu.Unlock()
54 })
55}
56
57func (b *referenceBarrier) acquire(ctx context.Context, exclusive bool) (out.GCLease, error) {
58 for {
59 b.mu.Lock()
60 if b.canAcquire(exclusive) {
61 if exclusive {
62 b.exclusive = true
63 } else {
64 b.shared++
65 }
66 b.mu.Unlock()
67 return &referenceLease{barrier: b, exclusive: exclusive}, nil
68 }
69 wait := b.released
70 b.mu.Unlock()
71
72 select {
73 case <-ctx.Done():
74 return nil, ctx.Err()
75 case <-wait:
76 }
77 }
78}
79
80func (b *referenceBarrier) canAcquire(exclusive bool) bool {
81 if exclusive {
82 return !b.exclusive && b.shared == 0
83 }
84 return !b.exclusive
85}
86
87func (b *referenceBarrier) AcquireShared(ctx context.Context) (out.GCLease, error) {
88 return b.acquire(ctx, false)
89}
90
91func (b *referenceBarrier) AcquireExclusive(ctx context.Context) (out.GCLease, error) {
92 return b.acquire(ctx, true)
93}
94
95// releaseAll exercises the contract through the interface, so a change
96// to the port breaks this test rather than silently changing semantics.
97func acquireShared(t *testing.T, barrier out.GCBarrier, ctx context.Context) out.GCLease {
98 t.Helper()
99 lease, err := barrier.AcquireShared(ctx)
100 if err != nil {
101 t.Fatalf("AcquireShared: %v", err)
102 }
103 return lease
104}
105
106func acquireExclusive(t *testing.T, barrier out.GCBarrier, ctx context.Context) out.GCLease {
107 t.Helper()
108 lease, err := barrier.AcquireExclusive(ctx)
109 if err != nil {
110 t.Fatalf("AcquireExclusive: %v", err)
111 }
112 return lease
113}
114
115func TestGCBarrierContractExcludesExclusiveDuringShared(t *testing.T) {
116 barrier := newReferenceBarrier()
117 ctx := context.Background()
118
119 shared := acquireShared(t, barrier, ctx)
120 defer shared.Release()
121
122 waitCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
123 defer cancel()
124 if _, err := barrier.AcquireExclusive(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
125 t.Fatalf("exclusive acquisition during a shared lease returned %v, want deadline exceeded", err)
126 }
127
128 shared.Release()
129 exclusive := acquireExclusive(t, barrier, ctx)
130 exclusive.Release()
131
132 // Release is idempotent: a double release must not corrupt counts.
133 shared.Release()
134 again := acquireShared(t, barrier, ctx)
135 again.Release()
136}
137
138func TestGCBarrierContractExcludesSharedDuringExclusive(t *testing.T) {
139 barrier := newReferenceBarrier()
140 ctx := context.Background()
141
142 exclusive := acquireExclusive(t, barrier, ctx)
143 defer exclusive.Release()
144
145 waitCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
146 defer cancel()
147 if _, err := barrier.AcquireShared(waitCtx); !errors.Is(err, context.DeadlineExceeded) {
148 t.Fatalf("shared acquisition during an exclusive lease returned %v, want deadline exceeded", err)
149 }
150
151 exclusive.Release()
152 shared := acquireShared(t, barrier, ctx)
153 shared.Release()
154}
155
156func TestGCBarrierContractPreCanceledContext(t *testing.T) {
157 barrier := newReferenceBarrier()
158 held := acquireExclusive(t, barrier, context.Background())
159 defer held.Release()
160
161 ctx, cancel := context.WithCancel(context.Background())
162 cancel()
163 if _, err := barrier.AcquireShared(ctx); !errors.Is(err, context.Canceled) {
164 t.Fatalf("canceled acquisition returned %v, want context.Canceled", err)
165 }
166}