github.com/rigado/snapd@v2.42.5-go-mod+incompatible/overlord/state/taskrunner_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package state_test 21 22 import ( 23 "errors" 24 "fmt" 25 "sort" 26 "strconv" 27 "strings" 28 "sync" 29 "time" 30 31 . "gopkg.in/check.v1" 32 "gopkg.in/tomb.v2" 33 34 "github.com/snapcore/snapd/overlord/state" 35 ) 36 37 type taskRunnerSuite struct{} 38 39 var _ = Suite(&taskRunnerSuite{}) 40 41 type stateBackend struct { 42 mu sync.Mutex 43 ensureBefore time.Duration 44 ensureBeforeSeen chan<- bool 45 } 46 47 func (b *stateBackend) Checkpoint([]byte) error { return nil } 48 49 func (b *stateBackend) EnsureBefore(d time.Duration) { 50 b.mu.Lock() 51 if d < b.ensureBefore { 52 b.ensureBefore = d 53 } 54 b.mu.Unlock() 55 if b.ensureBeforeSeen != nil { 56 b.ensureBeforeSeen <- true 57 } 58 } 59 60 func (b *stateBackend) RequestRestart(t state.RestartType) {} 61 62 func ensureChange(c *C, r *state.TaskRunner, sb *stateBackend, chg *state.Change) { 63 for i := 0; i < 20; i++ { 64 sb.ensureBefore = time.Hour 65 r.Ensure() 66 r.Wait() 67 chg.State().Lock() 68 s := chg.Status() 69 chg.State().Unlock() 70 if s.Ready() { 71 return 72 } 73 if sb.ensureBefore > 0 { 74 break 75 } 76 } 77 var statuses []string 78 chg.State().Lock() 79 for _, t := range chg.Tasks() { 80 statuses = append(statuses, t.Summary()+":"+t.Status().String()) 81 } 82 chg.State().Unlock() 83 c.Fatalf("Change didn't reach final state without blocking: %s", strings.Join(statuses, " ")) 84 } 85 86 // The result field encodes the expected order in which the task 87 // handlers will be called, assuming the provided setup is in place. 88 // 89 // Setup options: 90 // <task>:was-<status> - set task status before calling ensure (must be sensible) 91 // <task>:(do|undo)-block - block handler until task tomb dies 92 // <task>:(do|undo)-retry - return from handler with with state.Retry 93 // <task>:(do|undo)-error - return from handler with an error 94 // <task>:...:1,2 - one of the above, and add task to lanes 1 and 2 95 // chg:abort - call abort on the change 96 // 97 // Task wait order: ( t11 | t12 ) => ( t21 ) => ( t31 | t32 ) 98 // 99 // Task t12 has no undo. 100 // 101 // Final task statuses are tested based on the resulting events list. 102 // 103 var sequenceTests = []struct{ setup, result string }{{ 104 setup: "", 105 result: "t11:do t12:do t21:do t31:do t32:do", 106 }, { 107 setup: "t11:was-done t12:was-doing", 108 result: "t12:do t21:do t31:do t32:do", 109 }, { 110 setup: "t11:was-done t12:was-doing chg:abort", 111 result: "t11:undo", 112 }, { 113 setup: "t12:do-retry", 114 result: "t11:do t12:do t12:do-retry t12:do t21:do t31:do t32:do", 115 }, { 116 setup: "t11:do-block t12:do-error", 117 result: "t11:do t11:do-block t12:do t12:do-error t11:do-unblock t11:undo", 118 }, { 119 setup: "t11:do-error t12:do-block", 120 result: "t11:do t11:do-error t12:do t12:do-block t12:do-unblock", 121 }, { 122 setup: "t11:do-block t11:do-retry t12:do-error", 123 result: "t11:do t11:do-block t12:do t12:do-error t11:do-unblock t11:do-retry t11:undo", 124 }, { 125 setup: "t11:do-error t12:do-block t12:do-retry", 126 result: "t11:do t11:do-error t12:do t12:do-block t12:do-unblock t12:do-retry", 127 }, { 128 setup: "t31:do-error t21:undo-error", 129 result: "t11:do t12:do t21:do t31:do t31:do-error t32:do t32:undo t21:undo t21:undo-error t11:undo", 130 }, { 131 setup: "t21:do-set-ready", 132 result: "t11:do t12:do t21:do t31:do t32:do", 133 }, { 134 setup: "t31:do-error t21:undo-set-ready", 135 result: "t11:do t12:do t21:do t31:do t31:do-error t32:do t32:undo t21:undo t11:undo", 136 }, { 137 setup: "t11:was-done:1 t12:was-done:2 t21:was-done:1,2 t31:was-done:1 t32:do-error:2", 138 result: "t31:undo t32:do t32:do-error t21:undo t11:undo", 139 }, { 140 setup: "t11:was-done:1 t12:was-done:2 t21:was-done:2 t31:was-done:2 t32:do-error:2", 141 result: "t31:undo t32:do t32:do-error t21:undo", 142 }} 143 144 func (ts *taskRunnerSuite) TestSequenceTests(c *C) { 145 sb := &stateBackend{} 146 st := state.New(sb) 147 r := state.NewTaskRunner(st) 148 defer r.Stop() 149 150 ch := make(chan string, 256) 151 fn := func(label string) state.HandlerFunc { 152 return func(task *state.Task, tomb *tomb.Tomb) error { 153 st.Lock() 154 defer st.Unlock() 155 ch <- task.Summary() + ":" + label 156 var isSet bool 157 if task.Get(label+"-block", &isSet) == nil && isSet { 158 ch <- task.Summary() + ":" + label + "-block" 159 st.Unlock() 160 <-tomb.Dying() 161 st.Lock() 162 ch <- task.Summary() + ":" + label + "-unblock" 163 } 164 if task.Get(label+"-retry", &isSet) == nil && isSet { 165 task.Set(label+"-retry", false) 166 ch <- task.Summary() + ":" + label + "-retry" 167 return &state.Retry{} 168 } 169 if task.Get(label+"-error", &isSet) == nil && isSet { 170 ch <- task.Summary() + ":" + label + "-error" 171 return errors.New("boom") 172 } 173 if task.Get(label+"-set-ready", &isSet) == nil && isSet { 174 switch task.Status() { 175 case state.DoingStatus: 176 task.SetStatus(state.DoneStatus) 177 case state.UndoingStatus: 178 task.SetStatus(state.UndoneStatus) 179 } 180 } 181 return nil 182 } 183 } 184 r.AddHandler("do", fn("do"), nil) 185 r.AddHandler("do-undo", fn("do"), fn("undo")) 186 187 for _, test := range sequenceTests { 188 st.Lock() 189 190 // Delete previous changes. 191 st.Prune(1, 1, 1) 192 193 chg := st.NewChange("install", "...") 194 tasks := make(map[string]*state.Task) 195 for _, name := range strings.Fields("t11 t12 t21 t31 t32") { 196 if name == "t12" { 197 tasks[name] = st.NewTask("do", name) 198 } else { 199 tasks[name] = st.NewTask("do-undo", name) 200 } 201 chg.AddTask(tasks[name]) 202 } 203 tasks["t21"].WaitFor(tasks["t11"]) 204 tasks["t21"].WaitFor(tasks["t12"]) 205 tasks["t31"].WaitFor(tasks["t21"]) 206 tasks["t32"].WaitFor(tasks["t21"]) 207 st.Unlock() 208 209 c.Logf("-----") 210 c.Logf("Testing setup: %s", test.setup) 211 212 statuses := make(map[string]state.Status) 213 for s := state.DefaultStatus; s <= state.ErrorStatus; s++ { 214 statuses[strings.ToLower(s.String())] = s 215 } 216 217 // Reset and prepare initial task state. 218 st.Lock() 219 for _, t := range chg.Tasks() { 220 t.SetStatus(state.DefaultStatus) 221 t.Set("do-error", false) 222 t.Set("do-block", false) 223 t.Set("undo-error", false) 224 t.Set("undo-block", false) 225 } 226 for _, item := range strings.Fields(test.setup) { 227 parts := strings.Split(item, ":") 228 if parts[0] == "chg" && parts[1] == "abort" { 229 chg.Abort() 230 } else { 231 if strings.HasPrefix(parts[1], "was-") { 232 tasks[parts[0]].SetStatus(statuses[parts[1][4:]]) 233 } else { 234 tasks[parts[0]].Set(parts[1], true) 235 } 236 } 237 if len(parts) > 2 { 238 lanes := strings.Split(parts[2], ",") 239 for _, lane := range lanes { 240 n, err := strconv.Atoi(lane) 241 c.Assert(err, IsNil) 242 tasks[parts[0]].JoinLane(n) 243 } 244 } 245 } 246 st.Unlock() 247 248 // Run change until final. 249 ensureChange(c, r, sb, chg) 250 251 // Compute order of events observed. 252 var events []string 253 var done bool 254 for !done { 255 select { 256 case ev := <-ch: 257 events = append(events, ev) 258 // Make t11/t12 and t31/t32 always show up in the 259 // same order if they're next to each other. 260 for i := len(events) - 2; i >= 0; i-- { 261 prev := events[i] 262 next := events[i+1] 263 switch strings.Split(next, ":")[1] { 264 case "do-unblock", "undo-unblock": 265 default: 266 if prev[1] == next[1] && prev[2] > next[2] { 267 events[i], events[i+1] = next, prev 268 continue 269 } 270 } 271 break 272 } 273 default: 274 done = true 275 } 276 } 277 278 c.Logf("Expected result: %s", test.result) 279 c.Assert(strings.Join(events, " "), Equals, test.result, Commentf("setup: %s", test.setup)) 280 281 // Compute final expected status for tasks. 282 finalStatus := make(map[string]state.Status) 283 // ... default when no handler is called 284 for tname := range tasks { 285 finalStatus[tname] = state.HoldStatus 286 } 287 // ... overwrite based on relevant setup 288 for _, item := range strings.Fields(test.setup) { 289 parts := strings.Split(item, ":") 290 if parts[0] == "chg" && parts[1] == "abort" && strings.Contains(test.setup, "t12:was-doing") { 291 // t12 has no undo so must hold if asked to abort when was doing. 292 finalStatus["t12"] = state.HoldStatus 293 } 294 if !strings.HasPrefix(parts[1], "was-") { 295 continue 296 } 297 switch strings.TrimPrefix(parts[1], "was-") { 298 case "do", "doing", "done": 299 finalStatus[parts[0]] = state.DoneStatus 300 case "abort", "undo", "undoing", "undone": 301 if parts[0] == "t12" { 302 finalStatus[parts[0]] = state.DoneStatus // no undo for t12 303 } else { 304 finalStatus[parts[0]] = state.UndoneStatus 305 } 306 case "was-error": 307 finalStatus[parts[0]] = state.ErrorStatus 308 case "was-hold": 309 finalStatus[parts[0]] = state.ErrorStatus 310 } 311 } 312 // ... and overwrite based on events observed. 313 for _, ev := range events { 314 parts := strings.Split(ev, ":") 315 switch parts[1] { 316 case "do": 317 finalStatus[parts[0]] = state.DoneStatus 318 case "undo": 319 finalStatus[parts[0]] = state.UndoneStatus 320 case "do-error", "undo-error": 321 finalStatus[parts[0]] = state.ErrorStatus 322 case "do-retry": 323 if parts[0] == "t12" && finalStatus["t11"] == state.ErrorStatus { 324 // t12 has no undo so must hold if asked to abort on retry. 325 finalStatus["t12"] = state.HoldStatus 326 } 327 } 328 } 329 330 st.Lock() 331 var gotStatus, wantStatus []string 332 for _, task := range chg.Tasks() { 333 gotStatus = append(gotStatus, task.Summary()+":"+task.Status().String()) 334 wantStatus = append(wantStatus, task.Summary()+":"+finalStatus[task.Summary()].String()) 335 } 336 st.Unlock() 337 338 c.Logf("Expected statuses: %s", strings.Join(wantStatus, " ")) 339 comment := Commentf("calls: %s", test.result) 340 c.Assert(strings.Join(gotStatus, " "), Equals, strings.Join(wantStatus, " "), comment) 341 } 342 } 343 344 func (ts *taskRunnerSuite) TestExternalAbort(c *C) { 345 sb := &stateBackend{} 346 st := state.New(sb) 347 r := state.NewTaskRunner(st) 348 defer r.Stop() 349 350 ch := make(chan bool) 351 r.AddHandler("blocking", func(t *state.Task, tb *tomb.Tomb) error { 352 ch <- true 353 <-tb.Dying() 354 return nil 355 }, nil) 356 357 st.Lock() 358 chg := st.NewChange("install", "...") 359 t := st.NewTask("blocking", "...") 360 chg.AddTask(t) 361 st.Unlock() 362 363 r.Ensure() 364 <-ch 365 366 st.Lock() 367 chg.Abort() 368 st.Unlock() 369 370 // The Abort above must make Ensure kill the task, or this will never end. 371 ensureChange(c, r, sb, chg) 372 } 373 374 func (ts *taskRunnerSuite) TestStopHandlerJustFinishing(c *C) { 375 sb := &stateBackend{} 376 st := state.New(sb) 377 r := state.NewTaskRunner(st) 378 defer r.Stop() 379 380 ch := make(chan bool) 381 r.AddHandler("just-finish", func(t *state.Task, tb *tomb.Tomb) error { 382 ch <- true 383 <-tb.Dying() 384 // just ignore and actually finishes 385 return nil 386 }, nil) 387 388 st.Lock() 389 chg := st.NewChange("install", "...") 390 t := st.NewTask("just-finish", "...") 391 chg.AddTask(t) 392 st.Unlock() 393 394 r.Ensure() 395 <-ch 396 r.Stop() 397 398 st.Lock() 399 defer st.Unlock() 400 c.Check(t.Status(), Equals, state.DoneStatus) 401 c.Check(t.DoingTime(), Not(Equals), 0) 402 c.Check(t.UndoingTime(), Equals, time.Duration(0)) 403 } 404 405 func (ts *taskRunnerSuite) TestStopKinds(c *C) { 406 sb := &stateBackend{} 407 st := state.New(sb) 408 r := state.NewTaskRunner(st) 409 defer r.Stop() 410 411 ch1 := make(chan bool) 412 ch2 := make(chan bool) 413 r.AddHandler("just-finish1", func(t *state.Task, tb *tomb.Tomb) error { 414 ch1 <- true 415 <-tb.Dying() 416 // just ignore and actually finishes 417 return nil 418 }, nil) 419 r.AddHandler("just-finish2", func(t *state.Task, tb *tomb.Tomb) error { 420 ch2 <- true 421 <-tb.Dying() 422 // just ignore and actually finishes 423 return nil 424 }, nil) 425 426 st.Lock() 427 chg := st.NewChange("install", "...") 428 t1 := st.NewTask("just-finish1", "...") 429 t2 := st.NewTask("just-finish2", "...") 430 chg.AddTask(t1) 431 chg.AddTask(t2) 432 st.Unlock() 433 434 r.Ensure() 435 <-ch1 436 <-ch2 437 r.StopKinds("just-finish1") 438 439 st.Lock() 440 defer st.Unlock() 441 c.Check(t1.Status(), Equals, state.DoneStatus) 442 c.Check(t2.Status(), Equals, state.DoingStatus) 443 444 st.Unlock() 445 r.Stop() 446 st.Lock() 447 c.Check(t2.Status(), Equals, state.DoneStatus) 448 } 449 450 func (ts *taskRunnerSuite) TestErrorsOnStopAreRetried(c *C) { 451 sb := &stateBackend{} 452 st := state.New(sb) 453 r := state.NewTaskRunner(st) 454 defer r.Stop() 455 456 ch := make(chan bool) 457 r.AddHandler("error-on-stop", func(t *state.Task, tb *tomb.Tomb) error { 458 ch <- true 459 <-tb.Dying() 460 // error here could be due to cancellation, task will be retried 461 return errors.New("error at stop") 462 }, nil) 463 464 st.Lock() 465 chg := st.NewChange("install", "...") 466 t := st.NewTask("error-on-stop", "...") 467 chg.AddTask(t) 468 st.Unlock() 469 470 r.Ensure() 471 <-ch 472 r.Stop() 473 474 st.Lock() 475 defer st.Unlock() 476 // still Doing, will be retried 477 c.Check(t.Status(), Equals, state.DoingStatus) 478 } 479 480 func (ts *taskRunnerSuite) TestStopAskForRetry(c *C) { 481 sb := &stateBackend{} 482 st := state.New(sb) 483 r := state.NewTaskRunner(st) 484 defer r.Stop() 485 486 ch := make(chan bool) 487 r.AddHandler("ask-for-retry", func(t *state.Task, tb *tomb.Tomb) error { 488 ch <- true 489 <-tb.Dying() 490 // ask for retry 491 return &state.Retry{After: 2 * time.Minute} 492 }, nil) 493 494 st.Lock() 495 chg := st.NewChange("install", "...") 496 t := st.NewTask("ask-for-retry", "...") 497 chg.AddTask(t) 498 st.Unlock() 499 500 r.Ensure() 501 <-ch 502 r.Stop() 503 504 st.Lock() 505 defer st.Unlock() 506 c.Check(t.Status(), Equals, state.DoingStatus) 507 c.Check(t.AtTime().IsZero(), Equals, false) 508 } 509 510 func (ts *taskRunnerSuite) TestRetryAfterDuration(c *C) { 511 ensureBeforeTick := make(chan bool, 1) 512 sb := &stateBackend{ 513 ensureBefore: time.Hour, 514 ensureBeforeSeen: ensureBeforeTick, 515 } 516 st := state.New(sb) 517 r := state.NewTaskRunner(st) 518 defer r.Stop() 519 520 ch := make(chan bool) 521 ask := 0 522 r.AddHandler("ask-for-retry", func(t *state.Task, _ *tomb.Tomb) error { 523 ask++ 524 if ask == 1 { 525 return &state.Retry{After: time.Minute} 526 } 527 ch <- true 528 return nil 529 }, nil) 530 531 st.Lock() 532 chg := st.NewChange("install", "...") 533 t := st.NewTask("ask-for-retry", "...") 534 chg.AddTask(t) 535 st.Unlock() 536 537 tock := time.Now() 538 restore := state.MockTime(tock) 539 defer restore() 540 r.Ensure() // will run and be rescheduled in a minute 541 select { 542 case <-ensureBeforeTick: 543 case <-time.After(2 * time.Second): 544 c.Fatal("EnsureBefore wasn't called") 545 } 546 547 st.Lock() 548 defer st.Unlock() 549 c.Check(t.Status(), Equals, state.DoingStatus) 550 551 c.Check(ask, Equals, 1) 552 c.Check(sb.ensureBefore, Equals, 1*time.Minute) 553 schedule := t.AtTime() 554 c.Check(schedule.IsZero(), Equals, false) 555 556 state.MockTime(tock.Add(5 * time.Second)) 557 sb.ensureBefore = time.Hour 558 st.Unlock() 559 r.Ensure() // too soon 560 st.Lock() 561 562 c.Check(t.Status(), Equals, state.DoingStatus) 563 c.Check(ask, Equals, 1) 564 c.Check(sb.ensureBefore, Equals, 55*time.Second) 565 c.Check(t.AtTime().Equal(schedule), Equals, true) 566 567 state.MockTime(schedule) 568 sb.ensureBefore = time.Hour 569 st.Unlock() 570 r.Ensure() // time to run again 571 select { 572 case <-ch: 573 case <-time.After(2 * time.Second): 574 c.Fatal("handler wasn't called") 575 } 576 577 // wait for handler to finish 578 r.Wait() 579 580 st.Lock() 581 c.Check(t.Status(), Equals, state.DoneStatus) 582 c.Check(ask, Equals, 2) 583 c.Check(sb.ensureBefore, Equals, time.Hour) 584 c.Check(t.AtTime().IsZero(), Equals, true) 585 } 586 587 func (ts *taskRunnerSuite) testTaskSerialization(c *C, setupBlocked func(r *state.TaskRunner)) { 588 ensureBeforeTick := make(chan bool, 1) 589 sb := &stateBackend{ 590 ensureBefore: time.Hour, 591 ensureBeforeSeen: ensureBeforeTick, 592 } 593 st := state.New(sb) 594 r := state.NewTaskRunner(st) 595 defer r.Stop() 596 597 ch1 := make(chan bool) 598 ch2 := make(chan bool) 599 r.AddHandler("do1", func(t *state.Task, _ *tomb.Tomb) error { 600 ch1 <- true 601 ch1 <- true 602 return nil 603 }, nil) 604 r.AddHandler("do2", func(t *state.Task, _ *tomb.Tomb) error { 605 ch2 <- true 606 return nil 607 }, nil) 608 609 // setup blocked predicates 610 setupBlocked(r) 611 612 st.Lock() 613 chg := st.NewChange("install", "...") 614 t1 := st.NewTask("do1", "...") 615 chg.AddTask(t1) 616 t2 := st.NewTask("do2", "...") 617 chg.AddTask(t2) 618 st.Unlock() 619 620 r.Ensure() // will start only one, do1 621 622 select { 623 case <-ch1: 624 case <-time.After(2 * time.Second): 625 c.Fatal("do1 wasn't called") 626 } 627 628 c.Check(ensureBeforeTick, HasLen, 0) 629 c.Check(ch2, HasLen, 0) 630 631 r.Ensure() // won't yet start anything new 632 633 c.Check(ensureBeforeTick, HasLen, 0) 634 c.Check(ch2, HasLen, 0) 635 636 // finish do1 637 select { 638 case <-ch1: 639 case <-time.After(2 * time.Second): 640 c.Fatal("do1 wasn't continued") 641 } 642 643 // getting an EnsureBefore 0 call 644 select { 645 case <-ensureBeforeTick: 646 case <-time.After(2 * time.Second): 647 c.Fatal("EnsureBefore wasn't called") 648 } 649 c.Check(sb.ensureBefore, Equals, time.Duration(0)) 650 651 r.Ensure() // will start do2 652 653 select { 654 case <-ch2: 655 case <-time.After(2 * time.Second): 656 c.Fatal("do2 wasn't called") 657 } 658 659 // no more EnsureBefore calls 660 c.Check(ensureBeforeTick, HasLen, 0) 661 } 662 663 func (ts *taskRunnerSuite) TestTaskSerializationSetBlocked(c *C) { 664 // start first do1, and then do2 when nothing else is running 665 startedDo1 := false 666 ts.testTaskSerialization(c, func(r *state.TaskRunner) { 667 r.SetBlocked(func(t *state.Task, running []*state.Task) bool { 668 if t.Kind() == "do2" && (len(running) != 0 || !startedDo1) { 669 return true 670 } 671 if t.Kind() == "do1" { 672 startedDo1 = true 673 } 674 return false 675 }) 676 }) 677 } 678 679 func (ts *taskRunnerSuite) TestTaskSerializationAddBlocked(c *C) { 680 // start first do1, and then do2 when nothing else is running 681 startedDo1 := false 682 ts.testTaskSerialization(c, func(r *state.TaskRunner) { 683 r.AddBlocked(func(t *state.Task, running []*state.Task) bool { 684 if t.Kind() == "do2" && (len(running) != 0 || !startedDo1) { 685 return true 686 } 687 return false 688 }) 689 r.AddBlocked(func(t *state.Task, running []*state.Task) bool { 690 if t.Kind() == "do1" { 691 startedDo1 = true 692 } 693 return false 694 }) 695 }) 696 } 697 698 func (ts *taskRunnerSuite) TestPrematureChangeReady(c *C) { 699 sb := &stateBackend{} 700 st := state.New(sb) 701 r := state.NewTaskRunner(st) 702 defer r.Stop() 703 704 ch := make(chan bool) 705 r.AddHandler("block-undo", func(t *state.Task, tb *tomb.Tomb) error { return nil }, 706 func(t *state.Task, tb *tomb.Tomb) error { 707 ch <- true 708 <-ch 709 return nil 710 }) 711 r.AddHandler("fail", func(t *state.Task, tb *tomb.Tomb) error { 712 return errors.New("BAM") 713 }, nil) 714 715 st.Lock() 716 chg := st.NewChange("install", "...") 717 t1 := st.NewTask("block-undo", "...") 718 t2 := st.NewTask("fail", "...") 719 chg.AddTask(t1) 720 chg.AddTask(t2) 721 st.Unlock() 722 723 r.Ensure() // Error 724 r.Wait() 725 r.Ensure() // Block on undo 726 <-ch 727 728 defer func() { 729 ch <- true 730 r.Wait() 731 }() 732 733 st.Lock() 734 defer st.Unlock() 735 736 if chg.IsReady() || chg.Status().Ready() { 737 c.Errorf("Change considered ready prematurely") 738 } 739 740 c.Assert(chg.Err(), IsNil) 741 } 742 743 func (ts *taskRunnerSuite) TestOptionalHandler(c *C) { 744 sb := &stateBackend{} 745 st := state.New(sb) 746 r := state.NewTaskRunner(st) 747 748 r.AddOptionalHandler(func(t *state.Task) bool { return true }, 749 func(t *state.Task, tomb *tomb.Tomb) error { 750 return fmt.Errorf("optional handler error for %q", t.Kind()) 751 }, nil) 752 753 st.Lock() 754 chg := st.NewChange("install", "...") 755 t1 := st.NewTask("an unknown task", "...") 756 chg.AddTask(t1) 757 st.Unlock() 758 759 // Mark tasks as done. 760 ensureChange(c, r, sb, chg) 761 r.Stop() 762 763 st.Lock() 764 defer st.Unlock() 765 c.Assert(t1.Status(), Equals, state.ErrorStatus) 766 c.Assert(strings.Join(t1.Log(), ""), Matches, `.*optional handler error for "an unknown task"`) 767 } 768 769 func (ts *taskRunnerSuite) TestUndoSequence(c *C) { 770 sb := &stateBackend{} 771 st := state.New(sb) 772 r := state.NewTaskRunner(st) 773 774 var events []string 775 776 r.AddHandler("do-with-undo", 777 func(t *state.Task, tb *tomb.Tomb) error { 778 events = append(events, fmt.Sprintf("do-with-undo:%s", t.ID())) 779 return nil 780 }, func(t *state.Task, tb *tomb.Tomb) error { 781 events = append(events, fmt.Sprintf("undo:%s", t.ID())) 782 return nil 783 }) 784 r.AddHandler("do-no-undo", 785 func(t *state.Task, tb *tomb.Tomb) error { 786 events = append(events, fmt.Sprintf("do-no-undo:%s", t.ID())) 787 return nil 788 }, nil) 789 790 r.AddHandler("error-trigger", 791 func(t *state.Task, tb *tomb.Tomb) error { 792 events = append(events, fmt.Sprintf("do-with-error:%s", t.ID())) 793 return fmt.Errorf("error") 794 }, nil) 795 796 st.Lock() 797 chg := st.NewChange("install", "...") 798 799 var prev *state.Task 800 801 // create a sequence of tasks: 3 tasks with undo handlers, a task with 802 // no undo handler, 3 tasks with undo handler, a task with no undo 803 // handler, finally a task that errors out. Every task waits for previous 804 // taske. 805 for i := 0; i < 2; i++ { 806 for j := 0; j < 3; j++ { 807 t := st.NewTask("do-with-undo", "...") 808 if prev != nil { 809 t.WaitFor(prev) 810 } 811 chg.AddTask(t) 812 prev = t 813 } 814 815 t := st.NewTask("do-no-undo", "...") 816 t.WaitFor(prev) 817 chg.AddTask(t) 818 prev = t 819 } 820 821 terr := st.NewTask("error-trigger", "provoking undo") 822 terr.WaitFor(prev) 823 chg.AddTask(terr) 824 825 c.Check(chg.Tasks(), HasLen, 9) // sanity check 826 827 st.Unlock() 828 829 ensureChange(c, r, sb, chg) 830 r.Stop() 831 832 c.Assert(events, DeepEquals, []string{ 833 "do-with-undo:1", 834 "do-with-undo:2", 835 "do-with-undo:3", 836 "do-no-undo:4", 837 "do-with-undo:5", 838 "do-with-undo:6", 839 "do-with-undo:7", 840 "do-no-undo:8", 841 "do-with-error:9", 842 "undo:7", 843 "undo:6", 844 "undo:5", 845 "undo:3", 846 "undo:2", 847 "undo:1"}) 848 } 849 850 func (ts *taskRunnerSuite) TestKnownTaskKinds(c *C) { 851 st := state.New(nil) 852 r := state.NewTaskRunner(st) 853 r.AddHandler("task-kind-1", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil) 854 r.AddHandler("task-kind-2", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil) 855 856 kinds := r.KnownTaskKinds() 857 sort.Strings(kinds) 858 c.Assert(kinds, DeepEquals, []string{"task-kind-1", "task-kind-2"}) 859 } 860 861 func (ts *taskRunnerSuite) TestCleanup(c *C) { 862 sb := &stateBackend{} 863 st := state.New(sb) 864 r := state.NewTaskRunner(st) 865 defer r.Stop() 866 867 r.AddHandler("clean-it", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil) 868 r.AddHandler("other", func(t *state.Task, tb *tomb.Tomb) error { return nil }, nil) 869 870 called := 0 871 r.AddCleanup("clean-it", func(t *state.Task, tb *tomb.Tomb) error { 872 called++ 873 if called == 1 { 874 return fmt.Errorf("retry me") 875 } 876 return nil 877 }) 878 879 st.Lock() 880 chg := st.NewChange("install", "...") 881 t1 := st.NewTask("clean-it", "...") 882 t2 := st.NewTask("other", "...") 883 chg.AddTask(t1) 884 chg.AddTask(t2) 885 st.Unlock() 886 887 chgIsClean := func() bool { 888 st.Lock() 889 defer st.Unlock() 890 return chg.IsClean() 891 } 892 893 // Mark tasks as done. 894 ensureChange(c, r, sb, chg) 895 896 // First time it errors, then it works, then it's ignored. 897 c.Assert(chgIsClean(), Equals, false) 898 c.Assert(called, Equals, 0) 899 r.Ensure() 900 r.Wait() 901 c.Assert(chgIsClean(), Equals, false) 902 c.Assert(called, Equals, 1) 903 r.Ensure() 904 r.Wait() 905 c.Assert(chgIsClean(), Equals, true) 906 c.Assert(called, Equals, 2) 907 r.Ensure() 908 r.Wait() 909 c.Assert(chgIsClean(), Equals, true) 910 c.Assert(called, Equals, 2) 911 }