Skip to content

Commit 10a877b

Browse files
committed
feat: bump workspace deadline on user activity
Resolves #2995
1 parent 714c366 commit 10a877b

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

coderd/coderd.go

+2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ func New(options *Options) *API {
201201
httpmw.ExtractUserParam(api.Database),
202202
// Extracts the <workspace.agent> from the url
203203
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
204+
httpmw.BumpWorkspaceAutoStop(api.Logger, api.Database),
204205
)
205206
r.HandleFunc("/*", api.workspaceAppsProxyPath)
206207
}
@@ -421,6 +422,7 @@ func New(options *Options) *API {
421422
apiKeyMiddleware,
422423
httpmw.ExtractWorkspaceAgentParam(options.Database),
423424
httpmw.ExtractWorkspaceParam(options.Database),
425+
httpmw.BumpWorkspaceAutoStop(api.Logger, options.Database),
424426
)
425427
r.Get("/", api.workspaceAgent)
426428
r.Get("/pty", api.workspaceAgentPTY)

coderd/httpmw/workspacebump.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package httpmw
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"net/http"
7+
"time"
8+
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
"github.com/coder/coder/coderd/database"
13+
)
14+
15+
// BumpWorkspaceAutoStop automatically bumps the workspace's auto-off timer
16+
// if it is set to expire soon.
17+
// It must be ran after ExtractWorkspace.
18+
func BumpWorkspaceAutoStop(log slog.Logger, db database.Store) func(h http.Handler) http.Handler {
19+
return func(next http.Handler) http.Handler {
20+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21+
workspace := WorkspaceParam(r)
22+
23+
err := db.InTx(func(s database.Store) error {
24+
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
25+
if errors.Is(err, sql.ErrNoRows) {
26+
return nil
27+
} else if err != nil {
28+
return xerrors.Errorf("get latest workspace build: %w", err)
29+
}
30+
31+
job, err := s.GetProvisionerJobByID(r.Context(), build.JobID)
32+
if err != nil {
33+
return xerrors.Errorf("get provisioner job: %w", err)
34+
}
35+
36+
if build.Transition != database.WorkspaceTransitionStart || !job.CompletedAt.Valid {
37+
return nil
38+
}
39+
40+
if build.Deadline.IsZero() {
41+
// Workspace shutdown is manual
42+
return nil
43+
}
44+
45+
// We sent bumpThreshold slightly under bumpAmount to minimize DB writes.
46+
const (
47+
bumpAmount = time.Hour
48+
bumpThreshold = time.Hour - time.Minute*10
49+
)
50+
51+
if !build.Deadline.Before(time.Now().Add(bumpThreshold)) {
52+
return nil
53+
}
54+
55+
newDeadline := time.Now().Add(bumpAmount)
56+
57+
if err := s.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
58+
ID: build.ID,
59+
UpdatedAt: build.UpdatedAt,
60+
ProvisionerState: build.ProvisionerState,
61+
Deadline: newDeadline,
62+
}); err != nil {
63+
return xerrors.Errorf("update workspace build: %w", err)
64+
}
65+
return nil
66+
})
67+
68+
if err != nil {
69+
log.Error(r.Context(), "auto-bump", slog.Error(err))
70+
}
71+
72+
next.ServeHTTP(w, r)
73+
})
74+
}
75+
}

0 commit comments

Comments
 (0)