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

feat: bump workspace deadline on user activity#4119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ammario merged 10 commits intomainfromactivity-stop
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes fromall 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
79 changes: 79 additions & 0 deletionscoderd/activitybump.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
package coderd

import (
"context"
"database/sql"
"errors"
"time"

"golang.org/x/xerrors"

"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
)

// activityBumpWorkspace automatically bumps the workspace's auto-off timer
// if it is set to expire soon.
func activityBumpWorkspace(log slog.Logger, db database.Store, workspace database.Workspace) {
// We set a short timeout so if the app is under load, these
// low priority operations fail first.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()

err := db.InTx(func(s database.Store) error {
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if errors.Is(err, sql.ErrNoRows) {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is there a reason we don't want to surface an error? It seems like an inconsistency to me that a workspace is being bumped but doesn't exist.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Since the bump occurs asynchronously, there is no guarantee the workspace exists when this line executes. But, that is not an error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That makes sense 👍🏻. Should we btw consider moving the log above to blow the error handling?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

That was an artifact from testing so I will remove.

} else if err != nil {
return xerrors.Errorf("get latest workspace build: %w", err)
}

job, err := s.GetProvisionerJobByID(ctx, build.JobID)
if err != nil {
return xerrors.Errorf("get provisioner job: %w", err)
}

if build.Transition != database.WorkspaceTransitionStart || !job.CompletedAt.Valid {
return nil
}

if build.Deadline.IsZero() {
// Workspace shutdown is manual
return nil
}

// We sent bumpThreshold slightly under bumpAmount to minimize DB writes.
const (
bumpAmount = time.Hour
bumpThreshold = time.Hour - (time.Minute * 10)
)

if !build.Deadline.Before(time.Now().Add(bumpThreshold)) {
return nil
}

newDeadline := database.Now().Add(bumpAmount)

if err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: database.Now(),
ProvisionerState: build.ProvisionerState,
Deadline: newDeadline,
}); err != nil {
return xerrors.Errorf("update workspace build: %w", err)
}
return nil
})
if err != nil {
log.Error(
ctx, "bump failed",
slog.Error(err),
slog.F("workspace_id", workspace.ID),
)
} else {
log.Debug(
ctx, "bumped deadline from activity",
slog.F("workspace_id", workspace.ID),
)
}
}
100 changes: 100 additions & 0 deletionscoderd/activitybump_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
package coderd_test

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"

"cdr.dev/slog/sloggers/slogtest"

"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)

func TestWorkspaceActivityBump(t *testing.T) {
t.Parallel()

ctx := context.Background()

setupActivityTest := func(t *testing.T) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
var ttlMillis int64 = 60 * 1000

client, _, workspace, _ = setupProxyTest(t, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = &ttlMillis
})

// Sanity-check that deadline is near.
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.WithinDuration(t,
time.Now().Add(time.Duration(ttlMillis)*time.Millisecond),
workspace.LatestBuild.Deadline.Time, testutil.WaitShort,
)
firstDeadline := workspace.LatestBuild.Deadline.Time

_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)

return client, workspace, func(want bool) {
if !want {
// It is difficult to test the absence of a call in a non-racey
// way. In general, it is difficult for the API to generate
// false positive activity since Agent networking event
// is required. The Activity Bump behavior is also coupled with
// Last Used, so it would be obvious to the user if we
// are falsely recognizing activity.
time.Sleep(testutil.IntervalMedium)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This might not trigger a CI flake currently, but its behavior is racy. We can't guarantee that the goroutine has run to completion here so we also can't guarantee we're testing the correct thing.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yeah, it sucks, but I couldn't think of a clean alternative.

Copy link
Member

@mafredrimafredriSep 20, 2022
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

One option would be dependency injection, e.g. store the activity bumping method oncoderd.API, replace it with a wrapper in the test that can inform e.g. on a channel when it has run.

This would be a bit cleaner as an internal test so that nothing needs to be exported.

Another idea: I'm not sure what we're sending on the websocket, but could we write a payload informing the other side about what we did (e.g. event: bumped workspace)?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

One option would be dependency injection, e.g. store the activity bumping method on coderd.API, replace it with a wrapper in the test that can inform e.g. on a channel when it has run.

In this particular test we're looking for an absence of a call. So, it's not as simple as replacing the activity bump method. We should also strive to test behavior from the user's perspective. The more we dependency inject and replace internals in our tests the further away we're getting from the user. We may be better off accepting a little bit of raciness for more test accuracy here.

This would be a bit cleaner as an internal test so that nothing needs to be exported.

I, too, enjoy internal tests, but that is a codebase decision larger than this PR.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I added an explanatory comment to the code. Let me know your thoughts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In this particular test we're looking for an absence of a call.

I thought we would be testing for the presence of a call, but the absence of modification? What’s the purpose of testing for the absence of a call?

The only case that the call wouldn’t happen is if theupdateDB variable is false which seems like a detail that shouldn’t be tested in this test. If we really need to test that case then we should try to expose that some way so we don’t have racy tests.

In my personal opinion we should avoid test racyness as much as possible because those lead to CI flakes that in turn result in loss of developer productivity.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In the current implementation, sinceupdateDB is false, there be no call. Let's say we restructure the code so it always calls a replaceable function. In that case, there would still be raciness because we're looking to assert that the bumpnever happens. We can't wait for infinity, so we'd have to wait for some period or some event. We could wait for X ticks of the Agent Stat Loop, but there's still raciness.

workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, workspace.LatestBuild.Deadline.Time, firstDeadline)
return
}

// The Deadline bump occurs asynchronously.
require.Eventuallyf(t,
func() bool {
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
return workspace.LatestBuild.Deadline.Time != firstDeadline
},
testutil.WaitShort, testutil.IntervalFast,
"deadline %v never updated", firstDeadline,
)

require.WithinDuration(t, database.Now().Add(time.Hour), workspace.LatestBuild.Deadline.Time, time.Second)
}
}

t.Run("Dial", func(t *testing.T) {
t.Parallel()

client, workspace, assertBumped := setupActivityTest(t)

resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
conn, err := client.DialWorkspaceAgentTailnet(ctx, slogtest.Make(t, nil), resources[0].Agents[0].ID)
require.NoError(t, err)
defer conn.Close()

sshConn, err := conn.SSHClient()
require.NoError(t, err)
_ = sshConn.Close()

assertBumped(true)
})

t.Run("NoBump", func(t *testing.T) {
t.Parallel()

client, workspace, assertBumped := setupActivityTest(t)

// Benign operations like retrieving resources must not
// bump the deadline.
_, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)

assertBumped(false)
})
}
2 changes: 2 additions & 0 deletionscoderd/workspaceagents.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -616,6 +616,8 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques
)

if updateDB {
go activityBumpWorkspace(api.Logger.Named("activity_bump"), api.Database, workspace)

lastReport = rep

_, err = api.Database.InsertAgentStat(ctx, database.InsertAgentStatParams{
Expand Down
9 changes: 6 additions & 3 deletionscoderd/workspaceapps_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ const (
// setupProxyTest creates a workspace with an agent and some apps. It returns a
// codersdk client, the workspace, and the port number the test listener is
// running on.
func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspace, uint16) {
func setupProxyTest(t *testing.T, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (*codersdk.Client, uuid.UUID, codersdk.Workspace, uint16) {
// #nosec
ln, err := net.Listen("tcp", ":0")
require.NoError(t, err)
Expand All@@ -58,7 +58,9 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
require.True(t, ok)

client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
IncludeProvisionerDaemon: true,
AgentStatsRefreshInterval: time.Millisecond * 100,
MetricsCacheRefreshInterval: time.Millisecond * 100,
})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
Expand DownExpand Up@@ -95,7 +97,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, workspaceMutators...)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)

agentClient := codersdk.New(client.URL)
Expand All@@ -104,6 +106,7 @@ func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspa
FetchMetadata: agentClient.WorkspaceAgentMetadata,
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
Logger: slogtest.Make(t, nil).Named("agent"),
StatsReporter: agentClient.AgentReportStats,
})
t.Cleanup(func() {
_ = agentCloser.Close()
Expand Down
2 changes: 0 additions & 2 deletionscodersdk/workspaceagents.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,12 +281,10 @@ func (c *Client) DialWorkspaceAgentTailnet(ctx context.Context, logger slog.Logg
CompressionMode: websocket.CompressionDisabled,
})
if errors.Is(err, context.Canceled) {
_ = ws.Close(websocket.StatusAbnormalClosure, "")
return
}
if err != nil {
logger.Debug(ctx, "failed to dial", slog.Error(err))
_ = ws.Close(websocket.StatusAbnormalClosure, "")
continue
}
sendNode, errChan := tailnet.ServeCoordinator(websocket.NetConn(ctx, ws, websocket.MessageBinary), func(node []*tailnet.Node) error {
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,7 +55,8 @@ export const Language = {
timezoneLabel: "Timezone",
ttlLabel: "Time until shutdown (hours)",
ttlCausesShutdownHelperText: "Your workspace will shut down",
ttlCausesShutdownAfterStart: "after its next start",
ttlCausesShutdownAfterStart:
"after its next start. We delay shutdown by an hour whenever we detect activity",
ttlCausesNoShutdownHelperText: "Your workspace will not automatically shut down.",
formTitle: "Workspace schedule",
startSection: "Start",
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp