Skip to content

Commit eccdf45

Browse files
feat: add evictor (#16839)
1 parent 4b9c8de commit eccdf45

File tree

2 files changed

+105
-0
lines changed

2 files changed

+105
-0
lines changed

‎pkg/limits/evict.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package limits
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/coder/quartz"
8+
"github.com/go-kit/log"
9+
"github.com/go-kit/log/level"
10+
)
11+
12+
type Evictable interface {
13+
Evict(context.Context) error
14+
}
15+
16+
// Evictor runs scheduled evictions.
17+
type Evictor struct {
18+
ctx context.Context
19+
interval time.Duration
20+
target Evictable
21+
logger log.Logger
22+
23+
// Used for tests.
24+
clock quartz.Clock
25+
}
26+
27+
// NewEvictor returns a new evictor over the interval.
28+
func NewEvictor(ctx context.Context, interval time.Duration, target Evictable, logger log.Logger) (*Evictor, error) {
29+
return &Evictor{
30+
ctx: ctx,
31+
interval: interval,
32+
target: target,
33+
logger: logger,
34+
clock: quartz.NewReal(),
35+
}, nil
36+
}
37+
38+
// Runs the scheduler loop until the context is canceled.
39+
func (e *Evictor) Run() error {
40+
t := e.clock.TickerFunc(e.ctx, e.interval, e.doTick)
41+
return t.Wait()
42+
}
43+
44+
func (e *Evictor) doTick() error {
45+
ctx, cancel := context.WithTimeout(e.ctx, e.interval)
46+
defer cancel()
47+
if err := e.target.Evict(ctx); err != nil {
48+
level.Warn(e.logger).Log("failed to run eviction", "err", err.Error())
49+
}
50+
return nil
51+
}

‎pkg/limits/evict_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package limits
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/coder/quartz"
9+
"github.com/go-kit/log"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type mockEvictable struct {
14+
calls []time.Time
15+
clock quartz.Clock
16+
}
17+
18+
func (m *mockEvictable) Evict(_ context.Context) error {
19+
m.calls = append(m.calls, m.clock.Now())
20+
return nil
21+
}
22+
23+
func TestEvictor(t *testing.T) {
24+
// Set a timeout on the test.
25+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
26+
defer cancel()
27+
28+
clock := quartz.NewMock(t)
29+
m := mockEvictable{clock: clock}
30+
e, err := NewEvictor(ctx, time.Second, &m, log.NewNopLogger())
31+
require.NoError(t, err)
32+
e.clock = clock
33+
34+
// See https://github.com/coder/quartz/blob/v0.1.3/example_test.go#L48.
35+
trap := clock.Trap().TickerFunc()
36+
defer trap.Close()
37+
go e.Run() //nolint:errcheck
38+
call, err := trap.Wait(ctx)
39+
require.NoError(t, err)
40+
call.Release()
41+
42+
// Do a tick.
43+
clock.Advance(time.Second).MustWait(ctx)
44+
tick1 := clock.Now()
45+
require.Len(t, m.calls, 1)
46+
require.Equal(t, tick1, m.calls[0])
47+
48+
// Do another tick.
49+
clock.Advance(time.Second).MustWait(ctx)
50+
tick2 := clock.Now()
51+
require.Len(t, m.calls, 2)
52+
require.Equal(t, tick1, m.calls[0])
53+
require.Equal(t, tick2, m.calls[1])
54+
}

0 commit comments

Comments
 (0)