返回 DeepSeek-Reasonix
background_grace_test.go
根目录 / internal / workspacelease / background_grace_test.go
1 package workspacelease
2
3 import (
4 "context"
5 "testing"
6 "time"
7 )
8
9 func newOwnerWithGrace(t *testing.T, root, lockDir string, grace time.Duration) *Owner {
10 t.Helper()
11 owner, err := New(root, lockDir, nil)
12 if err != nil {
13 t.Fatalf("New: %v", err)
14 }
15 owner.graceAfter = grace
16 return owner
17 }
18
19 func waitForRelease(t *testing.T, owner *Owner) {
20 t.Helper()
21 deadline := time.Now().Add(2 * time.Second)
22 for time.Now().Before(deadline) {
23 if !owner.State().Acquired {
24 return
25 }
26 time.Sleep(time.Millisecond)
27 }
28 t.Fatal("workspace lease was never released")
29 }
30
31 // A resident background job (dev server, watcher) whose channel never closes
32 // must not own the workspace indefinitely once the session is idle.
33 func TestResidentBackgroundJobReleasesLeaseAfterGrace(t *testing.T) {
34 root, lockDir := t.TempDir(), t.TempDir()
35 owner := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond)
36
37 owner.BeginRun()
38 if err := owner.AcquireWrite(context.Background()); err != nil {
39 t.Fatalf("AcquireWrite: %v", err)
40 }
41 resident := make(chan struct{})
42 owner.RetainUntil(resident)
43 owner.EndRun()
44
45 waitForRelease(t, owner)
46
47 other, err := New(root, lockDir, nil)
48 if err != nil {
49 t.Fatalf("New other: %v", err)
50 }
51 other.BeginRun()
52 defer other.EndRun()
53 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
54 defer cancel()
55 if err := other.AcquireWrite(ctx); err != nil {
56 t.Fatalf("second session could not acquire the freed lease: %v", err)
57 }
58
59 // The job ending after the grace release must not release a second time.
60 close(resident)
61 time.Sleep(20 * time.Millisecond)
62 if !other.State().Acquired {
63 t.Fatal("the retained job released a lease it no longer owned")
64 }
65 }
66
67 // The grace release must never fire underneath a run that has already started,
68 // which is the path that would let two sessions write concurrently.
69 func TestRunCancelsPendingGraceRelease(t *testing.T) {
70 owner := newOwnerWithGrace(t, t.TempDir(), t.TempDir(), 20*time.Millisecond)
71
72 owner.BeginRun()
73 if err := owner.AcquireWrite(context.Background()); err != nil {
74 t.Fatalf("AcquireWrite: %v", err)
75 }
76 resident := make(chan struct{})
77 defer close(resident)
78 owner.RetainUntil(resident)
79 owner.EndRun()
80 owner.BeginRun()
81 defer owner.EndRun()
82
83 time.Sleep(200 * time.Millisecond)
84 if !owner.State().Acquired {
85 t.Fatal("grace release fired while a run was active")
86 }
87 }
88
89 // A background job that finishes on its own still releases immediately; the
90 // grace window is a ceiling, not an added delay.
91 func TestFinishedBackgroundJobReleasesWithoutWaitingForGrace(t *testing.T) {
92 owner := newOwnerWithGrace(t, t.TempDir(), t.TempDir(), 30*time.Second)
93
94 owner.BeginRun()
95 if err := owner.AcquireWrite(context.Background()); err != nil {
96 t.Fatalf("AcquireWrite: %v", err)
97 }
98 job := make(chan struct{})
99 owner.RetainUntil(job)
100 owner.EndRun()
101
102 if !owner.State().Acquired {
103 t.Fatal("lease released while the background job was still running")
104 }
105 close(job)
106 waitForRelease(t, owner)
107 }
108
109 // The grace release must leave the owner able to reacquire, so two sessions
110 // sharing a workspace alternate instead of deadlocking each other.
111 func TestGraceReleaseAllowsReacquireAndAlternation(t *testing.T) {
112 root, lockDir := t.TempDir(), t.TempDir()
113 a := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond)
114 b := newOwnerWithGrace(t, root, lockDir, 20*time.Millisecond)
115
116 a.BeginRun()
117 if err := a.AcquireWrite(context.Background()); err != nil {
118 t.Fatalf("a acquire: %v", err)
119 }
120 resident := make(chan struct{})
121 defer close(resident)
122 a.RetainUntil(resident)
123 a.EndRun()
124 waitForRelease(t, a)
125
126 // b takes the workspace while a's resident job is still running.
127 b.BeginRun()
128 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
129 defer cancel()
130 if err := b.AcquireWrite(ctx); err != nil {
131 t.Fatalf("b acquire after grace: %v", err)
132 }
133 b.EndRun()
134 waitForRelease(t, b)
135
136 a.BeginRun()
137 defer a.EndRun()
138 reacquire, cancelReacquire := context.WithTimeout(context.Background(), time.Second)
139 defer cancelReacquire()
140 if err := a.AcquireWrite(reacquire); err != nil {
141 t.Fatalf("a could not reacquire after releasing: %v", err)
142 }
143 if st := a.State(); !st.Acquired {
144 t.Fatalf("a state after reacquire: %+v", st)
145 }
146 }
147
147 lines GO