mirror of https://github.com/coder/coder.git
parent
0899548208
commit
d30945c5c5
|
@ -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
|
||||
} 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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
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)
|
||||
})
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue