@@ -2,6 +2,7 @@ package notifications
2
2
3
3
import (
4
4
"context"
5
+ "errors"
5
6
"io"
6
7
"sync"
7
8
"testing"
23
24
type mockNotificationsClient struct {
24
25
mockStream swapserverrpc.SwapServer_SubscribeNotificationsClient
25
26
subscribeErr error
27
+ attemptTimes []time.Time
26
28
timesCalled int
27
29
sync.Mutex
28
30
}
@@ -36,6 +38,7 @@ func (m *mockNotificationsClient) SubscribeNotifications(ctx context.Context,
36
38
defer m .Unlock ()
37
39
38
40
m .timesCalled ++
41
+ m .attemptTimes = append (m .attemptTimes , time .Now ())
39
42
if m .subscribeErr != nil {
40
43
return nil , m .subscribeErr
41
44
}
@@ -87,7 +90,11 @@ func (m *mockSubscribeNotificationsClient) RecvMsg(interface{}) error {
87
90
return nil
88
91
}
89
92
93
+ // TestManager_ReservationNotification tests that the Manager correctly
94
+ // forwards reservation notifications to subscribers.
90
95
func TestManager_ReservationNotification (t * testing.T ) {
96
+ t .Parallel ()
97
+
91
98
// Create a mock notification client
92
99
recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse , 1 )
93
100
errChan := make (chan error , 1 )
@@ -172,3 +179,169 @@ func getTestNotification(resId []byte) *swapserverrpc.SubscribeNotificationsResp
172
179
},
173
180
}
174
181
}
182
+
183
+ // TestManager_Backoff verifies that repeated failures in
184
+ // subscribeNotifications cause the Manager to space out subscription attempts
185
+ // via a predictable incremental backoff.
186
+ func TestManager_Backoff (t * testing.T ) {
187
+ t .Parallel ()
188
+
189
+ // We'll tolerate a bit of jitter in the timing checks.
190
+ const tolerance = 300 * time .Millisecond
191
+
192
+ recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse )
193
+ recvErrChan := make (chan error )
194
+
195
+ mockStream := & mockSubscribeNotificationsClient {
196
+ recvChan : recvChan ,
197
+ recvErrChan : recvErrChan ,
198
+ }
199
+
200
+ // Create a new mock client that will fail to subscribe.
201
+ mockClient := & mockNotificationsClient {
202
+ mockStream : mockStream ,
203
+ subscribeErr : errors .New ("failing on purpose" ),
204
+ }
205
+
206
+ // Manager with a successful CurrentToken so that it always tries
207
+ // to subscribe.
208
+ mgr := NewManager (& Config {
209
+ Client : mockClient ,
210
+ CurrentToken : func () (* l402.Token , error ) {
211
+ return & l402.Token {}, nil
212
+ },
213
+ })
214
+
215
+ // Run the manager in a background goroutine.
216
+ ctx , cancel := context .WithCancel (context .Background ())
217
+ defer cancel ()
218
+
219
+ var wg sync.WaitGroup
220
+ wg .Add (1 )
221
+ go func () {
222
+ defer wg .Done ()
223
+ // We ignore the returned error because the Manager returns
224
+ // nil on context cancel.
225
+ _ = mgr .Run (ctx )
226
+ }()
227
+
228
+ // Wait long enough to see at least 3 subscription attempts using
229
+ // the Manager's default pattern.
230
+ // We'll wait ~5 seconds total so we capture at least 3 attempts:
231
+ // - Attempt #1: immediate
232
+ // - Attempt #2: ~1 second
233
+ // - Attempt #3: ~3 seconds after that etc.
234
+ time .Sleep (5 * time .Second )
235
+
236
+ // Cancel the contedt to stop the manager.
237
+ cancel ()
238
+ wg .Wait ()
239
+
240
+ // Check how many attempts we made.
241
+ require .GreaterOrEqual (t , len (mockClient .attemptTimes ), 3 ,
242
+ "expected at least 3 attempts within 5 seconds" ,
243
+ )
244
+
245
+ expectedDelay := time .Second
246
+ for i := 1 ; i < len (mockClient .attemptTimes ); i ++ {
247
+ // The expected delay for the i-th gap (comparing attempt i to
248
+ // attempt i-1) is i seconds (because the manager increments
249
+ // the backoff by 1 second each time).
250
+ actualDelay := mockClient .attemptTimes [i ].Sub (
251
+ mockClient .attemptTimes [i - 1 ],
252
+ )
253
+
254
+ require .InDeltaf (
255
+ t , expectedDelay , actualDelay , float64 (tolerance ),
256
+ "Attempt %d -> Attempt %d delay should be ~%v, got %v" ,
257
+ i , i + 1 , expectedDelay , actualDelay ,
258
+ )
259
+
260
+ expectedDelay += time .Second
261
+ }
262
+ }
263
+
264
+ // TestManager_MinAliveConnTime verifies that the Manager enforces the minimum
265
+ // alive connection time before considering a subscription successful.
266
+ func TestManager_MinAliveConnTime (t * testing.T ) {
267
+ t .Parallel ()
268
+
269
+ // Tolerance to allow for scheduling jitter.
270
+ const tolerance = 300 * time .Millisecond
271
+
272
+ // Set a small MinAliveConnTime so the test doesn't run too long.
273
+ // Once a subscription stays alive longer than 2s, the manager resets
274
+ // its backoff to 10s on the next loop iteration.
275
+ const minAlive = 1 * time .Second
276
+
277
+ // We'll provide a channel for incoming notifications
278
+ // and another for forcing errors to close the subscription.
279
+ recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse )
280
+ recvErrChan := make (chan error )
281
+
282
+ mockStream := & mockSubscribeNotificationsClient {
283
+ recvChan : recvChan ,
284
+ recvErrChan : recvErrChan ,
285
+ }
286
+
287
+ // No immediate error from SubscribeNotifications, so it "succeeds".
288
+ // We trigger subscription closure by sending an error to recvErrChan.
289
+ mockClient := & mockNotificationsClient {
290
+ mockStream : mockStream ,
291
+ // subscribeErr stays nil => success on each call.
292
+ }
293
+
294
+ // Create a Manager that uses our mock client and enforces
295
+ // MinAliveConnTime=2s.
296
+ mgr := NewManager (& Config {
297
+ Client : mockClient ,
298
+ MinAliveConnTime : minAlive ,
299
+ CurrentToken : func () (* l402.Token , error ) {
300
+ return & l402.Token {}, nil
301
+ },
302
+ })
303
+
304
+ ctx , cancel := context .WithCancel (context .Background ())
305
+ var wg sync.WaitGroup
306
+ wg .Add (1 )
307
+ go func () {
308
+ defer wg .Done ()
309
+ _ = mgr .Run (ctx )
310
+ }()
311
+
312
+ // Let the subscription stay alive for 2s, which is >1s (minAlive).
313
+ // Then force an error to end the subscription. The manager sees
314
+ // it stayed connected ~2s and resets its backoff to 10s.
315
+ go func () {
316
+ time .Sleep (2 * time .Second )
317
+ recvErrChan <- errors .New ("mock subscription closed" )
318
+ }()
319
+
320
+ // Wait enough time (~13s) to see:
321
+ // - First subscription (2s)
322
+ // - Manager resets to 10s
323
+ // - Second subscription attempt starts ~10s later.
324
+ time .Sleep (13 * time .Second )
325
+
326
+ // Signal EOF so the subscription stops.
327
+ close (recvChan )
328
+
329
+ // Stop the manager and wait for cleanup.
330
+ cancel ()
331
+ wg .Wait ()
332
+
333
+ // Expect at least 2 attempts in attemptTimes:
334
+ // 1) The one that stayed alive for 2s,
335
+ // 2) The next attempt ~10s after that.
336
+ require .GreaterOrEqual (
337
+ t , len (mockClient .attemptTimes ), 2 ,
338
+ "expected at least 2 attempts with a successful subscription" ,
339
+ )
340
+
341
+ require .InDeltaf (
342
+ t , 12 * time .Second ,
343
+ mockClient .attemptTimes [1 ].Sub (mockClient .attemptTimes [0 ]),
344
+ float64 (tolerance ),
345
+ "Second attempt should occur ~2s after the first" ,
346
+ )
347
+ }
0 commit comments