Files
metrics/lib/storage/date_metric_id_cache_test.go
Artem FetishevandGitHub ce45fa4f4a lib/storage: shard dateMetricIDCache (#10486)
Use the same sharded implementation as in metricIDCache. The change is
basically a copy-paste. The only difference is that the rotation period
remains `1h` instead `1m` in order not to break the fix for #10064.

Signed-off-by: Artem Fetishev <[email protected]>
2026-02-18 18:16:37 +01:00

158 lines
3.7 KiB
Go

package storage
import (
"fmt"
"sync"
"testing"
"time"
)
func newDateMetricIDCacheShard() *dateMetricIDCacheShard {
var c dateMetricIDCacheShard
c.prev = newByDateMetricIDMap()
c.next = newByDateMetricIDMap()
c.curr.Store(newByDateMetricIDMap())
return &c
}
func TestDateMetricIDCacheShardSerial(t *testing.T) {
c := newDateMetricIDCacheShard()
if err := testDateMetricIDCacheShard(c, false); err != nil {
t.Fatalf("unexpected error: %s", err)
}
}
func TestDateMetricIDCacheShardConcurrent(t *testing.T) {
c := newDateMetricIDCacheShard()
ch := make(chan error, 5)
for range 5 {
go func() {
ch <- testDateMetricIDCacheShard(c, true)
}()
}
for range 5 {
select {
case err := <-ch:
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
case <-time.After(time.Second * 5):
t.Fatalf("timeout")
}
}
}
func testDateMetricIDCacheShard(c *dateMetricIDCacheShard, concurrent bool) error {
type dmk struct {
date uint64
metricID uint64
}
m := make(map[dmk]bool)
for i := range int(1e5) {
date := uint64(i) % 2
metricID := uint64(i) % 1237
if !concurrent && c.Has(date, metricID) {
if !m[dmk{date, metricID}] {
return fmt.Errorf("c.Has(%d, %d) must return false, but returned true", date, metricID)
}
continue
}
c.Set(date, metricID)
m[dmk{date, metricID}] = true
if !concurrent && !c.Has(date, metricID) {
return fmt.Errorf("c.Has(%d, %d) must return true, but returned false", date, metricID)
}
if i%11234 == 0 {
c.mu.Lock()
c.syncLocked()
c.mu.Unlock()
}
if i%34323 == 0 {
// Two rotations are needed to clear the cache.
c.rotate()
c.rotate()
m = make(map[dmk]bool)
}
}
// Verify fast path after sync.
for i := range int(1e5) {
date := uint64(i) % 2
metricID := uint64(i) % 123
c.Set(date, metricID)
}
c.mu.Lock()
c.syncLocked()
c.mu.Unlock()
for i := range int(1e5) {
date := uint64(i) % 2
metricID := uint64(i) % 123
if !concurrent && !c.Has(date, metricID) {
return fmt.Errorf("c.Has(%d, %d) must return true after sync", date, metricID)
}
}
// Verify that cache becomes empty after two rotations.
if n := c.Stats().Size; !concurrent && n < 123 {
return fmt.Errorf("c.EntriesCount must return at least 123; returned %d", n)
}
c.rotate()
if n := c.Stats().Size; !concurrent && n < 123 {
return fmt.Errorf("c.EntriesCount must return at least 123; returned %d", n)
}
c.rotate()
if n := c.Stats().Size; !concurrent && n > 0 {
return fmt.Errorf("c.EntriesCount must return 0 after reset; returned %d", n)
}
return nil
}
func TestDateMetricIDCacheIsConsistent(_ *testing.T) {
const (
generation = 1
date = 1
concurrency = 2
numMetrics = 100000
)
dmc := newDateMetricIDCache()
defer dmc.MustStop()
var wg sync.WaitGroup
for i := range concurrency {
wg.Go(func() {
for id := uint64(i * numMetrics); id < uint64((i+1)*numMetrics); id++ {
dmc.Set(date, id)
if !dmc.Has(date, id) {
panic(fmt.Errorf("dmc.Has(metricID=%d): unexpected cache miss after adding the entry to cache", id))
}
}
})
}
wg.Wait()
}
func TestDateMetricIDCache_Size(t *testing.T) {
dmc := newDateMetricIDCache()
defer dmc.MustStop()
for i := range 100_000 {
date := 12345 + uint64(i%30)
metricID := uint64(i)
dmc.Set(date, metricID)
if got, want := dmc.Stats().Size, uint64(i+1); got != want {
t.Fatalf("unexpected size: got %d, want %d", got, want)
}
}
// Retrieve all entries and check the cache size again.
for i := range 100_000 {
date := 12345 + uint64(i%30)
metricID := uint64(i)
if !dmc.Has(date, metricID) {
t.Fatalf("entry not in cache: (date=%d, metricID=%d)", date, metricID)
}
}
if got, want := dmc.Stats().Size, uint64(100_000); got != want {
t.Fatalf("unexpected size: got %d, want %d", got, want)
}
}