mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
feat: implement expiration policy logic for prebuilds (#17996)
## Summary This PR introduces support for expiration policies in prebuilds. The TTL (time-to-live) is retrieved from the Terraform configuration ([terraform-provider-coder PR](https://github.com/coder/terraform-provider-coder/pull/404)): ``` prebuilds = { instances = 2 expiration_policy { ttl = 86400 } } ``` **Note**: Since there is no need for precise TTL enforcement down to the second, in this implementation expired prebuilds are handled in a single reconciliation cycle: they are deleted, and new instances are created only if needed to match the desired count. ## Changes * The outcome of a reconciliation cycle is now expressed as a slice of reconciliation actions, instead of a single aggregated action. * Adjusted reconciliation logic to delete expired prebuilds and guarantee that the number of desired instances is correct. * Updated relevant data structures and methods to support expiration policies parameters. * Added documentation to `Prebuilt workspaces` page * Update `terraform-provider-coder` to version 2.5.0: https://github.com/coder/terraform-provider-coder/releases/tag/v2.5.0 Depends on: https://github.com/coder/terraform-provider-coder/pull/404 Fixes: https://github.com/coder/coder/issues/17916
This commit is contained in:
@ -23,6 +23,7 @@ type options struct {
|
||||
presetName string
|
||||
prebuiltWorkspaceID uuid.UUID
|
||||
workspaceName string
|
||||
ttl int32
|
||||
}
|
||||
|
||||
// templateID is common across all option sets.
|
||||
@ -34,6 +35,7 @@ const (
|
||||
optionSet0 = iota
|
||||
optionSet1
|
||||
optionSet2
|
||||
optionSet3
|
||||
)
|
||||
|
||||
var opts = map[uint]options{
|
||||
@ -61,6 +63,15 @@ var opts = map[uint]options{
|
||||
prebuiltWorkspaceID: uuid.UUID{33},
|
||||
workspaceName: "prebuilds2",
|
||||
},
|
||||
optionSet3: {
|
||||
templateID: templateID,
|
||||
templateVersionID: uuid.UUID{41},
|
||||
presetID: uuid.UUID{42},
|
||||
presetName: "my-preset",
|
||||
prebuiltWorkspaceID: uuid.UUID{43},
|
||||
workspaceName: "prebuilds3",
|
||||
ttl: 5, // seconds
|
||||
},
|
||||
}
|
||||
|
||||
// A new template version with a preset without prebuilds configured should result in no prebuilds being created.
|
||||
@ -82,10 +93,7 @@ func TestNoPrebuilds(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
validateState(t, prebuilds.ReconciliationState{ /*all zero values*/ }, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 0,
|
||||
}, *actions)
|
||||
validateActions(t, nil, actions)
|
||||
}
|
||||
|
||||
// A new template version with a preset with prebuilds configured should result in a new prebuild being created.
|
||||
@ -109,10 +117,12 @@ func TestNetNew(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Desired: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
// A new template version is created with a preset with prebuilds configured; this outdates the older version and
|
||||
@ -149,10 +159,12 @@ func TestOutdatedPrebuilds(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
|
||||
},
|
||||
}, actions)
|
||||
|
||||
// WHEN: calculating the current preset's state.
|
||||
ps, err = snapshot.FilterByPreset(current.presetID)
|
||||
@ -163,10 +175,12 @@ func TestOutdatedPrebuilds(t *testing.T) {
|
||||
actions, err = ps.CalculateActions(clock, backoffInterval)
|
||||
require.NoError(t, err)
|
||||
validateState(t, prebuilds.ReconciliationState{Desired: 1}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
// Make sure that outdated prebuild will be deleted, even if deletion of another outdated prebuild is already in progress.
|
||||
@ -214,10 +228,12 @@ func TestDeleteOutdatedPrebuilds(t *testing.T) {
|
||||
Deleting: 1,
|
||||
}, *state)
|
||||
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID},
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
// A new template version is created with a preset with prebuilds configured; while a prebuild is provisioning up or down,
|
||||
@ -233,7 +249,7 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired int32
|
||||
running int32
|
||||
inProgress int32
|
||||
checkFn func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions)
|
||||
checkFn func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions)
|
||||
}{
|
||||
// With no running prebuilds and one starting, no creations/deletions should take place.
|
||||
{
|
||||
@ -242,11 +258,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 1,
|
||||
running: 0,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Desired: 1, Starting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With one running prebuild and one starting, no creations/deletions should occur since we're approaching the correct state.
|
||||
@ -256,11 +270,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 2,
|
||||
running: 1,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Starting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With one running prebuild and one starting, no creations/deletions should occur
|
||||
@ -271,11 +283,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 2,
|
||||
running: 2,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Starting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With one prebuild desired and one stopping, a new prebuild will be created.
|
||||
@ -285,11 +295,13 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 1,
|
||||
running: 0,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Desired: 1, Stopping: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
},
|
||||
},
|
||||
@ -300,11 +312,13 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 3,
|
||||
running: 2,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 3, Stopping: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
},
|
||||
},
|
||||
@ -315,11 +329,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 3,
|
||||
running: 3,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 3, Desired: 3, Stopping: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With one prebuild desired and one deleting, a new prebuild will be created.
|
||||
@ -329,11 +341,13 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 1,
|
||||
running: 0,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Desired: 1, Deleting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
},
|
||||
},
|
||||
@ -344,11 +358,13 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 2,
|
||||
running: 1,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Deleting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
},
|
||||
},
|
||||
@ -359,11 +375,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 2,
|
||||
running: 2,
|
||||
inProgress: 1,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Deleting: 1}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With 3 prebuilds desired, 1 running, and 2 starting, no creations should occur since the builds are in progress.
|
||||
@ -373,9 +387,9 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 3,
|
||||
running: 1,
|
||||
inProgress: 2,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 3, Starting: 2}, state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{ActionType: prebuilds.ActionTypeCreate, Create: 0}, actions)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With 3 prebuilds desired, 5 running, and 2 deleting, no deletions should occur since the builds are in progress.
|
||||
@ -385,17 +399,20 @@ func TestInProgressActions(t *testing.T) {
|
||||
desired: 3,
|
||||
running: 5,
|
||||
inProgress: 2,
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) {
|
||||
checkFn: func(state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 5, Desired: 3, Deleting: 2, Extraneous: 2}
|
||||
expectedActions := prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
assert.EqualValuesf(t, expectedActions.ActionType, actions.ActionType, "'ActionType' did not match expectation")
|
||||
assert.Len(t, actions.DeleteIDs, 2, "'deleteIDs' did not match expectation")
|
||||
assert.EqualValuesf(t, expectedActions.Create, actions.Create, "'create' did not match expectation")
|
||||
assert.EqualValuesf(t, expectedActions.BackoffUntil, actions.BackoffUntil, "'BackoffUntil' did not match expectation")
|
||||
require.Equal(t, len(expectedActions), len(actions))
|
||||
assert.EqualValuesf(t, expectedActions[0].ActionType, actions[0].ActionType, "'ActionType' did not match expectation")
|
||||
assert.Len(t, actions[0].DeleteIDs, 2, "'deleteIDs' did not match expectation")
|
||||
assert.EqualValuesf(t, expectedActions[0].Create, actions[0].Create, "'create' did not match expectation")
|
||||
assert.EqualValuesf(t, expectedActions[0].BackoffUntil, actions[0].BackoffUntil, "'BackoffUntil' did not match expectation")
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -450,7 +467,7 @@ func TestInProgressActions(t *testing.T) {
|
||||
state := ps.CalculateState()
|
||||
actions, err := ps.CalculateActions(clock, backoffInterval)
|
||||
require.NoError(t, err)
|
||||
tc.checkFn(*state, *actions)
|
||||
tc.checkFn(*state, actions)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -496,10 +513,187 @@ func TestExtraneous(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 2, Desired: 1, Extraneous: 1, Eligible: 2,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{older},
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{older},
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
// A prebuild is considered Expired when it has exceeded their time-to-live (TTL)
|
||||
// specified in the preset's cache invalidation invalidate_after_secs parameter.
|
||||
func TestExpiredPrebuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
current := opts[optionSet3]
|
||||
clock := quartz.NewMock(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
running int32
|
||||
desired int32
|
||||
expired int32
|
||||
checkFn func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions)
|
||||
}{
|
||||
// With 2 running prebuilds, none of which are expired, and the desired count is met,
|
||||
// no deletions or creations should occur.
|
||||
{
|
||||
name: "no expired prebuilds - no actions taken",
|
||||
running: 2,
|
||||
desired: 2,
|
||||
expired: 0,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 0}, state)
|
||||
validateActions(t, nil, actions)
|
||||
},
|
||||
},
|
||||
// With 2 running prebuilds, 1 of which is expired, the expired prebuild should be deleted,
|
||||
// and one new prebuild should be created to maintain the desired count.
|
||||
{
|
||||
name: "one expired prebuild – deleted and replaced",
|
||||
running: 2,
|
||||
desired: 2,
|
||||
expired: 1,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 1}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID},
|
||||
},
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
// With 2 running prebuilds, both expired, both should be deleted,
|
||||
// and 2 new prebuilds created to match the desired count.
|
||||
{
|
||||
name: "all prebuilds expired – all deleted and recreated",
|
||||
running: 2,
|
||||
desired: 2,
|
||||
expired: 2,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 2}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID, runningPrebuilds[1].ID},
|
||||
},
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 2,
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
// With 4 running prebuilds, 2 of which are expired, and the desired count is 2,
|
||||
// the expired prebuilds should be deleted. No new creations are needed
|
||||
// since removing the expired ones brings actual = desired.
|
||||
{
|
||||
name: "expired prebuilds deleted to reach desired count",
|
||||
running: 4,
|
||||
desired: 2,
|
||||
expired: 2,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 4, Desired: 2, Expired: 2, Extraneous: 0}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID, runningPrebuilds[1].ID},
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
// With 4 running prebuilds (1 expired), and the desired count is 2,
|
||||
// the first action should delete the expired one,
|
||||
// and the second action should delete one additional (non-expired) prebuild
|
||||
// to eliminate the remaining excess.
|
||||
{
|
||||
name: "expired prebuild deleted first, then extraneous",
|
||||
running: 4,
|
||||
desired: 2,
|
||||
expired: 1,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 4, Desired: 2, Expired: 1, Extraneous: 1}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
// First action correspond to deleting the expired prebuild,
|
||||
// and the second action corresponds to deleting the extraneous prebuild
|
||||
// corresponding to the oldest one after the expired prebuild
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID},
|
||||
},
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[1].ID},
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// GIVEN: a preset.
|
||||
defaultPreset := preset(true, tc.desired, current)
|
||||
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
||||
defaultPreset,
|
||||
}
|
||||
|
||||
// GIVEN: running prebuilt workspaces for the preset.
|
||||
running := make([]database.GetRunningPrebuiltWorkspacesRow, 0, tc.running)
|
||||
expiredCount := 0
|
||||
ttlDuration := time.Duration(defaultPreset.Ttl.Int32)
|
||||
for range tc.running {
|
||||
name, err := prebuilds.GenerateName()
|
||||
require.NoError(t, err)
|
||||
prebuildCreateAt := time.Now()
|
||||
if int(tc.expired) > expiredCount {
|
||||
// Update the prebuild workspace createdAt to exceed its TTL (5 seconds)
|
||||
prebuildCreateAt = prebuildCreateAt.Add(-ttlDuration - 10*time.Second)
|
||||
expiredCount++
|
||||
}
|
||||
running = append(running, database.GetRunningPrebuiltWorkspacesRow{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
TemplateID: current.templateID,
|
||||
TemplateVersionID: current.templateVersionID,
|
||||
CurrentPresetID: uuid.NullUUID{UUID: current.presetID, Valid: true},
|
||||
Ready: false,
|
||||
CreatedAt: prebuildCreateAt,
|
||||
})
|
||||
}
|
||||
|
||||
// WHEN: calculating the current preset's state.
|
||||
snapshot := prebuilds.NewGlobalSnapshot(presets, running, nil, nil, nil)
|
||||
ps, err := snapshot.FilterByPreset(current.presetID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// THEN: we should identify that this prebuild is expired.
|
||||
state := ps.CalculateState()
|
||||
actions, err := ps.CalculateActions(clock, backoffInterval)
|
||||
require.NoError(t, err)
|
||||
tc.checkFn(running, *state, actions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A template marked as deprecated will not have prebuilds running.
|
||||
@ -536,10 +730,12 @@ func TestDeprecated(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{current.prebuiltWorkspaceID},
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{current.prebuiltWorkspaceID},
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
// If the latest build failed, backoff exponentially with the given interval.
|
||||
@ -587,10 +783,12 @@ func TestLatestBuildFailed(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 0, Desired: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeBackoff,
|
||||
BackoffUntil: lastBuildTime.Add(time.Duration(numFailed) * backoffInterval),
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeBackoff,
|
||||
BackoffUntil: lastBuildTime.Add(time.Duration(numFailed) * backoffInterval),
|
||||
},
|
||||
}, actions)
|
||||
|
||||
// WHEN: calculating the other preset's state.
|
||||
psOther, err := snapshot.FilterByPreset(other.presetID)
|
||||
@ -603,10 +801,7 @@ func TestLatestBuildFailed(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 1, Desired: 1, Eligible: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
BackoffUntil: time.Time{},
|
||||
}, *actions)
|
||||
validateActions(t, nil, actions)
|
||||
|
||||
// WHEN: the clock is advanced a backoff interval.
|
||||
clock.Advance(backoffInterval + time.Microsecond)
|
||||
@ -620,11 +815,12 @@ func TestLatestBuildFailed(t *testing.T) {
|
||||
validateState(t, prebuilds.ReconciliationState{
|
||||
Actual: 0, Desired: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1, // <--- NOTE: we're now able to create a new prebuild because the interval has elapsed.
|
||||
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1, // <--- NOTE: we're now able to create a new prebuild because the interval has elapsed.
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
|
||||
func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
|
||||
@ -684,10 +880,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
|
||||
Starting: 1,
|
||||
Desired: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 0,
|
||||
}, *actions)
|
||||
validateActions(t, nil, actions)
|
||||
}
|
||||
|
||||
// One prebuild has to be created for preset 2. Make sure preset 1 doesn't block preset 2.
|
||||
@ -703,14 +896,23 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
|
||||
Starting: 0,
|
||||
Desired: 1,
|
||||
}, *state)
|
||||
validateActions(t, prebuilds.ReconciliationActions{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
}, *actions)
|
||||
validateActions(t, []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}, actions)
|
||||
}
|
||||
}
|
||||
|
||||
func preset(active bool, instances int32, opts options, muts ...func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
|
||||
ttl := sql.NullInt32{}
|
||||
if opts.ttl > 0 {
|
||||
ttl = sql.NullInt32{
|
||||
Valid: true,
|
||||
Int32: opts.ttl,
|
||||
}
|
||||
}
|
||||
entry := database.GetTemplatePresetsWithPrebuildsRow{
|
||||
TemplateID: opts.templateID,
|
||||
TemplateVersionID: opts.templateVersionID,
|
||||
@ -723,6 +925,7 @@ func preset(active bool, instances int32, opts options, muts ...func(row databas
|
||||
},
|
||||
Deleted: false,
|
||||
Deprecated: false,
|
||||
Ttl: ttl,
|
||||
}
|
||||
|
||||
for _, mut := range muts {
|
||||
@ -758,6 +961,6 @@ func validateState(t *testing.T, expected, actual prebuilds.ReconciliationState)
|
||||
|
||||
// validateActions is a convenience func to make tests more readable; it exploits the fact that the default states for
|
||||
// prebuilds align with zero values.
|
||||
func validateActions(t *testing.T, expected, actual prebuilds.ReconciliationActions) {
|
||||
func validateActions(t *testing.T, expected, actual []*prebuilds.ReconciliationActions) {
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
Reference in New Issue
Block a user