Files
traces/tempodb/backend/cache/cache_test.go
Joe ElliottandGitHub 647ea9f7b9 [BugFix] Add object name to cache key (#4982)
* Add object name to cache key

Signed-off-by: Joe Elliott <[email protected]>

* fix test

Signed-off-by: Joe Elliott <[email protected]>

* test

Signed-off-by: Joe Elliott <[email protected]>

---------

Signed-off-by: Joe Elliott <[email protected]>
2025-04-11 08:02:46 -04:00

308 lines
8.8 KiB
Go

package cache
import (
"bytes"
"context"
"io"
"testing"
"time"
"github.com/go-kit/log"
"github.com/google/uuid"
"github.com/grafana/tempo/pkg/cache"
"github.com/grafana/tempo/pkg/util/test"
"github.com/grafana/tempo/tempodb/backend"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCacheFor(t *testing.T) {
reader, _, err := NewCache(&BloomConfig{
CacheMaxBlockAge: time.Hour,
CacheMinCompactionLevel: 1,
}, nil, nil, test.NewMockProvider(), log.NewNopLogger())
require.NoError(t, err)
rw := reader.(*readerWriter)
// test.MockProvider will return the same cache for all requests. we need
// to override individual caches so that the test can validate the cache returned
rw.footerCache = test.NewMockClient()
rw.columnIdxCache = test.NewMockClient()
rw.offsetIdxCache = test.NewMockClient()
rw.traceIDIdxCache = test.NewMockClient()
rw.bloomCache = test.NewMockClient()
testCases := []struct {
name string
cacheInfo *backend.CacheInfo
expectedCache cache.Cache
}{
// first three caches are unconditionally returned
{
name: "footer is always returned",
cacheInfo: &backend.CacheInfo{Role: cache.RoleParquetFooter},
expectedCache: rw.footerCache,
},
{
name: "col idx is always returned",
cacheInfo: &backend.CacheInfo{Role: cache.RoleParquetColumnIdx},
expectedCache: rw.columnIdxCache,
},
{
name: "offset idx is always returned",
cacheInfo: &backend.CacheInfo{Role: cache.RoleParquetOffsetIdx},
expectedCache: rw.offsetIdxCache,
},
{
name: "trace id idx is always returned",
cacheInfo: &backend.CacheInfo{Role: cache.RoleTraceIDIdx},
expectedCache: rw.traceIDIdxCache,
},
// bloom cache is returned if the meta is valid given the bloom config
{
name: "bloom - no meta means no cache",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
},
},
{
name: "bloom - compaction lvl and start time valid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 1, StartTime: time.Now()},
},
expectedCache: rw.bloomCache,
},
{
name: "bloom - start time invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 1, StartTime: time.Now().Add(-2 * time.Hour)},
},
},
{
name: "bloom - compaction lvl invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 2, StartTime: time.Now()},
},
},
{
name: "bloom - both invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 2, StartTime: time.Now().Add(-2 * time.Hour)},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expectedCache, rw.cacheFor(tt.cacheInfo))
})
}
}
func TestCacheForReturnsBloomWithNoConfig(t *testing.T) {
reader, _, err := NewCache(nil, nil, nil, test.NewMockProvider(), log.NewNopLogger())
require.NoError(t, err)
rw := reader.(*readerWriter)
testCases := []struct {
name string
cacheInfo *backend.CacheInfo
expectedCache cache.Cache
}{
// bloom cache is returned if the meta is valid given the bloom config
{
name: "bloom - no meta means no cache",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
},
expectedCache: rw.bloomCache,
},
{
name: "bloom - compaction lvl and start time valid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 1, StartTime: time.Now()},
},
expectedCache: rw.bloomCache,
},
{
name: "bloom - start time invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 1, StartTime: time.Now().Add(-2 * time.Hour)},
},
expectedCache: rw.bloomCache,
},
{
name: "bloom - compaction lvl invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 2, StartTime: time.Now()},
},
expectedCache: rw.bloomCache,
},
{
name: "bloom - both invalid",
cacheInfo: &backend.CacheInfo{
Role: cache.RoleBloom,
Meta: &backend.BlockMeta{CompactionLevel: 2, StartTime: time.Now().Add(-2 * time.Hour)},
},
expectedCache: rw.bloomCache,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expectedCache, rw.cacheFor(tt.cacheInfo))
})
}
}
func TestReadWrite(t *testing.T) {
tenantID := "test"
blockID := uuid.New()
tests := []struct {
name string
readerRead []byte
readerName string
cacheInfo *backend.CacheInfo
expectedRead []byte
expectedCache []byte
}{
{
name: "should cache",
readerName: "foo",
readerRead: []byte{0x02},
cacheInfo: &backend.CacheInfo{Role: cache.RoleParquetFooter},
expectedRead: []byte{0x02},
expectedCache: []byte{0x02},
},
{
name: "should not cache",
readerName: "bar",
cacheInfo: &backend.CacheInfo{Role: cache.Role("foo")}, // fake role name will not find a matching cache
readerRead: []byte{0x02},
expectedRead: []byte{0x02},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockR := &backend.MockRawReader{
R: tt.readerRead,
}
mockW := &backend.MockRawWriter{}
// READ
r, _, err := NewCache(nil, mockR, mockW, test.NewMockProvider(), log.NewNopLogger())
require.NoError(t, err)
ctx := context.Background()
reader, _, _ := r.Read(ctx, tt.readerName, backend.KeyPathForBlock(blockID, tenantID), tt.cacheInfo)
read, _ := io.ReadAll(reader)
assert.Equal(t, tt.expectedRead, read)
// clear reader and re-request
mockR.R = nil
reader, _, _ = r.Read(ctx, tt.readerName, backend.KeyPathForBlock(blockID, tenantID), tt.cacheInfo)
read, _ = io.ReadAll(reader)
assert.Equal(t, len(tt.expectedCache), len(read))
// WRITE
_, w, err := NewCache(nil, mockR, mockW, test.NewMockProvider(), log.NewNopLogger())
require.NoError(t, err)
_ = w.Write(ctx, tt.readerName, backend.KeyPathForBlock(blockID, tenantID), bytes.NewReader(tt.readerRead), int64(len(tt.readerRead)), tt.cacheInfo)
reader, _, _ = r.Read(ctx, tt.readerName, backend.KeyPathForBlock(blockID, tenantID), tt.cacheInfo)
read, _ = io.ReadAll(reader)
assert.Equal(t, len(tt.expectedCache), len(read))
})
}
}
func TestList(t *testing.T) {
tenantID := "test"
blockID := uuid.New()
tests := []struct {
name string
readerList []string
expectedList []string
expectedCache []string
}{
{
name: "list passthrough",
readerList: []string{"1"},
expectedList: []string{"1"},
expectedCache: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockR := &backend.MockRawReader{
L: tt.readerList,
}
mockW := &backend.MockRawWriter{}
rw, _, _ := NewCache(nil, mockR, mockW, test.NewMockProvider(), log.NewNopLogger())
ctx := context.Background()
list, _ := rw.List(ctx, backend.KeyPathForBlock(blockID, tenantID))
assert.Equal(t, tt.expectedList, list)
// clear reader and re-request. things should be cached!
mockR.L = nil
// list is not cached
list, _ = rw.List(ctx, backend.KeyPathForBlock(blockID, tenantID))
assert.Equal(t, tt.expectedCache, list)
})
}
}
func TestCacheKeys(t *testing.T) {
provider := test.NewMockProvider()
reader, _, err := NewCache(&BloomConfig{
CacheMaxBlockAge: time.Hour,
CacheMinCompactionLevel: 1,
}, nil, nil, provider, log.NewNopLogger())
require.NoError(t, err)
ctx := context.Background()
role := cache.RoleParquetFooter // role doesn't matter b/c the mock provider always returns the same cache
// Read : seed data at expected key
expectedKey := "bar:baz:foo" // keypath + object name
expectedData := []byte("test-read")
provider.CacheFor(role).Store(ctx, []string{expectedKey}, [][]byte{expectedData})
// make request and confirm it returns
actualReader, actualBytes, err := reader.Read(ctx, "foo", backend.KeyPath{"bar", "baz"}, &backend.CacheInfo{Role: role})
require.NoError(t, err)
require.Equal(t, len(expectedData), int(actualBytes))
actualData, err := io.ReadAll(actualReader)
require.NoError(t, err)
require.Equal(t, expectedData, actualData)
// ReadRange : seed data at expected key
expectedKey = "bar:baz:foo:10:10" // keypath + object name + offset + length
expectedData = []byte("test-range")
provider.CacheFor(role).Store(ctx, []string{expectedKey}, [][]byte{expectedData})
// make request and confirm it returns
actualBuffer := make([]byte, 10)
err = reader.ReadRange(ctx, "foo", backend.KeyPath{"bar", "baz"}, 10, actualBuffer, &backend.CacheInfo{Role: role})
require.NoError(t, err)
require.Equal(t, expectedData, actualBuffer)
}