Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
47cb6fc
feat: implement autoscaling mechanism for prebuilds
evgeniy-scherbina May 29, 2025
0f4d521
fix: precision issue with is-within-range function
evgeniy-scherbina Jun 13, 2025
b957eb6
test: fix is-within-range test
evgeniy-scherbina Jun 13, 2025
de920b8
test: minor fixes in is-within-range test
evgeniy-scherbina Jun 13, 2025
5d31b43
test: fix tests failed due to is-within-range function
evgeniy-scherbina Jun 13, 2025
b3130a8
fix: migration numbers
evgeniy-scherbina Jun 13, 2025
0565fb7
feat: define functions for validation of schedules
evgeniy-scherbina Jun 15, 2025
99d76e5
Merge remote-tracking branch 'origin/main' into yevhenii/prebuilds-au…
evgeniy-scherbina Jun 15, 2025
bdfcd9e
regenerate protofiles
evgeniy-scherbina Jun 15, 2025
ea7766f
test: improve test coverage for schedules-overlap function
evgeniy-scherbina Jun 15, 2025
1e78317
test: improve test coverage for validate-schedules function
evgeniy-scherbina Jun 16, 2025
5d99ef1
refactor: add logger to preset-snapshot
evgeniy-scherbina Jun 16, 2025
158b92e
refactor: fallback to default on error in calc-desired-instances
evgeniy-scherbina Jun 16, 2025
396d080
refactor: change signature of calc-desired-instances
evgeniy-scherbina Jun 16, 2025
12dba6c
fix: improve schedule validation
evgeniy-scherbina Jun 16, 2025
3fa98b2
fix: bug related to DOM and DOW interpretation
evgeniy-scherbina Jun 16, 2025
8a063ff
fix: add schedules validation
evgeniy-scherbina Jun 16, 2025
12d3d88
fix: use TimeRange function instead of Weekly
evgeniy-scherbina Jun 16, 2025
3facaf2
fix: linter
evgeniy-scherbina Jun 17, 2025
6ce7ff2
fix: migration numbers
evgeniy-scherbina Jun 17, 2025
178676f
Merge remote-tracking branch 'origin/main' into yevhenii/prebuilds-au…
evgeniy-scherbina Jun 17, 2025
368fb36
deps: update version of tf-provider-coder
evgeniy-scherbina Jun 17, 2025
44b67ae
refactor: use ValidateSchedules from tf-provider-coder
evgeniy-scherbina Jun 17, 2025
df202c2
fix: CR's fixes
evgeniy-scherbina Jun 17, 2025
affee62
Update coderd/database/dbgen/dbgen.go
evgeniy-scherbina Jun 17, 2025
909d950
refactor: rename instances to desired_instances for consistency
evgeniy-scherbina Jun 17, 2025
e08f8d2
fix: make gen
evgeniy-scherbina Jun 17, 2025
4da1e64
fix: migrations test
evgeniy-scherbina Jun 17, 2025
4d2557c
fix: optimize get-preset-prebuild-schedules query
evgeniy-scherbina Jun 17, 2025
faf9ec7
refactor: rename DB method
evgeniy-scherbina Jun 17, 2025
821eda7
refactor: rename autoscaling to scheduling
evgeniy-scherbina Jun 18, 2025
7617042
refactor: rename autoscaling to scheduling (migration files)
evgeniy-scherbina Jun 18, 2025
6c2350f
refactor: minor refactor after renaming
evgeniy-scherbina Jun 18, 2025
cacdb1f
deps: update version of tf-provider-coder
evgeniy-scherbina Jun 18, 2025
8d2c08e
gen: make gen
evgeniy-scherbina Jun 18, 2025
5600054
docs: document api changes in proto/version.go
evgeniy-scherbina Jun 18, 2025
2215a9e
test: extend convert-resources test with prebuilds.scheduling
evgeniy-scherbina Jun 18, 2025
69ab8e1
fix: make gen
evgeniy-scherbina Jun 19, 2025
4a29496
refactor: make fmt
evgeniy-scherbina Jun 19, 2025
8edb051
deps: update version of tf-provider-coder
evgeniy-scherbina Jun 19, 2025
954bf66
Merge remote-tracking branch 'origin/main' into yevhenii/prebuilds-au…
evgeniy-scherbina Jun 19, 2025
2d945cf
fix: migration numbers
evgeniy-scherbina Jun 19, 2025
533a2b5
refactor: fix formatting
evgeniy-scherbina Jun 19, 2025
2b7119a
fix: set scheduling_timezone to empty string by default
evgeniy-scherbina Jun 19, 2025
96caa18
refactor: improve logging
evgeniy-scherbina Jun 19, 2025
83872d6
refactor: improve logging
evgeniy-scherbina Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: change signature of calc-desired-instances
  • Loading branch information
evgeniy-scherbina committed Jun 16, 2025
commit 396d08006e28847eccace914c54f952a8502ca38
25 changes: 9 additions & 16 deletions coderd/prebuilds/preset_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ func MatchesCron(cronExpression string, at time.Time) (bool, error) {
// CalculateDesiredInstances returns the number of desired instances based on the provided time.
// If the time matches any defined autoscaling schedule, the corresponding number of instances is returned.
// Otherwise, it falls back to the default number of instances specified in the prebuild configuration.
func (p PresetSnapshot) CalculateDesiredInstances(at time.Time) (int32, error) {
func (p PresetSnapshot) CalculateDesiredInstances(at time.Time) int32 {
if len(p.PrebuildSchedules) == 0 {
// If no schedules are defined, fall back to the default desired instance count
return p.Preset.DesiredInstances.Int32, nil
return p.Preset.DesiredInstances.Int32
}

// Validate that the provided timezone is valid
Expand All @@ -144,7 +144,7 @@ func (p PresetSnapshot) CalculateDesiredInstances(at time.Time) (int32, error) {
slog.Error(err))

// If timezone is invalid, fall back to the default desired instance count
return p.Preset.DesiredInstances.Int32, nil
return p.Preset.DesiredInstances.Int32
}

// Look for a schedule whose cron expression matches the provided time
Expand All @@ -160,12 +160,12 @@ func (p PresetSnapshot) CalculateDesiredInstances(at time.Time) (int32, error) {
continue
}
if matches {
return schedule.Instances, nil
return schedule.Instances
}
}

// If no schedule matches, fall back to the default desired instance count
return p.Preset.DesiredInstances.Int32, nil
return p.Preset.DesiredInstances.Int32
}

// CalculateState computes the current state of prebuilds for a preset, including:
Expand All @@ -180,7 +180,7 @@ func (p PresetSnapshot) CalculateDesiredInstances(at time.Time) (int32, error) {
// and calculates appropriate counts based on the current state of running prebuilds and
// in-progress transitions. This state information is used to determine what reconciliation
// actions are needed to reach the desired state.
func (p PresetSnapshot) CalculateState() (*ReconciliationState, error) {
func (p PresetSnapshot) CalculateState() *ReconciliationState {
var (
actual int32
desired int32
Expand All @@ -196,11 +196,7 @@ func (p PresetSnapshot) CalculateState() (*ReconciliationState, error) {
expired = int32(len(p.Expired))

if p.isActive() {
var err error
desired, err = p.CalculateDesiredInstances(p.clock.Now())
if err != nil {
return nil, xerrors.Errorf("failed to calculate number of desired instances: %w", err)
}
desired = p.CalculateDesiredInstances(p.clock.Now())
eligible = p.countEligible()
extraneous = max(actual-expired-desired, 0)
}
Expand All @@ -217,7 +213,7 @@ func (p PresetSnapshot) CalculateState() (*ReconciliationState, error) {
Starting: starting,
Stopping: stopping,
Deleting: deleting,
}, nil
}
}

// CalculateActions determines what actions are needed to reconcile the current state with the desired state.
Expand Down Expand Up @@ -272,10 +268,7 @@ func (p PresetSnapshot) isActive() bool {
//
// The function returns a list of actions to be executed to achieve the desired state.
func (p PresetSnapshot) handleActiveTemplateVersion() (actions []*ReconciliationActions, err error) {
state, err := p.CalculateState()
if err != nil {
return nil, xerrors.Errorf("failed to calculate state: %w", err)
}
state := p.CalculateState()

// If we have expired prebuilds, delete them
if state.Expired > 0 {
Expand Down
51 changes: 17 additions & 34 deletions coderd/prebuilds/preset_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ func TestNoPrebuilds(t *testing.T) {
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand All @@ -113,8 +112,7 @@ func TestNetNew(t *testing.T) {
ps, err := snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand Down Expand Up @@ -157,8 +155,7 @@ func TestOutdatedPrebuilds(t *testing.T) {
require.NoError(t, err)

// THEN: we should identify that this prebuild is outdated and needs to be deleted.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand All @@ -176,8 +173,7 @@ func TestOutdatedPrebuilds(t *testing.T) {
require.NoError(t, err)

// THEN: we should not be blocked from creating a new prebuild while the outdate one deletes.
state, err = ps.CalculateState()
require.NoError(t, err)
state = ps.CalculateState()
actions, err = ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{Desired: 1}, *state)
Expand Down Expand Up @@ -226,8 +222,7 @@ func TestDeleteOutdatedPrebuilds(t *testing.T) {

// THEN: we should identify that this prebuild is outdated and needs to be deleted.
// Despite the fact that deletion of another outdated prebuild is already in progress.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand Down Expand Up @@ -471,8 +466,7 @@ func TestInProgressActions(t *testing.T) {
require.NoError(t, err)

// THEN: we should identify that this prebuild is in progress.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
tc.checkFn(*state, actions)
Expand Down Expand Up @@ -515,8 +509,7 @@ func TestExtraneous(t *testing.T) {
require.NoError(t, err)

// THEN: an extraneous prebuild is detected and marked for deletion.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand Down Expand Up @@ -697,8 +690,7 @@ func TestExpiredPrebuilds(t *testing.T) {
require.NoError(t, err)

// THEN: we should identify that this prebuild is expired.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
tc.checkFn(running, *state, actions)
Expand Down Expand Up @@ -734,8 +726,7 @@ func TestDeprecated(t *testing.T) {
require.NoError(t, err)

// THEN: all running prebuilds should be deleted because the template is deprecated.
state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand Down Expand Up @@ -788,8 +779,7 @@ func TestLatestBuildFailed(t *testing.T) {
require.NoError(t, err)

// THEN: reconciliation should backoff.
state, err := psCurrent.CalculateState()
require.NoError(t, err)
state := psCurrent.CalculateState()
actions, err := psCurrent.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand All @@ -807,8 +797,7 @@ func TestLatestBuildFailed(t *testing.T) {
require.NoError(t, err)

// THEN: it should NOT be in backoff because all is OK.
state, err = psOther.CalculateState()
require.NoError(t, err)
state = psOther.CalculateState()
actions, err = psOther.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand All @@ -822,8 +811,7 @@ func TestLatestBuildFailed(t *testing.T) {
// THEN: a new prebuild should be created.
psCurrent, err = snapshot.FilterByPreset(current.presetID)
require.NoError(t, err)
state, err = psCurrent.CalculateState()
require.NoError(t, err)
state = psCurrent.CalculateState()
actions, err = psCurrent.CalculateActions(clock, backoffInterval)
require.NoError(t, err)
validateState(t, prebuilds.ReconciliationState{
Expand Down Expand Up @@ -886,8 +874,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
ps, err := snapshot.FilterByPreset(presetOpts1.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand All @@ -903,8 +890,7 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
ps, err := snapshot.FilterByPreset(presetOpts2.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand Down Expand Up @@ -1008,8 +994,7 @@ func TestPrebuildAutoscaling(t *testing.T) {
ps, err := snapshot.FilterByPreset(presetOpts1.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand All @@ -1030,8 +1015,7 @@ func TestPrebuildAutoscaling(t *testing.T) {
ps, err := snapshot.FilterByPreset(presetOpts2.presetID)
require.NoError(t, err)

state, err := ps.CalculateState()
require.NoError(t, err)
state := ps.CalculateState()
actions, err := ps.CalculateActions(clock, backoffInterval)
require.NoError(t, err)

Expand Down Expand Up @@ -1402,8 +1386,7 @@ func TestCalculateDesiredInstances(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
desiredInstances, err := tc.snapshot.CalculateDesiredInstances(tc.at)
require.NoError(t, err)
desiredInstances := tc.snapshot.CalculateDesiredInstances(tc.at)
require.Equal(t, tc.expectedCalculatedInstances, desiredInstances)
})
}
Expand Down
6 changes: 1 addition & 5 deletions enterprise/coderd/prebuilds/metricscollector.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,7 @@ func (mc *MetricsCollector) Collect(metricsCh chan<- prometheus.Metric) {
mc.logger.Error(context.Background(), "failed to filter by preset", slog.Error(err))
continue
}
state, err := presetSnapshot.CalculateState()
if err != nil {
mc.logger.Error(context.Background(), "failed to calculate state for preset", slog.Error(err))
continue
}
state := presetSnapshot.CalculateState()

metricsCh <- prometheus.MustNewConstMetric(desiredPrebuildsDesc, prometheus.GaugeValue, float64(state.Desired), preset.TemplateName, preset.Name, preset.OrganizationName)
metricsCh <- prometheus.MustNewConstMetric(runningPrebuildsDesc, prometheus.GaugeValue, float64(state.Actual), preset.TemplateName, preset.Name, preset.OrganizationName)
Expand Down
11 changes: 2 additions & 9 deletions enterprise/coderd/prebuilds/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,11 +442,7 @@ func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.Pres
}
}

state, err := ps.CalculateState()
if err != nil {
logger.Error(ctx, "failed to calculate state for preset", slog.Error(err))
return err
}
state := ps.CalculateState()
actions, err := c.CalculateActions(ctx, ps)
if err != nil {
logger.Error(ctx, "failed to calculate actions for preset", slog.Error(err))
Expand Down Expand Up @@ -620,10 +616,7 @@ func (c *StoreReconciler) executeReconciliationAction(ctx context.Context, logge
// Unexpected things happen (i.e. bugs or bitflips); let's defend against disastrous outcomes.
// See https://blog.robertelder.org/causes-of-bit-flips-in-computer-memory/.
// This is obviously not comprehensive protection against this sort of problem, but this is one essential check.
desired, err := ps.CalculateDesiredInstances(c.clock.Now())
if err != nil {
return xerrors.Errorf("failed to calculate desired instances: %w", err)
}
desired := ps.CalculateDesiredInstances(c.clock.Now())

if action.Create > desired {
logger.Critical(ctx, "determined excessive count of prebuilds to create; clamping to desired count",
Expand Down
12 changes: 4 additions & 8 deletions enterprise/coderd/prebuilds/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1368,8 +1368,7 @@ func TestFailedBuildBackoff(t *testing.T) {
require.Len(t, snapshot.Presets, 1)
presetState, err := snapshot.FilterByPreset(preset.ID)
require.NoError(t, err)
state, err := presetState.CalculateState()
require.NoError(t, err)
state := presetState.CalculateState()
actions, err := reconciler.CalculateActions(ctx, *presetState)
require.NoError(t, err)
require.Equal(t, 1, len(actions))
Expand All @@ -1393,8 +1392,7 @@ func TestFailedBuildBackoff(t *testing.T) {
require.NoError(t, err)
presetState, err = snapshot.FilterByPreset(preset.ID)
require.NoError(t, err)
newState, err := presetState.CalculateState()
require.NoError(t, err)
newState := presetState.CalculateState()
newActions, err := reconciler.CalculateActions(ctx, *presetState)
require.NoError(t, err)
require.Equal(t, 1, len(newActions))
Expand All @@ -1412,8 +1410,7 @@ func TestFailedBuildBackoff(t *testing.T) {
require.NoError(t, err)
presetState, err = snapshot.FilterByPreset(preset.ID)
require.NoError(t, err)
state, err = presetState.CalculateState()
require.NoError(t, err)
state = presetState.CalculateState()
actions, err = reconciler.CalculateActions(ctx, *presetState)
require.NoError(t, err)
require.Equal(t, 1, len(actions))
Expand All @@ -1436,8 +1433,7 @@ func TestFailedBuildBackoff(t *testing.T) {
require.NoError(t, err)
presetState, err = snapshot.FilterByPreset(preset.ID)
require.NoError(t, err)
state, err = presetState.CalculateState()
require.NoError(t, err)
state = presetState.CalculateState()
actions, err = reconciler.CalculateActions(ctx, *presetState)
require.NoError(t, err)
require.Equal(t, 1, len(actions))
Expand Down
Loading