Like Prometheus, but for logs.
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.
loki/pkg/storage/chunk/cache/embeddedcache_test.go

243 lines
9.3 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.
BloomShipper: add cache for downloaded blocks (#11394) adapted embeddedcache.go to store downloaded bloom blocks on local filesystem. **What this PR does / why we need it**: This cache will be used by bloom-gateway to not download the blocks each time. In the cache we store the objects(`cachedBlock` struct) that contain: 1. `blockDirectory`: the `path` to the directory where the block was extracted 2. `activeQueriers`: thread-safe counter of active block users. This field is important because we do not want the directory to be removed while it's in use. 3. rest fields are needed for service needs When the downloadingWorker receives this entity, it increases the counter `activeQueriers` and creates blockQuerier wrapper that has `Close` function that will decrease the counter once blockQuerier is not needed anymore. When the cache entry is being removed from the cache, the cache calls the function `removeDirectoryAsync` which asynchronously removes the block's folder. This function checks every `Xms` if there are still active block queriers and once `activeQueriers` count is `0` the folder will be removed. Also, there is a timeout in this function, and once the timeout is reached, the folder will be force removed. **Special notes for your reviewer**: * If the cache is disabled, then the blocks will be downloaded each time the block is requested. * If the cache is used, the folder will be deleted by embeddedCache when it reaches memory size limit or items count limit. Otherwise, the block's folder will be deleted when `Close` function is called. **Checklist** - [x] Reviewed the [`CONTRIBUTING.md`](https://github.com/grafana/loki/blob/main/CONTRIBUTING.md) guide (**required**) - [x] Documentation added - [x] Tests updated - [ ] `CHANGELOG.md` updated - [ ] If the change is worth mentioning in the release notes, add `add-to-release-notes` label - [ ] Changes that require user attention or interaction to upgrade are documented in `docs/sources/setup/upgrade/_index.md` - [ ] For Helm chart changes bump the Helm chart version in `production/helm/loki/Chart.yaml` and update `production/helm/loki/CHANGELOG.md` and `production/helm/loki/README.md`. [Example PR](https://github.com/grafana/loki/commit/d10549e3ece02120974929894ee333d07755d213) - [ ] If the change is deprecating or removing a configuration option, update the `deprecated-config.yaml` and `deleted-config.yaml` files respectively in the `tools/deprecated-config-checker` directory. [Example PR](https://github.com/grafana/loki/pull/10840/commits/0d4416a4b03739583349934b96f272fb4f685d15) --------- Signed-off-by: Vladyslav Diachenko <vlad.diachenko@grafana.com>
2 years ago
valueCap := (1e6 / cnt) - sizeOf(&Entry[string, []byte]{
Key: "00",
})
BloomShipper: add cache for downloaded blocks (#11394) adapted embeddedcache.go to store downloaded bloom blocks on local filesystem. **What this PR does / why we need it**: This cache will be used by bloom-gateway to not download the blocks each time. In the cache we store the objects(`cachedBlock` struct) that contain: 1. `blockDirectory`: the `path` to the directory where the block was extracted 2. `activeQueriers`: thread-safe counter of active block users. This field is important because we do not want the directory to be removed while it's in use. 3. rest fields are needed for service needs When the downloadingWorker receives this entity, it increases the counter `activeQueriers` and creates blockQuerier wrapper that has `Close` function that will decrease the counter once blockQuerier is not needed anymore. When the cache entry is being removed from the cache, the cache calls the function `removeDirectoryAsync` which asynchronously removes the block's folder. This function checks every `Xms` if there are still active block queriers and once `activeQueriers` count is `0` the folder will be removed. Also, there is a timeout in this function, and once the timeout is reached, the folder will be force removed. **Special notes for your reviewer**: * If the cache is disabled, then the blocks will be downloaded each time the block is requested. * If the cache is used, the folder will be deleted by embeddedCache when it reaches memory size limit or items count limit. Otherwise, the block's folder will be deleted when `Close` function is called. **Checklist** - [x] Reviewed the [`CONTRIBUTING.md`](https://github.com/grafana/loki/blob/main/CONTRIBUTING.md) guide (**required**) - [x] Documentation added - [x] Tests updated - [ ] `CHANGELOG.md` updated - [ ] If the change is worth mentioning in the release notes, add `add-to-release-notes` label - [ ] Changes that require user attention or interaction to upgrade are documented in `docs/sources/setup/upgrade/_index.md` - [ ] For Helm chart changes bump the Helm chart version in `production/helm/loki/Chart.yaml` and update `production/helm/loki/CHANGELOG.md` and `production/helm/loki/README.md`. [Example PR](https://github.com/grafana/loki/commit/d10549e3ece02120974929894ee333d07755d213) - [ ] If the change is deprecating or removing a configuration option, update the `deprecated-config.yaml` and `deleted-config.yaml` files respectively in the `tools/deprecated-config-checker` directory. [Example PR](https://github.com/grafana/loki/pull/10840/commits/0d4416a4b03739583349934b96f272fb4f685d15) --------- Signed-off-by: Vladyslav Diachenko <vlad.diachenko@grafana.com>
2 years ago
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(_ string, _ []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)
BloomShipper: add cache for downloaded blocks (#11394) adapted embeddedcache.go to store downloaded bloom blocks on local filesystem. **What this PR does / why we need it**: This cache will be used by bloom-gateway to not download the blocks each time. In the cache we store the objects(`cachedBlock` struct) that contain: 1. `blockDirectory`: the `path` to the directory where the block was extracted 2. `activeQueriers`: thread-safe counter of active block users. This field is important because we do not want the directory to be removed while it's in use. 3. rest fields are needed for service needs When the downloadingWorker receives this entity, it increases the counter `activeQueriers` and creates blockQuerier wrapper that has `Close` function that will decrease the counter once blockQuerier is not needed anymore. When the cache entry is being removed from the cache, the cache calls the function `removeDirectoryAsync` which asynchronously removes the block's folder. This function checks every `Xms` if there are still active block queriers and once `activeQueriers` count is `0` the folder will be removed. Also, there is a timeout in this function, and once the timeout is reached, the folder will be force removed. **Special notes for your reviewer**: * If the cache is disabled, then the blocks will be downloaded each time the block is requested. * If the cache is used, the folder will be deleted by embeddedCache when it reaches memory size limit or items count limit. Otherwise, the block's folder will be deleted when `Close` function is called. **Checklist** - [x] Reviewed the [`CONTRIBUTING.md`](https://github.com/grafana/loki/blob/main/CONTRIBUTING.md) guide (**required**) - [x] Documentation added - [x] Tests updated - [ ] `CHANGELOG.md` updated - [ ] If the change is worth mentioning in the release notes, add `add-to-release-notes` label - [ ] Changes that require user attention or interaction to upgrade are documented in `docs/sources/setup/upgrade/_index.md` - [ ] For Helm chart changes bump the Helm chart version in `production/helm/loki/Chart.yaml` and update `production/helm/loki/CHANGELOG.md` and `production/helm/loki/README.md`. [Example PR](https://github.com/grafana/loki/commit/d10549e3ece02120974929894ee333d07755d213) - [ ] If the change is deprecating or removing a configuration option, update the `deprecated-config.yaml` and `deleted-config.yaml` files respectively in the `tools/deprecated-config-checker` directory. [Example PR](https://github.com/grafana/loki/pull/10840/commits/0d4416a4b03739583349934b96f272fb4f685d15) --------- Signed-off-by: Vladyslav Diachenko <vlad.diachenko@grafana.com>
2 years ago
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(_ string, _ []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)
c.lock.RLock()
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")
c.lock.RUnlock()
// Expire the item.
time.Sleep(2 * cfg.TTL)
_, ok = c.Get(ctx, key4)
require.False(t, ok)
c.lock.RLock()
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.lock.RUnlock()
c.Stop()
}
func genBytes(n uint8) []byte {
arr := make([]byte, n)
for i := range arr {
arr[i] = byte(i)
}
return arr
}