Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions coderd/x/chatd/auto_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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,
Expand Down
22 changes: 17 additions & 5 deletions coderd/x/chatd/auto_archive_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand All @@ -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():
Expand Down
Loading