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

Commitd30945c

Browse files
authored
feat: bump workspace deadline on user activity (#4119)
Resolves#2995
1 parent0899548 commitd30945c

File tree

6 files changed

+189
-6
lines changed

6 files changed

+189
-6
lines changed

‎coderd/activitybump.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package coderd
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"time"
8+
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
"github.com/coder/coder/coderd/database"
13+
)
14+
15+
// activityBumpWorkspace automatically bumps the workspace's auto-off timer
16+
// if it is set to expire soon.
17+
funcactivityBumpWorkspace(log slog.Logger,db database.Store,workspace database.Workspace) {
18+
// We set a short timeout so if the app is under load, these
19+
// low priority operations fail first.
20+
ctx,cancel:=context.WithTimeout(context.Background(),time.Second*15)
21+
defercancel()
22+
23+
err:=db.InTx(func(s database.Store)error {
24+
build,err:=s.GetLatestWorkspaceBuildByWorkspaceID(ctx,workspace.ID)
25+
iferrors.Is(err,sql.ErrNoRows) {
26+
returnnil
27+
}elseiferr!=nil {
28+
returnxerrors.Errorf("get latest workspace build: %w",err)
29+
}
30+
31+
job,err:=s.GetProvisionerJobByID(ctx,build.JobID)
32+
iferr!=nil {
33+
returnxerrors.Errorf("get provisioner job: %w",err)
34+
}
35+
36+
ifbuild.Transition!=database.WorkspaceTransitionStart||!job.CompletedAt.Valid {
37+
returnnil
38+
}
39+
40+
ifbuild.Deadline.IsZero() {
41+
// Workspace shutdown is manual
42+
returnnil
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+
returnnil
53+
}
54+
55+
newDeadline:=database.Now().Add(bumpAmount)
56+
57+
iferr:=s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
58+
ID:build.ID,
59+
UpdatedAt:database.Now(),
60+
ProvisionerState:build.ProvisionerState,
61+
Deadline:newDeadline,
62+
});err!=nil {
63+
returnxerrors.Errorf("update workspace build: %w",err)
64+
}
65+
returnnil
66+
})
67+
iferr!=nil {
68+
log.Error(
69+
ctx,"bump failed",
70+
slog.Error(err),
71+
slog.F("workspace_id",workspace.ID),
72+
)
73+
}else {
74+
log.Debug(
75+
ctx,"bumped deadline from activity",
76+
slog.F("workspace_id",workspace.ID),
77+
)
78+
}
79+
}

‎coderd/activitybump_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package coderd_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"cdr.dev/slog/sloggers/slogtest"
11+
12+
"github.com/coder/coder/coderd/coderdtest"
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/coder/testutil"
16+
)
17+
18+
funcTestWorkspaceActivityBump(t*testing.T) {
19+
t.Parallel()
20+
21+
ctx:=context.Background()
22+
23+
setupActivityTest:=func(t*testing.T) (client*codersdk.Client,workspace codersdk.Workspace,assertBumpedfunc(wantbool)) {
24+
varttlMillisint64=60*1000
25+
26+
client,_,workspace,_=setupProxyTest(t,func(cwr*codersdk.CreateWorkspaceRequest) {
27+
cwr.TTLMillis=&ttlMillis
28+
})
29+
30+
// Sanity-check that deadline is near.
31+
workspace,err:=client.Workspace(ctx,workspace.ID)
32+
require.NoError(t,err)
33+
require.WithinDuration(t,
34+
time.Now().Add(time.Duration(ttlMillis)*time.Millisecond),
35+
workspace.LatestBuild.Deadline.Time,testutil.WaitShort,
36+
)
37+
firstDeadline:=workspace.LatestBuild.Deadline.Time
38+
39+
_=coderdtest.AwaitWorkspaceAgents(t,client,workspace.LatestBuild.ID)
40+
41+
returnclient,workspace,func(wantbool) {
42+
if!want {
43+
// It is difficult to test the absence of a call in a non-racey
44+
// way. In general, it is difficult for the API to generate
45+
// false positive activity since Agent networking event
46+
// is required. The Activity Bump behavior is also coupled with
47+
// Last Used, so it would be obvious to the user if we
48+
// are falsely recognizing activity.
49+
time.Sleep(testutil.IntervalMedium)
50+
workspace,err=client.Workspace(ctx,workspace.ID)
51+
require.NoError(t,err)
52+
require.Equal(t,workspace.LatestBuild.Deadline.Time,firstDeadline)
53+
return
54+
}
55+
56+
// The Deadline bump occurs asynchronously.
57+
require.Eventuallyf(t,
58+
func()bool {
59+
workspace,err=client.Workspace(ctx,workspace.ID)
60+
require.NoError(t,err)
61+
returnworkspace.LatestBuild.Deadline.Time!=firstDeadline
62+
},
63+
testutil.WaitShort,testutil.IntervalFast,
64+
"deadline %v never updated",firstDeadline,
65+
)
66+
67+
require.WithinDuration(t,database.Now().Add(time.Hour),workspace.LatestBuild.Deadline.Time,time.Second)
68+
}
69+
}
70+
71+
t.Run("Dial",func(t*testing.T) {
72+
t.Parallel()
73+
74+
client,workspace,assertBumped:=setupActivityTest(t)
75+
76+
resources:=coderdtest.AwaitWorkspaceAgents(t,client,workspace.LatestBuild.ID)
77+
conn,err:=client.DialWorkspaceAgentTailnet(ctx,slogtest.Make(t,nil),resources[0].Agents[0].ID)
78+
require.NoError(t,err)
79+
deferconn.Close()
80+
81+
sshConn,err:=conn.SSHClient()
82+
require.NoError(t,err)
83+
_=sshConn.Close()
84+
85+
assertBumped(true)
86+
})
87+
88+
t.Run("NoBump",func(t*testing.T) {
89+
t.Parallel()
90+
91+
client,workspace,assertBumped:=setupActivityTest(t)
92+
93+
// Benign operations like retrieving resources must not
94+
// bump the deadline.
95+
_,err:=client.WorkspaceResourcesByBuild(ctx,workspace.LatestBuild.ID)
96+
require.NoError(t,err)
97+
98+
assertBumped(false)
99+
})
100+
}

