Merge lp:~chipaca/ubuntu-push/client-session-redialer-mash into lp:ubuntu-push
- client-session-redialer-mash
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | John Lenton | ||||
Approved revision: | 56 | ||||
Merged at revision: | 54 | ||||
Proposed branch: | lp:~chipaca/ubuntu-push/client-session-redialer-mash | ||||
Merge into: | lp:ubuntu-push | ||||
Diff against target: |
709 lines (+267/-160) 10 files modified
bus/connectivity/connectivity.go (+2/-1) bus/endpoint.go (+0/-7) bus/testing/testing_endpoint.go (+0/-3) bus/testing/testing_endpoint_test.go (+0/-6) client/client.go (+21/-48) client/client_test.go (+0/-7) client/session/session.go (+34/-1) client/session/session_test.go (+76/-1) util/redialer.go (+71/-36) util/redialer_test.go (+63/-50) |
||||
To merge this branch: | bzr merge lp:~chipaca/ubuntu-push/client-session-redialer-mash | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Samuele Pedroni | Approve | ||
Review via email: mp+204813@code.launchpad.net |
Commit message
Reworked or refactored util/redialer, and a bit of client/session, and client.
Description of the change
Reworked and/or refactored util/redialer, so i could refactor session, so i could fix client. Or something like that anyway.
John Lenton (chipaca) wrote : | # |
Samuele Pedroni (pedronis) wrote : | # |
636 +func (s *RedialerSuite) TestTwoStops(c *C) {
637 + endp := testibus.
638 + countCh := make(chan uint32)
639 + ar := NewAutoRedialer
640 + go func() { countCh <- ar.Redial() }()
641 + ar.Stop()
642 + ar.Stop()
643 }
misses some Checks? otherwise it could be simply folded in the previous test
Samuele Pedroni (pedronis) wrote : | # |
260 + sess.retrier.Stop()
there is no test that fails if I comment out that line
Samuele Pedroni (pedronis) wrote : | # |
254 + sess.retrier.Stop()
should this be a Close perhaps? also no test fails if I comment that out
Samuele Pedroni (pedronis) wrote : | # |
otherwise looks very much an improvement
Samuele Pedroni (pedronis) wrote : | # |
ah, looking at make coverage-html output:
func (sess *ClientSession) AutoRedial(doneCh chan uint32) {
// sess.retrier.Stop()
go func() { doneCh <- sess.retrier.
}
seems not to be covered at all by tests in client/session
John Lenton (chipaca) wrote : | # |
TestTwoStops was just checking that the second Stop() didn't panic nor block; I've folded it into the previous test as you suggested, because there the state after the first Stop() is ensured by the select (I'd have to copy the test all over again, otherwise, really).
- 54. By John Lenton
-
made AutoRedialer an interface
- 55. By John Lenton
-
tested the closing of the retrier by session.Close and session.AutoRedial
John Lenton (chipaca) wrote : | # |
Added tests for AutoRedial, and for stopping the retrier. Coverage now at 100%. Huzzah.
- 56. By John Lenton
-
stopRedial
Preview Diff
1 | === modified file 'bus/connectivity/connectivity.go' |
2 | --- bus/connectivity/connectivity.go 2014-01-28 22:18:39 +0000 |
3 | +++ bus/connectivity/connectivity.go 2014-02-05 13:24:10 +0000 |
4 | @@ -64,7 +64,8 @@ |
5 | func (cs *connectedState) start() networkmanager.State { |
6 | var initial networkmanager.State |
7 | for { |
8 | - cs.connAttempts += util.AutoRedial(cs.endp) |
9 | + ar := util.NewAutoRedialer(cs.endp) |
10 | + cs.connAttempts += ar.Redial() |
11 | nm := networkmanager.New(cs.endp, cs.log) |
12 | |
13 | // Get the current state. |
14 | |
15 | === modified file 'bus/endpoint.go' |
16 | --- bus/endpoint.go 2014-01-27 12:41:54 +0000 |
17 | +++ bus/endpoint.go 2014-02-05 13:24:10 +0000 |
18 | @@ -22,7 +22,6 @@ |
19 | "fmt" |
20 | "launchpad.net/go-dbus/v1" |
21 | "launchpad.net/ubuntu-push/logger" |
22 | - "time" |
23 | ) |
24 | |
25 | /***************************************************************** |
26 | @@ -37,7 +36,6 @@ |
27 | Dial() error |
28 | Close() |
29 | String() string |
30 | - Jitter(time.Duration) time.Duration |
31 | } |
32 | |
33 | type endpoint struct { |
34 | @@ -137,11 +135,6 @@ |
35 | return fmt.Sprintf("<Connection to %s %#v>", endp.bus, endp.addr) |
36 | } |
37 | |
38 | -// Jitter() returns 0: no need to jitter D-Bus connections. |
39 | -func (endp *endpoint) Jitter(_ time.Duration) time.Duration { |
40 | - return 0 |
41 | -} |
42 | - |
43 | /* |
44 | private methods |
45 | */ |
46 | |
47 | === modified file 'bus/testing/testing_endpoint.go' |
48 | --- bus/testing/testing_endpoint.go 2014-02-03 20:17:14 +0000 |
49 | +++ bus/testing/testing_endpoint.go 2014-02-05 13:24:10 +0000 |
50 | @@ -135,8 +135,5 @@ |
51 | // see Endpoint's Close. This one does nothing. |
52 | func (tc *testingEndpoint) Close() {} |
53 | |
54 | -// see Endpoint's Jitter. |
55 | -func (tc *testingEndpoint) Jitter(_ time.Duration) time.Duration { return 0 } |
56 | - |
57 | // ensure testingEndpoint implements bus.Endpoint |
58 | var _ bus.Endpoint = &testingEndpoint{} |
59 | |
60 | === modified file 'bus/testing/testing_endpoint_test.go' |
61 | --- bus/testing/testing_endpoint_test.go 2014-02-03 20:17:14 +0000 |
62 | +++ bus/testing/testing_endpoint_test.go 2014-02-05 13:24:10 +0000 |
63 | @@ -171,9 +171,3 @@ |
64 | endp := NewTestingEndpoint(condition.Fail2Work(2), nil, "hello there") |
65 | c.Check(endp.String(), Matches, ".*Still Broken.*hello there.*") |
66 | } |
67 | - |
68 | -// Test testingEndpoints have no jitters |
69 | -func (s *TestingBusSuite) TestEndpointJitter(c *C) { |
70 | - endp := NewTestingEndpoint(nil, nil) |
71 | - c.Check(endp.Jitter(time.Duration(42)), Equals, time.Duration(0)) |
72 | -} |
73 | |
74 | === modified file 'client/client.go' |
75 | --- client/client.go 2014-02-04 18:19:14 +0000 |
76 | +++ client/client.go 2014-02-05 13:24:10 +0000 |
77 | @@ -51,20 +51,19 @@ |
78 | |
79 | // Client is the Ubuntu Push Notifications client-side daemon. |
80 | type Client struct { |
81 | - config ClientConfig |
82 | - log logger.Logger |
83 | - pem []byte |
84 | - idder identifier.Id |
85 | - deviceId string |
86 | - notificationsEndp bus.Endpoint |
87 | - urlDispatcherEndp bus.Endpoint |
88 | - connectivityEndp bus.Endpoint |
89 | - connCh chan bool |
90 | - hasConnectivity bool |
91 | - actionsCh <-chan notifications.RawActionReply |
92 | - session *session.ClientSession |
93 | - sessionRetrierStopper chan bool |
94 | - sessionRetryCh chan uint32 |
95 | + config ClientConfig |
96 | + log logger.Logger |
97 | + pem []byte |
98 | + idder identifier.Id |
99 | + deviceId string |
100 | + notificationsEndp bus.Endpoint |
101 | + urlDispatcherEndp bus.Endpoint |
102 | + connectivityEndp bus.Endpoint |
103 | + connCh chan bool |
104 | + hasConnectivity bool |
105 | + actionsCh <-chan notifications.RawActionReply |
106 | + session *session.ClientSession |
107 | + sessionConnectedCh chan uint32 |
108 | } |
109 | |
110 | // Configure loads the configuration specified in configPath, and sets it up. |
111 | @@ -86,8 +85,8 @@ |
112 | client.urlDispatcherEndp = bus.SessionBus.Endpoint(urldispatcher.BusAddress, client.log) |
113 | client.connectivityEndp = bus.SystemBus.Endpoint(networkmanager.BusAddress, client.log) |
114 | |
115 | - client.connCh = make(chan bool) |
116 | - client.sessionRetryCh = make(chan uint32) |
117 | + client.connCh = make(chan bool, 1) |
118 | + client.sessionConnectedCh = make(chan uint32, 1) |
119 | |
120 | if client.config.CertPEMFile != "" { |
121 | client.pem, err = ioutil.ReadFile(client.config.CertPEMFile) |
122 | @@ -119,8 +118,8 @@ |
123 | go connectivity.ConnectedState(client.connectivityEndp, |
124 | client.config.ConnectivityConfig, client.log, client.connCh) |
125 | iniCh := make(chan uint32) |
126 | - go func() { iniCh <- util.AutoRedial(client.notificationsEndp) }() |
127 | - go func() { iniCh <- util.AutoRedial(client.urlDispatcherEndp) }() |
128 | + go func() { iniCh <- util.NewAutoRedialer(client.notificationsEndp).Redial() }() |
129 | + go func() { iniCh <- util.NewAutoRedialer(client.urlDispatcherEndp).Redial() }() |
130 | <-iniCh |
131 | <-iniCh |
132 | |
133 | @@ -140,32 +139,6 @@ |
134 | return nil |
135 | } |
136 | |
137 | -// connectSession kicks off the session connection dance |
138 | -func (client *Client) connectSession() { |
139 | - // XXX: lp:1276199 |
140 | - if client.sessionRetrierStopper != nil { |
141 | - client.sessionRetrierStopper <- true |
142 | - client.sessionRetrierStopper = nil |
143 | - } |
144 | - ar := &util.AutoRetrier{ |
145 | - make(chan bool, 1), |
146 | - client.session.Dial, |
147 | - util.Jitter} |
148 | - client.sessionRetrierStopper = ar.Stop |
149 | - go func() { client.sessionRetryCh <- ar.Retry() }() |
150 | -} |
151 | - |
152 | -// disconnectSession disconnects the session |
153 | -func (client *Client) disconnectSession() { |
154 | - // XXX: lp:1276199 |
155 | - if client.sessionRetrierStopper != nil { |
156 | - client.sessionRetrierStopper <- true |
157 | - client.sessionRetrierStopper = nil |
158 | - } else { |
159 | - client.session.Close() |
160 | - } |
161 | -} |
162 | - |
163 | // handleConnState deals with connectivity events |
164 | func (client *Client) handleConnState(hasConnectivity bool) { |
165 | if client.hasConnectivity == hasConnectivity { |
166 | @@ -174,9 +147,9 @@ |
167 | } |
168 | client.hasConnectivity = hasConnectivity |
169 | if hasConnectivity { |
170 | - client.connectSession() |
171 | + client.session.AutoRedial(client.sessionConnectedCh) |
172 | } else { |
173 | - client.disconnectSession() |
174 | + client.session.Close() |
175 | } |
176 | } |
177 | |
178 | @@ -185,7 +158,7 @@ |
179 | // if we're not connected, we don't really care |
180 | client.log.Errorf("session exited: %s", err) |
181 | if client.hasConnectivity { |
182 | - client.connectSession() |
183 | + client.session.AutoRedial(client.sessionConnectedCh) |
184 | } |
185 | } |
186 | |
187 | @@ -232,7 +205,7 @@ |
188 | notifhandler() |
189 | case err := <-client.session.ErrCh: |
190 | errhandler(err) |
191 | - case count := <-client.sessionRetryCh: |
192 | + case count := <-client.sessionConnectedCh: |
193 | client.log.Debugf("Session connected after %d attempts", count) |
194 | } |
195 | } |
196 | |
197 | === modified file 'client/client_test.go' |
198 | --- client/client_test.go 2014-02-04 13:39:06 +0000 |
199 | +++ client/client_test.go 2014-02-05 13:24:10 +0000 |
200 | @@ -312,14 +312,9 @@ |
201 | cli := new(Client) |
202 | cli.log = debuglog |
203 | cli.initSession() |
204 | - // let's pretend the client had a previous attempt at connecting still pending |
205 | - // (hard to trigger in real life, but possible) |
206 | - ch := make(chan bool, 1) |
207 | - cli.sessionRetrierStopper = ch |
208 | |
209 | c.Assert(cli.hasConnectivity, Equals, false) |
210 | cli.handleConnState(true) |
211 | - c.Check(len(ch), Equals, 1) |
212 | c.Check(cli.hasConnectivity, Equals, true) |
213 | c.Assert(cli.session, NotNil) |
214 | } |
215 | @@ -355,12 +350,10 @@ |
216 | cli := new(Client) |
217 | cli.log = debuglog |
218 | cli.session, _ = session.NewSession(string(cli.config.Addr), cli.pem, cli.config.ExchangeTimeout.Duration, cli.deviceId, debuglog) |
219 | - cli.sessionRetrierStopper = make(chan bool, 1) |
220 | cli.hasConnectivity = true |
221 | |
222 | cli.handleConnState(false) |
223 | c.Check(cli.session.State(), Equals, session.Disconnected) |
224 | - c.Check(cli.sessionRetrierStopper, IsNil) |
225 | } |
226 | |
227 | /***************************************************************** |
228 | |
229 | === modified file 'client/session/session.go' |
230 | --- client/session/session.go 2014-02-04 13:10:18 +0000 |
231 | +++ client/session/session.go 2014-02-05 13:24:10 +0000 |
232 | @@ -26,6 +26,8 @@ |
233 | "launchpad.net/ubuntu-push/client/session/levelmap" |
234 | "launchpad.net/ubuntu-push/logger" |
235 | "launchpad.net/ubuntu-push/protocol" |
236 | + "launchpad.net/ubuntu-push/util" |
237 | + "math/rand" |
238 | "net" |
239 | "sync/atomic" |
240 | "time" |
241 | @@ -68,6 +70,7 @@ |
242 | TLS *tls.Config |
243 | proto protocol.Protocol |
244 | pingInterval time.Duration |
245 | + retrier util.AutoRedialer |
246 | // status |
247 | stateP *uint32 |
248 | ErrCh chan error |
249 | @@ -119,7 +122,24 @@ |
250 | return nil |
251 | } |
252 | |
253 | +func (sess *ClientSession) stopRedial() { |
254 | + if sess.retrier != nil { |
255 | + sess.retrier.Stop() |
256 | + sess.retrier = nil |
257 | + } |
258 | +} |
259 | + |
260 | +func (sess *ClientSession) AutoRedial(doneCh chan uint32) { |
261 | + sess.stopRedial() |
262 | + sess.retrier = util.NewAutoRedialer(sess) |
263 | + go func() { doneCh <- sess.retrier.Redial() }() |
264 | +} |
265 | + |
266 | func (sess *ClientSession) Close() { |
267 | + sess.stopRedial() |
268 | + sess.doClose() |
269 | +} |
270 | +func (sess *ClientSession) doClose() { |
271 | if sess.Connection != nil { |
272 | sess.Connection.Close() |
273 | // we ignore Close errors, on purpose (the thinking being that |
274 | @@ -258,6 +278,15 @@ |
275 | return err |
276 | } |
277 | |
278 | +// This Jitter returns a random time.Duration somewhere in [-spread, spread]. |
279 | +func (sess *ClientSession) Jitter(spread time.Duration) time.Duration { |
280 | + if spread < 0 { |
281 | + panic("spread must be non-negative") |
282 | + } |
283 | + n := int64(spread) |
284 | + return time.Duration(rand.Int63n(2*n+1) - n) |
285 | +} |
286 | + |
287 | // Dial takes the session from newly created (or newly disconnected) |
288 | // to running the main loop. |
289 | func (sess *ClientSession) Dial() error { |
290 | @@ -267,5 +296,9 @@ |
291 | // keep on trying. |
292 | panic("can't Dial() without a protocol constructor.") |
293 | } |
294 | - return sess.run(sess.Close, sess.connect, sess.start, sess.loop) |
295 | + return sess.run(sess.doClose, sess.connect, sess.start, sess.loop) |
296 | +} |
297 | + |
298 | +func init() { |
299 | + rand.Seed(time.Now().Unix()) // good enough for us (we're not using it for crypto) |
300 | } |
301 | |
302 | === modified file 'client/session/session_test.go' |
303 | --- client/session/session_test.go 2014-02-04 13:02:38 +0000 |
304 | +++ client/session/session_test.go 2014-02-05 13:24:10 +0000 |
305 | @@ -256,6 +256,50 @@ |
306 | c.Check(sess.State(), Equals, Disconnected) |
307 | } |
308 | |
309 | +type derp struct{ stopped bool } |
310 | + |
311 | +func (*derp) Redial() uint32 { return 0 } |
312 | +func (d *derp) Stop() { d.stopped = true } |
313 | + |
314 | +func (cs *clientSessionSuite) TestCloseStopsRetrier(c *C) { |
315 | + sess, err := NewSession("", nil, 0, "wah", debuglog) |
316 | + c.Assert(err, IsNil) |
317 | + ar := new(derp) |
318 | + sess.retrier = ar |
319 | + c.Check(ar.stopped, Equals, false) |
320 | + sess.Close() |
321 | + c.Check(ar.stopped, Equals, true) |
322 | + sess.Close() // double close check |
323 | + c.Check(ar.stopped, Equals, true) |
324 | +} |
325 | + |
326 | +/**************************************************************** |
327 | + AutoRedial() tests |
328 | +****************************************************************/ |
329 | + |
330 | +func (cs *clientSessionSuite) TestAutoRedialWorks(c *C) { |
331 | + // checks that AutoRedial sets up a retrier and tries redialing it |
332 | + sess, err := NewSession("", nil, 0, "wah", debuglog) |
333 | + c.Assert(err, IsNil) |
334 | + ar := new(derp) |
335 | + sess.retrier = ar |
336 | + c.Check(ar.stopped, Equals, false) |
337 | + sess.AutoRedial(nil) |
338 | + c.Check(ar.stopped, Equals, true) |
339 | +} |
340 | + |
341 | +func (cs *clientSessionSuite) TestAutoRedialStopsRetrier(c *C) { |
342 | + // checks that AutoRedial stops the previous retrier |
343 | + sess, err := NewSession("", nil, 0, "wah", debuglog) |
344 | + c.Assert(err, IsNil) |
345 | + ch := make(chan uint32) |
346 | + c.Check(sess.retrier, IsNil) |
347 | + sess.AutoRedial(ch) |
348 | + c.Assert(sess.retrier, NotNil) |
349 | + sess.retrier.Stop() |
350 | + c.Check(<-ch, Not(Equals), 0) |
351 | +} |
352 | + |
353 | /**************************************************************** |
354 | handlePing() tests |
355 | ****************************************************************/ |
356 | @@ -624,12 +668,43 @@ |
357 | } |
358 | |
359 | /**************************************************************** |
360 | + Jitter() tests |
361 | +****************************************************************/ |
362 | + |
363 | +func (s *clientSessionSuite) TestJitter(c *C) { |
364 | + sess, err := NewSession("", nil, 0, "wah", helpers.NewTestLogger(c, "debug")) |
365 | + c.Assert(err, IsNil) |
366 | + num_tries := 20 // should do the math |
367 | + spread := time.Second // |
368 | + has_neg := false |
369 | + has_pos := false |
370 | + has_zero := true |
371 | + for i := 0; i < num_tries; i++ { |
372 | + n := sess.Jitter(spread) |
373 | + if n > 0 { |
374 | + has_pos = true |
375 | + } else if n < 0 { |
376 | + has_neg = true |
377 | + } else { |
378 | + has_zero = true |
379 | + } |
380 | + } |
381 | + c.Check(has_neg, Equals, true) |
382 | + c.Check(has_pos, Equals, true) |
383 | + c.Check(has_zero, Equals, true) |
384 | + |
385 | + // a negative spread is caught in the reasonable place |
386 | + c.Check(func() { sess.Jitter(time.Duration(-1)) }, PanicMatches, |
387 | + "spread must be non-negative") |
388 | +} |
389 | + |
390 | +/**************************************************************** |
391 | Dial() tests |
392 | ****************************************************************/ |
393 | |
394 | func (cs *clientSessionSuite) TestDialPanics(c *C) { |
395 | // one last unhappy test |
396 | - sess, err := NewSession("", nil, 0, "wah", debuglog) |
397 | + sess, err := NewSession("", nil, 0, "wah", helpers.NewTestLogger(c, "debug")) |
398 | c.Assert(err, IsNil) |
399 | sess.Protocolator = nil |
400 | c.Check(sess.Dial, PanicMatches, ".*protocol constructor.") |
401 | |
402 | === modified file 'util/redialer.go' |
403 | --- util/redialer.go 2014-02-04 13:02:38 +0000 |
404 | +++ util/redialer.go 2014-02-05 13:24:10 +0000 |
405 | @@ -17,7 +17,6 @@ |
406 | package util |
407 | |
408 | import ( |
409 | - "math/rand" |
410 | "sync" |
411 | "time" |
412 | ) |
413 | @@ -27,7 +26,12 @@ |
414 | // fails. |
415 | type Dialer interface { |
416 | Dial() error |
417 | - String() string |
418 | +} |
419 | + |
420 | +// A Jitterer is a Dialer that wants to vary the backoff a little (to avoid a |
421 | +// thundering herd, for example). |
422 | +type Jitterer interface { |
423 | + Dialer |
424 | Jitter(time.Duration) time.Duration |
425 | } |
426 | |
427 | @@ -35,17 +39,16 @@ |
428 | var timeouts []time.Duration |
429 | var trwlock sync.RWMutex |
430 | |
431 | -var ( // for use in testing |
432 | - quitRedialing chan bool = make(chan bool) |
433 | -) |
434 | - |
435 | +// retrieve the list of timeouts used for exponential backoff |
436 | func Timeouts() []time.Duration { |
437 | trwlock.RLock() |
438 | defer trwlock.RUnlock() |
439 | return timeouts |
440 | } |
441 | |
442 | -// for testing |
443 | +// for testing: change the default timeouts for the provided ones, |
444 | +// returning the defaults (the idea being you reset them on test |
445 | +// teardown) |
446 | func SwapTimeouts(newTimeouts []time.Duration) (oldTimeouts []time.Duration) { |
447 | trwlock.Lock() |
448 | defer trwlock.Unlock() |
449 | @@ -53,32 +56,60 @@ |
450 | return |
451 | } |
452 | |
453 | -// Jitter returns a random time.Duration somewhere in [-spread, spread]. |
454 | -// |
455 | -// This is meant as a default implementation for Dialers to use if wanted. |
456 | -func Jitter(spread time.Duration) time.Duration { |
457 | - if spread < 0 { |
458 | - panic("spread must be non-negative") |
459 | +// An AutoRedialer's Redial() method retries its dialer's Dial() method until |
460 | +// it stops returning an error. It does exponential (optionally jitter'ed) |
461 | +// backoff. |
462 | +type AutoRedialer interface { |
463 | + Redial() uint32 // Redial keeps on calling Dial until it stops returning an error. |
464 | + Stop() // Stop shuts down the given AutoRedialer, if it is still retrying. |
465 | +} |
466 | + |
467 | +type autoRedialer struct { |
468 | + stop chan bool |
469 | + lock sync.RWMutex |
470 | + dial func() error |
471 | + jitter func(time.Duration) time.Duration |
472 | +} |
473 | + |
474 | +func (ar *autoRedialer) Stop() { |
475 | + if ar != nil { |
476 | + ar.lock.RLock() |
477 | + defer ar.lock.RUnlock() |
478 | + if ar.stop != nil { |
479 | + ar.stop <- true |
480 | + } |
481 | } |
482 | - n := int64(spread) |
483 | - return time.Duration(rand.Int63n(2*n+1) - n) |
484 | -} |
485 | - |
486 | -type AutoRetrier struct { |
487 | - Stop chan bool |
488 | - Dial func() error |
489 | - Jitter func(time.Duration) time.Duration |
490 | -} |
491 | - |
492 | -// AutoRetry keeps on calling Dial until it stops returning an error. It does |
493 | +} |
494 | + |
495 | +func (ar *autoRedialer) shutdown() { |
496 | + ar.lock.Lock() |
497 | + defer ar.lock.Unlock() |
498 | + close(ar.stop) |
499 | + ar.stop = nil |
500 | +} |
501 | + |
502 | +// Redial keeps on calling Dial until it stops returning an error. It does |
503 | // exponential backoff, adding the output of Jitter at each step back. |
504 | -func (ar *AutoRetrier) Retry() uint32 { |
505 | +func (ar *autoRedialer) Redial() uint32 { |
506 | + if ar == nil { |
507 | + // at least it's better than the segfault... |
508 | + panic("you can't Redial a nil AutoRedialer") |
509 | + } |
510 | + if ar.stop == nil { |
511 | + panic("this AutoRedialer has already been shut down") |
512 | + } |
513 | + defer ar.shutdown() |
514 | + |
515 | + ar.lock.RLock() |
516 | + stop := ar.stop |
517 | + ar.lock.RUnlock() |
518 | + |
519 | var timeout time.Duration |
520 | var dialAttempts uint32 = 0 // unsigned so it can wrap safely ... |
521 | timeouts := Timeouts() |
522 | var numTimeouts uint32 = uint32(len(timeouts)) |
523 | for { |
524 | - if ar.Dial() == nil { |
525 | + if ar.dial() == nil { |
526 | return dialAttempts + 1 |
527 | } |
528 | if dialAttempts < numTimeouts { |
529 | @@ -86,22 +117,27 @@ |
530 | } else { |
531 | timeout = timeouts[numTimeouts-1] |
532 | } |
533 | - timeout += ar.Jitter(timeout) |
534 | + if ar.jitter != nil { |
535 | + timeout += ar.jitter(timeout) |
536 | + } |
537 | dialAttempts++ |
538 | select { |
539 | - case <-ar.Stop: |
540 | + case <-stop: |
541 | return dialAttempts |
542 | - case <-time.NewTimer(timeout).C: |
543 | + case <-time.After(timeout): |
544 | } |
545 | } |
546 | } |
547 | |
548 | -// AutoRedialer takes a Dialer and retries its Dial() method until it |
549 | -// stops returning an error. It does exponential (optionally |
550 | -// jitter'ed) backoff. |
551 | -func AutoRedial(dialer Dialer) uint32 { |
552 | - ar := &AutoRetrier{quitRedialing, dialer.Dial, dialer.Jitter} |
553 | - return ar.Retry() |
554 | +// returns a stoppable AutoRedialer using the provided Dialer. If the Dialer |
555 | +// is also a Jitterer, the backoff will be jittered. |
556 | +func NewAutoRedialer(dialer Dialer) AutoRedialer { |
557 | + ar := &autoRedialer{stop: make(chan bool), dial: dialer.Dial} |
558 | + jitterer, ok := dialer.(Jitterer) |
559 | + if ok { |
560 | + ar.jitter = jitterer.Jitter |
561 | + } |
562 | + return ar |
563 | } |
564 | |
565 | func init() { |
566 | @@ -111,5 +147,4 @@ |
567 | timeouts[i] = time.Duration(n) * time.Second |
568 | } |
569 | SwapTimeouts(timeouts) |
570 | - rand.Seed(time.Now().Unix()) // good enough for us (not crypto, yadda) |
571 | } |
572 | |
573 | === modified file 'util/redialer_test.go' |
574 | --- util/redialer_test.go 2014-02-04 13:02:38 +0000 |
575 | +++ util/redialer_test.go 2014-02-05 13:24:10 +0000 |
576 | @@ -17,9 +17,9 @@ |
577 | package util |
578 | |
579 | import ( |
580 | - "errors" |
581 | "io/ioutil" |
582 | . "launchpad.net/gocheck" |
583 | + "launchpad.net/ubuntu-push/bus" |
584 | testibus "launchpad.net/ubuntu-push/bus/testing" |
585 | "launchpad.net/ubuntu-push/logger" |
586 | "launchpad.net/ubuntu-push/testing/condition" |
587 | @@ -46,60 +46,73 @@ |
588 | s.timeouts = nil |
589 | } |
590 | |
591 | +// Redial() tests |
592 | + |
593 | func (s *RedialerSuite) TestWorks(c *C) { |
594 | endp := testibus.NewTestingEndpoint(condition.Fail2Work(3), nil) |
595 | - // instead of bus.Dial(), we do AutoRedial(bus) |
596 | - c.Check(AutoRedial(endp), Equals, uint32(4)) |
597 | -} |
598 | - |
599 | -func (s *RedialerSuite) TestCanBeStopped(c *C) { |
600 | + ar := NewAutoRedialer(endp) |
601 | + c.Check(ar.(*autoRedialer).stop, NotNil) |
602 | + c.Check(ar.Redial(), Equals, uint32(4)) |
603 | + // and on success, the stopper goes away |
604 | + c.Check(ar.(*autoRedialer).stop, IsNil) |
605 | +} |
606 | + |
607 | +func (s *RedialerSuite) TestRetryNil(c *C) { |
608 | + var ar *autoRedialer |
609 | + c.Check(ar.Redial, Not(PanicMatches), ".* nil pointer dereference") |
610 | +} |
611 | + |
612 | +func (s *RedialerSuite) TestRetryTwice(c *C) { |
613 | + endp := testibus.NewTestingEndpoint(condition.Work(true), nil) |
614 | + ar := NewAutoRedialer(endp) |
615 | + c.Check(ar.Redial(), Equals, uint32(1)) |
616 | + c.Check(ar.Redial, PanicMatches, ".*shut.?down.*") |
617 | +} |
618 | + |
619 | +type JitteringEndpoint struct { |
620 | + bus.Endpoint |
621 | + jittered int |
622 | +} |
623 | + |
624 | +func (j *JitteringEndpoint) Jitter(time.Duration) time.Duration { |
625 | + j.jittered++ |
626 | + return 0 |
627 | +} |
628 | + |
629 | +func (s *RedialerSuite) TestJitterWorks(c *C) { |
630 | + endp := &JitteringEndpoint{ |
631 | + testibus.NewTestingEndpoint(condition.Fail2Work(3), nil), |
632 | + 0, |
633 | + } |
634 | + ar := NewAutoRedialer(endp) |
635 | + c.Check(ar.Redial(), Equals, uint32(4)) |
636 | + c.Check(endp.jittered, Equals, 3) |
637 | +} |
638 | + |
639 | +// Stop() tests |
640 | + |
641 | +func (s *RedialerSuite) TestStopWorksOnNil(c *C) { |
642 | + // as a convenience, Stop() should succeed on nil |
643 | + // (a nil retrier certainly isn't retrying!) |
644 | + var ar *autoRedialer |
645 | + c.Check(ar, IsNil) |
646 | + ar.Stop() // nothing happens |
647 | +} |
648 | + |
649 | +func (s *RedialerSuite) TestStopStops(c *C) { |
650 | endp := testibus.NewTestingEndpoint(condition.Work(false), nil) |
651 | - ch := make(chan uint32) |
652 | - go func() { ch <- AutoRedial(endp) }() |
653 | - quitRedialing <- true |
654 | + countCh := make(chan uint32) |
655 | + ar := NewAutoRedialer(endp) |
656 | + go func() { countCh <- ar.Redial() }() |
657 | + ar.Stop() |
658 | select { |
659 | - case n := <-ch: |
660 | + case n := <-countCh: |
661 | c.Check(n, Equals, uint32(1)) |
662 | - case <-time.Tick(20 * time.Millisecond): |
663 | + case <-time.After(20 * time.Millisecond): |
664 | c.Fatal("timed out waiting for redial") |
665 | } |
666 | -} |
667 | - |
668 | -func (s *RedialerSuite) TestAutoRetry(c *C) { |
669 | - cond := condition.Fail2Work(5) |
670 | - f := func() error { |
671 | - if cond.OK() { |
672 | - return nil |
673 | - } else { |
674 | - return errors.New("X") |
675 | - } |
676 | - } |
677 | - jitter := func(time.Duration) time.Duration { return 0 } |
678 | - ar := &AutoRetrier{nil, f, jitter} |
679 | - c.Check(ar.Retry(), Equals, uint32(6)) |
680 | -} |
681 | - |
682 | -func (s *RedialerSuite) TestJitter(c *C) { |
683 | - num_tries := 20 // should do the math |
684 | - spread := time.Second // |
685 | - has_neg := false |
686 | - has_pos := false |
687 | - has_zero := true |
688 | - for i := 0; i < num_tries; i++ { |
689 | - n := Jitter(spread) |
690 | - if n > 0 { |
691 | - has_pos = true |
692 | - } else if n < 0 { |
693 | - has_neg = true |
694 | - } else { |
695 | - has_zero = true |
696 | - } |
697 | - } |
698 | - c.Check(has_neg, Equals, true) |
699 | - c.Check(has_pos, Equals, true) |
700 | - c.Check(has_zero, Equals, true) |
701 | - |
702 | - // a negative spread is caught in the reasonable place |
703 | - c.Check(func() { Jitter(time.Duration(-1)) }, PanicMatches, |
704 | - "spread must be non-negative") |
705 | + // on Stop(), the stopper goes away too |
706 | + c.Check(ar.(*autoRedialer).stop, IsNil) |
707 | + // and the next Stop() doesn't panic nor block |
708 | + ar.Stop() |
709 | } |
So, in this branch: AutoRetry functions; always create an
* reworked redialer:
* split Dialer into Dialer and Jitterer; only Jitterers are
jittered. Also removed String from the Dialer interface.
* moved the jitter default implementation to be the session's;
moved the test
* got rid of the AutoRedial/
AutoRedialer (via NewAutoRedialer) and use it.
* got rid of quitRedialing, because
* gave AutoRedialers a Stop() method that would do the right
thing.
* session:
* added AutoRedial, that sets up and launches an AutoRedialer on
itself.
* Close() now stops the AutoRedialer
* client & session:
* connectSession is now session.AutoRedial; got rid of the extra
channel needed to be able to stop it. Renamed the channel used
to communicate with the autoredialer for clarity.
* disconnectSession is now session.Close()