Skip to content

Commit 2b31224

Browse files
committed
Add backend
1 parent 6c3ab60 commit 2b31224

File tree

5 files changed

+192
-79
lines changed

5 files changed

+192
-79
lines changed

coderd/coderd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func New(options *Options) *API {
204204
httpmw.ExtractUserParam(api.Database),
205205
// Extracts the <workspace.agent> from the url
206206
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
207-
httpmw.BumpWorkspaceAutoStop(api.Logger, api.Database),
207+
httpmw.ActivityBumpWorkspace(api.Logger, api.Database),
208208
)
209209
r.HandleFunc("/*", api.workspaceAppsProxyPath)
210210
}
@@ -431,7 +431,7 @@ func New(options *Options) *API {
431431
apiKeyMiddleware,
432432
httpmw.ExtractWorkspaceAgentParam(options.Database),
433433
httpmw.ExtractWorkspaceParam(options.Database),
434-
httpmw.BumpWorkspaceAutoStop(api.Logger, options.Database),
434+
httpmw.ActivityBumpWorkspace(api.Logger, options.Database),
435435
)
436436
r.Get("/", api.workspaceAgent)
437437
r.Get("/dial", api.workspaceAgentDial)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package httpmw
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"net/http"
8+
"time"
9+
10+
"golang.org/x/xerrors"
11+
12+
"cdr.dev/slog"
13+
"github.com/coder/coder/coderd/database"
14+
)
15+
16+
// ActivityBumpWorkspace automatically bumps the workspace's auto-off timer
17+
// if it is set to expire soon.
18+
// It must be ran after ExtractWorkspace.
19+
func ActivityBumpWorkspace(log slog.Logger, db database.Store) func(h http.Handler) http.Handler {
20+
log = log.Named("activity_bump")
21+
22+
return func(next http.Handler) http.Handler {
23+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
workspace := WorkspaceParam(r)
25+
log.Debug(r.Context(), "middleware called")
26+
// We run the bump logic asynchronously since the result doesn't
27+
// affect the response.
28+
go func() {
29+
// We cannot use the Request context since the goroutine
30+
// may be around after the request terminates.
31+
// We set a short timeout so if the app is under load, these
32+
// low priority operations fail first.
33+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
34+
defer cancel()
35+
36+
err := db.InTx(func(s database.Store) error {
37+
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
38+
log.Debug(ctx, "build", slog.F("build", build))
39+
if errors.Is(err, sql.ErrNoRows) {
40+
return nil
41+
} else if err != nil {
42+
return xerrors.Errorf("get latest workspace build: %w", err)
43+
}
44+
45+
job, err := s.GetProvisionerJobByID(ctx, build.JobID)
46+
if err != nil {
47+
return xerrors.Errorf("get provisioner job: %w", err)
48+
}
49+
50+
if build.Transition != database.WorkspaceTransitionStart || !job.CompletedAt.Valid {
51+
return nil
52+
}
53+
54+
if build.Deadline.IsZero() {
55+
// Workspace shutdown is manual
56+
return nil
57+
}
58+
59+
// We sent bumpThreshold slightly under bumpAmount to minimize DB writes.
60+
const (
61+
bumpAmount = time.Hour
62+
bumpThreshold = time.Hour - (time.Minute * 10)
63+
)
64+
65+
if !build.Deadline.Before(time.Now().Add(bumpThreshold)) {
66+
return nil
67+
}
68+
69+
newDeadline := time.Now().Add(bumpAmount)
70+
71+
if err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
72+
ID: build.ID,
73+
UpdatedAt: build.UpdatedAt,
74+
ProvisionerState: build.ProvisionerState,
75+
Deadline: newDeadline,
76+
}); err != nil {
77+
return xerrors.Errorf("update workspace build: %w", err)
78+
}
79+
return nil
80+
})
81+
82+
if err != nil {
83+
log.Error(ctx, "bump failed", slog.Error(err))
84+
}
85+
}()
86+
next.ServeHTTP(w, r)
87+
})
88+
}
89+
}

coderd/httpmw/workspacebump.go

Lines changed: 0 additions & 75 deletions
This file was deleted.

coderd/workspaceapps_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const (
3636
// setupProxyTest creates a workspace with an agent and some apps. It returns a
3737
// codersdk client, the workspace, and the port number the test listener is
3838
// running on.
39-
func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspace, uint16) {
39+
func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (*codersdk.Client, uuid.UUID, codersdk.Workspace, uint16) {
4040
// #nosec
4141
ln, err := net.Listen("tcp", ":0")
4242
require.NoError(t, err)
@@ -95,7 +95,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
9595
})
9696
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
9797
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
98-
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
98+
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, workspaceMutators...)
9999
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
100100

101101
agentClient := codersdk.New(client.URL)
@@ -209,6 +209,21 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
209209
require.Equal(t, http.StatusOK, resp.StatusCode)
210210
})
211211

212+
t.Run("Proxies", func(t *testing.T) {
213+
t.Parallel()
214+
215+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
216+
defer cancel()
217+
218+
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?"+proxyTestAppQuery, nil)
219+
require.NoError(t, err)
220+
defer resp.Body.Close()
221+
body, err := io.ReadAll(resp.Body)
222+
require.NoError(t, err)
223+
require.Equal(t, proxyTestAppBody, string(body))
224+
require.Equal(t, http.StatusOK, resp.StatusCode)
225+
})
226+
212227
t.Run("ProxyError", func(t *testing.T) {
213228
t.Parallel()
214229

coderd/workspaces_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,90 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
10291029
require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code")
10301030
})
10311031
}
1032+
func TestWorkspaceActivityBump(t *testing.T) {
1033+
t.Parallel()
1034+
1035+
ctx := context.Background()
1036+
setupActivityTest := func(t *testing.T) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
1037+
var ttlMillis int64 = 60 * 1000
1038+
1039+
client, _, workspace, _ = setupProxyTest(t, func(cwr *codersdk.CreateWorkspaceRequest) {
1040+
cwr.TTLMillis = &ttlMillis
1041+
})
1042+
1043+
// Sanity-check that deadline is near.
1044+
workspace, err := client.Workspace(ctx, workspace.ID)
1045+
require.NoError(t, err)
1046+
require.WithinDuration(t,
1047+
time.Now().Add(time.Duration(ttlMillis)*time.Millisecond),
1048+
workspace.LatestBuild.Deadline.Time, testutil.WaitShort,
1049+
)
1050+
firstDeadline := workspace.LatestBuild.Deadline.Time
1051+
1052+
return client, workspace, func(want bool) {
1053+
if !want {
1054+
time.Sleep(testutil.IntervalMedium)
1055+
workspace, err = client.Workspace(ctx, workspace.ID)
1056+
require.NoError(t, err)
1057+
require.Equal(t, workspace.LatestBuild.Deadline.Time, firstDeadline)
1058+
return
1059+
}
1060+
1061+
// The Deadline bump occurs asynchronously.
1062+
require.Eventuallyf(t,
1063+
func() bool {
1064+
workspace, err = client.Workspace(ctx, workspace.ID)
1065+
require.NoError(t, err)
1066+
return workspace.LatestBuild.Deadline.Time != firstDeadline
1067+
},
1068+
testutil.WaitShort, testutil.IntervalFast,
1069+
"deadline %v never updated", firstDeadline,
1070+
)
1071+
1072+
require.WithinDuration(t, time.Now().Add(time.Hour), workspace.LatestBuild.Deadline.Time, time.Second)
1073+
}
1074+
}
1075+
1076+
t.Run("Apps", func(t *testing.T) {
1077+
t.Parallel()
1078+
1079+
client, workspace, assertBumped := setupActivityTest(t)
1080+
1081+
// A request to the /apps/ endpoint extends the deadline an hour.
1082+
// The particular app doesn't matter. The deadline is extended
1083+
// regardless of error state.
1084+
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
1085+
require.NoError(t, err)
1086+
resp.Body.Close()
1087+
assertBumped(true)
1088+
})
1089+
1090+
t.Run("Dial", func(t *testing.T) {
1091+
t.Parallel()
1092+
1093+
client, workspace, assertBumped := setupActivityTest(t)
1094+
1095+
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
1096+
conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
1097+
require.NoError(t, err)
1098+
_ = conn.Close()
1099+
1100+
assertBumped(true)
1101+
})
1102+
1103+
t.Run("NoBump", func(t *testing.T) {
1104+
t.Parallel()
1105+
1106+
client, workspace, assertBumped := setupActivityTest(t)
1107+
1108+
// Doing some inactive operation like retrieving resources must not
1109+
// bump the deadline.
1110+
_, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
1111+
require.NoError(t, err)
1112+
1113+
assertBumped(false)
1114+
})
1115+
}
10321116

10331117
func TestWorkspaceUpdateTTL(t *testing.T) {
10341118
t.Parallel()

0 commit comments

Comments
 (0)