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  }