mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* chore: fix flake, use time closer to actual test The tests were queued, and the autostart time was being set to the time the table was created, not when the test was actually being run. This diff was causing failures in CI
291 lines
12 KiB
Go
291 lines
12 KiB
Go
package agentapi_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/coderd/agentapi"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/testutil"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_ActivityBumpWorkspace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We test the below in multiple timezones specifically
|
|
// chosen to trigger timezone-related bugs.
|
|
timezones := []string{
|
|
"Asia/Kolkata", // No DST, positive fractional offset
|
|
"Canada/Newfoundland", // DST, negative fractional offset
|
|
"Europe/Paris", // DST, positive offset
|
|
"US/Arizona", // No DST, negative offset
|
|
"UTC", // Baseline
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
transition database.WorkspaceTransition
|
|
jobCompletedAt sql.NullTime
|
|
buildDeadlineOffset *time.Duration
|
|
maxDeadlineOffset *time.Duration
|
|
workspaceTTL time.Duration
|
|
templateTTL time.Duration
|
|
templateDisallowsUserAutostop bool
|
|
expectedBump time.Duration
|
|
// If the tests get queued, we need to be able to set the next autostart
|
|
// based on the actual time the unit test is running.
|
|
nextAutostart func(now time.Time) time.Time
|
|
}{
|
|
{
|
|
name: "NotFinishedYet",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{},
|
|
buildDeadlineOffset: ptr.Ref(8 * time.Hour),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: 0,
|
|
},
|
|
{
|
|
name: "ManualShutdown",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now()},
|
|
buildDeadlineOffset: nil,
|
|
expectedBump: 0,
|
|
},
|
|
{
|
|
name: "NotTimeToBumpYet",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now()},
|
|
buildDeadlineOffset: ptr.Ref(8 * time.Hour),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: 0,
|
|
},
|
|
{
|
|
// Expected bump is 0 because the original deadline is more than 1 hour
|
|
// out, so a bump would decrease the deadline.
|
|
name: "BumpLessThanDeadline",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(8*time.Hour - 30*time.Minute),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: 0,
|
|
},
|
|
{
|
|
name: "TimeToBump",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(-30 * time.Minute),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: time.Hour,
|
|
},
|
|
{
|
|
name: "TimeToBumpNextAutostart",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-30 * time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(-30 * time.Minute),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: 8*time.Hour + 30*time.Minute,
|
|
nextAutostart: func(now time.Time) time.Time { return now.Add(time.Minute * 30) },
|
|
},
|
|
{
|
|
name: "MaxDeadline",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(time.Minute), // last chance to bump!
|
|
maxDeadlineOffset: ptr.Ref(time.Minute * 30),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: time.Minute * 30,
|
|
},
|
|
{
|
|
// A workspace that is still running, has passed its deadline, but has not
|
|
// yet been auto-stopped should still bump the deadline.
|
|
name: "PastDeadlineStillBumps",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-24 * time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(-time.Minute),
|
|
workspaceTTL: 8 * time.Hour,
|
|
expectedBump: time.Hour,
|
|
},
|
|
{
|
|
// A stopped workspace should never bump.
|
|
name: "StoppedWorkspace",
|
|
transition: database.WorkspaceTransitionStop,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-time.Minute)},
|
|
buildDeadlineOffset: ptr.Ref(-time.Minute),
|
|
workspaceTTL: 8 * time.Hour,
|
|
},
|
|
{
|
|
// A workspace built from a template that disallows user autostop should bump
|
|
// by the template TTL instead.
|
|
name: "TemplateDisallowsUserAutostop",
|
|
transition: database.WorkspaceTransitionStart,
|
|
jobCompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now().Add(-3 * time.Hour)},
|
|
buildDeadlineOffset: ptr.Ref(-30 * time.Minute),
|
|
workspaceTTL: 2 * time.Hour,
|
|
templateTTL: 10 * time.Hour,
|
|
templateDisallowsUserAutostop: true,
|
|
expectedBump: 10*time.Hour + (time.Minute * 30),
|
|
nextAutostart: func(now time.Time) time.Time { return now.Add(time.Minute * 30) },
|
|
},
|
|
} {
|
|
tt := tt
|
|
for _, tz := range timezones {
|
|
tz := tz
|
|
t.Run(tt.name+"/"+tz, func(t *testing.T) {
|
|
t.Parallel()
|
|
nextAutostart := tt.nextAutostart
|
|
if tt.nextAutostart == nil {
|
|
nextAutostart = func(now time.Time) time.Time { return time.Time{} }
|
|
}
|
|
|
|
var (
|
|
now = dbtime.Now()
|
|
ctx = testutil.Context(t, testutil.WaitShort)
|
|
log = slogtest.Make(t, nil)
|
|
db, _ = dbtestutil.NewDB(t, dbtestutil.WithTimezone(tz))
|
|
org = dbgen.Organization(t, db, database.Organization{})
|
|
user = dbgen.User(t, db, database.User{
|
|
Status: database.UserStatusActive,
|
|
})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: org.ID,
|
|
CreatedBy: user.ID,
|
|
})
|
|
template = dbgen.Template(t, db, database.Template{
|
|
OrganizationID: org.ID,
|
|
ActiveVersionID: templateVersion.ID,
|
|
CreatedBy: user.ID,
|
|
})
|
|
ws = dbgen.Workspace(t, db, database.Workspace{
|
|
OwnerID: user.ID,
|
|
OrganizationID: org.ID,
|
|
TemplateID: template.ID,
|
|
Ttl: sql.NullInt64{Valid: true, Int64: int64(tt.workspaceTTL)},
|
|
})
|
|
job = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
|
OrganizationID: org.ID,
|
|
CompletedAt: tt.jobCompletedAt,
|
|
})
|
|
_ = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: job.ID,
|
|
})
|
|
buildID = uuid.New()
|
|
)
|
|
|
|
require.NoError(t, db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
|
|
ID: template.ID,
|
|
UpdatedAt: dbtime.Now(),
|
|
AllowUserAutostop: !tt.templateDisallowsUserAutostop,
|
|
DefaultTTL: int64(tt.templateTTL),
|
|
}), "unexpected error updating template schedule")
|
|
|
|
var buildNumber int32 = 1
|
|
// Insert a number of previous workspace builds.
|
|
for i := 0; i < 5; i++ {
|
|
insertPrevWorkspaceBuild(t, db, org.ID, templateVersion.ID, ws.ID, database.WorkspaceTransitionStart, buildNumber)
|
|
buildNumber++
|
|
insertPrevWorkspaceBuild(t, db, org.ID, templateVersion.ID, ws.ID, database.WorkspaceTransitionStop, buildNumber)
|
|
buildNumber++
|
|
}
|
|
|
|
// dbgen.WorkspaceBuild automatically sets deadline to now+1 hour if not set
|
|
var buildDeadline time.Time
|
|
if tt.buildDeadlineOffset != nil {
|
|
buildDeadline = now.Add(*tt.buildDeadlineOffset)
|
|
}
|
|
var maxDeadline time.Time
|
|
if tt.maxDeadlineOffset != nil {
|
|
maxDeadline = now.Add(*tt.maxDeadlineOffset)
|
|
}
|
|
err := db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
|
|
ID: buildID,
|
|
CreatedAt: dbtime.Now(),
|
|
UpdatedAt: dbtime.Now(),
|
|
BuildNumber: buildNumber,
|
|
InitiatorID: user.ID,
|
|
Reason: database.BuildReasonInitiator,
|
|
WorkspaceID: ws.ID,
|
|
JobID: job.ID,
|
|
TemplateVersionID: templateVersion.ID,
|
|
Transition: tt.transition,
|
|
Deadline: buildDeadline,
|
|
MaxDeadline: maxDeadline,
|
|
})
|
|
require.NoError(t, err, "unexpected error inserting workspace build")
|
|
bld, err := db.GetWorkspaceBuildByID(ctx, buildID)
|
|
require.NoError(t, err, "unexpected error fetching inserted workspace build")
|
|
|
|
// Validate our initial state before bump
|
|
require.Equal(t, tt.transition, bld.Transition, "unexpected transition before bump")
|
|
require.Equal(t, tt.jobCompletedAt.Time.UTC(), job.CompletedAt.Time.UTC(), "unexpected job completed at before bump")
|
|
require.Equal(t, buildDeadline.UTC(), bld.Deadline.UTC(), "unexpected build deadline before bump")
|
|
require.Equal(t, maxDeadline.UTC(), bld.MaxDeadline.UTC(), "unexpected max deadline before bump")
|
|
require.Equal(t, tt.workspaceTTL, time.Duration(ws.Ttl.Int64), "unexpected workspace TTL before bump")
|
|
|
|
// Wait a bit before bumping as dbtime is rounded to the nearest millisecond.
|
|
// This should also hopefully be enough for Windows time resolution to register
|
|
// a tick (win32 max timer resolution is apparently between 0.5 and 15.6ms)
|
|
<-time.After(testutil.IntervalFast)
|
|
|
|
// Bump duration is measured from the time of the bump, so we measure from here.
|
|
start := dbtime.Now()
|
|
agentapi.ActivityBumpWorkspace(ctx, log, db, bld.WorkspaceID, nextAutostart(start))
|
|
end := dbtime.Now()
|
|
|
|
// Validate our state after bump
|
|
updatedBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, bld.WorkspaceID)
|
|
require.NoError(t, err, "unexpected error getting latest workspace build")
|
|
require.Equal(t, bld.MaxDeadline.UTC(), updatedBuild.MaxDeadline.UTC(), "max_deadline should not have changed")
|
|
if tt.expectedBump == 0 {
|
|
assert.Equal(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should not have bumped updated_at")
|
|
assert.Equal(t, bld.Deadline.UTC(), updatedBuild.Deadline.UTC(), "should not have bumped deadline")
|
|
return
|
|
}
|
|
assert.NotEqual(t, bld.UpdatedAt.UTC(), updatedBuild.UpdatedAt.UTC(), "should have bumped updated_at")
|
|
if tt.maxDeadlineOffset != nil {
|
|
assert.Equal(t, bld.MaxDeadline.UTC(), updatedBuild.MaxDeadline.UTC(), "new deadline must equal original max deadline")
|
|
return
|
|
}
|
|
|
|
// Assert that the bump occurred between start and end. 1min buffer on either side.
|
|
expectedDeadlineStart := start.Add(tt.expectedBump).Add(time.Minute * -1)
|
|
expectedDeadlineEnd := end.Add(tt.expectedBump).Add(time.Minute)
|
|
require.GreaterOrEqual(t, updatedBuild.Deadline, expectedDeadlineStart, "new deadline should be greater than or equal to start")
|
|
require.LessOrEqual(t, updatedBuild.Deadline, expectedDeadlineEnd, "new deadline should be less than or equal to end")
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func insertPrevWorkspaceBuild(t *testing.T, db database.Store, orgID, tvID, workspaceID uuid.UUID, transition database.WorkspaceTransition, buildNumber int32) {
|
|
t.Helper()
|
|
|
|
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
|
OrganizationID: orgID,
|
|
})
|
|
_ = dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: job.ID,
|
|
})
|
|
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
BuildNumber: buildNumber,
|
|
WorkspaceID: workspaceID,
|
|
JobID: job.ID,
|
|
TemplateVersionID: tvID,
|
|
Transition: transition,
|
|
})
|
|
}
|