package coderd import ( "context" "database/sql" "errors" "time" "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" "github.com/coder/coder/v2/coderd/database" ) // activityBumpWorkspace automatically bumps the workspace's auto-off timer // if it is set to expire soon. func activityBumpWorkspace(ctx context.Context, log slog.Logger, db database.Store, workspaceID uuid.UUID) { // We set a short timeout so if the app is under load, these // low priority operations fail first. ctx, cancel := context.WithTimeout(ctx, time.Second*15) defer cancel() err := db.InTx(func(s database.Store) error { build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) 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 } workspace, err := s.GetWorkspaceByID(ctx, workspaceID) if err != nil { return xerrors.Errorf("get workspace: %w", err) } var ( // We bump by the original TTL to prevent counter-intuitive behavior // as the TTL wraps. For example, if I set the TTL to 12 hours, sign off // work at midnight, come back at 10am, I would want another full day // of uptime. In the prior implementation, the workspace would enter // a state of always expiring 1 hour in the future bumpAmount = time.Duration(workspace.Ttl.Int64) // DB writes are expensive so we only bump when 5% of the deadline // has elapsed. bumpEvery = bumpAmount / 20 timeSinceLastBump = bumpAmount - time.Until(build.Deadline) ) if timeSinceLastBump < bumpEvery { return nil } if bumpAmount == 0 { return nil } newDeadline := database.Now().Add(bumpAmount) if !build.MaxDeadline.IsZero() && newDeadline.After(build.MaxDeadline) { newDeadline = build.MaxDeadline } if err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{ ID: build.ID, UpdatedAt: database.Now(), ProvisionerState: build.ProvisionerState, Deadline: newDeadline, MaxDeadline: build.MaxDeadline, }); err != nil { return xerrors.Errorf("update workspace build: %w", err) } return nil }, nil) if err != nil { if !xerrors.Is(err, context.Canceled) && !database.IsQueryCanceledError(err) { // Bump will fail if the context is canceled, but this is ok. log.Error(ctx, "bump failed", slog.Error(err), slog.F("workspace_id", workspaceID), ) } return } log.Debug(ctx, "bumped deadline from activity", slog.F("workspace_id", workspaceID), ) }