fix: prevent notification for dormant delete on dormant-removal (#21427)

Ensure we do not send "Marked for deletion" notifications when disabling dormancy deletion
This commit is contained in:
Danielle Maywood
2026-01-06 16:26:28 +00:00
committed by GitHub
parent ef45ce4dfb
commit 467c8bbd6b
2 changed files with 112 additions and 23 deletions
+26 -23
View File
@@ -140,8 +140,8 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
}
var (
template database.Template
markedForDeletion []database.WorkspaceTable
template database.Template
dormantWorkspacesUpdated []database.WorkspaceTable
)
err = db.InTx(func(tx database.Store) error {
ctx, span := tracing.StartSpanWithName(ctx, "(*schedule.EnterpriseTemplateScheduleStore).Set()-InTx()")
@@ -176,7 +176,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
// to ensure workspaces are being cleaned up correctly. Similarly if we are
// disabling it (by passing 0), then we want to delete nullify the deleting_at
// fields of all the template workspaces.
markedForDeletion, err = tx.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams{
dormantWorkspacesUpdated, err = tx.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, database.UpdateWorkspacesDormantDeletingAtByTemplateIDParams{
TemplateID: tpl.ID,
TimeTilDormantAutodeleteMs: opts.TimeTilDormantAutoDelete.Milliseconds(),
DormantAt: dormantAt,
@@ -267,27 +267,30 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
}
}
for _, ws := range markedForDeletion {
if opts.TimeTilDormantAutoDelete > 0 {
dormantTime := s.now().Add(opts.TimeTilDormantAutoDelete)
_, err = s.enqueuer.Enqueue(
// nolint:gocritic // Need actor to enqueue notification
dbauthz.AsNotifier(ctx),
ws.OwnerID,
notifications.TemplateWorkspaceMarkedForDeletion,
map[string]string{
"name": ws.Name,
"reason": "an update to the template's dormancy",
"timeTilDormant": humanize.Time(dormantTime),
},
"scheduletemplate",
// Associate this notification with all the related entities.
ws.ID,
ws.OwnerID,
ws.TemplateID,
ws.OrganizationID,
)
if err != nil {
s.logger.Warn(ctx, "failed to notify of workspace marked for deletion", slog.Error(err), slog.F("workspace_id", ws.ID))
for _, ws := range dormantWorkspacesUpdated {
_, err = s.enqueuer.Enqueue(
// nolint:gocritic // Need actor to enqueue notification
dbauthz.AsNotifier(ctx),
ws.OwnerID,
notifications.TemplateWorkspaceMarkedForDeletion,
map[string]string{
"name": ws.Name,
"reason": "an update to the template's dormancy",
"timeTilDormant": humanize.Time(dormantTime),
},
"scheduletemplate",
// Associate this notification with all the related entities.
ws.ID,
ws.OwnerID,
ws.TemplateID,
ws.OrganizationID,
)
if err != nil {
s.logger.Warn(ctx, "failed to notify of workspace marked for deletion", slog.Error(err), slog.F("workspace_id", ws.ID))
}
}
}
@@ -836,6 +836,92 @@ func TestNotifications(t *testing.T) {
"deleted workspace should not receive notifications")
}
})
// Disabling dormancy auto-deletion should not send "marked for deletion" notifications.
t.Run("DisablingAutoDeleteSendsNoNotifications", func(t *testing.T) {
t.Parallel()
var (
db, _ = dbtestutil.NewDB(t)
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{
CreatedBy: user.ID,
})
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{
"foo": "bar",
},
})
timeTilDormant = time.Minute * 2
timeTilDormantAutoDelete = time.Minute * 4
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: templateJob.ID,
OrganizationID: templateJob.OrganizationID,
})
template = dbgen.Template(t, db, database.Template{
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
OrganizationID: templateJob.OrganizationID,
})
)
// Given: Dormancy auto deletion is enabled
ctx := testutil.Context(t, testutil.WaitShort)
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: template.ID,
UpdatedAt: dbtime.Now(),
TimeTilDormant: int64(timeTilDormant),
TimeTilDormantAutoDelete: int64(timeTilDormantAutoDelete),
})
require.NoError(t, err)
// Given: A workspace that is marked as dormant
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
OrganizationID: templateJob.OrganizationID,
LastUsedAt: time.Now().Add(-time.Hour),
})
dormantAt := workspace.LastUsedAt.Add(timeTilDormant)
workspace, err = db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
ID: workspace.ID,
DormantAt: sql.NullTime{
Time: dormantAt,
Valid: true,
},
})
require.NoError(t, err)
require.True(t, workspace.DeletingAt.Valid, "deleting_at should be set when marking workspace dormant")
// Setup dependencies
notifyEnq := notificationstest.NewFakeEnqueuer()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
require.NoError(t, err)
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
userQuietHoursStorePtr.Store(&userQuietHoursStore)
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
// When: We disable dormancy auto-delete
_, err = templateScheduleStore.Set(dbauthz.AsNotifier(ctx), db, template, agplschedule.TemplateScheduleOptions{
TimeTilDormant: timeTilDormant,
TimeTilDormantAutoDelete: 0,
})
require.NoError(t, err)
// Then: We expect deleting_at to be removed
updated, err := db.GetWorkspaceByID(ctx, workspace.ID)
require.NoError(t, err)
require.False(t, updated.DeletingAt.Valid, "deleting_at should be cleared when auto-deletion is disabled")
// Then: We expect no notifications to have been sent
sent := notifyEnq.Sent()
require.Len(t, sent, 0, "no notifications should be sent when disabling dormancy auto-deletion")
})
}
func TestTemplateTTL(t *testing.T) {