Merge lp:~pedronis/ubuntu-push/takethebus-and-fixes into lp:ubuntu-push/automatic

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 368
Merged at revision: 365
Proposed branch: lp:~pedronis/ubuntu-push/takethebus-and-fixes
Merge into: lp:ubuntu-push/automatic
Diff against target: 538 lines (+183/-85)
7 files modified
bus/connectivity/connectivity.go (+12/-7)
bus/connectivity/connectivity_test.go (+93/-32)
bus/notifications/raw_test.go (+7/-4)
bus/testing/testing_endpoint.go (+42/-30)
bus/testing/testing_endpoint_test.go (+10/-9)
client/client_test.go (+11/-3)
client/service/postal_test.go (+8/-0)
To merge this branch: bzr merge lp:~pedronis/ubuntu-push/takethebus-and-fixes
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Review via email: mp+250900@code.launchpad.net

This proposal supersedes a proposal from 2015-02-24.

Commit message

TestTakeTheBusWorks doesn't block anymore, fixed leaking of webcheckers

Description of the change

TestTakeTheBusWorks doesn't block anymore, fixed leaking of webcheckers,

to get there support using channels to control WatchSignal in tests,

don't reuse testing Endpoint retvals,

sadly bit of whack a mole:

144 + // XXX this may be stolen by an old watch => dead lock

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) : Posted in a previous version of this proposal
review: Approve
Revision history for this message
John Lenton (chipaca) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bus/connectivity/connectivity.go'
2--- bus/connectivity/connectivity.go 2015-01-22 11:05:37 +0000
3+++ bus/connectivity/connectivity.go 2015-02-25 11:05:41 +0000
4@@ -87,15 +87,15 @@
5 }
6 cs.log.Debugf("got initial state of %s", initial)
7
8+ conCh, err = nm.WatchPrimaryConnection()
9+ if err != nil {
10+ cs.log.Debugf("failed to set up the connection watch: %s", err)
11+ goto Continue
12+ }
13+
14 primary = nm.GetPrimaryConnection()
15 cs.log.Debugf("primary connection starts as %#v", primary)
16
17- conCh, err = nm.WatchPrimaryConnection()
18- if err != nil {
19- cs.log.Debugf("failed to set up the connection watch: %s", err)
20- goto Continue
21- }
22-
23 cs.networkStateCh = stateCh
24 cs.networkConCh = conCh
25
26@@ -155,7 +155,12 @@
27 case <-cs.timer.C:
28 if cs.currentState == networkmanager.ConnectedGlobal {
29 log.Debugf("connectivity: timer signal, state: ConnectedGlobal, checking...")
30- cs.webgetCh = make(chan bool)
31+ // use a buffered channel, otherwise
32+ // we may leak webcheckers that cannot
33+ // send their result because we have
34+ // cleared webgetCh and wont receive
35+ // on it
36+ cs.webgetCh = make(chan bool, 1)
37 go cs.webget(cs.webgetCh)
38 }
39
40
41=== modified file 'bus/connectivity/connectivity_test.go'
42--- bus/connectivity/connectivity_test.go 2015-01-21 17:55:35 +0000
43+++ bus/connectivity/connectivity_test.go 2015-02-25 11:05:41 +0000
44@@ -58,32 +58,52 @@
45 s.log = helpers.NewTestLogger(c, "debug")
46 }
47
48+var (
49+ helloCon = dbus.ObjectPath("hello")
50+ helloConProps = map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{helloCon}}
51+)
52+
53 /*
54 tests for connectedState's Start() method
55 */
56
57 // when given a working config and bus, Start() will work
58 func (s *ConnSuite) TestStartWorks(c *C) {
59- endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.Connecting))
60+ endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true), uint32(networkmanager.Connecting), helloCon)
61 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: endp}
62
63+ nopTicker := make(chan []interface{})
64+ testingbus.SetWatchSource(endp, "StateChanged", nopTicker)
65+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
66+ defer close(nopTicker)
67+
68 c.Check(cs.start(), Equals, networkmanager.Connecting)
69 }
70
71 // if the bus fails a couple of times, we're still OK
72 func (s *ConnSuite) TestStartRetriesConnect(c *C) {
73- endp := testingbus.NewTestingEndpoint(condition.Fail2Work(2), condition.Work(true), uint32(networkmanager.Connecting))
74+ endp := testingbus.NewTestingEndpoint(condition.Fail2Work(2), condition.Work(true), uint32(networkmanager.Connecting), helloCon)
75 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: endp}
76
77+ nopTicker := make(chan []interface{})
78+ testingbus.SetWatchSource(endp, "StateChanged", nopTicker)
79+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
80+ defer close(nopTicker)
81+
82 c.Check(cs.start(), Equals, networkmanager.Connecting)
83 c.Check(cs.connAttempts, Equals, uint32(3)) // 1 more than the Fail2Work
84 }
85
86 // when the calls to NetworkManager fails for a bit, we're still OK
87 func (s *ConnSuite) TestStartRetriesCall(c *C) {
88- endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Fail2Work(5), uint32(networkmanager.Connecting))
89+ endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Fail2Work(5), uint32(networkmanager.Connecting), helloCon)
90 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: endp}
91
92+ nopTicker := make(chan []interface{})
93+ testingbus.SetWatchSource(endp, "StateChanged", nopTicker)
94+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
95+ defer close(nopTicker)
96+
97 c.Check(cs.start(), Equals, networkmanager.Connecting)
98
99 c.Check(cs.connAttempts, Equals, uint32(6))
100@@ -94,9 +114,17 @@
101 cond := condition.Chain(3, condition.Work(true), 1, condition.Work(false),
102 1, condition.Work(true))
103
104- endp := testingbus.NewTestingEndpoint(condition.Work(true), cond, uint32(networkmanager.Connecting))
105+ endp := testingbus.NewTestingEndpoint(condition.Work(true), cond,
106+ uint32(networkmanager.Connecting), helloCon,
107+ uint32(networkmanager.Connecting), helloCon,
108+ )
109 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: endp}
110
111+ nopTicker := make(chan []interface{})
112+ testingbus.SetWatchSource(endp, "StateChanged", nopTicker)
113+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
114+ defer close(nopTicker)
115+
116 c.Check(cs.start(), Equals, networkmanager.Connecting)
117 }
118
119@@ -105,17 +133,26 @@
120 // watch, we recover and try again.
121 func (s *ConnSuite) TestStartRetriesWatch(c *C) {
122 nmcond := condition.Chain(
123- 1, condition.Work(true), // 1 call to nm works
124+ 2, condition.Work(true), // 2 call to nm works
125 1, condition.Work(false), // 1 call to nm fails
126 0, condition.Work(true)) // and everything works from there on
127 endp := testingbus.NewTestingEndpoint(condition.Work(true), nmcond,
128 uint32(networkmanager.Connecting),
129- uint32(networkmanager.ConnectedGlobal))
130+ uint32(networkmanager.Connecting),
131+ helloCon,
132+ )
133 cs := connectedState{config: ConnectivityConfig{}, log: s.log, endp: endp}
134+ watchTicker := make(chan []interface{}, 1)
135+ nopTicker := make(chan []interface{})
136+ testingbus.SetWatchSource(endp, "StateChanged", watchTicker)
137+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
138+ defer close(nopTicker)
139+ defer close(watchTicker)
140
141 c.Check(cs.start(), Equals, networkmanager.Connecting)
142 c.Check(cs.connAttempts, Equals, uint32(2))
143- c.Check(<-cs.networkStateCh, Equals, networkmanager.Connecting)
144+ // XXX this may be stolen by an old watch => dead lock
145+ watchTicker <- []interface{}{uint32(networkmanager.ConnectedGlobal)}
146 c.Check(<-cs.networkStateCh, Equals, networkmanager.ConnectedGlobal)
147 }
148
149@@ -285,12 +322,16 @@
150 }
151
152 endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true),
153- uint32(networkmanager.ConnectedGlobal),
154- uint32(networkmanager.Disconnected),
155+ uint32(networkmanager.Disconnected),
156+ helloCon,
157+ uint32(networkmanager.Disconnected),
158+ helloCon,
159 )
160
161- watchTicker := make(chan bool)
162- testingbus.SetWatchTicker(endp, watchTicker)
163+ watchTicker := make(chan []interface{})
164+ testingbus.SetWatchSource(endp, "StateChanged", watchTicker)
165+ nopTicker := make(chan []interface{})
166+ testingbus.SetWatchSource(endp, "PropertiesChanged", nopTicker)
167
168 out := make(chan bool)
169 dt := time.Second / 10
170@@ -298,19 +339,31 @@
171 go ConnectedState(endp, cfg, s.log, out)
172 var v bool
173 expecteds := []struct {
174- p bool
175- s string
176- n int
177+ p bool
178+ s string
179+ todo string
180 }{
181- {false, "first state is always false", 0},
182- {true, "then it should be true as per ConnectedGlobal above", 0},
183- {false, "then it should be false (Disconnected)", 2},
184- {false, "then it should be false again because it's restarted", 2},
185+ {false, "first state is always false", ""},
186+ {true, "then it should be true as per ConnectedGlobal above", "ConnectedGlobal"},
187+ {false, "then it should be false (Disconnected)", "Disconnected"},
188+ {false, "then it should be false again because it's restarted", "close"},
189 }
190
191+ defer func() {
192+ if watchTicker != nil {
193+ close(watchTicker)
194+ }
195+ }()
196+ defer close(nopTicker)
197 for i, expected := range expecteds {
198- for j := 0; j < expected.n; j++ {
199- watchTicker <- true
200+ switch expected.todo {
201+ case "ConnectedGlobal":
202+ watchTicker <- []interface{}{uint32(networkmanager.ConnectedGlobal)}
203+ case "Disconnected":
204+ watchTicker <- []interface{}{uint32(networkmanager.Disconnected)}
205+ case "close":
206+ close(watchTicker)
207+ watchTicker = nil
208 }
209 timer.Reset(dt)
210 select {
211@@ -335,11 +388,13 @@
212
213 endp := testingbus.NewTestingEndpoint(condition.Work(true), condition.Work(true),
214 uint32(networkmanager.ConnectedGlobal),
215- map[string]dbus.Variant{"PrimaryConnection": dbus.Variant{dbus.ObjectPath("hello")}},
216+ helloCon,
217 )
218
219- watchTicker := make(chan bool)
220- testingbus.SetWatchTicker(endp, watchTicker)
221+ watchTicker := make(chan []interface{})
222+ testingbus.SetWatchSource(endp, "PropertiesChanged", watchTicker)
223+ nopTicker := make(chan []interface{})
224+ testingbus.SetWatchSource(endp, "StateChanged", nopTicker)
225
226 out := make(chan bool)
227 dt := time.Second / 10
228@@ -347,19 +402,25 @@
229 go ConnectedState(endp, cfg, s.log, out)
230 var v bool
231 expecteds := []struct {
232- p bool
233- s string
234- n int
235+ p bool
236+ s string
237+ changedConn bool
238 }{
239- {false, "first state is always false", 0},
240- {true, "then it should be true as per ConnectedGlobal above", 0},
241- {false, "then, false (PrimaryConnection changed)", 2},
242- {true, "then it should be true (webcheck passed)", 0},
243+ {false, "first state is always false", false},
244+ {true, "then it should be true as per ConnectedGlobal above", false},
245+ {false, "then, false (PrimaryConnection changed)", true},
246+ {true, "then it should be true (webcheck passed)", false},
247 }
248
249+ defer func() {
250+ if watchTicker != nil {
251+ close(watchTicker)
252+ }
253+ }()
254+ defer close(nopTicker)
255 for i, expected := range expecteds {
256- for j := 0; j < expected.n; j++ {
257- watchTicker <- true
258+ if expected.changedConn {
259+ watchTicker <- []interface{}{helloConProps}
260 }
261 timer.Reset(dt)
262 select {
263
264=== modified file 'bus/notifications/raw_test.go'
265--- bus/notifications/raw_test.go 2014-08-15 10:33:04 +0000
266+++ bus/notifications/raw_test.go 2015-02-25 11:05:41 +0000
267@@ -111,14 +111,16 @@
268 errstr string
269 endp bus.Endpoint
270 works bool
271+ src chan []interface{}
272 }
273
274 func (s *RawSuite) TestWatchActionsToleratesDBusWeirdness(c *C) {
275 X := func(errstr string, args ...interface{}) tst {
276- endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true), args)
277- // stop the endpoint from closing the channel:
278- testibus.SetWatchTicker(endp, make(chan bool))
279- return tst{errstr, endp, errstr == ""}
280+ endp := testibus.NewMultiValuedTestingEndpoint(nil, condition.Work(true))
281+ src := make(chan []interface{}, 1)
282+ testibus.SetWatchSource(endp, "ActionInvoked", src)
283+ src <- args
284+ return tst{errstr, endp, errstr == "", src}
285 }
286
287 ts := []tst{
288@@ -146,6 +148,7 @@
289 }
290 c.Check(s.log.Captured(), Matches, `(?ms).*`+t.errstr+`.*`)
291 s.log.ResetCapture()
292+ close(t.src)
293 }
294
295 }
296
297=== modified file 'bus/testing/testing_endpoint.go'
298--- bus/testing/testing_endpoint.go 2015-01-29 09:48:40 +0000
299+++ bus/testing/testing_endpoint.go 2015-02-25 11:05:41 +0000
300@@ -36,13 +36,15 @@
301 }
302
303 type testingEndpoint struct {
304- dialCond condition.Interface
305- callCond condition.Interface
306- retvals [][]interface{}
307- watchTicker chan bool
308- watchLck sync.RWMutex
309- callArgs []callArgs
310- callArgsLck sync.RWMutex
311+ dialCond condition.Interface
312+ callCond condition.Interface
313+ usedLck sync.Mutex
314+ used int
315+ retvals [][]interface{}
316+ watchSources map[string]chan []interface{}
317+ watchLck sync.RWMutex
318+ callArgs []callArgs
319+ callArgsLck sync.RWMutex
320 }
321
322 // Build a bus.Endpoint that calls OK() on its condition before returning
323@@ -51,7 +53,7 @@
324 // NOTE: Call() always returns the first return value; Watch() will provide
325 // each of them in turn, irrespective of whether Call has been called.
326 func NewMultiValuedTestingEndpoint(dialCond condition.Interface, callCond condition.Interface, retvalses ...[]interface{}) bus.Endpoint {
327- return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses}
328+ return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses, watchSources: make(map[string]chan []interface{})}
329 }
330
331 func NewTestingEndpoint(dialCond condition.Interface, callCond condition.Interface, retvals ...interface{}) bus.Endpoint {
332@@ -59,15 +61,15 @@
333 for i, x := range retvals {
334 retvalses[i] = []interface{}{x}
335 }
336- return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses}
337+ return &testingEndpoint{dialCond: dialCond, callCond: callCond, retvals: retvalses, watchSources: make(map[string]chan []interface{})}
338 }
339
340-// If SetWatchTicker is called with a non-nil watchTicker, it is used
341-// instead of the default timeout to wait while sending values over
342-// WatchSignal. Set it to nil again to restore default behaviour.
343-func SetWatchTicker(tc bus.Endpoint, watchTicker chan bool) {
344+// If SetWatchSource is called with a non-nil watchSource, it is used
345+// instead of the default timeout and retvals to get values to send
346+// over WatchSignal. Set it to nil again to restore default behaviour.
347+func SetWatchSource(tc bus.Endpoint, member string, watchSource chan []interface{}) {
348 tc.(*testingEndpoint).watchLck.Lock()
349- tc.(*testingEndpoint).watchTicker = watchTicker
350+ tc.(*testingEndpoint).watchSources[member] = watchSource
351 tc.(*testingEndpoint).watchLck.Unlock()
352 }
353
354@@ -80,23 +82,29 @@
355
356 // See Endpoint's WatchSignal. This WatchSignal will check its condition to
357 // decide whether to return an error, or provide each of its return values
358+// or values from the previously set watchSource for member.
359 func (tc *testingEndpoint) WatchSignal(member string, f func(...interface{}), d func()) error {
360 if tc.callCond.OK() {
361 go func() {
362- for _, v := range tc.retvals {
363+ tc.watchLck.RLock()
364+ source := tc.watchSources[member]
365+ tc.watchLck.RUnlock()
366+ if source == nil {
367+ tc.usedLck.Lock()
368+ idx := tc.used
369+ tc.used++
370+ tc.usedLck.Unlock()
371+ source = make(chan []interface{})
372+ go func() {
373+ for _, v := range tc.retvals[idx:] {
374+ source <- v
375+ time.Sleep(10 * time.Millisecond)
376+ }
377+ close(source)
378+ }()
379+ }
380+ for v := range source {
381 f(v...)
382- tc.watchLck.RLock()
383- ticker := tc.watchTicker
384- tc.watchLck.RUnlock()
385- if ticker != nil {
386- _, ok := <-ticker
387- if !ok {
388- // bail out
389- return
390- }
391- } else {
392- time.Sleep(10 * time.Millisecond)
393- }
394 }
395 d()
396 }()
397@@ -116,20 +124,24 @@
398 if tc.callCond.OK() {
399 expected := len(rvs)
400 var provided int
401- if len(tc.retvals) == 0 {
402+ tc.usedLck.Lock()
403+ idx := tc.used
404+ tc.used++
405+ tc.usedLck.Unlock()
406+ if len(tc.retvals) <= idx {
407 if expected != 0 {
408 panic("No return values provided!")
409 }
410 provided = 0
411 } else {
412- provided = len(tc.retvals[0])
413+ provided = len(tc.retvals[idx])
414 }
415 if provided != expected {
416 return errors.New("provided/expected return vals mismatch")
417 }
418 if provided != 0 {
419 x := dbus.NewMethodCallMessage("", "", "", "")
420- err := x.AppendArgs(tc.retvals[0]...)
421+ err := x.AppendArgs(tc.retvals[idx]...)
422 if err != nil {
423 return err
424 }
425
426=== modified file 'bus/testing/testing_endpoint_test.go'
427--- bus/testing/testing_endpoint_test.go 2014-07-04 23:00:42 +0000
428+++ bus/testing/testing_endpoint_test.go 2015-02-25 11:05:41 +0000
429@@ -134,21 +134,22 @@
430 c.Check(e, NotNil)
431 }
432
433-// Test WatchSignal can use the WatchTicker instead of a timeout (if
434+// Test WatchSignal can use a watchSource instead of a timeout and retvals (if
435 // the former is not nil)
436-func (s *TestingEndpointSuite) TestWatchTicker(c *C) {
437- watchTicker := make(chan bool, 3)
438- watchTicker <- true
439- watchTicker <- true
440- watchTicker <- true
441+func (s *TestingEndpointSuite) TestWatchSources(c *C) {
442+ watchTicker := make(chan []interface{}, 3)
443+ watchTicker <- []interface{}{true}
444+ watchTicker <- []interface{}{true}
445+ watchTicker <- []interface{}{true}
446 c.Assert(len(watchTicker), Equals, 3)
447
448 endp := NewTestingEndpoint(nil, condition.Work(true), 0, 0)
449- SetWatchTicker(endp, watchTicker)
450+ SetWatchSource(endp, "what", watchTicker)
451 ch := make(chan int)
452 e := endp.WatchSignal("what", func(us ...interface{}) {}, func() { close(ch) })
453 c.Check(e, IsNil)
454
455+ close(watchTicker)
456 // wait for the destructor to be called
457 select {
458 case <-time.Tick(10 * time.Millisecond):
459@@ -156,8 +157,8 @@
460 case <-ch:
461 }
462
463- // now if all went well, the ticker will have been tuck twice.
464- c.Assert(len(watchTicker), Equals, 1)
465+ // now if all went well, the ticker will have been exhausted.
466+ c.Assert(len(watchTicker), Equals, 0)
467 }
468
469 // Tests that GetProperty() works
470
471=== modified file 'client/client_test.go'
472--- client/client_test.go 2015-02-06 13:09:16 +0000
473+++ client/client_test.go 2015-02-25 11:05:41 +0000
474@@ -31,6 +31,7 @@
475 "testing"
476 "time"
477
478+ "launchpad.net/go-dbus/v1"
479 . "launchpad.net/gocheck"
480
481 "launchpad.net/ubuntu-push/accounts"
482@@ -657,13 +658,19 @@
483 // testing endpoints
484 cCond := condition.Fail2Work(7)
485 cEndp := testibus.NewTestingEndpoint(cCond, condition.Work(true),
486- uint32(networkmanager.ConnectedGlobal),
487+ uint32(networkmanager.Connecting),
488+ dbus.ObjectPath("hello"),
489+ uint32(networkmanager.Connecting),
490+ dbus.ObjectPath("hello"),
491 )
492 siCond := condition.Fail2Work(2)
493 siEndp := testibus.NewMultiValuedTestingEndpoint(siCond, condition.Work(true), []interface{}{int32(101), "mako", "daily", "Unknown", map[string]string{}})
494- tickerCh := make(chan bool)
495- testibus.SetWatchTicker(cEndp, tickerCh)
496+ tickerCh := make(chan []interface{})
497+ nopTickerCh := make(chan []interface{})
498+ testibus.SetWatchSource(cEndp, "StateChanged", tickerCh)
499+ testibus.SetWatchSource(cEndp, "PropertiesChanged", nopTickerCh)
500 defer close(tickerCh)
501+ defer close(nopTickerCh)
502 // ok, create the thing
503 cli := NewPushClient(cs.configPath, cs.leveldbPath)
504 cli.log = cs.log
505@@ -679,6 +686,7 @@
506 c.Assert(cli.takeTheBus(), IsNil)
507
508 c.Check(takeNextBool(cli.connCh), Equals, false)
509+ tickerCh <- []interface{}{uint32(networkmanager.ConnectedGlobal)}
510 c.Check(takeNextBool(cli.connCh), Equals, true)
511 // the connectivity endpoint retried until connected
512 c.Check(cCond.OK(), Equals, true)
513
514=== modified file 'client/service/postal_test.go'
515--- client/service/postal_test.go 2014-09-09 22:54:04 +0000
516+++ client/service/postal_test.go 2015-02-25 11:05:41 +0000
517@@ -552,6 +552,10 @@
518 svc.fallbackVibration = &launch_helper.Vibration{Pattern: []uint32{1}}
519 c.Assert(svc.Start(), IsNil)
520
521+ nopTicker := make(chan []interface{})
522+ testibus.SetWatchSource(endp, "ActionInvoked", nopTicker)
523+ defer close(nopTicker)
524+
525 // Persist is false so we just check the log
526 card := &launch_helper.Card{Icon: "icon-value", Summary: "summary-value", Body: "body-value", Popup: true, Persist: false}
527 vib := json.RawMessage(`true`)
528@@ -837,6 +841,10 @@
529 }
530
531 func (ps *postalSuite) TestBlacklisted(c *C) {
532+ ps.winStackBus = testibus.NewTestingEndpoint(condition.Work(true), condition.Work(true), []windowstack.WindowsInfo{},
533+ []windowstack.WindowsInfo{},
534+ []windowstack.WindowsInfo{},
535+ []windowstack.WindowsInfo{})
536 svc := ps.replaceBuses(NewPostalService(ps.cfg, ps.log))
537 svc.Start()
538 ps.blacklisted = false

Subscribers

People subscribed via source and target branches