Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6f6e73a

Browse files
authored
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-coderPR](coder/terraform-provider-coder#404)):```prebuilds = { instances = 2 expiration_policy { ttl = 86400 } }```**Note**: Since there is no need for precise TTL enforcement down to thesecond, in this implementation expired prebuilds are handled in a singlereconciliation cycle: they are deleted, and new instances are createdonly if needed to match the desired count.## Changes* The outcome of a reconciliation cycle is now expressed as a slice ofreconciliation actions, instead of a single aggregated action.* Adjusted reconciliation logic to delete expired prebuilds andguarantee that the number of desired instances is correct.* Updated relevant data structures and methods to support expirationpolicies 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.0Depends on:coder/terraform-provider-coder#404Fixes:#17916
1 parent589f186 commit6f6e73a

File tree

18 files changed

+1501
-992
lines changed

18 files changed

+1501
-992
lines changed

‎coderd/database/queries.sql.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries/prebuilds.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ SELECT
3535
tvp.id,
3636
tvp.name,
3737
tvp.desired_instancesAS desired_instances,
38+
tvp.invalidate_after_secsAS ttl,
3839
tvp.prebuild_status,
3940
t.deleted,
4041
t.deprecated!=''AS deprecated

‎coderd/prebuilds/global_snapshot.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package prebuilds
22

33
import (
4+
"time"
5+
46
"github.com/google/uuid"
57
"golang.org/x/xerrors"
68

@@ -41,13 +43,17 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err
4143
returnnil,xerrors.Errorf("no preset found with ID %q",presetID)
4244
}
4345

46+
// Only include workspaces that have successfully started
4447
running:=slice.Filter(s.RunningPrebuilds,func(prebuild database.GetRunningPrebuiltWorkspacesRow)bool {
4548
if!prebuild.CurrentPresetID.Valid {
4649
returnfalse
4750
}
4851
returnprebuild.CurrentPresetID.UUID==preset.ID
4952
})
5053

54+
// Separate running workspaces into non-expired and expired based on the preset's TTL
55+
nonExpired,expired:=filterExpiredWorkspaces(preset,running)
56+
5157
inProgress:=slice.Filter(s.PrebuildsInProgress,func(prebuild database.CountInProgressPrebuildsRow)bool {
5258
returnprebuild.PresetID.UUID==preset.ID
5359
})
@@ -66,9 +72,33 @@ func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, err
6672

6773
return&PresetSnapshot{
6874
Preset:preset,
69-
Running:running,
75+
Running:nonExpired,
76+
Expired:expired,
7077
InProgress:inProgress,
7178
Backoff:backoffPtr,
7279
IsHardLimited:isHardLimited,
7380
},nil
7481
}
82+
83+
// filterExpiredWorkspaces splits running workspaces into expired and non-expired
84+
// based on the preset's TTL.
85+
// If TTL is missing or zero, all workspaces are considered non-expired.
86+
funcfilterExpiredWorkspaces(preset database.GetTemplatePresetsWithPrebuildsRow,runningWorkspaces []database.GetRunningPrebuiltWorkspacesRow) (nonExpired []database.GetRunningPrebuiltWorkspacesRow,expired []database.GetRunningPrebuiltWorkspacesRow) {
87+
if!preset.Ttl.Valid {
88+
returnrunningWorkspaces,expired
89+
}
90+
91+
ttl:=time.Duration(preset.Ttl.Int32)*time.Second
92+
ifttl<=0 {
93+
returnrunningWorkspaces,expired
94+
}
95+
96+
for_,prebuild:=rangerunningWorkspaces {
97+
iftime.Since(prebuild.CreatedAt)>ttl {
98+
expired=append(expired,prebuild)
99+
}else {
100+
nonExpired=append(nonExpired,prebuild)
101+
}
102+
}
103+
returnnonExpired,expired
104+
}

‎coderd/prebuilds/preset_snapshot.go

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@ const (
3131
// PresetSnapshot is a filtered view of GlobalSnapshot focused on a single preset.
3232
// It contains the raw data needed to calculate the current state of a preset's prebuilds,
3333
// including running prebuilds, in-progress builds, and backoff information.
34+
// - Running: prebuilds running and non-expired
35+
// - Expired: prebuilds running and expired due to the preset's TTL
36+
// - InProgress: prebuilds currently in progress
37+
// - Backoff: holds failure info to decide if prebuild creation should be backed off
3438
typePresetSnapshotstruct {
3539
Preset database.GetTemplatePresetsWithPrebuildsRow
3640
Running []database.GetRunningPrebuiltWorkspacesRow
41+
Expired []database.GetRunningPrebuiltWorkspacesRow
3742
InProgress []database.CountInProgressPrebuildsRow
3843
Backoff*database.GetPresetsBackoffRow
3944
IsHardLimitedbool
@@ -43,10 +48,11 @@ type PresetSnapshot struct {
4348
// calculated from a PresetSnapshot. While PresetSnapshot contains raw data,
4449
// ReconciliationState contains derived metrics that are directly used to
4550
// determine what actions are needed (create, delete, or backoff).
46-
// For example, it calculates how many prebuilds areeligible, how many are
47-
// extraneous, and how many are in various transition states.
51+
// For example, it calculates how many prebuilds areexpired, eligible,
52+
//how many areextraneous, and how many are in various transition states.
4853
typeReconciliationStatestruct {
49-
Actualint32// Number of currently running prebuilds
54+
Actualint32// Number of currently running prebuilds, i.e., non-expired, expired and extraneous prebuilds
55+
Expiredint32// Number of currently running prebuilds that exceeded their allowed time-to-live (TTL)
5056
Desiredint32// Number of prebuilds desired as defined in the preset
5157
Eligibleint32// Number of prebuilds that are ready to be claimed
5258
Extraneousint32// Number of extra running prebuilds beyond the desired count
@@ -78,7 +84,8 @@ func (ra *ReconciliationActions) IsNoop() bool {
7884
}
7985

8086
// CalculateState computes the current state of prebuilds for a preset, including:
81-
// - Actual: Number of currently running prebuilds
87+
// - Actual: Number of currently running prebuilds, i.e., non-expired and expired prebuilds
88+
// - Expired: Number of currently running expired prebuilds
8289
// - Desired: Number of prebuilds desired as defined in the preset
8390
// - Eligible: Number of prebuilds that are ready to be claimed
8491
// - Extraneous: Number of extra running prebuilds beyond the desired count
@@ -92,23 +99,28 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState {
9299
var (
93100
actualint32
94101
desiredint32
102+
expiredint32
95103
eligibleint32
96104
extraneousint32
97105
)
98106

99-
// #nosec G115 - Safe conversion as p.Running slice length is expected to be within int32 range
100-
actual=int32(len(p.Running))
107+
// #nosec G115 - Safe conversion as p.Running and p.Expired slice length is expected to be within int32 range
108+
actual=int32(len(p.Running)+len(p.Expired))
109+
110+
// #nosec G115 - Safe conversion as p.Expired slice length is expected to be within int32 range
111+
expired=int32(len(p.Expired))
101112

102113
ifp.isActive() {
103114
desired=p.Preset.DesiredInstances.Int32
104115
eligible=p.countEligible()
105-
extraneous=max(actual-desired,0)
116+
extraneous=max(actual-expired-desired,0)
106117
}
107118

108119
starting,stopping,deleting:=p.countInProgress()
109120

110121
return&ReconciliationState{
111122
Actual:actual,
123+
Expired:expired,
112124
Desired:desired,
113125
Eligible:eligible,
114126
Extraneous:extraneous,
@@ -126,14 +138,15 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState {
126138
// 3. For active presets, it calculates the number of prebuilds to create or delete based on:
127139
// - The desired number of instances
128140
// - Currently running prebuilds
141+
// - Currently running expired prebuilds
129142
// - Prebuilds in transition states (starting/stopping/deleting)
130143
// - Any extraneous prebuilds that need to be removed
131144
//
132145
// The function returns a ReconciliationActions struct that will have exactly one action type set:
133146
// - ActionTypeBackoff: Only BackoffUntil is set, indicating when to retry
134147
// - ActionTypeCreate: Only Create is set, indicating how many prebuilds to create
135148
// - ActionTypeDelete: Only DeleteIDs is set, containing IDs of prebuilds to delete
136-
func (pPresetSnapshot)CalculateActions(clock quartz.Clock,backoffInterval time.Duration) (*ReconciliationActions,error) {
149+
func (pPresetSnapshot)CalculateActions(clock quartz.Clock,backoffInterval time.Duration) ([]*ReconciliationActions,error) {
137150
// TODO: align workspace states with how we represent them on the FE and the CLI
138151
// right now there's some slight differences which can lead to additional prebuilds being created
139152

@@ -158,45 +171,77 @@ func (p PresetSnapshot) isActive() bool {
158171
returnp.Preset.UsingActiveVersion&&!p.Preset.Deleted&&!p.Preset.Deprecated
159172
}
160173

161-
// handleActiveTemplateVersion deletes excess prebuilds if there are too many,
162-
// otherwise creates new ones to reach the desired count.
163-
func (pPresetSnapshot)handleActiveTemplateVersion() (*ReconciliationActions,error) {
174+
// handleActiveTemplateVersion determines the reconciliation actions for a preset with an active template version.
175+
// It ensures the system moves towards the desired number of healthy prebuilds.
176+
//
177+
// The reconciliation follows this order:
178+
// 1. Delete expired prebuilds: These are no longer valid and must be removed first.
179+
// 2. Delete extraneous prebuilds: After expired ones are removed, if the number of running non-expired prebuilds
180+
// still exceeds the desired count, the oldest prebuilds are deleted to reduce excess.
181+
// 3. Create missing prebuilds: If the number of non-expired, non-starting prebuilds is still below the desired count,
182+
// create the necessary number of prebuilds to reach the target.
183+
//
184+
// The function returns a list of actions to be executed to achieve the desired state.
185+
func (pPresetSnapshot)handleActiveTemplateVersion() (actions []*ReconciliationActions,errerror) {
164186
state:=p.CalculateState()
165187

166-
// If we have more prebuilds than desired, delete the oldest ones
188+
// If we have expired prebuilds, delete them
189+
ifstate.Expired>0 {
190+
vardeleteIDs []uuid.UUID
191+
for_,expired:=rangep.Expired {
192+
deleteIDs=append(deleteIDs,expired.ID)
193+
}
194+
actions=append(actions,
195+
&ReconciliationActions{
196+
ActionType:ActionTypeDelete,
197+
DeleteIDs:deleteIDs,
198+
})
199+
}
200+
201+
// If we still have more prebuilds than desired, delete the oldest ones
167202
ifstate.Extraneous>0 {
168-
return&ReconciliationActions{
169-
ActionType:ActionTypeDelete,
170-
DeleteIDs:p.getOldestPrebuildIDs(int(state.Extraneous)),
171-
},nil
203+
actions=append(actions,
204+
&ReconciliationActions{
205+
ActionType:ActionTypeDelete,
206+
DeleteIDs:p.getOldestPrebuildIDs(int(state.Extraneous)),
207+
})
172208
}
173209

210+
// Number of running prebuilds excluding the recently deleted Expired
211+
runningValid:=state.Actual-state.Expired
212+
174213
// Calculate how many new prebuilds we need to create
175214
// We subtract starting prebuilds since they're already being created
176-
prebuildsToCreate:=max(state.Desired-state.Actual-state.Starting,0)
215+
prebuildsToCreate:=max(state.Desired-runningValid-state.Starting,0)
216+
ifprebuildsToCreate>0 {
217+
actions=append(actions,
218+
&ReconciliationActions{
219+
ActionType:ActionTypeCreate,
220+
Create:prebuildsToCreate,
221+
})
222+
}
177223

178-
return&ReconciliationActions{
179-
ActionType:ActionTypeCreate,
180-
Create:prebuildsToCreate,
181-
},nil
224+
returnactions,nil
182225
}
183226

184227
// handleInactiveTemplateVersion deletes all running prebuilds except those already being deleted
185228
// to avoid duplicate deletion attempts.
186-
func (pPresetSnapshot)handleInactiveTemplateVersion() (*ReconciliationActions,error) {
229+
func (pPresetSnapshot)handleInactiveTemplateVersion() ([]*ReconciliationActions,error) {
187230
prebuildsToDelete:=len(p.Running)
188231
deleteIDs:=p.getOldestPrebuildIDs(prebuildsToDelete)
189232

190-
return&ReconciliationActions{
191-
ActionType:ActionTypeDelete,
192-
DeleteIDs:deleteIDs,
233+
return []*ReconciliationActions{
234+
{
235+
ActionType:ActionTypeDelete,
236+
DeleteIDs:deleteIDs,
237+
},
193238
},nil
194239
}
195240

196241
// needsBackoffPeriod checks if we should delay prebuild creation due to recent failures.
197242
// If there were failures, it calculates a backoff period based on the number of failures
198243
// and returns true if we're still within that period.
199-
func (pPresetSnapshot)needsBackoffPeriod(clock quartz.Clock,backoffInterval time.Duration) (*ReconciliationActions,bool) {
244+
func (pPresetSnapshot)needsBackoffPeriod(clock quartz.Clock,backoffInterval time.Duration) ([]*ReconciliationActions,bool) {
200245
ifp.Backoff==nil||p.Backoff.NumFailed==0 {
201246
returnnil,false
202247
}
@@ -205,9 +250,11 @@ func (p PresetSnapshot) needsBackoffPeriod(clock quartz.Clock, backoffInterval t
205250
returnnil,false
206251
}
207252

208-
return&ReconciliationActions{
209-
ActionType:ActionTypeBackoff,
210-
BackoffUntil:backoffUntil,
253+
return []*ReconciliationActions{
254+
{
255+
ActionType:ActionTypeBackoff,
256+
BackoffUntil:backoffUntil,
257+
},
211258
},true
212259
}
213260

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp