Skip to content

Commit c43c2ff

Browse files
committed
Add backend
1 parent 10a877b commit c43c2ff

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
@@ -201,7 +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),
204+
httpmw.ActivityBumpWorkspace(api.Logger, api.Database),
205205
)
206206
r.HandleFunc("/*", api.workspaceAppsProxyPath)
207207
}
@@ -422,7 +422,7 @@ func New(options *Options) *API {
422422
apiKeyMiddleware,
423423
httpmw.ExtractWorkspaceAgentParam(options.Database),
424424
httpmw.ExtractWorkspaceParam(options.Database),
425-
httpmw.BumpWorkspaceAutoStop(api.Logger, options.Database),
425+
httpmw.ActivityBumpWorkspace(api.Logger, options.Database),
426426
)
427427
r.Get("/", api.workspaceAgent)
428428
r.Get("/pty", api.workspaceAgentPTY)
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)
@@ -208,6 +208,21 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
208208
require.Equal(t, http.StatusOK, resp.StatusCode)
209209
})
210210

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

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy