返回 DeepSeek-Reasonix
reconnect_test.go
根目录 / internal / remote / reconnect_test.go
1 package remote
2
3 import (
4 "context"
5 "math/rand"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/remote/sshtest"
11 )
12
13 func deterministicRand() *rand.Rand { return rand.New(rand.NewSource(1)) }
14
15 // fakeClock is a controllable Clock. After() channels fire when advance() moves
16 // past their deadline.
17 type fakeClock struct {
18 mu sync.Mutex
19 now time.Time
20 waiters []fakeWaiter
21 }
22
23 type fakeWaiter struct {
24 at time.Time
25 ch chan time.Time
26 }
27
28 func newFakeClock() *fakeClock {
29 return &fakeClock{now: time.Unix(1_700_000_000, 0)}
30 }
31
32 func (c *fakeClock) Now() time.Time {
33 c.mu.Lock()
34 defer c.mu.Unlock()
35 return c.now
36 }
37
38 func (c *fakeClock) After(d time.Duration) <-chan time.Time {
39 c.mu.Lock()
40 defer c.mu.Unlock()
41 ch := make(chan time.Time, 1)
42 if d <= 0 {
43 ch <- c.now
44 return ch
45 }
46 c.waiters = append(c.waiters, fakeWaiter{at: c.now.Add(d), ch: ch})
47 return ch
48 }
49
50 // advance moves time forward, firing any waiters whose deadline is reached.
51 func (c *fakeClock) advance(d time.Duration) {
52 c.mu.Lock()
53 c.now = c.now.Add(d)
54 now := c.now
55 var remaining []fakeWaiter
56 var fire []chan time.Time
57 for _, w := range c.waiters {
58 if !w.at.After(now) {
59 fire = append(fire, w.ch)
60 } else {
61 remaining = append(remaining, w)
62 }
63 }
64 c.waiters = remaining
65 c.mu.Unlock()
66 for _, ch := range fire {
67 ch <- now
68 }
69 }
70
71 func (c *fakeClock) pendingWaiters() int {
72 c.mu.Lock()
73 defer c.mu.Unlock()
74 return len(c.waiters)
75 }
76
77 // TestReconnectAfterConnectionDrop verifies the supervisor detects a dropped
78 // connection and reconnects, emitting Connecting -> Connected -> Reconnecting
79 // -> Connected.
80 func TestReconnectAfterConnectionDrop(t *testing.T) {
81 srv := sshtest.Start(t, sshtest.Options{Password: "x"})
82
83 var mu sync.Mutex
84 var states []Status
85 host, _ := ResolveHost(nil, "test@"+srv.Addr, nil)
86 c, err := New(Options{
87 Host: host,
88 HostKeys: managedOnlyPolicy(t, true),
89 Auth: AuthOptions{DisableAgent: true, Password: func() (string, error) { return "x", nil }},
90 // Real clock here: we rely on the actual keepalive to notice the drop
91 // quickly, so keep intervals short.
92 Keepalive: KeepalivePolicy{Interval: 50 * time.Millisecond, MaxMisses: 1, Timeout: 200 * time.Millisecond},
93 Backoff: BackoffPolicy{Initial: 10 * time.Millisecond, Max: 50 * time.Millisecond},
94 })
95 if err != nil {
96 t.Fatal(err)
97 }
98 reconnected := make(chan struct{}, 1)
99 connectedCount := 0
100 c.Subscribe(func(ev StatusEvent) {
101 mu.Lock()
102 states = append(states, ev.Status)
103 if ev.Status == StatusConnected {
104 connectedCount++
105 if connectedCount == 2 {
106 select {
107 case reconnected <- struct{}{}:
108 default:
109 }
110 }
111 }
112 mu.Unlock()
113 })
114
115 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
116 defer cancel()
117 if err := c.Start(ctx); err != nil {
118 t.Fatalf("Start: %v", err)
119 }
120 defer c.Close()
121
122 // Drop every server-side connection to force a reconnect.
123 srv.DropConnections()
124
125 select {
126 case <-reconnected:
127 case <-time.After(10 * time.Second):
128 t.Fatalf("never reconnected; states=%v", snapshot(&mu, &states))
129 }
130
131 got := snapshot(&mu, &states)
132 if !containsStatus(got, StatusReconnecting) {
133 t.Fatalf("no Reconnecting status observed: %v", got)
134 }
135 }
136
137 // TestBackoffUsesClock drives the backoff purely through the fake clock: a
138 // failed first reconnect must wait on clock.After before retrying.
139 func TestBackoffSleepHonorsContextCancel(t *testing.T) {
140 clock := newFakeClock()
141 c := &Client{
142 opts: Options{Backoff: BackoffPolicy{Initial: time.Second, Max: 10 * time.Second}},
143 clock: clock,
144 rng: deterministicRand(),
145 }
146 ctx, cancel := context.WithCancel(context.Background())
147 done := make(chan bool, 1)
148 go func() { done <- c.sleepBackoff(ctx, 1) }()
149
150 // Wait until the sleeper registers its waiter, then cancel.
151 waitForWaiters(t, clock, 1)
152 cancel()
153 select {
154 case ok := <-done:
155 if ok {
156 t.Fatal("sleepBackoff returned true after ctx cancel")
157 }
158 case <-time.After(2 * time.Second):
159 t.Fatal("sleepBackoff did not return after ctx cancel")
160 }
161 }
162
163 func TestBackoffSleepFiresOnClock(t *testing.T) {
164 clock := newFakeClock()
165 c := &Client{
166 opts: Options{Backoff: BackoffPolicy{Initial: time.Second, Max: 10 * time.Second}},
167 clock: clock,
168 rng: deterministicRand(),
169 }
170 done := make(chan bool, 1)
171 go func() { done <- c.sleepBackoff(context.Background(), 1) }()
172 waitForWaiters(t, clock, 1)
173 clock.advance(2 * time.Second) // past any ceiling in [0, 1s]
174 select {
175 case ok := <-done:
176 if !ok {
177 t.Fatal("sleepBackoff returned false without cancel")
178 }
179 case <-time.After(2 * time.Second):
180 t.Fatal("sleepBackoff never fired on clock advance")
181 }
182 }
183
184 func snapshot(mu *sync.Mutex, s *[]Status) []Status {
185 mu.Lock()
186 defer mu.Unlock()
187 out := make([]Status, len(*s))
188 copy(out, *s)
189 return out
190 }
191
192 func containsStatus(states []Status, want Status) bool {
193 for _, s := range states {
194 if s == want {
195 return true
196 }
197 }
198 return false
199 }
200
201 func waitForWaiters(t *testing.T, c *fakeClock, n int) {
202 t.Helper()
203 for i := 0; i < 200; i++ {
204 if c.pendingWaiters() >= n {
205 return
206 }
207 time.Sleep(5 * time.Millisecond)
208 }
209 t.Fatalf("clock never registered %d waiter(s)", n)
210 }
211
211 lines GO