Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions api/v1alpha1/coderworkspacepoolmember_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ type CoderWorkspacePoolMemberStatus struct {
// BindExtended is true after the one deadline extension at bind.
// +optional
BindExtended bool `json:"bindExtended,omitempty"`
// BindExtendDeadline is the deadline that the extension at bind asks
// Coder for. The controller stores it before the first extend request.
// While BindExtended is false, a Coder deadline at or after it proves
// that the extension happened.
// +optional
BindExtendDeadline *metav1.Time `json:"bindExtendDeadline,omitempty"`
// DeleteBuildID is the ID of the current delete build.
// +optional
DeleteBuildID string `json:"deleteBuildID,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions config/crd/dormant/coder.com_coderworkspacepoolmembers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ spec:
CoderWorkspacePoolMemberStatus defines the observed state of a
CoderWorkspacePoolMember.
properties:
bindExtendDeadline:
description: |-
BindExtendDeadline is the deadline that the extension at bind asks
Coder for. The controller stores it before the first extend request.
While BindExtended is false, a Coder deadline at or after it proves
that the extension happened.
format: date-time
type: string
bindExtended:
description: BindExtended is true after the one deadline extension
at bind.
Expand Down
91 changes: 69 additions & 22 deletions internal/controller/coderworkspacepoolmember_extend.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ func (r *CoderWorkspacePoolMemberReconciler) keepWarm(
if step != nil || err != nil {
return step, err
}
deadline := workspace.LatestBuild.Deadline
build := workspace.LatestBuild
deadline, target := build.Deadline, member.Status.BindExtendDeadline
switch {
case (!deadline.Valid || deadline.Time.IsZero()) && bound:
member.Status.BindExtended = true
return coderWait(reason, "Workspace %s has no autostop deadline, so the bind needs no extension.", name), nil
case !deadline.Valid || deadline.Time.IsZero():
return coderWait(reason, "Workspace %s is ready and has no autostop deadline.", name), nil
case bound && target != nil && !deadline.Time.Before(target.Time):
// An earlier extend request took effect, even if its status write
// was lost.
member.Status.BindExtended = true
return poolMemberBound(member, deadline.Time), nil
case !bound && deadline.Time.Sub(now) >= poolMemberExtendBefore:
return coderWait(reason, "Every agent of workspace %s is ready.", name), nil
}
Expand All @@ -63,49 +69,90 @@ func (r *CoderWorkspacePoolMemberReconciler) keepWarm(
return nil, err
}
// The reads can take a while, so the new deadline counts from a fresh
// clock reading. Coder refuses one less than about 30 minutes ahead.
now = r.Clock.Now()
extendTo := now.Add(max(time.Duration(template.DefaultTTLMillis)*time.Millisecond, poolMemberMinExtension))
if bound && !deadline.Time.Before(extendTo) {
// clock reading.
extendTo := func() (time.Time, bool) {
now = r.Clock.Now() // lastExtendTime is this reading.
return poolMemberExtendTo(now, time.Duration(template.DefaultTTLMillis)*time.Millisecond, build.MaxDeadline)
}
to, ok := extendTo()
switch {
case bound && !deadline.Time.Before(to):
// The workspace already has a full TTL: an extension would shorten it.
member.Status.BindExtended = true
return coderWait(reason, "Workspace %s is bound to claim %s. Its deadline %s needs no extension.",
name, member.Spec.ClaimRef.Name, deadline.Time.UTC().Format(time.RFC3339)), nil
}
if bound {
// At most one extension at bind: store the flag before the request,
// so a lost status write after it never leads to a second one.
case !ok && bound:
member.Status.BindExtended = true
return coderWait(reason, "Workspace %s is bound to claim %s. Its max deadline %s allows no extension.",
name, member.Spec.ClaimRef.Name, build.MaxDeadline.Time.UTC().Format(time.RFC3339)), nil
case !ok:
// Coder would refuse the request. Rule 2 replaces the member after
// Coder stops the workspace.
return coderWait("MaxDeadline", "Coder stops workspace %s at its max deadline %s, which allows no extension.",
name, build.MaxDeadline.Time.UTC().Format(time.RFC3339)), nil
}
if bound && target == nil {
// Store the target before the request: a later read then proves
// whether the extension happened, whatever status write is lost.
member.Status.BindExtendDeadline = &metav1.Time{Time: to}
if err := r.writeStatus(ctx, member, before); err != nil {
return nil, err
}
*before = *member.Status.DeepCopy()
}
err = sdk.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{Deadline: extendTo})
if bound {
// The write can take a while: count again from a fresh clock
// reading. Never ask for less than the stored target, so that a
// successful request always proves the extension.
to, _ = extendTo()
if stored := member.Status.BindExtendDeadline.Time; to.Before(stored) {
to = stored
}
}
err = sdk.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{Deadline: to})
switch status := coderStatus(err); {
case err == nil:
case status == http.StatusConflict:
// The workspace changed after the read, for example a new build.
// Read again at the next poll.
member.Status.BindExtended = false
return coderWait(reason, "Coder could not extend workspace %s now: %s", name, coderErrorSummary(err)), nil
case status == http.StatusBadRequest:
// For example the template's max deadline. The deadline stays, and
// a bound member does not try again.
member.Status.BindExtended = bound
// The new deadline was too soon for Coder's clock, or past a max
// deadline that changed after the read. The deadline stays, and a
// bound member does not try again.
if bound {
member.Status.BindExtended = true
} else {
reason = "ExtendRefused"
}
return coderWait(reason, "Coder refused to extend workspace %s: %s", name, coderErrorSummary(err)), nil
default:
// Only a request that provably changed nothing is sent again. After
// an uncertain result, the bind extension counts as done.
if createHadNoEffect(err) {
member.Status.BindExtended = false
}
// The next poll reads the deadline: a bound member then sees
// whether the request took effect.
return coderStepUnavailable("extend workspace", err), nil
}
deadlineText := extendTo.UTC().Format(time.RFC3339)
if bound {
return coderWait(reason, "Workspace %s is bound to claim %s. Its deadline is now %s.", name, member.Spec.ClaimRef.Name, deadlineText), nil
member.Status.BindExtended = true
return poolMemberBound(member, to), nil
}
member.Status.LastExtendTime = &metav1.Time{Time: now}
return coderWait(reason, "The controller extended the deadline of workspace %s to %s.", name, deadlineText), nil
return coderWait(reason, "The controller extended the deadline of workspace %s to %s.", name, to.UTC().Format(time.RFC3339)), nil
}

// poolMemberExtendTo returns the deadline of an extension at now: now plus
// the template's default TTL, at least 30 minutes, at most the build's max
// deadline, in whole seconds as status stores them. It reports false when
// the max deadline leaves no extension that Coder accepts.
func poolMemberExtendTo(now time.Time, ttl time.Duration, maxDeadline codersdk.NullTime) (time.Time, bool) {
now = now.Truncate(time.Second)
to := now.Add(max(ttl, poolMemberMinExtension)).Truncate(time.Second)
if maxDeadline.Valid && !maxDeadline.Time.IsZero() && to.After(maxDeadline.Time) {
to = maxDeadline.Time.Truncate(time.Second)
}
return to, !to.Before(now.Add(poolMemberMinExtension))
}

func poolMemberBound(member *coderv1alpha1.CoderWorkspacePoolMember, deadline time.Time) *coderStep {
return coderWait("Bound", "Workspace %s is bound to claim %s. Its deadline is now %s.",
member.Status.WorkspaceName, member.Spec.ClaimRef.Name, deadline.UTC().Format(time.RFC3339))
}
Loading
Loading