From d42db447f955e463f808debb4e375015e4f622f2 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 18 Jun 2026 10:23:50 +0000 Subject: [PATCH] fix(coderd/x/chatd): reset auto archive ticker after runs --- coderd/x/chatd/auto_archive.go | 18 ++++++++++++---- coderd/x/chatd/auto_archive_internal_test.go | 22 +++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/coderd/x/chatd/auto_archive.go b/coderd/x/chatd/auto_archive.go index e045447632d60..6283aa3afe33f 100644 --- a/coderd/x/chatd/auto_archive.go +++ b/coderd/x/chatd/auto_archive.go @@ -33,13 +33,19 @@ type autoArchivedChat struct { } func (w *chatWorker) archiveLoop(ctx context.Context) { + run := func(start time.Time) { + w.archiveOnce(ctx, dbtime.Time(start).UTC()) + } + run(w.opts.Clock.Now("chatworker", "auto-archive")) + ticker := w.opts.Clock.NewTicker(w.opts.ArchiveInterval, "chatworker", "auto-archive") - defer ticker.Stop() - w.archiveOnce(ctx, dbtime.Time(w.opts.Clock.Now("chatworker", "auto-archive")).UTC()) + defer ticker.Stop("chatworker", "auto-archive") for { select { case tick := <-ticker.C: - w.archiveOnce(ctx, dbtime.Time(tick).UTC()) + ticker.Stop("chatworker", "auto-archive") + run(tick) + ticker.Reset(w.opts.ArchiveInterval, "chatworker", "auto-archive") case <-ctx.Done(): return } @@ -65,7 +71,11 @@ func (w *chatWorker) archiveOnce(ctx context.Context, start time.Time) { return } - archiveCutoff := dbtime.StartOfDay(start).Add(-time.Duration(autoArchiveDays) * 24 * time.Hour) + // Anchor the cutoff at 00:00 UTC so every chat with activity on the + // same UTC calendar date stays eligible or ineligible for the whole + // day. This avoids trickling chats into auto-archive as wall-clock + // time advances. + archiveCutoff := dbtime.StartOfDay(start.UTC()).Add(-time.Duration(autoArchiveDays) * 24 * time.Hour) rows, err := w.opts.Store.GetAutoArchiveInactiveChatCandidates(ctx, database.GetAutoArchiveInactiveChatCandidatesParams{ ArchiveCutoff: archiveCutoff, LimitCount: w.opts.ArchiveBatchSize, diff --git a/coderd/x/chatd/auto_archive_internal_test.go b/coderd/x/chatd/auto_archive_internal_test.go index 8c2e68b924400..0d50bfb7f0c78 100644 --- a/coderd/x/chatd/auto_archive_internal_test.go +++ b/coderd/x/chatd/auto_archive_internal_test.go @@ -668,8 +668,14 @@ func TestWorker_AutoArchiveLoopRunsImmediatelyAndOnTick(t *testing.T) { opts.ArchiveInterval = time.Minute worker := f.newArchiveWorkerWithOptions(t, opts) - trap := mClock.Trap().NewTicker("chatworker", "auto-archive") - defer trap.Close() + nowTrap := mClock.Trap().Now("chatworker", "auto-archive") + defer nowTrap.Close() + tickerTrap := mClock.Trap().NewTicker("chatworker", "auto-archive") + defer tickerTrap.Close() + tickerStopTrap := mClock.Trap().TickerStop("chatworker", "auto-archive") + defer tickerStopTrap.Close() + tickerResetTrap := mClock.Trap().TickerReset("chatworker", "auto-archive") + defer tickerResetTrap.Close() loopCtx, cancel := context.WithCancel(ctx) done := make(chan struct{}) @@ -678,20 +684,26 @@ func TestWorker_AutoArchiveLoopRunsImmediatelyAndOnTick(t *testing.T) { worker.archiveLoop(loopCtx) }() - // archiveLoop creates the ticker before the immediate startup tick. - trap.MustWait(ctx).MustRelease(ctx) + nowTrap.MustWait(ctx).MustRelease(ctx) testutil.Eventually(ctx, t, func(context.Context) bool { return f.archived(t, first.ID) }, testutil.IntervalFast, "immediate startup tick should archive the first candidate") + tickerTrap.MustWait(ctx).MustRelease(ctx) // A second candidate is only archived once the interval ticker fires. second := f.createArchiveCandidate(t, now.Add(-120*24*time.Hour)) - mClock.Advance(time.Minute).MustWait(ctx) + advanced := mClock.Advance(time.Minute) + tickerStopTrap.MustWait(ctx).MustRelease(ctx) testutil.Eventually(ctx, t, func(context.Context) bool { return f.archived(t, second.ID) }, testutil.IntervalFast, "interval tick should archive the second candidate") + resetCall := tickerResetTrap.MustWait(ctx) + require.Equal(t, time.Minute, resetCall.Duration) + resetCall.MustRelease(ctx) + advanced.MustWait(ctx) cancel() + tickerStopTrap.MustWait(ctx).MustRelease(ctx) select { case <-done: case <-ctx.Done():