github.com/uchennaokeke444/nomad@v0.11.8/nomad/deploymentwatcher/deployments_watcher_test.go (about) 1 package deploymentwatcher 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 memdb "github.com/hashicorp/go-memdb" 9 "github.com/hashicorp/nomad/helper" 10 "github.com/hashicorp/nomad/helper/testlog" 11 "github.com/hashicorp/nomad/helper/uuid" 12 "github.com/hashicorp/nomad/nomad/mock" 13 "github.com/hashicorp/nomad/nomad/structs" 14 "github.com/hashicorp/nomad/testutil" 15 "github.com/stretchr/testify/assert" 16 mocker "github.com/stretchr/testify/mock" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func testDeploymentWatcher(t *testing.T, qps float64, batchDur time.Duration) (*Watcher, *mockBackend) { 21 m := newMockBackend(t) 22 w := NewDeploymentsWatcher(testlog.HCLogger(t), m, qps, batchDur) 23 return w, m 24 } 25 26 func defaultTestDeploymentWatcher(t *testing.T) (*Watcher, *mockBackend) { 27 return testDeploymentWatcher(t, LimitStateQueriesPerSecond, CrossDeploymentUpdateBatchDuration) 28 } 29 30 // Tests that the watcher properly watches for deployments and reconciles them 31 func TestWatcher_WatchDeployments(t *testing.T) { 32 t.Parallel() 33 require := require.New(t) 34 w, m := defaultTestDeploymentWatcher(t) 35 36 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 37 return true 38 })).Return(nil).Maybe() 39 40 // Create three jobs 41 j1, j2, j3 := mock.Job(), mock.Job(), mock.Job() 42 require.Nil(m.state.UpsertJob(100, j1)) 43 require.Nil(m.state.UpsertJob(101, j2)) 44 require.Nil(m.state.UpsertJob(102, j3)) 45 46 // Create three deployments all running 47 d1, d2, d3 := mock.Deployment(), mock.Deployment(), mock.Deployment() 48 d1.JobID = j1.ID 49 d2.JobID = j2.ID 50 d3.JobID = j3.ID 51 52 // Upsert the first deployment 53 require.Nil(m.state.UpsertDeployment(103, d1)) 54 55 // Next list 3 56 block1 := make(chan time.Time) 57 go func() { 58 <-block1 59 require.Nil(m.state.UpsertDeployment(104, d2)) 60 require.Nil(m.state.UpsertDeployment(105, d3)) 61 }() 62 63 //// Next list 3 but have one be terminal 64 block2 := make(chan time.Time) 65 d3terminal := d3.Copy() 66 d3terminal.Status = structs.DeploymentStatusFailed 67 go func() { 68 <-block2 69 require.Nil(m.state.UpsertDeployment(106, d3terminal)) 70 }() 71 72 w.SetEnabled(true, m.state) 73 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 74 func(err error) { require.Equal(1, watchersCount(w), "1 deployment returned") }) 75 76 close(block1) 77 testutil.WaitForResult(func() (bool, error) { return 3 == watchersCount(w), nil }, 78 func(err error) { require.Equal(3, watchersCount(w), "3 deployment returned") }) 79 80 close(block2) 81 testutil.WaitForResult(func() (bool, error) { return 2 == watchersCount(w), nil }, 82 func(err error) { require.Equal(3, watchersCount(w), "3 deployment returned - 1 terminal") }) 83 } 84 85 // Tests that calls against an unknown deployment fail 86 func TestWatcher_UnknownDeployment(t *testing.T) { 87 t.Parallel() 88 assert := assert.New(t) 89 require := require.New(t) 90 w, m := defaultTestDeploymentWatcher(t) 91 w.SetEnabled(true, m.state) 92 93 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 94 return true 95 })).Return(nil).Maybe() 96 97 // The expected error is that it should be an unknown deployment 98 dID := uuid.Generate() 99 expected := fmt.Sprintf("unknown deployment %q", dID) 100 101 // Request setting the health against an unknown deployment 102 req := &structs.DeploymentAllocHealthRequest{ 103 DeploymentID: dID, 104 HealthyAllocationIDs: []string{uuid.Generate()}, 105 } 106 var resp structs.DeploymentUpdateResponse 107 err := w.SetAllocHealth(req, &resp) 108 if assert.NotNil(err, "should have error for unknown deployment") { 109 require.Contains(err.Error(), expected) 110 } 111 112 // Request promoting against an unknown deployment 113 req2 := &structs.DeploymentPromoteRequest{ 114 DeploymentID: dID, 115 All: true, 116 } 117 err = w.PromoteDeployment(req2, &resp) 118 if assert.NotNil(err, "should have error for unknown deployment") { 119 require.Contains(err.Error(), expected) 120 } 121 122 // Request pausing against an unknown deployment 123 req3 := &structs.DeploymentPauseRequest{ 124 DeploymentID: dID, 125 Pause: true, 126 } 127 err = w.PauseDeployment(req3, &resp) 128 if assert.NotNil(err, "should have error for unknown deployment") { 129 require.Contains(err.Error(), expected) 130 } 131 132 // Request failing against an unknown deployment 133 req4 := &structs.DeploymentFailRequest{ 134 DeploymentID: dID, 135 } 136 err = w.FailDeployment(req4, &resp) 137 if assert.NotNil(err, "should have error for unknown deployment") { 138 require.Contains(err.Error(), expected) 139 } 140 } 141 142 // Test setting an unknown allocation's health 143 func TestWatcher_SetAllocHealth_Unknown(t *testing.T) { 144 t.Parallel() 145 assert := assert.New(t) 146 require := require.New(t) 147 w, m := defaultTestDeploymentWatcher(t) 148 149 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 150 return true 151 })).Return(nil).Maybe() 152 153 // Create a job, and a deployment 154 j := mock.Job() 155 d := mock.Deployment() 156 d.JobID = j.ID 157 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 158 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 159 160 // require that we get a call to UpsertDeploymentAllocHealth 161 a := mock.Alloc() 162 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 163 DeploymentID: d.ID, 164 Healthy: []string{a.ID}, 165 Eval: true, 166 } 167 matcher := matchDeploymentAllocHealthRequest(matchConfig) 168 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 169 170 w.SetEnabled(true, m.state) 171 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 172 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 173 174 // Call SetAllocHealth 175 req := &structs.DeploymentAllocHealthRequest{ 176 DeploymentID: d.ID, 177 HealthyAllocationIDs: []string{a.ID}, 178 } 179 var resp structs.DeploymentUpdateResponse 180 err := w.SetAllocHealth(req, &resp) 181 if assert.NotNil(err, "Set health of unknown allocation") { 182 require.Contains(err.Error(), "unknown") 183 } 184 require.Equal(1, watchersCount(w), "Deployment should still be active") 185 } 186 187 // Test setting allocation health 188 func TestWatcher_SetAllocHealth_Healthy(t *testing.T) { 189 t.Parallel() 190 require := require.New(t) 191 w, m := defaultTestDeploymentWatcher(t) 192 193 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 194 return true 195 })).Return(nil).Maybe() 196 197 // Create a job, alloc, and a deployment 198 j := mock.Job() 199 d := mock.Deployment() 200 d.JobID = j.ID 201 a := mock.Alloc() 202 a.DeploymentID = d.ID 203 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 204 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 205 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 206 207 // require that we get a call to UpsertDeploymentAllocHealth 208 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 209 DeploymentID: d.ID, 210 Healthy: []string{a.ID}, 211 Eval: true, 212 } 213 matcher := matchDeploymentAllocHealthRequest(matchConfig) 214 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 215 216 w.SetEnabled(true, m.state) 217 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 218 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 219 220 // Call SetAllocHealth 221 req := &structs.DeploymentAllocHealthRequest{ 222 DeploymentID: d.ID, 223 HealthyAllocationIDs: []string{a.ID}, 224 } 225 var resp structs.DeploymentUpdateResponse 226 err := w.SetAllocHealth(req, &resp) 227 require.Nil(err, "SetAllocHealth") 228 require.Equal(1, watchersCount(w), "Deployment should still be active") 229 m.AssertCalled(t, "UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)) 230 } 231 232 // Test setting allocation unhealthy 233 func TestWatcher_SetAllocHealth_Unhealthy(t *testing.T) { 234 t.Parallel() 235 require := require.New(t) 236 w, m := defaultTestDeploymentWatcher(t) 237 238 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 239 return true 240 })).Return(nil).Maybe() 241 242 // Create a job, alloc, and a deployment 243 j := mock.Job() 244 d := mock.Deployment() 245 d.JobID = j.ID 246 a := mock.Alloc() 247 a.DeploymentID = d.ID 248 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 249 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 250 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 251 252 // require that we get a call to UpsertDeploymentAllocHealth 253 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 254 DeploymentID: d.ID, 255 Unhealthy: []string{a.ID}, 256 Eval: true, 257 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 258 DeploymentID: d.ID, 259 Status: structs.DeploymentStatusFailed, 260 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 261 }, 262 } 263 matcher := matchDeploymentAllocHealthRequest(matchConfig) 264 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 265 266 w.SetEnabled(true, m.state) 267 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 268 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 269 270 // Call SetAllocHealth 271 req := &structs.DeploymentAllocHealthRequest{ 272 DeploymentID: d.ID, 273 UnhealthyAllocationIDs: []string{a.ID}, 274 } 275 var resp structs.DeploymentUpdateResponse 276 err := w.SetAllocHealth(req, &resp) 277 require.Nil(err, "SetAllocHealth") 278 279 testutil.WaitForResult(func() (bool, error) { return 0 == watchersCount(w), nil }, 280 func(err error) { require.Equal(0, watchersCount(w), "Should have no deployment") }) 281 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 282 } 283 284 // Test setting allocation unhealthy and that there should be a rollback 285 func TestWatcher_SetAllocHealth_Unhealthy_Rollback(t *testing.T) { 286 t.Parallel() 287 require := require.New(t) 288 w, m := defaultTestDeploymentWatcher(t) 289 290 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 291 return true 292 })).Return(nil).Maybe() 293 294 // Create a job, alloc, and a deployment 295 j := mock.Job() 296 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 297 j.TaskGroups[0].Update.MaxParallel = 2 298 j.TaskGroups[0].Update.AutoRevert = true 299 j.TaskGroups[0].Update.ProgressDeadline = 0 300 j.Stable = true 301 d := mock.Deployment() 302 d.JobID = j.ID 303 d.TaskGroups["web"].AutoRevert = true 304 a := mock.Alloc() 305 a.DeploymentID = d.ID 306 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 307 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 308 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 309 310 // Upsert the job again to get a new version 311 j2 := j.Copy() 312 j2.Stable = false 313 // Modify the job to make its specification different 314 j2.Meta["foo"] = "bar" 315 316 require.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 317 318 // require that we get a call to UpsertDeploymentAllocHealth 319 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 320 DeploymentID: d.ID, 321 Unhealthy: []string{a.ID}, 322 Eval: true, 323 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 324 DeploymentID: d.ID, 325 Status: structs.DeploymentStatusFailed, 326 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 327 }, 328 JobVersion: helper.Uint64ToPtr(0), 329 } 330 matcher := matchDeploymentAllocHealthRequest(matchConfig) 331 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 332 333 w.SetEnabled(true, m.state) 334 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 335 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 336 337 // Call SetAllocHealth 338 req := &structs.DeploymentAllocHealthRequest{ 339 DeploymentID: d.ID, 340 UnhealthyAllocationIDs: []string{a.ID}, 341 } 342 var resp structs.DeploymentUpdateResponse 343 err := w.SetAllocHealth(req, &resp) 344 require.Nil(err, "SetAllocHealth") 345 346 testutil.WaitForResult(func() (bool, error) { return 0 == watchersCount(w), nil }, 347 func(err error) { require.Equal(0, watchersCount(w), "Should have no deployment") }) 348 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 349 } 350 351 // Test setting allocation unhealthy on job with identical spec and there should be no rollback 352 func TestWatcher_SetAllocHealth_Unhealthy_NoRollback(t *testing.T) { 353 t.Parallel() 354 require := require.New(t) 355 w, m := defaultTestDeploymentWatcher(t) 356 357 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 358 return true 359 })).Return(nil).Maybe() 360 361 // Create a job, alloc, and a deployment 362 j := mock.Job() 363 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 364 j.TaskGroups[0].Update.MaxParallel = 2 365 j.TaskGroups[0].Update.AutoRevert = true 366 j.TaskGroups[0].Update.ProgressDeadline = 0 367 j.Stable = true 368 d := mock.Deployment() 369 d.JobID = j.ID 370 d.TaskGroups["web"].AutoRevert = true 371 a := mock.Alloc() 372 a.DeploymentID = d.ID 373 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 374 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 375 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 376 377 // Upsert the job again to get a new version 378 j2 := j.Copy() 379 j2.Stable = false 380 381 require.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 382 383 // require that we get a call to UpsertDeploymentAllocHealth 384 matchConfig := &matchDeploymentAllocHealthRequestConfig{ 385 DeploymentID: d.ID, 386 Unhealthy: []string{a.ID}, 387 Eval: true, 388 DeploymentUpdate: &structs.DeploymentStatusUpdate{ 389 DeploymentID: d.ID, 390 Status: structs.DeploymentStatusFailed, 391 StatusDescription: structs.DeploymentStatusDescriptionFailedAllocations, 392 }, 393 JobVersion: nil, 394 } 395 matcher := matchDeploymentAllocHealthRequest(matchConfig) 396 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher)).Return(nil) 397 398 w.SetEnabled(true, m.state) 399 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 400 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 401 402 // Call SetAllocHealth 403 req := &structs.DeploymentAllocHealthRequest{ 404 DeploymentID: d.ID, 405 UnhealthyAllocationIDs: []string{a.ID}, 406 } 407 var resp structs.DeploymentUpdateResponse 408 err := w.SetAllocHealth(req, &resp) 409 require.Nil(err, "SetAllocHealth") 410 411 testutil.WaitForResult(func() (bool, error) { return 0 == watchersCount(w), nil }, 412 func(err error) { require.Equal(0, watchersCount(w), "Should have no deployment") }) 413 m.AssertNumberOfCalls(t, "UpdateDeploymentAllocHealth", 1) 414 } 415 416 // Test promoting a deployment 417 func TestWatcher_PromoteDeployment_HealthyCanaries(t *testing.T) { 418 t.Parallel() 419 require := require.New(t) 420 w, m := defaultTestDeploymentWatcher(t) 421 422 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 423 return true 424 })).Return(nil).Maybe() 425 426 // Create a job, canary alloc, and a deployment 427 j := mock.Job() 428 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 429 j.TaskGroups[0].Update.MaxParallel = 2 430 j.TaskGroups[0].Update.Canary = 1 431 j.TaskGroups[0].Update.ProgressDeadline = 0 432 d := mock.Deployment() 433 d.JobID = j.ID 434 a := mock.Alloc() 435 d.TaskGroups[a.TaskGroup].DesiredCanaries = 1 436 d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID} 437 a.DeploymentStatus = &structs.AllocDeploymentStatus{ 438 Healthy: helper.BoolToPtr(true), 439 } 440 a.DeploymentID = d.ID 441 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 442 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 443 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 444 445 // require that we get a call to UpsertDeploymentPromotion 446 matchConfig := &matchDeploymentPromoteRequestConfig{ 447 Promotion: &structs.DeploymentPromoteRequest{ 448 DeploymentID: d.ID, 449 All: true, 450 }, 451 Eval: true, 452 } 453 matcher := matchDeploymentPromoteRequest(matchConfig) 454 m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil) 455 456 // We may get an update for the desired transition. 457 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 458 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 459 460 w.SetEnabled(true, m.state) 461 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 462 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 463 464 // Call PromoteDeployment 465 req := &structs.DeploymentPromoteRequest{ 466 DeploymentID: d.ID, 467 All: true, 468 } 469 var resp structs.DeploymentUpdateResponse 470 err := w.PromoteDeployment(req, &resp) 471 require.Nil(err, "PromoteDeployment") 472 require.Equal(1, watchersCount(w), "Deployment should still be active") 473 m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher)) 474 } 475 476 // Test promoting a deployment with unhealthy canaries 477 func TestWatcher_PromoteDeployment_UnhealthyCanaries(t *testing.T) { 478 t.Parallel() 479 require := require.New(t) 480 w, m := defaultTestDeploymentWatcher(t) 481 482 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 483 return true 484 })).Return(nil).Maybe() 485 486 // Create a job, canary alloc, and a deployment 487 j := mock.Job() 488 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 489 j.TaskGroups[0].Update.MaxParallel = 2 490 j.TaskGroups[0].Update.Canary = 2 491 j.TaskGroups[0].Update.ProgressDeadline = 0 492 d := mock.Deployment() 493 d.JobID = j.ID 494 a := mock.Alloc() 495 d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID} 496 d.TaskGroups[a.TaskGroup].DesiredCanaries = 2 497 a.DeploymentID = d.ID 498 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 499 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 500 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 501 502 // require that we get a call to UpsertDeploymentPromotion 503 matchConfig := &matchDeploymentPromoteRequestConfig{ 504 Promotion: &structs.DeploymentPromoteRequest{ 505 DeploymentID: d.ID, 506 All: true, 507 }, 508 Eval: true, 509 } 510 matcher := matchDeploymentPromoteRequest(matchConfig) 511 m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher)).Return(nil) 512 513 w.SetEnabled(true, m.state) 514 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 515 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 516 517 // Call SetAllocHealth 518 req := &structs.DeploymentPromoteRequest{ 519 DeploymentID: d.ID, 520 All: true, 521 } 522 var resp structs.DeploymentUpdateResponse 523 err := w.PromoteDeployment(req, &resp) 524 if assert.NotNil(t, err, "PromoteDeployment") { 525 // 0/2 because the old version has been stopped but the canary isn't marked healthy yet 526 require.Contains(err.Error(), `Task group "web" has 0/2 healthy allocations`, "Should error because canary isn't marked healthy") 527 } 528 529 require.Equal(1, watchersCount(w), "Deployment should still be active") 530 m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher)) 531 } 532 533 func TestWatcher_AutoPromoteDeployment(t *testing.T) { 534 t.Parallel() 535 w, m := defaultTestDeploymentWatcher(t) 536 now := time.Now() 537 538 // Create 1 UpdateStrategy, 1 job (1 TaskGroup), 2 canaries, and 1 deployment 539 upd := structs.DefaultUpdateStrategy.Copy() 540 upd.AutoPromote = true 541 upd.MaxParallel = 2 542 upd.Canary = 2 543 upd.ProgressDeadline = 5 * time.Second 544 545 j := mock.Job() 546 j.TaskGroups[0].Update = upd 547 548 d := mock.Deployment() 549 d.JobID = j.ID 550 // This is created in scheduler.computeGroup at runtime, where properties from the 551 // UpdateStrategy are copied in 552 d.TaskGroups = map[string]*structs.DeploymentState{ 553 "web": { 554 AutoPromote: upd.AutoPromote, 555 AutoRevert: upd.AutoRevert, 556 ProgressDeadline: upd.ProgressDeadline, 557 DesiredTotal: 2, 558 }, 559 } 560 561 alloc := func() *structs.Allocation { 562 a := mock.Alloc() 563 a.DeploymentID = d.ID 564 a.CreateTime = now.UnixNano() 565 a.ModifyTime = now.UnixNano() 566 a.DeploymentStatus = &structs.AllocDeploymentStatus{ 567 Canary: true, 568 } 569 return a 570 } 571 572 a := alloc() 573 b := alloc() 574 575 d.TaskGroups[a.TaskGroup].PlacedCanaries = []string{a.ID, b.ID} 576 d.TaskGroups[a.TaskGroup].DesiredCanaries = 2 577 require.NoError(t, m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 578 require.NoError(t, m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 579 require.NoError(t, m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a, b}), "UpsertAllocs") 580 581 // ============================================================= 582 // Support method calls 583 584 // clear UpdateDeploymentStatus default expectation 585 m.Mock.ExpectedCalls = nil 586 587 matchConfig0 := &matchDeploymentStatusUpdateConfig{ 588 DeploymentID: d.ID, 589 Status: structs.DeploymentStatusFailed, 590 StatusDescription: structs.DeploymentStatusDescriptionProgressDeadline, 591 Eval: true, 592 } 593 matcher0 := matchDeploymentStatusUpdateRequest(matchConfig0) 594 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher0)).Return(nil) 595 596 matchConfig1 := &matchDeploymentAllocHealthRequestConfig{ 597 DeploymentID: d.ID, 598 Healthy: []string{a.ID, b.ID}, 599 Eval: true, 600 } 601 matcher1 := matchDeploymentAllocHealthRequest(matchConfig1) 602 m.On("UpdateDeploymentAllocHealth", mocker.MatchedBy(matcher1)).Return(nil) 603 604 matchConfig2 := &matchDeploymentPromoteRequestConfig{ 605 Promotion: &structs.DeploymentPromoteRequest{ 606 DeploymentID: d.ID, 607 All: true, 608 }, 609 Eval: true, 610 } 611 matcher2 := matchDeploymentPromoteRequest(matchConfig2) 612 m.On("UpdateDeploymentPromotion", mocker.MatchedBy(matcher2)).Return(nil) 613 // ============================================================= 614 615 // Start the deployment 616 w.SetEnabled(true, m.state) 617 testutil.WaitForResult(func() (bool, error) { return 1 == len(w.watchers), nil }, 618 func(err error) { require.Equal(t, 1, len(w.watchers), "Should have 1 deployment") }) 619 620 // Mark the canaries healthy 621 req := &structs.DeploymentAllocHealthRequest{ 622 DeploymentID: d.ID, 623 HealthyAllocationIDs: []string{a.ID, b.ID}, 624 } 625 var resp structs.DeploymentUpdateResponse 626 // Calls w.raft.UpdateDeploymentAllocHealth, which is implemented by StateStore in 627 // state.UpdateDeploymentAllocHealth via a raft shim? 628 err := w.SetAllocHealth(req, &resp) 629 require.NoError(t, err) 630 631 ws := memdb.NewWatchSet() 632 633 testutil.WaitForResult( 634 func() (bool, error) { 635 ds, _ := m.state.DeploymentsByJobID(ws, j.Namespace, j.ID, true) 636 d = ds[0] 637 return 2 == d.TaskGroups["web"].HealthyAllocs, nil 638 }, 639 func(err error) { require.NoError(t, err) }, 640 ) 641 642 require.Equal(t, 1, len(w.watchers), "Deployment should still be active") 643 m.AssertCalled(t, "UpdateDeploymentPromotion", mocker.MatchedBy(matcher2)) 644 645 require.Equal(t, "running", d.Status) 646 require.True(t, d.TaskGroups["web"].Promoted) 647 648 a1, _ := m.state.AllocByID(ws, a.ID) 649 require.False(t, a1.DeploymentStatus.Canary) 650 require.Equal(t, "pending", a1.ClientStatus) 651 require.Equal(t, "run", a1.DesiredStatus) 652 653 b1, _ := m.state.AllocByID(ws, b.ID) 654 require.False(t, b1.DeploymentStatus.Canary) 655 } 656 657 // Test pausing a deployment that is running 658 func TestWatcher_PauseDeployment_Pause_Running(t *testing.T) { 659 t.Parallel() 660 require := require.New(t) 661 w, m := defaultTestDeploymentWatcher(t) 662 663 // clear UpdateDeploymentStatus default expectation 664 m.Mock.ExpectedCalls = nil 665 666 // Create a job and a deployment 667 j := mock.Job() 668 d := mock.Deployment() 669 d.JobID = j.ID 670 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 671 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 672 673 // require that we get a call to UpsertDeploymentStatusUpdate 674 matchConfig := &matchDeploymentStatusUpdateConfig{ 675 DeploymentID: d.ID, 676 Status: structs.DeploymentStatusPaused, 677 StatusDescription: structs.DeploymentStatusDescriptionPaused, 678 } 679 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 680 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 681 682 w.SetEnabled(true, m.state) 683 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 684 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 685 686 // Call PauseDeployment 687 req := &structs.DeploymentPauseRequest{ 688 DeploymentID: d.ID, 689 Pause: true, 690 } 691 var resp structs.DeploymentUpdateResponse 692 err := w.PauseDeployment(req, &resp) 693 require.Nil(err, "PauseDeployment") 694 695 require.Equal(1, watchersCount(w), "Deployment should still be active") 696 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 697 } 698 699 // Test pausing a deployment that is paused 700 func TestWatcher_PauseDeployment_Pause_Paused(t *testing.T) { 701 t.Parallel() 702 require := require.New(t) 703 w, m := defaultTestDeploymentWatcher(t) 704 705 // clear UpdateDeploymentStatus default expectation 706 m.Mock.ExpectedCalls = nil 707 708 // Create a job and a deployment 709 j := mock.Job() 710 d := mock.Deployment() 711 d.JobID = j.ID 712 d.Status = structs.DeploymentStatusPaused 713 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 714 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 715 716 // require that we get a call to UpsertDeploymentStatusUpdate 717 matchConfig := &matchDeploymentStatusUpdateConfig{ 718 DeploymentID: d.ID, 719 Status: structs.DeploymentStatusPaused, 720 StatusDescription: structs.DeploymentStatusDescriptionPaused, 721 } 722 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 723 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 724 725 w.SetEnabled(true, m.state) 726 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 727 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 728 729 // Call PauseDeployment 730 req := &structs.DeploymentPauseRequest{ 731 DeploymentID: d.ID, 732 Pause: true, 733 } 734 var resp structs.DeploymentUpdateResponse 735 err := w.PauseDeployment(req, &resp) 736 require.Nil(err, "PauseDeployment") 737 738 require.Equal(1, watchersCount(w), "Deployment should still be active") 739 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 740 } 741 742 // Test unpausing a deployment that is paused 743 func TestWatcher_PauseDeployment_Unpause_Paused(t *testing.T) { 744 t.Parallel() 745 require := require.New(t) 746 w, m := defaultTestDeploymentWatcher(t) 747 748 // Create a job and a deployment 749 j := mock.Job() 750 d := mock.Deployment() 751 d.JobID = j.ID 752 d.Status = structs.DeploymentStatusPaused 753 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 754 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 755 756 // require that we get a call to UpsertDeploymentStatusUpdate 757 matchConfig := &matchDeploymentStatusUpdateConfig{ 758 DeploymentID: d.ID, 759 Status: structs.DeploymentStatusRunning, 760 StatusDescription: structs.DeploymentStatusDescriptionRunning, 761 Eval: true, 762 } 763 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 764 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 765 766 w.SetEnabled(true, m.state) 767 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 768 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 769 770 // Call PauseDeployment 771 req := &structs.DeploymentPauseRequest{ 772 DeploymentID: d.ID, 773 Pause: false, 774 } 775 var resp structs.DeploymentUpdateResponse 776 err := w.PauseDeployment(req, &resp) 777 require.Nil(err, "PauseDeployment") 778 779 require.Equal(1, watchersCount(w), "Deployment should still be active") 780 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 781 } 782 783 // Test unpausing a deployment that is running 784 func TestWatcher_PauseDeployment_Unpause_Running(t *testing.T) { 785 t.Parallel() 786 require := require.New(t) 787 w, m := defaultTestDeploymentWatcher(t) 788 789 // Create a job and a deployment 790 j := mock.Job() 791 d := mock.Deployment() 792 d.JobID = j.ID 793 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 794 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 795 796 // require that we get a call to UpsertDeploymentStatusUpdate 797 matchConfig := &matchDeploymentStatusUpdateConfig{ 798 DeploymentID: d.ID, 799 Status: structs.DeploymentStatusRunning, 800 StatusDescription: structs.DeploymentStatusDescriptionRunning, 801 Eval: true, 802 } 803 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 804 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 805 806 w.SetEnabled(true, m.state) 807 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 808 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 809 810 // Call PauseDeployment 811 req := &structs.DeploymentPauseRequest{ 812 DeploymentID: d.ID, 813 Pause: false, 814 } 815 var resp structs.DeploymentUpdateResponse 816 err := w.PauseDeployment(req, &resp) 817 require.Nil(err, "PauseDeployment") 818 819 require.Equal(1, watchersCount(w), "Deployment should still be active") 820 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 821 } 822 823 // Test failing a deployment that is running 824 func TestWatcher_FailDeployment_Running(t *testing.T) { 825 t.Parallel() 826 require := require.New(t) 827 w, m := defaultTestDeploymentWatcher(t) 828 829 // Create a job and a deployment 830 j := mock.Job() 831 d := mock.Deployment() 832 d.JobID = j.ID 833 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 834 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 835 836 // require that we get a call to UpsertDeploymentStatusUpdate 837 matchConfig := &matchDeploymentStatusUpdateConfig{ 838 DeploymentID: d.ID, 839 Status: structs.DeploymentStatusFailed, 840 StatusDescription: structs.DeploymentStatusDescriptionFailedByUser, 841 Eval: true, 842 } 843 matcher := matchDeploymentStatusUpdateRequest(matchConfig) 844 m.On("UpdateDeploymentStatus", mocker.MatchedBy(matcher)).Return(nil) 845 846 w.SetEnabled(true, m.state) 847 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 848 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 849 850 // Call PauseDeployment 851 req := &structs.DeploymentFailRequest{ 852 DeploymentID: d.ID, 853 } 854 var resp structs.DeploymentUpdateResponse 855 err := w.FailDeployment(req, &resp) 856 require.Nil(err, "FailDeployment") 857 858 require.Equal(1, watchersCount(w), "Deployment should still be active") 859 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(matcher)) 860 } 861 862 // Tests that the watcher properly watches for allocation changes and takes the 863 // proper actions 864 func TestDeploymentWatcher_Watch_NoProgressDeadline(t *testing.T) { 865 t.Parallel() 866 require := require.New(t) 867 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 868 869 // Create a job, alloc, and a deployment 870 j := mock.Job() 871 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 872 j.TaskGroups[0].Update.MaxParallel = 2 873 j.TaskGroups[0].Update.AutoRevert = true 874 j.TaskGroups[0].Update.ProgressDeadline = 0 875 j.Stable = true 876 d := mock.Deployment() 877 d.JobID = j.ID 878 d.TaskGroups["web"].AutoRevert = true 879 a := mock.Alloc() 880 a.DeploymentID = d.ID 881 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 882 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 883 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 884 885 // Upsert the job again to get a new version 886 j2 := j.Copy() 887 // Modify the job to make its specification different 888 j2.Meta["foo"] = "bar" 889 j2.Stable = false 890 require.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 891 892 // require that we will get a update allocation call only once. This will 893 // verify that the watcher is batching allocation changes 894 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 895 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 896 897 // require that we get a call to UpsertDeploymentStatusUpdate 898 c := &matchDeploymentStatusUpdateConfig{ 899 DeploymentID: d.ID, 900 Status: structs.DeploymentStatusFailed, 901 StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0), 902 JobVersion: helper.Uint64ToPtr(0), 903 Eval: true, 904 } 905 m2 := matchDeploymentStatusUpdateRequest(c) 906 m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil) 907 908 w.SetEnabled(true, m.state) 909 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 910 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 911 912 // Update the allocs health to healthy which should create an evaluation 913 for i := 0; i < 5; i++ { 914 req := &structs.ApplyDeploymentAllocHealthRequest{ 915 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 916 DeploymentID: d.ID, 917 HealthyAllocationIDs: []string{a.ID}, 918 }, 919 } 920 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 921 } 922 923 // Wait for there to be one eval 924 testutil.WaitForResult(func() (bool, error) { 925 ws := memdb.NewWatchSet() 926 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 927 if err != nil { 928 return false, err 929 } 930 931 if l := len(evals); l != 1 { 932 return false, fmt.Errorf("Got %d evals; want 1", l) 933 } 934 935 return true, nil 936 }, func(err error) { 937 t.Fatal(err) 938 }) 939 940 // Update the allocs health to unhealthy which should create a job rollback, 941 // status update and eval 942 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 943 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 944 DeploymentID: d.ID, 945 UnhealthyAllocationIDs: []string{a.ID}, 946 }, 947 } 948 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 949 950 // Wait for there to be one eval 951 testutil.WaitForResult(func() (bool, error) { 952 ws := memdb.NewWatchSet() 953 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 954 if err != nil { 955 return false, err 956 } 957 958 if l := len(evals); l != 2 { 959 return false, fmt.Errorf("Got %d evals; want 1", l) 960 } 961 962 return true, nil 963 }, func(err error) { 964 t.Fatal(err) 965 }) 966 967 m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1)) 968 969 // After we upsert the job version will go to 2. So use this to require the 970 // original call happened. 971 c2 := &matchDeploymentStatusUpdateConfig{ 972 DeploymentID: d.ID, 973 Status: structs.DeploymentStatusFailed, 974 StatusDescription: structs.DeploymentStatusDescriptionRollback(structs.DeploymentStatusDescriptionFailedAllocations, 0), 975 JobVersion: helper.Uint64ToPtr(2), 976 Eval: true, 977 } 978 m3 := matchDeploymentStatusUpdateRequest(c2) 979 m.AssertCalled(t, "UpdateDeploymentStatus", mocker.MatchedBy(m3)) 980 testutil.WaitForResult(func() (bool, error) { return 0 == watchersCount(w), nil }, 981 func(err error) { require.Equal(0, watchersCount(w), "Should have no deployment") }) 982 } 983 984 func TestDeploymentWatcher_Watch_ProgressDeadline(t *testing.T) { 985 t.Parallel() 986 require := require.New(t) 987 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 988 989 // Create a job, alloc, and a deployment 990 j := mock.Job() 991 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 992 j.TaskGroups[0].Update.MaxParallel = 2 993 j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond 994 j.Stable = true 995 d := mock.Deployment() 996 d.JobID = j.ID 997 d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond 998 a := mock.Alloc() 999 now := time.Now() 1000 a.CreateTime = now.UnixNano() 1001 a.ModifyTime = now.UnixNano() 1002 a.DeploymentID = d.ID 1003 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1004 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1005 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 1006 1007 // require that we get a call to UpsertDeploymentStatusUpdate 1008 c := &matchDeploymentStatusUpdateConfig{ 1009 DeploymentID: d.ID, 1010 Status: structs.DeploymentStatusFailed, 1011 StatusDescription: structs.DeploymentStatusDescriptionProgressDeadline, 1012 Eval: true, 1013 } 1014 m2 := matchDeploymentStatusUpdateRequest(c) 1015 m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil) 1016 1017 w.SetEnabled(true, m.state) 1018 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1019 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1020 1021 // Update the alloc to be unhealthy and require that nothing happens. 1022 a2 := a.Copy() 1023 a2.DeploymentStatus = &structs.AllocDeploymentStatus{ 1024 Healthy: helper.BoolToPtr(false), 1025 Timestamp: now, 1026 } 1027 require.Nil(m.state.UpdateAllocsFromClient(100, []*structs.Allocation{a2})) 1028 1029 // Wait for the deployment to be failed 1030 testutil.WaitForResult(func() (bool, error) { 1031 d, err := m.state.DeploymentByID(nil, d.ID) 1032 if err != nil { 1033 return false, err 1034 } 1035 1036 return d.Status == structs.DeploymentStatusFailed, fmt.Errorf("bad status %q", d.Status) 1037 }, func(err error) { 1038 t.Fatal(err) 1039 }) 1040 1041 // require there are is only one evaluation 1042 testutil.WaitForResult(func() (bool, error) { 1043 ws := memdb.NewWatchSet() 1044 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 1045 if err != nil { 1046 return false, err 1047 } 1048 1049 if l := len(evals); l != 1 { 1050 return false, fmt.Errorf("Got %d evals; want 1", l) 1051 } 1052 1053 return true, nil 1054 }, func(err error) { 1055 t.Fatal(err) 1056 }) 1057 } 1058 1059 // Test that progress deadline handling works when there are multiple groups 1060 func TestDeploymentWatcher_ProgressCutoff(t *testing.T) { 1061 t.Parallel() 1062 require := require.New(t) 1063 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 1064 1065 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 1066 return true 1067 })).Return(nil).Maybe() 1068 1069 // Create a job, alloc, and a deployment 1070 j := mock.Job() 1071 j.TaskGroups[0].Count = 1 1072 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1073 j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond 1074 j.TaskGroups = append(j.TaskGroups, j.TaskGroups[0].Copy()) 1075 j.TaskGroups[1].Name = "foo" 1076 j.TaskGroups[1].Update.ProgressDeadline = 1 * time.Second 1077 j.Stable = true 1078 1079 d := mock.Deployment() 1080 d.JobID = j.ID 1081 d.TaskGroups["web"].DesiredTotal = 1 1082 d.TaskGroups["foo"] = d.TaskGroups["web"].Copy() 1083 d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond 1084 d.TaskGroups["foo"].ProgressDeadline = 1 * time.Second 1085 1086 a := mock.Alloc() 1087 now := time.Now() 1088 a.CreateTime = now.UnixNano() 1089 a.ModifyTime = now.UnixNano() 1090 a.DeploymentID = d.ID 1091 1092 a2 := mock.Alloc() 1093 a2.TaskGroup = "foo" 1094 a2.CreateTime = now.UnixNano() 1095 a2.ModifyTime = now.UnixNano() 1096 a2.DeploymentID = d.ID 1097 1098 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1099 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1100 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a, a2}), "UpsertAllocs") 1101 1102 // We may get an update for the desired transition. 1103 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 1104 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 1105 1106 w.SetEnabled(true, m.state) 1107 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1108 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1109 1110 watcher, err := w.getOrCreateWatcher(d.ID) 1111 require.NoError(err) 1112 require.NotNil(watcher) 1113 1114 d1, err := m.state.DeploymentByID(nil, d.ID) 1115 require.NoError(err) 1116 1117 done := watcher.doneGroups(d1) 1118 require.Contains(done, "web") 1119 require.False(done["web"]) 1120 require.Contains(done, "foo") 1121 require.False(done["foo"]) 1122 1123 cutoff1 := watcher.getDeploymentProgressCutoff(d1) 1124 require.False(cutoff1.IsZero()) 1125 1126 // Update the first allocation to be healthy 1127 a3 := a.Copy() 1128 a3.DeploymentStatus = &structs.AllocDeploymentStatus{Healthy: helper.BoolToPtr(true)} 1129 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a3}), "UpsertAllocs") 1130 1131 // Get the updated deployment 1132 d2, err := m.state.DeploymentByID(nil, d.ID) 1133 require.NoError(err) 1134 1135 done = watcher.doneGroups(d2) 1136 require.Contains(done, "web") 1137 require.True(done["web"]) 1138 require.Contains(done, "foo") 1139 require.False(done["foo"]) 1140 1141 cutoff2 := watcher.getDeploymentProgressCutoff(d2) 1142 require.False(cutoff2.IsZero()) 1143 require.True(cutoff1.UnixNano() < cutoff2.UnixNano()) 1144 1145 // Update the second allocation to be healthy 1146 a4 := a2.Copy() 1147 a4.DeploymentStatus = &structs.AllocDeploymentStatus{Healthy: helper.BoolToPtr(true)} 1148 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a4}), "UpsertAllocs") 1149 1150 // Get the updated deployment 1151 d3, err := m.state.DeploymentByID(nil, d.ID) 1152 require.NoError(err) 1153 1154 done = watcher.doneGroups(d3) 1155 require.Contains(done, "web") 1156 require.True(done["web"]) 1157 require.Contains(done, "foo") 1158 require.True(done["foo"]) 1159 1160 cutoff3 := watcher.getDeploymentProgressCutoff(d2) 1161 require.True(cutoff3.IsZero()) 1162 } 1163 1164 // Test that we will allow the progress deadline to be reached when the canaries 1165 // are healthy but we haven't promoted 1166 func TestDeploymentWatcher_Watch_ProgressDeadline_Canaries(t *testing.T) { 1167 t.Parallel() 1168 require := require.New(t) 1169 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 1170 1171 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 1172 return true 1173 })).Return(nil).Maybe() 1174 1175 // Create a job, alloc, and a deployment 1176 j := mock.Job() 1177 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1178 j.TaskGroups[0].Update.Canary = 1 1179 j.TaskGroups[0].Update.MaxParallel = 1 1180 j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond 1181 j.Stable = true 1182 d := mock.Deployment() 1183 d.StatusDescription = structs.DeploymentStatusDescriptionRunningNeedsPromotion 1184 d.JobID = j.ID 1185 d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond 1186 d.TaskGroups["web"].DesiredCanaries = 1 1187 a := mock.Alloc() 1188 now := time.Now() 1189 a.CreateTime = now.UnixNano() 1190 a.ModifyTime = now.UnixNano() 1191 a.DeploymentID = d.ID 1192 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1193 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1194 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 1195 1196 // require that we will get a createEvaluation call only once. This will 1197 // verify that the watcher is batching allocation changes 1198 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 1199 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 1200 1201 w.SetEnabled(true, m.state) 1202 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1203 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1204 1205 // Update the alloc to be unhealthy and require that nothing happens. 1206 a2 := a.Copy() 1207 a2.DeploymentStatus = &structs.AllocDeploymentStatus{ 1208 Healthy: helper.BoolToPtr(true), 1209 Timestamp: now, 1210 } 1211 require.Nil(m.state.UpdateAllocsFromClient(m.nextIndex(), []*structs.Allocation{a2})) 1212 1213 // Wait for the deployment to cross the deadline 1214 dout, err := m.state.DeploymentByID(nil, d.ID) 1215 require.NoError(err) 1216 require.NotNil(dout) 1217 state := dout.TaskGroups["web"] 1218 require.NotNil(state) 1219 time.Sleep(state.RequireProgressBy.Add(time.Second).Sub(now)) 1220 1221 // Require the deployment is still running 1222 dout, err = m.state.DeploymentByID(nil, d.ID) 1223 require.NoError(err) 1224 require.NotNil(dout) 1225 require.Equal(structs.DeploymentStatusRunning, dout.Status) 1226 require.Equal(structs.DeploymentStatusDescriptionRunningNeedsPromotion, dout.StatusDescription) 1227 1228 // require there are is only one evaluation 1229 testutil.WaitForResult(func() (bool, error) { 1230 ws := memdb.NewWatchSet() 1231 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 1232 if err != nil { 1233 return false, err 1234 } 1235 1236 if l := len(evals); l != 1 { 1237 return false, fmt.Errorf("Got %d evals; want 1", l) 1238 } 1239 1240 return true, nil 1241 }, func(err error) { 1242 t.Fatal(err) 1243 }) 1244 } 1245 1246 // Test that a promoted deployment with alloc healthy updates create 1247 // evals to move the deployment forward 1248 func TestDeploymentWatcher_PromotedCanary_UpdatedAllocs(t *testing.T) { 1249 t.Parallel() 1250 require := require.New(t) 1251 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 1252 1253 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 1254 return true 1255 })).Return(nil).Maybe() 1256 1257 // Create a job, alloc, and a deployment 1258 j := mock.Job() 1259 j.TaskGroups[0].Count = 2 1260 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1261 j.TaskGroups[0].Update.Canary = 1 1262 j.TaskGroups[0].Update.MaxParallel = 1 1263 j.TaskGroups[0].Update.ProgressDeadline = 50 * time.Millisecond 1264 j.Stable = true 1265 1266 d := mock.Deployment() 1267 d.TaskGroups["web"].DesiredTotal = 2 1268 d.TaskGroups["web"].DesiredCanaries = 1 1269 d.TaskGroups["web"].HealthyAllocs = 1 1270 d.StatusDescription = structs.DeploymentStatusDescriptionRunning 1271 d.JobID = j.ID 1272 d.TaskGroups["web"].ProgressDeadline = 50 * time.Millisecond 1273 d.TaskGroups["web"].RequireProgressBy = time.Now().Add(50 * time.Millisecond) 1274 1275 a := mock.Alloc() 1276 now := time.Now() 1277 a.CreateTime = now.UnixNano() 1278 a.ModifyTime = now.UnixNano() 1279 a.DeploymentID = d.ID 1280 a.DeploymentStatus = &structs.AllocDeploymentStatus{ 1281 Healthy: helper.BoolToPtr(true), 1282 Timestamp: now, 1283 } 1284 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1285 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1286 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 1287 1288 w.SetEnabled(true, m.state) 1289 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1290 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1291 1292 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 1293 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Twice() 1294 1295 // Create another alloc 1296 a2 := a.Copy() 1297 a2.ID = uuid.Generate() 1298 now = time.Now() 1299 a2.CreateTime = now.UnixNano() 1300 a2.ModifyTime = now.UnixNano() 1301 a2.DeploymentStatus = &structs.AllocDeploymentStatus{ 1302 Healthy: helper.BoolToPtr(true), 1303 Timestamp: now, 1304 } 1305 d.TaskGroups["web"].RequireProgressBy = time.Now().Add(2 * time.Second) 1306 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1307 // Wait until batch eval period passes before updating another alloc 1308 time.Sleep(1 * time.Second) 1309 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs") 1310 1311 // Wait for the deployment to cross the deadline 1312 dout, err := m.state.DeploymentByID(nil, d.ID) 1313 require.NoError(err) 1314 require.NotNil(dout) 1315 state := dout.TaskGroups["web"] 1316 require.NotNil(state) 1317 time.Sleep(state.RequireProgressBy.Add(time.Second).Sub(now)) 1318 1319 // There should be two evals 1320 testutil.WaitForResult(func() (bool, error) { 1321 ws := memdb.NewWatchSet() 1322 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 1323 if err != nil { 1324 return false, err 1325 } 1326 1327 if l := len(evals); l != 2 { 1328 return false, fmt.Errorf("Got %d evals; want 2", l) 1329 } 1330 1331 return true, nil 1332 }, func(err error) { 1333 t.Fatal(err) 1334 }) 1335 } 1336 1337 // Test scenario where deployment initially has no progress deadline 1338 // After the deployment is updated, a failed alloc's DesiredTransition should be set 1339 func TestDeploymentWatcher_Watch_StartWithoutProgressDeadline(t *testing.T) { 1340 t.Parallel() 1341 require := require.New(t) 1342 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 1343 1344 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 1345 return true 1346 })).Return(nil).Maybe() 1347 1348 // Create a job, and a deployment 1349 j := mock.Job() 1350 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1351 j.TaskGroups[0].Update.MaxParallel = 2 1352 j.TaskGroups[0].Update.ProgressDeadline = 500 * time.Millisecond 1353 j.Stable = true 1354 d := mock.Deployment() 1355 d.JobID = j.ID 1356 1357 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1358 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1359 1360 a := mock.Alloc() 1361 a.CreateTime = time.Now().UnixNano() 1362 a.DeploymentID = d.ID 1363 1364 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 1365 1366 d.TaskGroups["web"].ProgressDeadline = 500 * time.Millisecond 1367 // Update the deployment with a progress deadline 1368 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1369 1370 // Match on DesiredTransition set to Reschedule for the failed alloc 1371 m1 := matchUpdateAllocDesiredTransitionReschedule([]string{a.ID}) 1372 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 1373 1374 w.SetEnabled(true, m.state) 1375 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1376 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1377 1378 // Update the alloc to be unhealthy 1379 a2 := a.Copy() 1380 a2.DeploymentStatus = &structs.AllocDeploymentStatus{ 1381 Healthy: helper.BoolToPtr(false), 1382 Timestamp: time.Now(), 1383 } 1384 require.Nil(m.state.UpdateAllocsFromClient(m.nextIndex(), []*structs.Allocation{a2})) 1385 1386 // Wait for the alloc's DesiredState to set reschedule 1387 testutil.WaitForResult(func() (bool, error) { 1388 a, err := m.state.AllocByID(nil, a.ID) 1389 if err != nil { 1390 return false, err 1391 } 1392 dt := a.DesiredTransition 1393 shouldReschedule := dt.Reschedule != nil && *dt.Reschedule 1394 return shouldReschedule, fmt.Errorf("Desired Transition Reschedule should be set but got %v", shouldReschedule) 1395 }, func(err error) { 1396 t.Fatal(err) 1397 }) 1398 } 1399 1400 // Tests that the watcher fails rollback when the spec hasn't changed 1401 func TestDeploymentWatcher_RollbackFailed(t *testing.T) { 1402 t.Parallel() 1403 require := require.New(t) 1404 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Millisecond) 1405 1406 // Create a job, alloc, and a deployment 1407 j := mock.Job() 1408 j.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1409 j.TaskGroups[0].Update.MaxParallel = 2 1410 j.TaskGroups[0].Update.AutoRevert = true 1411 j.TaskGroups[0].Update.ProgressDeadline = 0 1412 j.Stable = true 1413 d := mock.Deployment() 1414 d.JobID = j.ID 1415 d.TaskGroups["web"].AutoRevert = true 1416 a := mock.Alloc() 1417 a.DeploymentID = d.ID 1418 require.Nil(m.state.UpsertJob(m.nextIndex(), j), "UpsertJob") 1419 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d), "UpsertDeployment") 1420 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a}), "UpsertAllocs") 1421 1422 // Upsert the job again to get a new version 1423 j2 := j.Copy() 1424 // Modify the job to make its specification different 1425 j2.Stable = false 1426 require.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob2") 1427 1428 // require that we will get a createEvaluation call only once. This will 1429 // verify that the watcher is batching allocation changes 1430 m1 := matchUpdateAllocDesiredTransitions([]string{d.ID}) 1431 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 1432 1433 // require that we get a call to UpsertDeploymentStatusUpdate with roll back failed as the status 1434 c := &matchDeploymentStatusUpdateConfig{ 1435 DeploymentID: d.ID, 1436 Status: structs.DeploymentStatusFailed, 1437 StatusDescription: structs.DeploymentStatusDescriptionRollbackNoop(structs.DeploymentStatusDescriptionFailedAllocations, 0), 1438 JobVersion: nil, 1439 Eval: true, 1440 } 1441 m2 := matchDeploymentStatusUpdateRequest(c) 1442 m.On("UpdateDeploymentStatus", mocker.MatchedBy(m2)).Return(nil) 1443 1444 w.SetEnabled(true, m.state) 1445 testutil.WaitForResult(func() (bool, error) { return 1 == watchersCount(w), nil }, 1446 func(err error) { require.Equal(1, watchersCount(w), "Should have 1 deployment") }) 1447 1448 // Update the allocs health to healthy which should create an evaluation 1449 for i := 0; i < 5; i++ { 1450 req := &structs.ApplyDeploymentAllocHealthRequest{ 1451 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 1452 DeploymentID: d.ID, 1453 HealthyAllocationIDs: []string{a.ID}, 1454 }, 1455 } 1456 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 1457 } 1458 1459 // Wait for there to be one eval 1460 testutil.WaitForResult(func() (bool, error) { 1461 ws := memdb.NewWatchSet() 1462 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 1463 if err != nil { 1464 return false, err 1465 } 1466 1467 if l := len(evals); l != 1 { 1468 return false, fmt.Errorf("Got %d evals; want 1", l) 1469 } 1470 1471 return true, nil 1472 }, func(err error) { 1473 t.Fatal(err) 1474 }) 1475 1476 // Update the allocs health to unhealthy which will cause attempting a rollback, 1477 // fail in that step, do status update and eval 1478 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 1479 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 1480 DeploymentID: d.ID, 1481 UnhealthyAllocationIDs: []string{a.ID}, 1482 }, 1483 } 1484 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 1485 1486 // Wait for there to be one eval 1487 testutil.WaitForResult(func() (bool, error) { 1488 ws := memdb.NewWatchSet() 1489 evals, err := m.state.EvalsByJob(ws, j.Namespace, j.ID) 1490 if err != nil { 1491 return false, err 1492 } 1493 1494 if l := len(evals); l != 2 { 1495 return false, fmt.Errorf("Got %d evals; want 1", l) 1496 } 1497 1498 return true, nil 1499 }, func(err error) { 1500 t.Fatal(err) 1501 }) 1502 1503 m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1)) 1504 1505 // verify that the job version hasn't changed after upsert 1506 m.state.JobByID(nil, structs.DefaultNamespace, j.ID) 1507 require.Equal(uint64(0), j.Version, "Expected job version 0 but got ", j.Version) 1508 } 1509 1510 // Test allocation updates and evaluation creation is batched between watchers 1511 func TestWatcher_BatchAllocUpdates(t *testing.T) { 1512 t.Parallel() 1513 require := require.New(t) 1514 w, m := testDeploymentWatcher(t, 1000.0, 1*time.Second) 1515 1516 m.On("UpdateDeploymentStatus", mocker.MatchedBy(func(args *structs.DeploymentStatusUpdateRequest) bool { 1517 return true 1518 })).Return(nil).Maybe() 1519 1520 // Create a job, alloc, for two deployments 1521 j1 := mock.Job() 1522 j1.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1523 j1.TaskGroups[0].Update.ProgressDeadline = 0 1524 d1 := mock.Deployment() 1525 d1.JobID = j1.ID 1526 a1 := mock.Alloc() 1527 a1.Job = j1 1528 a1.JobID = j1.ID 1529 a1.DeploymentID = d1.ID 1530 1531 j2 := mock.Job() 1532 j2.TaskGroups[0].Update = structs.DefaultUpdateStrategy.Copy() 1533 j2.TaskGroups[0].Update.ProgressDeadline = 0 1534 d2 := mock.Deployment() 1535 d2.JobID = j2.ID 1536 a2 := mock.Alloc() 1537 a2.Job = j2 1538 a2.JobID = j2.ID 1539 a2.DeploymentID = d2.ID 1540 1541 require.Nil(m.state.UpsertJob(m.nextIndex(), j1), "UpsertJob") 1542 require.Nil(m.state.UpsertJob(m.nextIndex(), j2), "UpsertJob") 1543 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d1), "UpsertDeployment") 1544 require.Nil(m.state.UpsertDeployment(m.nextIndex(), d2), "UpsertDeployment") 1545 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a1}), "UpsertAllocs") 1546 require.Nil(m.state.UpsertAllocs(m.nextIndex(), []*structs.Allocation{a2}), "UpsertAllocs") 1547 1548 // require that we will get a createEvaluation call only once and it contains 1549 // both deployments. This will verify that the watcher is batching 1550 // allocation changes 1551 m1 := matchUpdateAllocDesiredTransitions([]string{d1.ID, d2.ID}) 1552 m.On("UpdateAllocDesiredTransition", mocker.MatchedBy(m1)).Return(nil).Once() 1553 1554 w.SetEnabled(true, m.state) 1555 testutil.WaitForResult(func() (bool, error) { return 2 == watchersCount(w), nil }, 1556 func(err error) { require.Equal(2, watchersCount(w), "Should have 2 deployment") }) 1557 1558 // Update the allocs health to healthy which should create an evaluation 1559 req := &structs.ApplyDeploymentAllocHealthRequest{ 1560 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 1561 DeploymentID: d1.ID, 1562 HealthyAllocationIDs: []string{a1.ID}, 1563 }, 1564 } 1565 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req), "UpsertDeploymentAllocHealth") 1566 1567 req2 := &structs.ApplyDeploymentAllocHealthRequest{ 1568 DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{ 1569 DeploymentID: d2.ID, 1570 HealthyAllocationIDs: []string{a2.ID}, 1571 }, 1572 } 1573 require.Nil(m.state.UpdateDeploymentAllocHealth(m.nextIndex(), req2), "UpsertDeploymentAllocHealth") 1574 1575 // Wait for there to be one eval for each job 1576 testutil.WaitForResult(func() (bool, error) { 1577 ws := memdb.NewWatchSet() 1578 evals1, err := m.state.EvalsByJob(ws, j1.Namespace, j1.ID) 1579 if err != nil { 1580 return false, err 1581 } 1582 1583 evals2, err := m.state.EvalsByJob(ws, j2.Namespace, j2.ID) 1584 if err != nil { 1585 return false, err 1586 } 1587 1588 if l := len(evals1); l != 1 { 1589 return false, fmt.Errorf("Got %d evals for job %v; want 1", l, j1.ID) 1590 } 1591 1592 if l := len(evals2); l != 1 { 1593 return false, fmt.Errorf("Got %d evals for job 2; want 1", l) 1594 } 1595 1596 return true, nil 1597 }, func(err error) { 1598 t.Fatal(err) 1599 }) 1600 1601 m.AssertCalled(t, "UpdateAllocDesiredTransition", mocker.MatchedBy(m1)) 1602 testutil.WaitForResult(func() (bool, error) { return 2 == watchersCount(w), nil }, 1603 func(err error) { require.Equal(2, watchersCount(w), "Should have 2 deployment") }) 1604 } 1605 1606 func watchersCount(w *Watcher) int { 1607 w.l.Lock() 1608 defer w.l.Unlock() 1609 1610 return len(w.watchers) 1611 }