- Notifications
You must be signed in to change notification settings - Fork1.1k
feat: implement expiration policy logic for prebuilds#17996
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
a38ee7ca904d3fb0abd30f43ea2cc7e442c28a62747482bfb04c0e7c8e5caae4b1fbb530771cf266c44514be03f7a24eea7666090File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| package prebuilds | ||
| import ( | ||
| "time" | ||
| "github.com/google/uuid" | ||
| "golang.org/x/xerrors" | ||
| @@ -41,13 +43,17 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err | ||
| return nil, xerrors.Errorf("no preset found with ID %q", presetID) | ||
| } | ||
| // Only include workspaces that have successfully started | ||
| running := slice.Filter(s.RunningPrebuilds, func(prebuild database.GetRunningPrebuiltWorkspacesRow) bool { | ||
| if !prebuild.CurrentPresetID.Valid { | ||
| return false | ||
| } | ||
| return prebuild.CurrentPresetID.UUID == preset.ID | ||
| }) | ||
| // Separate running workspaces into non-expired and expired based on the preset's TTL | ||
| nonExpired, expired := filterExpiredWorkspaces(preset, running) | ||
| inProgress := slice.Filter(s.PrebuildsInProgress, func(prebuild database.CountInProgressPrebuildsRow) bool { | ||
| return prebuild.PresetID.UUID == preset.ID | ||
| }) | ||
| @@ -66,9 +72,33 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err | ||
| return &PresetSnapshot{ | ||
| Preset: preset, | ||
| Running: nonExpired, | ||
| Expired: expired, | ||
| InProgress: inProgress, | ||
| Backoff: backoffPtr, | ||
| IsHardLimited: isHardLimited, | ||
| }, nil | ||
| } | ||
| // filterExpiredWorkspaces splits running workspaces into expired and non-expired | ||
| // based on the preset's InvalidateAfterSecs TTL. If TTL is missing or zero, | ||
| // all workspaces are considered non-expired. | ||
| func filterExpiredWorkspaces(preset database.GetTemplatePresetsWithPrebuildsRow, runningWorkspaces []database.GetRunningPrebuiltWorkspacesRow) (nonExpired []database.GetRunningPrebuiltWorkspacesRow, expired []database.GetRunningPrebuiltWorkspacesRow) { | ||
ContributorAuthor
| ||
| if !preset.InvalidateAfterSecs.Valid { | ||
| return runningWorkspaces, expired | ||
| } | ||
| ttl := time.Duration(preset.InvalidateAfterSecs.Int32) * time.Second | ||
| if ttl <= 0 { | ||
| return runningWorkspaces, expired | ||
| } | ||
| for _, prebuild := range runningWorkspaces { | ||
| if time.Since(prebuild.CreatedAt) > ttl { | ||
| expired = append(expired, prebuild) | ||
| } else { | ||
| nonExpired = append(nonExpired, prebuild) | ||
| } | ||
| } | ||
| return nonExpired, expired | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -31,9 +31,14 @@ const ( | ||
| // PresetSnapshot is a filtered view of GlobalSnapshot focused on a single preset. | ||
| // It contains the raw data needed to calculate the current state of a preset's prebuilds, | ||
| // including running prebuilds, in-progress builds, and backoff information. | ||
| // - Running: prebuilds running and non-expired | ||
| // - Expired: prebuilds running and expired due to the preset's TTL | ||
| // - InProgress: prebuilds currently in progress | ||
| // - Backoff: holds failure info to decide if prebuild creation should be backed off | ||
| type PresetSnapshot struct { | ||
| Preset database.GetTemplatePresetsWithPrebuildsRow | ||
| Running []database.GetRunningPrebuiltWorkspacesRow | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I’m not a fan of the | ||
| Expired []database.GetRunningPrebuiltWorkspacesRow | ||
| InProgress []database.CountInProgressPrebuildsRow | ||
| Backoff *database.GetPresetsBackoffRow | ||
| IsHardLimited bool | ||
| @@ -43,10 +48,11 @@ type PresetSnapshot struct { | ||
| // calculated from a PresetSnapshot. While PresetSnapshot contains raw data, | ||
| // ReconciliationState contains derived metrics that are directly used to | ||
| // determine what actions are needed (create, delete, or backoff). | ||
| // For example, it calculates how many prebuilds areexpired, eligible, | ||
| //how many areextraneous, and how many are in various transition states. | ||
| type ReconciliationState struct { | ||
| Actual int32 // Number of currently valid running prebuilds, i.e., non-expired prebuilds | ||
ssncferreira marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| Expired int32 // Number of currently running prebuilds that exceeded their allowed time-to-live (TTL) | ||
| Desired int32 // Number of prebuilds desired as defined in the preset | ||
| Eligible int32 // Number of prebuilds that are ready to be claimed | ||
| Extraneous int32 // Number of extra running prebuilds beyond the desired count | ||
| @@ -78,7 +84,8 @@ func (ra *ReconciliationActions) IsNoop() bool { | ||
| } | ||
| // CalculateState computes the current state of prebuilds for a preset, including: | ||
| // - Actual: Number of currently valid running prebuilds, i.e., non-expired prebuilds | ||
| // - Expired: Number of currently running expired prebuilds | ||
| // - Desired: Number of prebuilds desired as defined in the preset | ||
| // - Eligible: Number of prebuilds that are ready to be claimed | ||
| // - Extraneous: Number of extra running prebuilds beyond the desired count | ||
| @@ -92,13 +99,17 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState { | ||
| var ( | ||
| actual int32 | ||
| desired int32 | ||
| expired int32 | ||
| eligible int32 | ||
| extraneous int32 | ||
| ) | ||
| // #nosec G115 - Safe conversion as p.Running slice length is expected to be within int32 range | ||
| actual = int32(len(p.Running)) | ||
| // #nosec G115 - Safe conversion as p.Expired slice length is expected to be within int32 range | ||
| expired = int32(len(p.Expired)) | ||
SasSwart marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| if p.isActive() { | ||
| desired = p.Preset.DesiredInstances.Int32 | ||
| eligible = p.countEligible() | ||
| @@ -109,6 +120,7 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState { | ||
| return &ReconciliationState{ | ||
| Actual: actual, | ||
| Expired: expired, | ||
| Desired: desired, | ||
| Eligible: eligible, | ||
| Extraneous: extraneous, | ||
| @@ -125,15 +137,16 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState { | ||
| // 2. If the preset is inactive (template version is not active), it will delete all running prebuilds | ||
| // 3. For active presets, it calculates the number of prebuilds to create or delete based on: | ||
| // - The desired number of instances | ||
| // - Currently running non-expired prebuilds | ||
| // - Currently running expired prebuilds | ||
| // - Prebuilds in transition states (starting/stopping/deleting) | ||
| // - Any extraneous prebuilds that need to be removed | ||
| // | ||
| // The function returns a ReconciliationActions struct that will have exactly one action type set: | ||
| // - ActionTypeBackoff: Only BackoffUntil is set, indicating when to retry | ||
| // - ActionTypeCreate: Only Create is set, indicating how many prebuilds to create | ||
| // - ActionTypeDelete: Only DeleteIDs is set, containing IDs of prebuilds to delete | ||
| func (p PresetSnapshot) CalculateActions(clock quartz.Clock, backoffInterval time.Duration) ([]*ReconciliationActions, error) { | ||
| // TODO: align workspace states with how we represent them on the FE and the CLI | ||
| // right now there's some slight differences which can lead to additional prebuilds being created | ||
| @@ -158,45 +171,74 @@ func (p PresetSnapshot) isActive() bool { | ||
| return p.Preset.UsingActiveVersion && !p.Preset.Deleted && !p.Preset.Deprecated | ||
| } | ||
| // handleActiveTemplateVersion determines the reconciliation actions for a preset with an active template version. | ||
| // It ensures the system moves towards the desired number of healthy prebuilds. | ||
| // | ||
| // The reconciliation follows this order: | ||
| // 1. Delete expired prebuilds: These are no longer valid and must be removed first. | ||
| // 2. Delete extraneous prebuilds: After expired ones are removed, if the number of running prebuilds (excluding expired) | ||
| // still exceeds the desired count, the oldest prebuilds are deleted to reduce excess. | ||
| // 3. Create missing prebuilds: If the number of non-expired, non-starting prebuilds is still below the desired count, | ||
| // create the necessary number of prebuilds to reach the target. | ||
| // | ||
| // The function returns a list of actions to be executed to achieve the desired state. | ||
| func (p PresetSnapshot) handleActiveTemplateVersion() (actions []*ReconciliationActions, err error) { | ||
| state := p.CalculateState() | ||
| // If we have expired prebuilds, delete them | ||
| if state.Expired > 0 { | ||
| var deleteIDs []uuid.UUID | ||
| for _, expired := range p.Expired { | ||
| deleteIDs = append(deleteIDs, expired.ID) | ||
| } | ||
| actions = append(actions, | ||
| &ReconciliationActions{ | ||
| ActionType: ActionTypeDelete, | ||
| DeleteIDs: deleteIDs, | ||
| }) | ||
| } | ||
| // If we still have more prebuilds than desired, delete the oldest ones | ||
| if state.Extraneous > 0 { | ||
| actions = append(actions, | ||
| &ReconciliationActions{ | ||
| ActionType: ActionTypeDelete, | ||
| DeleteIDs: p.getOldestPrebuildIDs(int(state.Extraneous)), | ||
| }) | ||
| } | ||
| // Calculate how many new prebuilds we need to create | ||
| // We subtract starting prebuilds since they're already being created | ||
| prebuildsToCreate := max(state.Desired-state.Actual-state.Starting, 0) | ||
| if prebuildsToCreate > 0 { | ||
| actions = append(actions, | ||
| &ReconciliationActions{ | ||
| ActionType: ActionTypeCreate, | ||
| Create: prebuildsToCreate, | ||
| }) | ||
| } | ||
| return actions, nil | ||
| } | ||
| // handleInactiveTemplateVersion deletes all running prebuilds except those already being deleted | ||
| // to avoid duplicate deletion attempts. | ||
| func (p PresetSnapshot) handleInactiveTemplateVersion() ([]*ReconciliationActions, error) { | ||
| prebuildsToDelete := len(p.Running) | ||
| deleteIDs := p.getOldestPrebuildIDs(prebuildsToDelete) | ||
| return []*ReconciliationActions{ | ||
| { | ||
| ActionType: ActionTypeDelete, | ||
| DeleteIDs: deleteIDs, | ||
| }, | ||
| }, nil | ||
| } | ||
| // needsBackoffPeriod checks if we should delay prebuild creation due to recent failures. | ||
| // If there were failures, it calculates a backoff period based on the number of failures | ||
| // and returns true if we're still within that period. | ||
| func (p PresetSnapshot) needsBackoffPeriod(clock quartz.Clock, backoffInterval time.Duration) ([]*ReconciliationActions, bool) { | ||
| if p.Backoff == nil || p.Backoff.NumFailed == 0 { | ||
| return nil, false | ||
| } | ||
| @@ -205,9 +247,11 @@ func (p PresetSnapshot) needsBackoffPeriod(clock quartz.Clock, backoffInterval t | ||
| return nil, false | ||
| } | ||
| return []*ReconciliationActions{ | ||
| { | ||
| ActionType: ActionTypeBackoff, | ||
| BackoffUntil: backoffUntil, | ||
| }, | ||
| }, true | ||
| } | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.