1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package dependency_test
9
"github.com/juju/errors"
10
"github.com/juju/testing"
11
jc "github.com/juju/testing/checkers"
12
gc "gopkg.in/check.v1"
14
coretesting "github.com/juju/juju/testing"
15
"github.com/juju/juju/worker"
16
"github.com/juju/juju/worker/dependency"
17
"github.com/juju/juju/worker/workertest"
20
type EngineSuite struct {
21
testing.IsolationSuite
25
var _ = gc.Suite(&EngineSuite{})
27
func (s *EngineSuite) SetUpTest(c *gc.C) {
28
s.IsolationSuite.SetUpTest(c)
29
s.fix = &engineFixture{}
32
func (s *EngineSuite) TestInstallConvenienceWrapper(c *gc.C) {
33
s.fix.run(c, func(engine *dependency.Engine) {
34
mh1 := newManifoldHarness()
35
mh2 := newManifoldHarness()
36
mh3 := newManifoldHarness()
38
err := dependency.Install(engine, dependency.Manifolds{
39
"mh1": mh1.Manifold(),
40
"mh2": mh2.Manifold(),
41
"mh3": mh3.Manifold(),
43
c.Assert(err, jc.ErrorIsNil)
51
func (s *EngineSuite) TestInstallNoInputs(c *gc.C) {
52
s.fix.run(c, func(engine *dependency.Engine) {
54
// Install a worker, check it starts.
55
mh1 := newManifoldHarness()
56
err := engine.Install("some-task", mh1.Manifold())
57
c.Assert(err, jc.ErrorIsNil)
60
// Install a second independent worker; check the first in untouched.
61
mh2 := newManifoldHarness()
62
err = engine.Install("other-task", mh2.Manifold())
63
c.Assert(err, jc.ErrorIsNil)
69
func (s *EngineSuite) TestInstallUnknownInputs(c *gc.C) {
70
s.fix.run(c, func(engine *dependency.Engine) {
72
// Install a worker with an unmet dependency, check it doesn't start
73
// (because the implementation returns ErrMissing).
74
mh1 := newManifoldHarness("later-task")
75
err := engine.Install("some-task", mh1.Manifold())
76
c.Assert(err, jc.ErrorIsNil)
79
// Install its dependency; check both start.
80
mh2 := newManifoldHarness()
81
err = engine.Install("later-task", mh2.Manifold())
82
c.Assert(err, jc.ErrorIsNil)
88
func (s *EngineSuite) TestDoubleInstall(c *gc.C) {
89
s.fix.run(c, func(engine *dependency.Engine) {
92
mh := newManifoldHarness()
93
err := engine.Install("some-task", mh.Manifold())
94
c.Assert(err, jc.ErrorIsNil)
97
// Can't install another worker with the same name.
98
err = engine.Install("some-task", mh.Manifold())
99
c.Assert(err, gc.ErrorMatches, `"some-task" manifold already installed`)
104
func (s *EngineSuite) TestInstallCycle(c *gc.C) {
105
s.fix.run(c, func(engine *dependency.Engine) {
107
// Install a worker with an unmet dependency.
108
mh1 := newManifoldHarness("robin-hood")
109
err := engine.Install("friar-tuck", mh1.Manifold())
110
c.Assert(err, jc.ErrorIsNil)
113
// Can't install another worker that creates a dependency cycle.
114
mh2 := newManifoldHarness("friar-tuck")
115
err = engine.Install("robin-hood", mh2.Manifold())
116
c.Assert(err, gc.ErrorMatches, `cannot install "robin-hood" manifold: cycle detected at .*`)
121
func (s *EngineSuite) TestInstallAlreadyStopped(c *gc.C) {
122
s.fix.run(c, func(engine *dependency.Engine) {
124
// Shut down the engine.
125
err := worker.Stop(engine)
126
c.Assert(err, jc.ErrorIsNil)
128
// Can't start a new task.
129
mh := newManifoldHarness()
130
err = engine.Install("some-task", mh.Manifold())
131
c.Assert(err, gc.ErrorMatches, "engine is shutting down")
136
func (s *EngineSuite) TestStartGetExistenceOnly(c *gc.C) {
137
s.fix.run(c, func(engine *dependency.Engine) {
139
// Start a task with a dependency.
140
mh1 := newManifoldHarness()
141
err := engine.Install("some-task", mh1.Manifold())
142
c.Assert(err, jc.ErrorIsNil)
143
mh1.AssertOneStart(c)
145
// Start another task that depends on it, ourselves depending on the
146
// implementation of manifoldHarness, which calls Get(foo, nil).
147
mh2 := newManifoldHarness("some-task")
148
err = engine.Install("other-task", mh2.Manifold())
149
c.Assert(err, jc.ErrorIsNil)
150
mh2.AssertOneStart(c)
154
func (s *EngineSuite) TestStartGetUndeclaredName(c *gc.C) {
155
s.fix.run(c, func(engine *dependency.Engine) {
157
// Install a task and make sure it's started.
158
mh1 := newManifoldHarness()
159
err := engine.Install("some-task", mh1.Manifold())
160
c.Assert(err, jc.ErrorIsNil)
161
mh1.AssertOneStart(c)
163
// Install another task with an undeclared dependency on the started task.
164
done := make(chan struct{})
165
err = engine.Install("other-task", dependency.Manifold{
166
Start: func(context dependency.Context) (worker.Worker, error) {
167
err := context.Get("some-task", nil)
168
c.Check(errors.Cause(err), gc.Equals, dependency.ErrMissing)
169
c.Check(err, gc.ErrorMatches, `"some-task" not declared: dependency not available`)
171
// Return a real worker so we don't keep restarting and potentially double-closing.
172
return startMinimalWorker(context)
175
c.Assert(err, jc.ErrorIsNil)
177
// Wait for the check to complete before we stop.
180
case <-time.After(coretesting.LongWait):
181
c.Fatalf("dependent task never started")
186
func (s *EngineSuite) testStartGet(c *gc.C, outErr error) {
187
s.fix.run(c, func(engine *dependency.Engine) {
189
// Start a task with an Output func that checks what it's passed, and wait for it to start.
190
var target interface{}
191
expectTarget := &target
192
mh1 := newManifoldHarness()
193
manifold := mh1.Manifold()
194
manifold.Output = func(worker worker.Worker, target interface{}) error {
195
// Check we got passed what we expect regardless...
196
c.Check(target, gc.DeepEquals, expectTarget)
197
// ...and return the configured error.
200
err := engine.Install("some-task", manifold)
201
c.Assert(err, jc.ErrorIsNil)
202
mh1.AssertOneStart(c)
204
// Start another that tries to use the above dependency.
205
done := make(chan struct{})
206
err = engine.Install("other-task", dependency.Manifold{
207
Inputs: []string{"some-task"},
208
Start: func(context dependency.Context) (worker.Worker, error) {
209
err := context.Get("some-task", &target)
210
// Check the result from some-task's Output func matches what we expect.
211
c.Check(err, gc.Equals, outErr)
213
// Return a real worker so we don't keep restarting and potentially double-closing.
214
return startMinimalWorker(context)
217
c.Check(err, jc.ErrorIsNil)
219
// Wait for the check to complete before we stop.
222
case <-time.After(coretesting.LongWait):
223
c.Fatalf("other-task never started")
228
func (s *EngineSuite) TestStartGetAccept(c *gc.C) {
229
s.testStartGet(c, nil)
232
func (s *EngineSuite) TestStartGetReject(c *gc.C) {
233
s.testStartGet(c, errors.New("not good enough"))
236
func (s *EngineSuite) TestStartAbortOnEngineKill(c *gc.C) {
237
s.fix.run(c, func(engine *dependency.Engine) {
238
starts := make(chan struct{}, 1000)
239
manifold := dependency.Manifold{
240
Start: func(context dependency.Context) (worker.Worker, error) {
243
case <-context.Abort():
244
case <-time.After(coretesting.LongWait):
245
c.Errorf("timed out")
247
return nil, errors.New("whatever")
250
err := engine.Install("task", manifold)
251
c.Assert(err, jc.ErrorIsNil)
255
case <-time.After(coretesting.LongWait):
256
c.Fatalf("timed out")
258
workertest.CleanKill(c, engine)
262
c.Fatalf("unexpected start")
268
func (s *EngineSuite) TestStartAbortOnDependencyChange(c *gc.C) {
269
s.fix.run(c, func(engine *dependency.Engine) {
270
starts := make(chan struct{}, 1000)
271
manifold := dependency.Manifold{
272
Inputs: []string{"parent"},
273
Start: func(context dependency.Context) (worker.Worker, error) {
276
case <-context.Abort():
277
case <-time.After(coretesting.LongWait):
278
c.Errorf("timed out")
280
return nil, errors.New("whatever")
283
err := engine.Install("child", manifold)
284
c.Assert(err, jc.ErrorIsNil)
288
case <-time.After(coretesting.LongWait):
289
c.Fatalf("timed out")
292
mh := newManifoldHarness()
293
err = engine.Install("parent", mh.Manifold())
294
c.Assert(err, jc.ErrorIsNil)
299
case <-time.After(coretesting.LongWait):
300
c.Fatalf("timed out")
302
workertest.CleanKill(c, engine)
306
c.Fatalf("unexpected start")
312
func (s *EngineSuite) TestErrorRestartsDependents(c *gc.C) {
313
s.fix.run(c, func(engine *dependency.Engine) {
315
// Start two tasks, one dependent on the other.
316
mh1 := newManifoldHarness()
317
err := engine.Install("error-task", mh1.Manifold())
318
c.Assert(err, jc.ErrorIsNil)
319
mh1.AssertOneStart(c)
321
mh2 := newManifoldHarness("error-task")
322
err = engine.Install("some-task", mh2.Manifold())
323
c.Assert(err, jc.ErrorIsNil)
324
mh2.AssertOneStart(c)
326
// Induce an error in the dependency...
327
mh1.InjectError(c, errors.New("ZAP"))
329
// ...and check that each task restarts once.
330
mh1.AssertOneStart(c)
331
mh2.AssertOneStart(c)
335
func (s *EngineSuite) TestErrorPreservesDependencies(c *gc.C) {
336
s.fix.run(c, func(engine *dependency.Engine) {
338
// Start two tasks, one dependent on the other.
339
mh1 := newManifoldHarness()
340
err := engine.Install("some-task", mh1.Manifold())
341
c.Assert(err, jc.ErrorIsNil)
342
mh1.AssertOneStart(c)
343
mh2 := newManifoldHarness("some-task")
344
err = engine.Install("error-task", mh2.Manifold())
345
c.Assert(err, jc.ErrorIsNil)
346
mh2.AssertOneStart(c)
348
// Induce an error in the dependent...
349
mh2.InjectError(c, errors.New("BLAM"))
351
// ...and check that only the dependent restarts.
353
mh2.AssertOneStart(c)
357
func (s *EngineSuite) TestCompletedWorkerNotRestartedOnExit(c *gc.C) {
358
s.fix.run(c, func(engine *dependency.Engine) {
361
mh1 := newManifoldHarness()
362
err := engine.Install("stop-task", mh1.Manifold())
363
c.Assert(err, jc.ErrorIsNil)
364
mh1.AssertOneStart(c)
366
// Stop it without error, and check it doesn't start again.
367
mh1.InjectError(c, nil)
372
func (s *EngineSuite) TestCompletedWorkerRestartedByDependencyChange(c *gc.C) {
373
s.fix.run(c, func(engine *dependency.Engine) {
375
// Start a task with a dependency.
376
mh1 := newManifoldHarness()
377
err := engine.Install("some-task", mh1.Manifold())
378
c.Assert(err, jc.ErrorIsNil)
379
mh1.AssertOneStart(c)
380
mh2 := newManifoldHarness("some-task")
381
err = engine.Install("stop-task", mh2.Manifold())
382
c.Assert(err, jc.ErrorIsNil)
383
mh2.AssertOneStart(c)
385
// Complete the dependent task successfully.
386
mh2.InjectError(c, nil)
389
// Bounce the dependency, and check the dependent is started again.
390
mh1.InjectError(c, errors.New("CLUNK"))
391
mh1.AssertOneStart(c)
392
mh2.AssertOneStart(c)
396
func (s *EngineSuite) TestRestartRestartsDependents(c *gc.C) {
397
s.fix.run(c, func(engine *dependency.Engine) {
399
// Start a dependency chain of 3 workers.
400
mh1 := newManifoldHarness()
401
err := engine.Install("error-task", mh1.Manifold())
402
c.Assert(err, jc.ErrorIsNil)
403
mh1.AssertOneStart(c)
404
mh2 := newManifoldHarness("error-task")
405
err = engine.Install("restart-task", mh2.Manifold())
406
c.Assert(err, jc.ErrorIsNil)
407
mh2.AssertOneStart(c)
408
mh3 := newManifoldHarness("restart-task")
409
err = engine.Install("consequent-restart-task", mh3.Manifold())
410
c.Assert(err, jc.ErrorIsNil)
411
mh3.AssertOneStart(c)
413
// Once they're all running, induce an error at the top level, which will
414
// cause the next level to be killed cleanly....
415
mh1.InjectError(c, errors.New("ZAP"))
417
// ...but should still cause all 3 workers to bounce.
418
mh1.AssertOneStart(c)
419
mh2.AssertOneStart(c)
420
mh3.AssertOneStart(c)
424
func (s *EngineSuite) TestIsFatal(c *gc.C) {
425
fatalErr := errors.New("KABOOM")
426
s.fix.isFatal = isFatalIf(fatalErr)
428
s.fix.run(c, func(engine *dependency.Engine) {
430
// Start two independent workers.
431
mh1 := newManifoldHarness()
432
err := engine.Install("some-task", mh1.Manifold())
433
c.Assert(err, jc.ErrorIsNil)
434
mh1.AssertOneStart(c)
435
mh2 := newManifoldHarness()
436
err = engine.Install("other-task", mh2.Manifold())
437
c.Assert(err, jc.ErrorIsNil)
438
mh2.AssertOneStart(c)
440
// Bounce one worker with Just Some Error; check that worker bounces.
441
mh1.InjectError(c, errors.New("splort"))
442
mh1.AssertOneStart(c)
445
// Bounce another worker with the fatal error; check the engine exits with
447
mh2.InjectError(c, fatalErr)
450
err = workertest.CheckKilled(c, engine)
451
c.Assert(err, gc.Equals, fatalErr)
455
func (s *EngineSuite) TestConfigFilter(c *gc.C) {
456
fatalErr := errors.New("kerrang")
457
s.fix.isFatal = isFatalIf(fatalErr)
458
reportErr := errors.New("meedly-meedly")
459
s.fix.filter = func(err error) error {
460
c.Check(err, gc.Equals, fatalErr)
464
s.fix.run(c, func(engine *dependency.Engine) {
467
mh1 := newManifoldHarness()
468
err := engine.Install("stop-task", mh1.Manifold())
469
c.Assert(err, jc.ErrorIsNil)
470
mh1.AssertOneStart(c)
472
// Inject the fatal error, and check what comes out.
473
mh1.InjectError(c, fatalErr)
474
err = workertest.CheckKilled(c, engine)
475
c.Assert(err, gc.Equals, reportErr)
479
func (s *EngineSuite) TestErrMissing(c *gc.C) {
480
s.fix.run(c, func(engine *dependency.Engine) {
482
// ErrMissing is implicitly and indirectly tested by the default
483
// manifoldHarness.start method throughout this suite, but this
484
// test explores its behaviour in pathological cases.
486
// Start a simple dependency.
487
mh1 := newManifoldHarness()
488
err := engine.Install("some-task", mh1.Manifold())
489
c.Assert(err, jc.ErrorIsNil)
490
mh1.AssertOneStart(c)
492
// Start a dependent that always complains ErrMissing.
493
mh2 := newManifoldHarness("some-task")
494
manifold := mh2.Manifold()
495
manifold.Start = func(_ dependency.Context) (worker.Worker, error) {
496
mh2.starts <- struct{}{}
497
return nil, errors.Trace(dependency.ErrMissing)
499
err = engine.Install("unmet-task", manifold)
500
c.Assert(err, jc.ErrorIsNil)
501
mh2.AssertOneStart(c)
503
// Bounce the dependency; check the dependent bounces once or twice (it will
504
// react to both the stop and the start of the dependency, but may be lucky
505
// enough to only restart once).
506
mh1.InjectError(c, errors.New("kerrang"))
507
mh1.AssertOneStart(c)
514
case <-time.After(coretesting.ShortWait):
518
c.Logf("saw %d starts", startCount)
519
c.Assert(startCount, jc.GreaterThan, 0)
520
c.Assert(startCount, jc.LessThan, 3)
522
// Stop the dependency for good; check the dependent is restarted just once.
523
mh1.InjectError(c, nil)
525
mh2.AssertOneStart(c)
529
func (s *EngineSuite) TestErrBounce(c *gc.C) {
530
s.fix.run(c, func(engine *dependency.Engine) {
532
// Start a simple dependency.
533
mh1 := newManifoldHarness()
534
err := engine.Install("some-task", mh1.Manifold())
535
c.Assert(err, jc.ErrorIsNil)
536
mh1.AssertOneStart(c)
538
// Start its dependent.
539
mh2 := newResourceIgnoringManifoldHarness("some-task")
540
err = engine.Install("another-task", mh2.Manifold())
541
c.Assert(err, jc.ErrorIsNil)
542
mh2.AssertOneStart(c)
544
// The parent requests bounce causing both to restart.
545
// Note(mjs): the lack of a restart delay is not specifically
546
// tested as I can't think of a reliable way to do this.
547
// TODO(fwereade): yeah, we need a clock to test this
549
mh1.InjectError(c, errors.Trace(dependency.ErrBounce))
550
mh1.AssertOneStart(c)
551
mh2.AssertStart(c) // Might restart more than once
555
func (s *EngineSuite) TestErrUninstall(c *gc.C) {
556
s.fix.run(c, func(engine *dependency.Engine) {
558
// Start a simple dependency.
559
mh1 := newManifoldHarness()
560
err := engine.Install("some-task", mh1.Manifold())
561
c.Assert(err, jc.ErrorIsNil)
562
mh1.AssertOneStart(c)
564
// Start its dependent. Note that in this case we want to record all start
565
// attempts, even if there are resource errors.
566
mh2 := newResourceIgnoringManifoldHarness("some-task")
567
err = engine.Install("another-task", mh2.Manifold())
568
c.Assert(err, jc.ErrorIsNil)
569
mh2.AssertOneStart(c)
571
// Uninstall the dependency; it should not be restarted, but its dependent should.
572
mh1.InjectError(c, errors.Trace(dependency.ErrUninstall))
574
mh2.AssertOneStart(c)
576
// Installing a new some-task manifold restarts the dependent.
577
mh3 := newManifoldHarness()
578
err = engine.Install("some-task", mh3.Manifold())
579
c.Assert(err, jc.ErrorIsNil)
580
mh3.AssertOneStart(c)
581
mh2.AssertOneStart(c)
585
func (s *EngineSuite) TestFilterStartError(c *gc.C) {
586
s.fix.isFatal = alwaysFatal
588
s.fix.run(c, func(engine *dependency.Engine) {
590
startErr := errors.New("grr crunch")
591
filterErr := errors.New("mew hiss")
593
err := engine.Install("task", dependency.Manifold{
594
Start: func(_ dependency.Context) (worker.Worker, error) {
597
Filter: func(in error) error {
598
c.Check(in, gc.Equals, startErr)
602
c.Assert(err, jc.ErrorIsNil)
604
err = workertest.CheckKilled(c, engine)
605
c.Check(err, gc.Equals, filterErr)
609
func (s *EngineSuite) TestFilterWorkerError(c *gc.C) {
610
s.fix.isFatal = alwaysFatal
612
s.fix.run(c, func(engine *dependency.Engine) {
614
injectErr := errors.New("arg squish")
615
filterErr := errors.New("blam dink")
617
mh := newManifoldHarness()
618
manifold := mh.Manifold()
619
manifold.Filter = func(in error) error {
620
c.Check(in, gc.Equals, injectErr)
623
err := engine.Install("task", manifold)
624
c.Assert(err, jc.ErrorIsNil)
627
mh.InjectError(c, injectErr)
628
err = workertest.CheckKilled(c, engine)
629
c.Check(err, gc.Equals, filterErr)
633
// TestWorstError starts an engine with two manifolds that always error
634
// with fatal errors. We test that the most important error is the one
635
// returned by the engine.
637
// This test uses manifolds whose workers ignore kill requests. We want
638
// this (dangerous!) behaviour so that we don't race over which fatal
639
// error is seen by the engine first.
640
func (s *EngineSuite) TestWorstError(c *gc.C) {
641
worstErr := errors.New("awful error")
643
s.fix.worstError = func(err1, err2 error) error {
647
s.fix.isFatal = alwaysFatal
649
s.fix.run(c, func(engine *dependency.Engine) {
651
mh1 := newErrorIgnoringManifoldHarness()
652
err := engine.Install("task", mh1.Manifold())
653
c.Assert(err, jc.ErrorIsNil)
654
mh1.AssertOneStart(c)
656
mh2 := newErrorIgnoringManifoldHarness()
657
err = engine.Install("another task", mh2.Manifold())
658
c.Assert(err, jc.ErrorIsNil)
659
mh2.AssertOneStart(c)
661
mh1.InjectError(c, errors.New("ping"))
662
mh2.InjectError(c, errors.New("pong"))
664
err = workertest.CheckKilled(c, engine)
665
c.Check(errors.Cause(err), gc.Equals, worstErr)
666
c.Check(callCount, gc.Equals, 2)
670
func (s *EngineSuite) TestConfigValidate(c *gc.C) {
672
breakConfig func(*dependency.EngineConfig)
675
func(config *dependency.EngineConfig) {
677
}, "IsFatal not specified",
679
func(config *dependency.EngineConfig) {
680
config.WorstError = nil
681
}, "WorstError not specified",
683
func(config *dependency.EngineConfig) {
684
config.ErrorDelay = -time.Second
685
}, "ErrorDelay is negative",
687
func(config *dependency.EngineConfig) {
688
config.BounceDelay = -time.Second
689
}, "BounceDelay is negative",
692
for i, test := range tests {
694
config := dependency.EngineConfig{
695
IsFatal: alwaysFatal,
696
WorstError: firstError,
697
ErrorDelay: time.Second,
698
BounceDelay: time.Second,
700
test.breakConfig(&config)
702
c.Logf("config validation...")
703
validateErr := config.Validate()
704
c.Check(validateErr, gc.ErrorMatches, test.err)
706
c.Logf("engine creation...")
707
engine, createErr := dependency.NewEngine(config)
708
c.Check(engine, gc.IsNil)
709
c.Check(createErr, gc.ErrorMatches, "invalid config: "+test.err)
713
func (s *EngineSuite) TestValidateEmptyManifolds(c *gc.C) {
714
err := dependency.Validate(dependency.Manifolds{})
715
c.Check(err, jc.ErrorIsNil)
718
func (s *EngineSuite) TestValidateTrivialCycle(c *gc.C) {
719
err := dependency.Validate(dependency.Manifolds{
720
"a": dependency.Manifold{Inputs: []string{"a"}},
722
c.Check(err.Error(), gc.Equals, `cycle detected at "a" (considering: map[a:true])`)
725
func (s *EngineSuite) TestValidateComplexManifolds(c *gc.C) {
727
// Create a bunch of manifolds with tangled but acyclic dependencies; check
728
// that they pass validation.
729
manifolds := dependency.Manifolds{
730
"root1": dependency.Manifold{},
731
"root2": dependency.Manifold{},
732
"mid1": dependency.Manifold{Inputs: []string{"root1"}},
733
"mid2": dependency.Manifold{Inputs: []string{"root1", "root2"}},
734
"leaf1": dependency.Manifold{Inputs: []string{"root2", "mid1"}},
735
"leaf2": dependency.Manifold{Inputs: []string{"root1", "mid2"}},
736
"leaf3": dependency.Manifold{Inputs: []string{"root1", "root2", "mid1", "mid2"}},
738
err := dependency.Validate(manifolds)
739
c.Check(err, jc.ErrorIsNil)
741
// Introduce a cycle; check the manifolds no longer validate.
742
manifolds["root1"] = dependency.Manifold{Inputs: []string{"leaf1"}}
743
err = dependency.Validate(manifolds)
744
c.Check(err, gc.ErrorMatches, "cycle detected at .*")