force ranked cache recalculation in Top after a skipped invalidation

This commit is contained in:
Nia Weiss 2020-08-26 09:40:23 -04:00
parent 475a9e3910
commit 68542ddc35
No known key found for this signature in database
GPG key ID: 895E83409BFDA1BB
3 changed files with 56 additions and 0 deletions

View file

@ -142,6 +142,7 @@ type rankCache struct {
entries map[uint64]uint64
rankings bitmapPairs // cached, ordered list
rankingsRead bool
dirty bool
updateN int
updateTime time.Time
@ -173,6 +174,11 @@ func NewRankCache(maxEntries uint32) *rankCache {
func (c *rankCache) Add(id uint64, n uint64) {
c.mu.Lock()
defer c.mu.Unlock()
// Flag the cache as dirty.
// This forces recalculation if top is called before the cache is recalculated.
c.dirty = true
// Ignore if the column count is below the threshold,
// unless the count is 0, which is effectively used
// to clear the cache value.
@ -190,6 +196,11 @@ func (c *rankCache) Add(id uint64, n uint64) {
func (c *rankCache) BulkAdd(id uint64, n uint64) {
c.mu.Lock()
defer c.mu.Unlock()
// Flag the cache as dirty.
// This forces recalculation if top is called before the cache is recalculated.
c.dirty = true
if n < c.thresholdValue {
delete(c.entries, id)
return
@ -246,6 +257,11 @@ func (c *rankCache) invalidate() {
// Don't invalidate more than once every X seconds.
// TODO: consider making this configurable.
if time.Since(c.updateTime).Seconds() < 10 {
// Skipping recalculation means that the ranked cache's growth is unbounded.
// This is somewhat necessary for now since recalculation is not cheap.
// The cache will remain flagged as dirty and will be recalculated if Top is called.
// This may cause unexpected memory growth, so record it in metrics for debugging purposes.
c.stats.Count(MetricInvalidateCacheSkipped, 1, 1.0)
return
}
c.stats.Count(MetricInvalidateCache, 1, 1.0)
@ -295,6 +311,9 @@ func (c *rankCache) recalculate() {
delete(c.entries, pair.ID)
}
}
// The cache is no longer dirty.
c.dirty = false
}
// SetStats defines the stats client used in the cache.
@ -307,6 +326,12 @@ func (c *rankCache) Top() []bitmapPair {
c.mu.Lock()
defer c.mu.Unlock()
if c.dirty {
// The cache is dirty, so we need to recalculate it to get a consistent view.
c.stats.Count(MetricReadDirtyCache, 1, 1.0)
c.recalculate()
}
c.rankingsRead = true
return c.rankings
}

View file

@ -15,6 +15,7 @@
package pilosa_test
import (
"reflect"
"testing"
"github.com/pilosa/pilosa/v2"
@ -54,3 +55,31 @@ func TestCache_Rank_Threshold(t *testing.T) {
t.Fatalf("unexpected cache value after BulkAdd: %d!=%d expected\n", cache.Get(5), 0)
}
}
// Test that consecutive writes show up in Top.
// On later writes, the cache skips recalculation to save CPU time.
// This used to mean that the later writes would not show up in Top.
// Now, the cache is flagged as dirty and recalculated during the call to Top.
func TestCache_Rank_Dirty(t *testing.T) {
cacheSize := uint32(5)
cache := pilosa.NewRankCache(cacheSize)
type pair struct{ ID, Count uint64 }
expect := []pair{
{5, 2},
{4, 1},
}
for _, v := range expect {
cache.Add(v.ID, v.Count)
}
var got []pair
for _, p := range cache.Top() {
got = append(got, pair(p))
}
if !reflect.DeepEqual(expect, got) {
t.Fatalf("wrote %v but got %v", expect, got)
}
}

View file

@ -22,6 +22,8 @@ const (
MetricDeleteAvailableShard = "delete_available_shard_total"
MetricRecalculateCache = "recalculate_cache_total"
MetricInvalidateCache = "invalidate_cache_total"
MetricInvalidateCacheSkipped = "invalidate_cache_skipped_total"
MetricReadDirtyCache = "dirty_cache_total"
MetricRankCacheLength = "rank_cache_length"
MetricCacheThresholdReached = "cache_threshold_reached_total"
MetricRow = "query_row_total"