mirror of https://github.com/grafana/loki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
9.2 KiB
238 lines
9.2 KiB
package cache
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/atomic"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEmbeddedCacheEviction(t *testing.T) {
|
|
const (
|
|
cnt = 10
|
|
evicted = 5
|
|
)
|
|
|
|
// compute value size such that 10 entries account to exactly 1MB.
|
|
// adding one more entry to the cache would result in eviction when MaxSizeMB is configured to a value of 1.
|
|
// value cap = target size of each entry (0.1MB) - size of cache entry with empty value.
|
|
valueCap := (1e6 / cnt) - sizeOf(&Entry[string, []byte]{
|
|
Key: "00",
|
|
})
|
|
|
|
itemTemplate := &Entry[string, []byte]{
|
|
Key: "00",
|
|
Value: make([]byte, 0, valueCap),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg EmbeddedCacheConfig
|
|
}{
|
|
{
|
|
name: "test-memory-eviction",
|
|
cfg: EmbeddedCacheConfig{MaxSizeMB: 1, TTL: 1 * time.Minute},
|
|
},
|
|
{
|
|
name: "test-items-eviction",
|
|
cfg: EmbeddedCacheConfig{MaxSizeItems: cnt, TTL: 1 * time.Minute},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
removedEntriesCount := atomic.NewInt64(0)
|
|
onEntryRemoved := func(key string, value []byte) {
|
|
removedEntriesCount.Inc()
|
|
}
|
|
c := NewTypedEmbeddedCache[string, []byte](test.name, test.cfg, nil, log.NewNopLogger(), "test", sizeOf, onEntryRemoved)
|
|
ctx := context.Background()
|
|
|
|
// Check put / get works
|
|
keys := []string{}
|
|
values := [][]byte{}
|
|
for i := 0; i < cnt; i++ {
|
|
key := fmt.Sprintf("%02d", i)
|
|
value := make([]byte, len(key), valueCap)
|
|
copy(value, key)
|
|
keys = append(keys, key)
|
|
values = append(values, value)
|
|
}
|
|
err := c.Store(ctx, keys, values)
|
|
require.NoError(t, err)
|
|
require.Len(t, c.entries, cnt)
|
|
|
|
reason := fullReason
|
|
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesAddedNew), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(reason)), float64(0))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(replacedReason)), float64(0))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(len(c.entries)))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(c.lru.Len()))
|
|
assert.Equal(t, testutil.ToFloat64(c.memoryBytes), float64(cnt*sizeOf(itemTemplate)))
|
|
|
|
for i := 0; i < cnt; i++ {
|
|
key := fmt.Sprintf("%02d", i)
|
|
value, ok := c.Get(ctx, key)
|
|
require.True(t, ok)
|
|
require.Equal(t, []byte(key), value)
|
|
}
|
|
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesAddedNew), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(reason)), float64(0))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(replacedReason)), float64(0))
|
|
assert.Equal(t, int64(0), removedEntriesCount.Load())
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(len(c.entries)))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(c.lru.Len()))
|
|
assert.Equal(t, testutil.ToFloat64(c.memoryBytes), float64(cnt*sizeOf(itemTemplate)))
|
|
|
|
// Check evictions
|
|
keys = []string{}
|
|
values = [][]byte{}
|
|
for i := cnt - evicted; i < cnt+evicted; i++ {
|
|
key := fmt.Sprintf("%02d", i)
|
|
value := make([]byte, len(key), valueCap)
|
|
copy(value, key)
|
|
keys = append(keys, key)
|
|
values = append(values, value)
|
|
}
|
|
err = c.Store(ctx, keys, values)
|
|
require.NoError(t, err)
|
|
require.Len(t, c.entries, cnt)
|
|
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesAddedNew), float64(cnt+evicted))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(reason)), float64(evicted))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(replacedReason)), float64(evicted))
|
|
assert.Equalf(t, int64(evicted+evicted), removedEntriesCount.Load(), "%d items were evicted and %d items were replaced", evicted, evicted)
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(len(c.entries)))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(c.lru.Len()))
|
|
assert.Equal(t, testutil.ToFloat64(c.memoryBytes), float64(cnt*sizeOf(itemTemplate)))
|
|
|
|
for i := 0; i < cnt-evicted; i++ {
|
|
_, ok := c.Get(ctx, fmt.Sprintf("%02d", i))
|
|
require.False(t, ok)
|
|
}
|
|
for i := cnt - evicted; i < cnt+evicted; i++ {
|
|
key := fmt.Sprintf("%02d", i)
|
|
value, ok := c.Get(ctx, key)
|
|
require.True(t, ok)
|
|
require.Equal(t, []byte(key), value)
|
|
}
|
|
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesAddedNew), float64(cnt+evicted))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(reason)), float64(evicted))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(replacedReason)), float64(evicted))
|
|
assert.Equal(t, int64(evicted+evicted), removedEntriesCount.Load(), "During this step the count of the calls must not be changed because we do read-only operations")
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(len(c.entries)))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(c.lru.Len()))
|
|
assert.Equal(t, testutil.ToFloat64(c.memoryBytes), float64(cnt*sizeOf(itemTemplate)))
|
|
|
|
// Check updates work
|
|
keys = []string{}
|
|
values = [][]byte{}
|
|
for i := cnt; i < cnt+evicted; i++ {
|
|
keys = append(keys, fmt.Sprintf("%02d", i))
|
|
vstr := fmt.Sprintf("%02d", i*2)
|
|
value := make([]byte, len(vstr), valueCap)
|
|
copy(value, vstr)
|
|
values = append(values, value)
|
|
}
|
|
err = c.Store(ctx, keys, values)
|
|
require.NoError(t, err)
|
|
require.Len(t, c.entries, cnt)
|
|
|
|
for i := cnt; i < cnt+evicted; i++ {
|
|
value, ok := c.Get(ctx, fmt.Sprintf("%02d", i))
|
|
require.True(t, ok)
|
|
require.Equal(t, []byte(fmt.Sprintf("%02d", i*2)), value)
|
|
}
|
|
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesAddedNew), float64(cnt+evicted))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(reason)), float64(evicted))
|
|
assert.Equalf(t, testutil.ToFloat64(c.entriesEvicted.WithLabelValues(replacedReason)), float64(evicted+evicted),
|
|
"During this step we replace %d more items", evicted)
|
|
assert.Equalf(t, int64(evicted+evicted+evicted), removedEntriesCount.Load(), "During this step we replace %d more items", evicted)
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(cnt))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(len(c.entries)))
|
|
assert.Equal(t, testutil.ToFloat64(c.entriesCurrent), float64(c.lru.Len()))
|
|
assert.Equal(t, testutil.ToFloat64(c.memoryBytes), float64(cnt*sizeOf(itemTemplate)))
|
|
|
|
c.Stop()
|
|
assert.Equal(t, int64(evicted*3), removedEntriesCount.Load(), "onEntryRemoved must not be called for the items during the stop")
|
|
}
|
|
}
|
|
|
|
func TestEmbeddedCacheExpiry(t *testing.T) {
|
|
key1, key2, key3, key4 := "01", "02", "03", "04"
|
|
data1, data2, data3, data4 := genBytes(32), genBytes(64), genBytes(128), genBytes(32)
|
|
|
|
memorySz := sizeOf(&Entry[string, []byte]{Key: key1, Value: data1}) +
|
|
sizeOf(&Entry[string, []byte]{Key: key2, Value: data2}) +
|
|
sizeOf(&Entry[string, []byte]{Key: key3, Value: data3})
|
|
|
|
cfg := EmbeddedCacheConfig{
|
|
MaxSizeItems: 3,
|
|
TTL: 100 * time.Millisecond,
|
|
PurgeInterval: 50 * time.Millisecond,
|
|
}
|
|
|
|
removedEntriesCount := atomic.NewInt64(0)
|
|
onEntryRemoved := func(key string, value []byte) {
|
|
removedEntriesCount.Inc()
|
|
}
|
|
c := NewTypedEmbeddedCache[string, []byte]("cache_exprity_test", cfg, nil, log.NewNopLogger(), "test", sizeOf, onEntryRemoved)
|
|
ctx := context.Background()
|
|
|
|
err := c.Store(ctx, []string{key1, key2, key3, key4}, [][]byte{data1, data2, data3, data4})
|
|
require.NoError(t, err)
|
|
|
|
value, ok := c.Get(ctx, key4)
|
|
require.True(t, ok)
|
|
require.Equal(t, data4, value)
|
|
|
|
_, ok = c.Get(ctx, key1)
|
|
require.False(t, ok)
|
|
|
|
assert.Equal(t, float64(4), testutil.ToFloat64(c.entriesAddedNew))
|
|
assert.Equal(t, float64(0), testutil.ToFloat64(c.entriesEvicted.WithLabelValues(expiredReason)))
|
|
assert.Equal(t, float64(1), testutil.ToFloat64(c.entriesEvicted.WithLabelValues(fullReason)))
|
|
assert.Equal(t, float64(3), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(len(c.entries)), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(c.lru.Len()), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(memorySz), testutil.ToFloat64(c.memoryBytes))
|
|
assert.Equal(t, int64(1), removedEntriesCount.Load(), "on removal callback had to be called for key1")
|
|
|
|
// Expire the item.
|
|
time.Sleep(2 * cfg.TTL)
|
|
_, ok = c.Get(ctx, key4)
|
|
require.False(t, ok)
|
|
|
|
assert.Equal(t, float64(4), testutil.ToFloat64(c.entriesAddedNew))
|
|
assert.Equal(t, float64(3), testutil.ToFloat64(c.entriesEvicted.WithLabelValues(expiredReason)))
|
|
assert.Equal(t, float64(1), testutil.ToFloat64(c.entriesEvicted.WithLabelValues(fullReason)))
|
|
assert.Equal(t, float64(0), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(len(c.entries)), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(c.lru.Len()), testutil.ToFloat64(c.entriesCurrent))
|
|
assert.Equal(t, float64(memorySz), testutil.ToFloat64(c.memoryBytes))
|
|
assert.Equal(t, int64(4), removedEntriesCount.Load(), "on removal callback had to be called for all 3 expired entries")
|
|
|
|
c.Stop()
|
|
}
|
|
|
|
func genBytes(n uint8) []byte {
|
|
arr := make([]byte, n)
|
|
for i := range arr {
|
|
arr[i] = byte(i)
|
|
}
|
|
return arr
|
|
}
|
|
|