‎coderd/workspaceagents.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,8 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
616616
)
617617

618618
ifupdateDB {
619+
goactivityBumpWorkspace(api.Logger.Named("activity_bump"),api.Database,workspace)
620+
619621
lastReport=rep
620622

621623
_,err=api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{

‎coderd/workspaceapps_test.go

Lines changed: 6 additions & 3 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-
funcsetupProxyTest(t*testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspace,uint16) {
39+
funcsetupProxyTest(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)
@@ -58,7 +58,9 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
5858
require.True(t,ok)
5959

6060
client:=coderdtest.New(t,&coderdtest.Options{
61-
IncludeProvisionerDaemon:true,
61+
IncludeProvisionerDaemon:true,
62+
AgentStatsRefreshInterval:time.Millisecond*100,
63+
MetricsCacheRefreshInterval:time.Millisecond*100,
6264
})
6365
user:=coderdtest.CreateFirstUser(t,client)
6466
authToken:=uuid.NewString()
@@ -95,7 +97,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
9597
})
9698
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
9799
coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
98-
workspace:=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
100+
workspace:=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID,workspaceMutators...)
99101
coderdtest.AwaitWorkspaceBuildJob(t,client,workspace.LatestBuild.ID)
100102

101103
agentClient:=codersdk.New(client.URL)
@@ -104,6 +106,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
104106
FetchMetadata:agentClient.WorkspaceAgentMetadata,
105107
CoordinatorDialer:agentClient.ListenWorkspaceAgentTailnet,
106108
Logger:slogtest.Make(t,nil).Named("agent"),
109+
StatsReporter:agentClient.AgentReportStats,
107110
})
108111
t.Cleanup(func() {
109112
_=agentCloser.Close()

‎codersdk/workspaceagents.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,10 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg
281281
CompressionMode:websocket.CompressionDisabled,
282282
})
283283
iferrors.Is(err,context.Canceled) {
284-
_=ws.Close(websocket.StatusAbnormalClosure,"")
285284
return
286285
}
287286
iferr!=nil {
288287
logger.Debug(ctx,"failed to dial",slog.Error(err))
289-
_=ws.Close(websocket.StatusAbnormalClosure,"")
290288
continue
291289
}
292290
sendNode,errChan:=tailnet.ServeCoordinator(websocket.NetConn(ctx,ws,websocket.MessageBinary),func(node []*tailnet.Node)error {

‎site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export const Language = {
5555
timezoneLabel:"Timezone",
5656
ttlLabel:"Time until shutdown (hours)",
5757
ttlCausesShutdownHelperText:"Your workspace will shut down",
58-
ttlCausesShutdownAfterStart:"after its next start",
58+
ttlCausesShutdownAfterStart:
59+
"after its next start. We delay shutdown by an hour whenever we detect activity",
5960
ttlCausesNoShutdownHelperText:"Your workspace will not automatically shut down.",
6061
formTitle:"Workspace schedule",
6162
startSection:"Start",

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp