VictoriaMetrics/lib/blockcache/blockcache_test.go
Aliaksandr Valialkin 010dc15d16
lib/blockcache: do not cache entries, which were attempted to be accessed 1 or 2 times
Previously entries which were accessed only 1 time weren't cached.
It has been appeared that some rarely executed heavy queries may read indexdb block twice
in a row instead of once. There is no need in caching such a block then.
This change should eliminate cache size spikes for indexdb/dataBlocks when such heavy queries are executed.

Expose -blockcache.missesBeforeCaching command-line flag, which can be used for fine-tuning
the number of cache misses needed before storing the block in the caching.
2023-11-10 22:28:03 +01:00

168 lines
5 KiB
Go

package blockcache
import (
"fmt"
"sync"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
)
func TestCache(t *testing.T) {
sizeMaxBytes := 64 * 1024
// Multiply sizeMaxBytes by the square of available CPU cores
// in order to get proper distribution of sizes between cache shards.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2204
cpus := cgroup.AvailableCPUs()
sizeMaxBytes *= cpus * cpus
getMaxSize := func() int {
return sizeMaxBytes
}
c := NewCache(getMaxSize)
defer c.MustStop()
if n := c.SizeBytes(); n != 0 {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, 0)
}
if n := c.SizeMaxBytes(); n != sizeMaxBytes {
t.Fatalf("unexpected SizeMaxBytes(); got %d; want %d", n, sizeMaxBytes)
}
offset := uint64(1234)
part := (interface{})("foobar")
k := Key{
Offset: offset,
Part: part,
}
var b testBlock
blockSize := b.SizeBytes()
// Put a single entry into cache
c.PutBlock(k, &b)
if n := c.Len(); n != 1 {
t.Fatalf("unexpected number of items in the cache; got %d; want %d", n, 1)
}
if n := c.SizeBytes(); n != blockSize {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, blockSize)
}
if n := c.Requests(); n != 0 {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 0)
}
if n := c.Misses(); n != 0 {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
}
// Obtain this entry from the cache
if b1 := c.GetBlock(k); b1 != &b {
t.Fatalf("unexpected block obtained; got %v; want %v", b1, &b)
}
if n := c.Requests(); n != 1 {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 1)
}
if n := c.Misses(); n != 0 {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
}
// Obtain non-existing entry from the cache
if b1 := c.GetBlock(Key{Offset: offset + 1}); b1 != nil {
t.Fatalf("unexpected non-nil block obtained for non-existing key: %v", b1)
}
if n := c.Requests(); n != 2 {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 2)
}
if n := c.Misses(); n != 1 {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 1)
}
// Remove entries for the given part from the cache
c.RemoveBlocksForPart(part)
if n := c.SizeBytes(); n != 0 {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, 0)
}
// Verify that the entry has been removed from the cache
if b1 := c.GetBlock(k); b1 != nil {
t.Fatalf("unexpected non-nil block obtained after removing all the blocks for the part; got %v", b1)
}
if n := c.Requests(); n != 3 {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 3)
}
if n := c.Misses(); n != 2 {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 2)
}
for i := 0; i < *missesBeforeCaching; i++ {
// Store the missed entry to the cache. It shouldn't be stored because of the previous cache miss
c.PutBlock(k, &b)
if n := c.SizeBytes(); n != 0 {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, 0)
}
// Verify that the entry wasn't stored to the cache.
if b1 := c.GetBlock(k); b1 != nil {
t.Fatalf("unexpected non-nil block obtained after removing all the blocks for the part; got %v", b1)
}
if n := c.Requests(); n != uint64(4+i) {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 4+i)
}
if n := c.Misses(); n != uint64(3+i) {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 3+i)
}
}
// Store the entry again. Now it must be stored because of the second cache miss.
c.PutBlock(k, &b)
if n := c.SizeBytes(); n != blockSize {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, blockSize)
}
if b1 := c.GetBlock(k); b1 != &b {
t.Fatalf("unexpected block obtained; got %v; want %v", b1, &b)
}
if n := c.Requests(); n != uint64(4+*missesBeforeCaching) {
t.Fatalf("unexpected number of requests; got %d; want %d", n, 4+*missesBeforeCaching)
}
if n := c.Misses(); n != uint64(2+*missesBeforeCaching) {
t.Fatalf("unexpected number of misses; got %d; want %d", n, 2+*missesBeforeCaching)
}
// Manually clean the cache. The entry shouldn't be deleted because it was recently accessed.
c.cleanPerKeyMisses()
c.cleanByTimeout()
if n := c.SizeBytes(); n != blockSize {
t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, blockSize)
}
}
func TestCacheConcurrentAccess(_ *testing.T) {
const sizeMaxBytes = 16 * 1024 * 1024
getMaxSize := func() int {
return sizeMaxBytes
}
c := NewCache(getMaxSize)
defer c.MustStop()
workers := 5
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func(worker int) {
defer wg.Done()
testCacheSetGet(c, worker)
}(i)
}
wg.Wait()
}
func testCacheSetGet(c *Cache, worker int) {
for i := 0; i < 1000; i++ {
part := (interface{})(i)
b := testBlock{}
k := Key{
Offset: uint64(worker*1000 + i),
Part: part,
}
c.PutBlock(k, &b)
if b1 := c.GetBlock(k); b1 != &b {
panic(fmt.Errorf("unexpected block obtained; got %v; want %v", b1, &b))
}
if b1 := c.GetBlock(Key{}); b1 != nil {
panic(fmt.Errorf("unexpected non-nil block obtained: %v", b1))
}
}
}
type testBlock struct{}
func (tb *testBlock) SizeBytes() int {
return 42
}