From 187f8082d617629274b6dd6034722febf81db8e8 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 15 Jan 2021 14:01:52 +0100 Subject: [PATCH 01/23] executor: do not lookup in cache twice There is no need to lookup ta given plan in the cache twice (in its normalized and non-normalized representation for its key): if the plan is normalized, it'll be stored into the cache in its normalized form. If it's not normalized, it'll be stored in its original form. Either way, the initial lookup with its non-normalized form is redundant. The raw `sql` content of a query only changes if the query has been normalized. In cases where it hasn't, there is no need to lookup the same key twice on cache Signed-off-by: Vicent Marti --- go/vt/vtgate/executor.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 4e29b8c8264..2ecbf00257a 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -1299,11 +1299,6 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. ignoreMaxMemoryRows := sqlparser.IgnoreMaxMaxMemoryRowsDirective(stmt) vcursor.SetIgnoreMaxMemoryRows(ignoreMaxMemoryRows) - planKey := vcursor.planPrefixKey() + ":" + sql - if plan, ok := e.plans.Get(planKey); ok { - return plan.(*engine.Plan), nil - } - // Normalize if possible and retry. if (e.normalize && sqlparser.CanNormalize(stmt)) || sqlparser.IsSetStatement(stmt) { parameterize := e.normalize // the public flag is called normalize @@ -1321,10 +1316,11 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. logStats.BindVariables = bindVars } - planKey = vcursor.planPrefixKey() + ":" + query + planKey := vcursor.planPrefixKey() + ":" + query if plan, ok := e.plans.Get(planKey); ok { return plan.(*engine.Plan), nil } + plan, err := planbuilder.BuildFromStmt(query, statement, vcursor, bindVarNeeds) if err != nil { return nil, err From 464106596ca0a7400596a1a3f32df412f8e97a2e Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 15 Jan 2021 17:33:02 +0100 Subject: [PATCH 02/23] plan: benchmark plan building for DML vs SELECT statements We have experienced caching issues with batch inserts in Vitess clusters, whose plans were polluting the shared plan cache. Before we can consider the trivial fix for this issue, which would be simply disabling caching for `INSERT` statements, we need to find out what's going to be the impact of disabling caching for this plans. Unfortunately, it looks like there isn't a significant performance difference between preparing a plan for an INSERT statement vs a SELECT one. Here's the output of two comparisons with a random sample of 32 of each statement: BenchmarkSelectVsDML/DML_(random_sample,_N=32) BenchmarkSelectVsDML/DML_(random_sample,_N=32)-16 766 1640575 ns/op 511073 B/op 6363 allocs/op BenchmarkSelectVsDML/Select_(random_sample,_N=32) BenchmarkSelectVsDML/Select_(random_sample,_N=32)-16 746 1479792 ns/op 274486 B/op 7730 allocs/op BenchmarkSelectVsDML/DML_(random_sample,_N=32) BenchmarkSelectVsDML/DML_(random_sample,_N=32)-16 823 1540039 ns/op 496079 B/op 5949 allocs/op BenchmarkSelectVsDML/Select_(random_sample,_N=32) BenchmarkSelectVsDML/Select_(random_sample,_N=32)-16 798 1526661 ns/op 275016 B/op 7730 allocs/op There is not a noticeable performance difference when preparing the INSERT statements. The only consistent metric is that INSERT statement plans allocate more memory than SELECT plans. Signed-off-by: Vicent Marti --- go/vt/vtgate/planbuilder/plan_test.go | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 473594f419b..0e7cbf6cff3 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "io/ioutil" + "math/rand" "os" "runtime/debug" "strings" @@ -618,6 +619,45 @@ func BenchmarkPlanner(b *testing.B) { } } +func BenchmarkSelectVsDML(b *testing.B) { + vschema := &vschemaWrapper{ + v: loadSchema(b, "schema_test.json"), + sysVarEnabled: true, + version: V4, + } + + var dmlCases []testCase + var selectCases []testCase + + for tc := range iterateExecFile("dml_cases.txt") { + if tc.output2ndPlanner != "" { + dmlCases = append(dmlCases, tc) + } + } + + for tc := range iterateExecFile("select_cases.txt") { + if tc.output2ndPlanner != "" { + selectCases = append(selectCases, tc) + } + } + + rand.Shuffle(len(dmlCases), func(i, j int) { + dmlCases[i], dmlCases[j] = dmlCases[j], dmlCases[i] + }) + + rand.Shuffle(len(selectCases), func(i, j int) { + selectCases[i], selectCases[j] = selectCases[j], selectCases[i] + }) + + b.Run("DML (random sample, N=32)", func(b *testing.B) { + benchmarkPlanner(b, V4, dmlCases[:32], vschema) + }) + + b.Run("Select (random sample, N=32)", func(b *testing.B) { + benchmarkPlanner(b, V4, selectCases[:32], vschema) + }) +} + func benchmarkPlanner(b *testing.B, version PlannerVersion, testCases []testCase, vschema *vschemaWrapper) { b.ReportAllocs() for n := 0; n < b.N; n++ { From f34f88413e645328c95a4632d61ac38c06799284 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Mon, 18 Jan 2021 12:36:14 +0100 Subject: [PATCH 03/23] cache: reduce API surface The current public API for the cache makes some assumptions that do not hold for more efficient cache implementations with admission policies. The following APIs have been replaced with equivalent ones or removed altogether: - `LRUCache.SetIfAbsent`: removed, not used - `LRUCache.Peek`: replaced with LRUCache.ForEach, since the original Peek was only used to iterate through the contents of the cache - `LRUCache.Keys`: likewise replaced with `ForEach` since the keys were only being accessed for iteration Signed-off-by: Vicent Marti --- go/cache/lru_cache.go | 36 ++-------- go/cache/lru_cache_test.go | 42 ----------- go/vt/vtexplain/vtexplain_vtgate.go | 11 +-- go/vt/vtgate/executor.go | 19 ++++- go/vt/vtgate/executor_scatter_stats.go | 14 ++-- go/vt/vtgate/executor_test.go | 77 ++++++++++----------- go/vt/vtgate/queryz.go | 17 +++-- go/vt/vttablet/tabletserver/query_engine.go | 57 +++++++-------- go/vt/vttablet/tabletserver/queryz.go | 15 ++-- go/vt/vttablet/tabletserver/queryz_test.go | 37 ++++++---- 10 files changed, 137 insertions(+), 188 deletions(-) diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index cf33235670a..d525bbc8d1c 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -89,18 +89,6 @@ func (lru *LRUCache) Get(key string) (v Value, ok bool) { return element.Value.(*entry).value, true } -// Peek returns a value from the cache without changing the LRU order. -func (lru *LRUCache) Peek(key string) (v Value, ok bool) { - lru.mu.Lock() - defer lru.mu.Unlock() - - element := lru.table[key] - if element == nil { - return nil, false - } - return element.Value.(*entry).value, true -} - // Set sets a value in the cache. func (lru *LRUCache) Set(key string, value Value) { lru.mu.Lock() @@ -113,19 +101,6 @@ func (lru *LRUCache) Set(key string, value Value) { } } -// SetIfAbsent will set the value in the cache if not present. If the -// value exists in the cache, we don't set it. -func (lru *LRUCache) SetIfAbsent(key string, value Value) { - lru.mu.Lock() - defer lru.mu.Unlock() - - if element := lru.table[key]; element != nil { - lru.moveToFront(element) - } else { - lru.addNew(key, value) - } -} - // Delete removes an entry from the cache, and returns if the entry existed. func (lru *LRUCache) Delete(key string) bool { lru.mu.Lock() @@ -221,17 +196,18 @@ func (lru *LRUCache) Oldest() (oldest time.Time) { return } -// Keys returns all the keys for the cache, ordered from most recently +// ForEach yields all the values for the cache, ordered from most recently // used to least recently used. -func (lru *LRUCache) Keys() []string { +func (lru *LRUCache) ForEach(callback func(value Value) bool) { lru.mu.Lock() defer lru.mu.Unlock() - keys := make([]string, 0, lru.list.Len()) for e := lru.list.Front(); e != nil; e = e.Next() { - keys = append(keys, e.Value.(*entry).key) + v := e.Value.(*entry) + if !callback(v.value) { + break + } } - return keys } // Items returns all the values for the cache, ordered from most recently diff --git a/go/cache/lru_cache_test.go b/go/cache/lru_cache_test.go index 9a7f09232e6..635642753f4 100644 --- a/go/cache/lru_cache_test.go +++ b/go/cache/lru_cache_test.go @@ -58,35 +58,12 @@ func TestSetInsertsValue(t *testing.T) { t.Errorf("Cache has incorrect value: %v != %v", data, v) } - k := cache.Keys() - if len(k) != 1 || k[0] != key { - t.Errorf("Cache.Keys() returned incorrect values: %v", k) - } values := cache.Items() if len(values) != 1 || values[0].Key != key { t.Errorf("Cache.Values() returned incorrect values: %v", values) } } -func TestSetIfAbsent(t *testing.T) { - cache := NewLRUCache(100) - data := &CacheValue{0} - key := "key" - cache.SetIfAbsent(key, data) - - v, ok := cache.Get(key) - if !ok || v.(*CacheValue) != data { - t.Errorf("Cache has incorrect value: %v != %v", data, v) - } - - cache.SetIfAbsent(key, &CacheValue{1}) - - v, ok = cache.Get(key) - if !ok || v.(*CacheValue) != data { - t.Errorf("Cache has incorrect value: %v != %v", data, v) - } -} - func TestGetValueWithMultipleTypes(t *testing.T) { cache := NewLRUCache(100) data := &CacheValue{0} @@ -160,25 +137,6 @@ func TestGetNonExistent(t *testing.T) { } } -func TestPeek(t *testing.T) { - cache := NewLRUCache(2) - val1 := &CacheValue{1} - cache.Set("key1", val1) - val2 := &CacheValue{1} - cache.Set("key2", val2) - // Make key1 the most recent. - cache.Get("key1") - // Peek key2. - if v, ok := cache.Peek("key2"); ok && v.(*CacheValue) != val2 { - t.Errorf("key2 received: %v, want %v", v, val2) - } - // Push key2 out - cache.Set("key3", &CacheValue{1}) - if v, ok := cache.Peek("key2"); ok { - t.Errorf("key2 received: %v, want absent", v) - } -} - func TestDelete(t *testing.T) { cache := NewLRUCache(100) value := &CacheValue{1} diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index ccdce29a55c..b23d4003de9 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -20,13 +20,13 @@ limitations under the License. package vtexplain import ( + "context" "fmt" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" - "context" - "vitess.io/vitess/go/vt/vterrors" "vitess.io/vitess/go/json2" @@ -201,11 +201,12 @@ func vtgateExecute(sql string) ([]*engine.Plan, map[string]*TabletActions, error } var plans []*engine.Plan - for _, item := range planCache.Items() { - plan := item.Value.(*engine.Plan) + planCache.ForEach(func(value cache.Value) bool { + plan := value.(*engine.Plan) plan.ExecTime = 0 plans = append(plans, plan) - } + return true + }) planCache.Clear() tabletActions := make(map[string]*TabletActions) diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 2ecbf00257a..a88fe6551f7 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -1339,6 +1339,23 @@ func skipQueryPlanCache(safeSession *SafeSession) bool { return safeSession.Options.SkipQueryPlanCache } +type cacheItem struct { + Key string + Value *engine.Plan +} + +func (e *Executor) debugCacheEntries() (items []cacheItem) { + e.plans.ForEach(func(value cache.Value) bool { + plan := value.(*engine.Plan) + items = append(items, cacheItem{ + Key: plan.Original, + Value: plan, + }) + return true + }) + return +} + // ServeHTTP shows the current plans in the query cache. func (e *Executor) ServeHTTP(response http.ResponseWriter, request *http.Request) { if err := acl.CheckAccessHTTP(request, acl.DEBUGGING); err != nil { @@ -1348,7 +1365,7 @@ func (e *Executor) ServeHTTP(response http.ResponseWriter, request *http.Request switch request.URL.Path { case pathQueryPlans: - returnAsJSON(response, e.plans.Items()) + returnAsJSON(response, e.debugCacheEntries()) case pathVSchema: returnAsJSON(response, e.VSchema()) case pathScatterStats: diff --git a/go/vt/vtgate/executor_scatter_stats.go b/go/vt/vtgate/executor_scatter_stats.go index 9ba7ae3ea3a..dd321e37b67 100644 --- a/go/vt/vtgate/executor_scatter_stats.go +++ b/go/vt/vtgate/executor_scatter_stats.go @@ -22,6 +22,7 @@ import ( "net/http" "time" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/proto/vtrpc" @@ -56,12 +57,12 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { totalExecTime := time.Duration(0) totalCount := uint64(0) + var err error plans := make([]*engine.Plan, 0) routes := make([]*engine.Route, 0) // First we go over all plans and collect statistics and all query plans for scatter queries - for _, item := range e.plans.Items() { - plan := item.Value.(*engine.Plan) - + e.plans.ForEach(func(value cache.Value) bool { + plan := value.(*engine.Plan) scatter := engine.Find(findScatter, plan.Instructions) readOnly := !engine.Exists(isUpdating, plan.Instructions) isScatter := scatter != nil @@ -69,7 +70,8 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { if isScatter { route, isRoute := scatter.(*engine.Route) if !isRoute { - return statsResults{}, vterrors.Errorf(vtrpc.Code_INTERNAL, "expected a route, but found a %v", scatter) + err = vterrors.Errorf(vtrpc.Code_INTERNAL, "expected a route, but found a %v", scatter) + return false } plans = append(plans, plan) routes = append(routes, route) @@ -83,6 +85,10 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { totalExecTime += plan.ExecTime totalCount += plan.ExecCount + return true + }) + if err != nil { + return statsResults{}, err } // Now we'll go over all scatter queries we've found and produce result items for each diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go index 80e32cfd979..a335d9871e0 100644 --- a/go/vt/vtgate/executor_test.go +++ b/go/vt/vtgate/executor_test.go @@ -29,6 +29,7 @@ import ( "testing" "time" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/test/utils" "vitess.io/vitess/go/vt/topo" @@ -1455,9 +1456,7 @@ func TestGetPlanUnnormalized(t *testing.T) { want := []string{ "@unknown:" + query1, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } @@ -1480,17 +1479,33 @@ func TestGetPlanUnnormalized(t *testing.T) { KsTestUnsharded + "@unknown:" + query1, "@unknown:" + query1, } - if diff := cmp.Diff(want, r.plans.Keys()); diff != "" { - t.Errorf("\n-want,+got:\n%s", diff) - } - //if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - // t.Errorf("Plan keys: %s, want %s", keys, want) - //} + assertCacheContains(t, r.plans, want) if logStats4.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats4.SQL) } } +func assertCacheSize(t *testing.T, c *cache.LRUCache, expected int) { + t.Helper() + var size int + c.ForEach(func(_ cache.Value) bool { + size++ + return true + }) + if size != expected { + t.Errorf("getPlan() expected cache to have size %d, but got: %d", expected, size) + } +} + +func assertCacheContains(t *testing.T, c *cache.LRUCache, want []string) { + t.Helper() + for _, wantKey := range want { + if _, ok := c.Get(wantKey); !ok { + t.Errorf("missing key in plan cache: %v", wantKey) + } + } +} + func TestGetPlanCacheUnnormalized(t *testing.T) { r, _, _, _ := createLegacyExecutorEnv() emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) @@ -1524,32 +1539,24 @@ func TestGetPlanCacheUnnormalized(t *testing.T) { logStats1 = NewLogStats(ctx, "Test", "", nil) _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 0 { - t.Errorf("Plan keys should be 0, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 1 { - t.Errorf("Plan keys should be 1, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 2) } func TestGetPlanCacheNormalized(t *testing.T) { @@ -1586,32 +1593,24 @@ func TestGetPlanCacheNormalized(t *testing.T) { logStats1 = NewLogStats(ctx, "Test", "", nil) _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 0 { - t.Errorf("Plan keys should be 0, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 1 { - t.Errorf("Plan keys should be 1, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) require.NoError(t, err) - if len(r.plans.Keys()) != 2 { - t.Errorf("Plan keys should be 2, got: %v", len(r.plans.Keys())) - } + assertCacheSize(t, r.plans, 2) } func TestGetPlanNormalized(t *testing.T) { @@ -1635,9 +1634,7 @@ func TestGetPlanNormalized(t *testing.T) { want := []string{ "@unknown:" + normalized, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) wantSQL := normalized + " /* comment 1 */" if logStats1.SQL != wantSQL { @@ -1691,9 +1688,7 @@ func TestGetPlanNormalized(t *testing.T) { KsTestUnsharded + "@unknown:" + normalized, "@unknown:" + normalized, } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) // Errors logStats7 := NewLogStats(ctx, "Test", "", nil) @@ -1702,9 +1697,7 @@ func TestGetPlanNormalized(t *testing.T) { if err == nil || err.Error() != wantErr { t.Errorf("getPlan(syntax): %v, want %s", err, wantErr) } - if keys := r.plans.Keys(); !reflect.DeepEqual(keys, want) { - t.Errorf("Plan keys: %s, want %s", keys, want) - } + assertCacheContains(t, r.plans, want) } func TestPassthroughDDL(t *testing.T) { diff --git a/go/vt/vtgate/queryz.go b/go/vt/vtgate/queryz.go index 19298574988..587e13c6f39 100644 --- a/go/vt/vtgate/queryz.go +++ b/go/vt/vtgate/queryz.go @@ -24,6 +24,7 @@ import ( "time" "vitess.io/vitess/go/acl" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/log" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/sqlparser" @@ -124,19 +125,15 @@ func queryzHandler(e *Executor, w http.ResponseWriter, r *http.Request) { defer logz.EndHTMLTable(w) w.Write(queryzHeader) - keys := e.plans.Keys() sorter := queryzSorter{ - rows: make([]*queryzRow, 0, len(keys)), + rows: nil, less: func(row1, row2 *queryzRow) bool { return row1.timePQ() > row2.timePQ() }, } - for _, v := range e.plans.Keys() { - result, ok := e.plans.Get(v) - if !ok { - continue - } - plan := result.(*engine.Plan) + + e.plans.ForEach(func(value cache.Value) bool { + plan := value.(*engine.Plan) Value := &queryzRow{ Query: logz.Wrappable(sqlparser.TruncateForUI(plan.Original)), } @@ -153,7 +150,9 @@ func queryzHandler(e *Executor, w http.ResponseWriter, r *http.Request) { Value.Color = "high" } sorter.rows = append(sorter.rows, Value) - } + return true + }) + sort.Sort(&sorter) for _, Value := range sorter.rows { if err := queryzTmpl.Execute(w, Value); err != nil { diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index ed0b2fb338a..8bf51aa3da6 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -399,14 +399,6 @@ func (qe *QueryEngine) getQuery(sql string) *TabletPlan { return nil } -// peekQuery fetches the plan without changing the LRU order. -func (qe *QueryEngine) peekQuery(sql string) *TabletPlan { - if cacheResult, ok := qe.plans.Peek(sql); ok { - return cacheResult.(*TabletPlan) - } - return nil -} - // SetQueryPlanCacheCap sets the query plan cache capacity. func (qe *QueryEngine) SetQueryPlanCacheCap(size int) { if size <= 0 { @@ -446,20 +438,19 @@ func (qe *QueryEngine) handleHTTPQueryPlans(response http.ResponseWriter, reques acl.SendError(response, err) return } - keys := qe.plans.Keys() + response.Header().Set("Content-Type", "text/plain") - response.Write([]byte(fmt.Sprintf("Length: %d\n", len(keys)))) - for _, v := range keys { - response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(v)))) - if plan := qe.peekQuery(v); plan != nil { - if b, err := json.MarshalIndent(plan.Plan, "", " "); err != nil { - response.Write([]byte(err.Error())) - } else { - response.Write(b) - } - response.Write(([]byte)("\n\n")) + qe.plans.ForEach(func(value cache.Value) bool { + plan := value.(*TabletPlan) + response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(plan.FullQuery.Query)))) + if b, err := json.MarshalIndent(plan.Plan, "", " "); err != nil { + response.Write([]byte(err.Error())) + } else { + response.Write(b) } - } + response.Write(([]byte)("\n\n")) + return true + }) } func (qe *QueryEngine) handleHTTPQueryStats(response http.ResponseWriter, request *http.Request) { @@ -467,20 +458,20 @@ func (qe *QueryEngine) handleHTTPQueryStats(response http.ResponseWriter, reques acl.SendError(response, err) return } - keys := qe.plans.Keys() response.Header().Set("Content-Type", "application/json; charset=utf-8") - qstats := make([]perQueryStats, 0, len(keys)) - for _, v := range keys { - if plan := qe.peekQuery(v); plan != nil { - var pqstats perQueryStats - pqstats.Query = unicoded(sqlparser.TruncateForUI(v)) - pqstats.Table = plan.TableName().String() - pqstats.Plan = plan.PlanID - pqstats.QueryCount, pqstats.Time, pqstats.MysqlTime, pqstats.RowCount, pqstats.ErrorCount = plan.Stats() - - qstats = append(qstats, pqstats) - } - } + var qstats []perQueryStats + qe.plans.ForEach(func(value cache.Value) bool { + plan := value.(*TabletPlan) + + var pqstats perQueryStats + pqstats.Query = unicoded(sqlparser.TruncateForUI(plan.FullQuery.Query)) + pqstats.Table = plan.TableName().String() + pqstats.Plan = plan.PlanID + pqstats.QueryCount, pqstats.Time, pqstats.MysqlTime, pqstats.RowCount, pqstats.ErrorCount = plan.Stats() + + qstats = append(qstats, pqstats) + return true + }) if b, err := json.MarshalIndent(qstats, "", " "); err != nil { response.Write([]byte(err.Error())) } else { diff --git a/go/vt/vttablet/tabletserver/queryz.go b/go/vt/vttablet/tabletserver/queryz.go index ef37e77c5cf..168a842f737 100644 --- a/go/vt/vttablet/tabletserver/queryz.go +++ b/go/vt/vttablet/tabletserver/queryz.go @@ -24,6 +24,7 @@ import ( "time" "vitess.io/vitess/go/acl" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/log" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/sqlparser" @@ -134,20 +135,19 @@ func queryzHandler(qe *QueryEngine, w http.ResponseWriter, r *http.Request) { defer logz.EndHTMLTable(w) w.Write(queryzHeader) - keys := qe.plans.Keys() sorter := queryzSorter{ - rows: make([]*queryzRow, 0, len(keys)), + rows: nil, less: func(row1, row2 *queryzRow) bool { return row1.timePQ() > row2.timePQ() }, } - for _, v := range qe.plans.Keys() { - plan := qe.peekQuery(v) + qe.plans.ForEach(func(value cache.Value) bool { + plan := value.(*TabletPlan) if plan == nil { - continue + return true } Value := &queryzRow{ - Query: logz.Wrappable(sqlparser.TruncateForUI(v)), + Query: logz.Wrappable(sqlparser.TruncateForUI(plan.FullQuery.Query)), Table: plan.TableName().String(), Plan: plan.PlanID, } @@ -164,7 +164,8 @@ func queryzHandler(qe *QueryEngine, w http.ResponseWriter, r *http.Request) { Value.Color = "high" } sorter.rows = append(sorter.rows, Value) - } + return true + }) sort.Sort(&sorter) for _, Value := range sorter.rows { if err := queryzTmpl.Execute(w, Value); err != nil { diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index cfcc26a45e5..556c29e1e8b 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -37,45 +37,52 @@ func TestQueryzHandler(t *testing.T) { req, _ := http.NewRequest("GET", "/schemaz", nil) qe := newTestQueryEngine(100, 10*time.Second, true, &dbconfigs.DBConfigs{}) + const query1 = "select name from test_table" plan1 := &TabletPlan{ Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, - PlanID: planbuilder.PlanSelect, + Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, + PlanID: planbuilder.PlanSelect, + FullQuery: sqlparser.BuildParsedQuery(query1), }, } plan1.AddStats(10, 2*time.Second, 1*time.Second, 2, 0) - qe.plans.Set("select name from test_table", plan1) + qe.plans.Set(query1, plan1) + const query2 = "insert into test_table values 1" plan2 := &TabletPlan{ Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, - PlanID: planbuilder.PlanDDL, + Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, + PlanID: planbuilder.PlanDDL, + FullQuery: sqlparser.BuildParsedQuery(query2), }, } plan2.AddStats(1, 2*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set("insert into test_table values 1", plan2) + qe.plans.Set(query2, plan2) + const query3 = "show tables" plan3 := &TabletPlan{ Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, - PlanID: planbuilder.PlanOtherRead, + Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, + PlanID: planbuilder.PlanOtherRead, + FullQuery: sqlparser.BuildParsedQuery(query3), }, } plan3.AddStats(1, 75*time.Millisecond, 50*time.Millisecond, 1, 0) - qe.plans.Set("show tables", plan3) + qe.plans.Set(query3, plan3) qe.plans.Set("", (*TabletPlan)(nil)) + hugeInsert := "insert into test_table values 0" + for i := 1; i < 1000; i++ { + hugeInsert = hugeInsert + fmt.Sprintf(", %d", i) + } plan4 := &TabletPlan{ Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, - PlanID: planbuilder.PlanOtherRead, + Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, + PlanID: planbuilder.PlanOtherRead, + FullQuery: sqlparser.BuildParsedQuery(hugeInsert), }, } plan4.AddStats(1, 1*time.Millisecond, 1*time.Millisecond, 1, 0) - hugeInsert := "insert into test_table values 0" - for i := 1; i < 1000; i++ { - hugeInsert = hugeInsert + fmt.Sprintf(", %d", i) - } qe.plans.Set(hugeInsert, plan4) qe.plans.Set("", (*TabletPlan)(nil)) From cbbba9ed945cedc5c07685348c6944036882c478 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Mon, 18 Jan 2021 16:59:07 +0100 Subject: [PATCH 04/23] cache: abstract into a Cache interface The `cache.LRUCache` struct has now been abstracted behind a generic Cache interface so that it can be swapped with more complex Cache implementations. Signed-off-by: Vicent Marti --- go/cache/cache.go | 58 +++++++ go/cache/lru_cache.go | 94 ++++------- go/cache/lru_cache_test.go | 148 +++++++++--------- go/cache/perf_test.go | 10 +- go/pools/numbered.go | 6 +- go/sync2/consolidator.go | 9 +- go/vt/vtexplain/vtexplain_vtgate.go | 3 +- go/vt/vtgate/engine/primitive.go | 7 - go/vt/vtgate/executor.go | 22 ++- go/vt/vtgate/executor_scatter_stats.go | 3 +- go/vt/vtgate/executor_test.go | 22 +-- go/vt/vtgate/queryz.go | 3 +- go/vt/vttablet/tabletserver/query_engine.go | 36 +++-- .../tabletserver/query_engine_test.go | 26 +-- go/vt/vttablet/tabletserver/queryz.go | 5 +- go/vt/vttablet/tabletserver/queryz_test.go | 40 ++--- 16 files changed, 245 insertions(+), 247 deletions(-) create mode 100644 go/cache/cache.go diff --git a/go/cache/cache.go b/go/cache/cache.go new file mode 100644 index 00000000000..612b5466fa7 --- /dev/null +++ b/go/cache/cache.go @@ -0,0 +1,58 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "encoding/json" + "time" +) + +// Cache is a generic interface type for a data structure that keeps recently used +// objects in memory and evicts them when it becomes full. +type Cache interface { + Get(key string) (interface{}, bool) + Set(key string, val interface{}, valueSize int64) + ForEach(callback func(interface{}) bool) + + Delete(key string) bool + Clear() + + Stats() *Stats + Capacity() int64 + SetCapacity(int64) +} + +// Stats are the internal statistics about a live Cache that can be queried at runtime +type Stats struct { + Length int64 `json:"Length"` + Size int64 `json:"CachedSize"` + Capacity int64 `json:"Capacity"` + Evictions int64 `json:"Evictions"` + Oldest time.Time `json:"OldestAccess"` +} + +// JSON returns the serialized statistics in JSON form +func (s *Stats) JSON() string { + if s == nil { + return "{}" + } + buf, err := json.Marshal(s) + if err != nil { + panic("cache.Stats failed to serialize (should never happen)") + } + return string(buf) +} diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index d525bbc8d1c..5bb7c2f6170 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -25,15 +25,16 @@ package cache import ( "container/list" - "fmt" "sync" "time" ) +var _ Cache = &LRUCache{} + // LRUCache is a typical LRU cache implementation. If the cache // reaches the capacity, the least recently used item is deleted from // the cache. Note the capacity is not the number of items, but the -// total sum of the Size() of each item. +// total sum of the CachedSize() of each item. type LRUCache struct { mu sync.Mutex @@ -46,22 +47,15 @@ type LRUCache struct { evictions int64 } -// Value is the interface values that go into LRUCache need to satisfy -type Value interface { - // Size returns how big this value is. If you want to just track - // the cache by number of objects, you may return the size as 1. - Size() int -} - // Item is what is stored in the cache type Item struct { Key string - Value Value + Value interface{} } type entry struct { key string - value Value + value interface{} size int64 timeAccessed time.Time } @@ -77,7 +71,7 @@ func NewLRUCache(capacity int64) *LRUCache { // Get returns a value from the cache, and marks the entry as most // recently used. -func (lru *LRUCache) Get(key string) (v Value, ok bool) { +func (lru *LRUCache) Get(key string) (v interface{}, ok bool) { lru.mu.Lock() defer lru.mu.Unlock() @@ -90,19 +84,19 @@ func (lru *LRUCache) Get(key string) (v Value, ok bool) { } // Set sets a value in the cache. -func (lru *LRUCache) Set(key string, value Value) { +func (lru *LRUCache) Set(key string, value interface{}, valueSize int64) { lru.mu.Lock() defer lru.mu.Unlock() if element := lru.table[key]; element != nil { - lru.updateInplace(element, value) + lru.updateInplace(element, value, valueSize) } else { - lru.addNew(key, value) + lru.addNew(key, value, valueSize) } } // Delete removes an entry from the cache, and returns if the entry existed. -func (lru *LRUCache) Delete(key string) bool { +func (lru *LRUCache) delete(key string) bool { lru.mu.Lock() defer lru.mu.Unlock() @@ -117,6 +111,11 @@ func (lru *LRUCache) Delete(key string) bool { return true } +// Delete removes an entry from the cache +func (lru *LRUCache) Delete(key string) { + lru.delete(key) +} + // Clear will clear the entire cache. func (lru *LRUCache) Clear() { lru.mu.Lock() @@ -139,36 +138,24 @@ func (lru *LRUCache) SetCapacity(capacity int64) { } // Stats returns a few stats on the cache. -func (lru *LRUCache) Stats() (length, size, capacity, evictions int64, oldest time.Time) { - lru.mu.Lock() - defer lru.mu.Unlock() - if lastElem := lru.list.Back(); lastElem != nil { - oldest = lastElem.Value.(*entry).timeAccessed - } - return int64(lru.list.Len()), lru.size, lru.capacity, lru.evictions, oldest -} - -// StatsJSON returns stats as a JSON object in a string. -func (lru *LRUCache) StatsJSON() string { +func (lru *LRUCache) Stats() *Stats { if lru == nil { - return "{}" + return nil } - l, s, c, e, o := lru.Stats() - return fmt.Sprintf("{\"Length\": %v, \"Size\": %v, \"Capacity\": %v, \"Evictions\": %v, \"OldestAccess\": \"%v\"}", l, s, c, e, o) -} -// Length returns how many elements are in the cache -func (lru *LRUCache) Length() int64 { lru.mu.Lock() defer lru.mu.Unlock() - return int64(lru.list.Len()) -} -// Size returns the sum of the objects' Size() method. -func (lru *LRUCache) Size() int64 { - lru.mu.Lock() - defer lru.mu.Unlock() - return lru.size + stats := &Stats{ + Length: int64(lru.list.Len()), + Size: lru.size, + Capacity: lru.capacity, + Evictions: lru.evictions, + } + if lastElem := lru.list.Back(); lastElem != nil { + stats.Oldest = lastElem.Value.(*entry).timeAccessed + } + return stats } // Capacity returns the cache maximum capacity. @@ -178,27 +165,9 @@ func (lru *LRUCache) Capacity() int64 { return lru.capacity } -// Evictions returns the eviction count. -func (lru *LRUCache) Evictions() int64 { - lru.mu.Lock() - defer lru.mu.Unlock() - return lru.evictions -} - -// Oldest returns the insertion time of the oldest element in the cache, -// or a IsZero() time if cache is empty. -func (lru *LRUCache) Oldest() (oldest time.Time) { - lru.mu.Lock() - defer lru.mu.Unlock() - if lastElem := lru.list.Back(); lastElem != nil { - oldest = lastElem.Value.(*entry).timeAccessed - } - return -} - // ForEach yields all the values for the cache, ordered from most recently // used to least recently used. -func (lru *LRUCache) ForEach(callback func(value Value) bool) { +func (lru *LRUCache) ForEach(callback func(value interface{}) bool) { lru.mu.Lock() defer lru.mu.Unlock() @@ -224,8 +193,7 @@ func (lru *LRUCache) Items() []Item { return items } -func (lru *LRUCache) updateInplace(element *list.Element, value Value) { - valueSize := int64(value.Size()) +func (lru *LRUCache) updateInplace(element *list.Element, value interface{}, valueSize int64) { sizeDiff := valueSize - element.Value.(*entry).size element.Value.(*entry).value = value element.Value.(*entry).size = valueSize @@ -239,8 +207,8 @@ func (lru *LRUCache) moveToFront(element *list.Element) { element.Value.(*entry).timeAccessed = time.Now() } -func (lru *LRUCache) addNew(key string, value Value) { - newEntry := &entry{key, value, int64(value.Size()), time.Now()} +func (lru *LRUCache) addNew(key string, value interface{}, valueSize int64) { + newEntry := &entry{key, value, valueSize, time.Now()} element := lru.list.PushFront(newEntry) lru.table[key] = element lru.size += newEntry.size diff --git a/go/cache/lru_cache_test.go b/go/cache/lru_cache_test.go index 635642753f4..966672c1efc 100644 --- a/go/cache/lru_cache_test.go +++ b/go/cache/lru_cache_test.go @@ -22,36 +22,30 @@ import ( "time" ) -type CacheValue struct { - size int -} - -func (cv *CacheValue) Size() int { - return cv.size -} +type CacheValue struct{} func TestInitialState(t *testing.T) { cache := NewLRUCache(5) - l, sz, c, e, _ := cache.Stats() - if l != 0 { - t.Errorf("length = %v, want 0", l) + stats := cache.Stats() + if stats.Length != 0 { + t.Errorf("length = %v, want 0", stats.Length) } - if sz != 0 { - t.Errorf("size = %v, want 0", sz) + if stats.Size != 0 { + t.Errorf("size = %v, want 0", stats.Size) } - if c != 5 { - t.Errorf("capacity = %v, want 5", c) + if stats.Capacity != 5 { + t.Errorf("capacity = %v, want 5", stats.Capacity) } - if e != 0 { - t.Errorf("evictions = %v, want 0", c) + if stats.Evictions != 0 { + t.Errorf("evictions = %v, want 0", stats.Evictions) } } func TestSetInsertsValue(t *testing.T) { cache := NewLRUCache(100) - data := &CacheValue{0} + data := &CacheValue{} key := "key" - cache.Set(key, data) + cache.Set(key, data, 0) v, ok := cache.Get(key) if !ok || v.(*CacheValue) != data { @@ -66,9 +60,9 @@ func TestSetInsertsValue(t *testing.T) { func TestGetValueWithMultipleTypes(t *testing.T) { cache := NewLRUCache(100) - data := &CacheValue{0} + data := &CacheValue{} key := "key" - cache.Set(key, data) + cache.Set(key, data, 0) v, ok := cache.Get("key") if !ok || v.(*CacheValue) != data { @@ -83,27 +77,27 @@ func TestGetValueWithMultipleTypes(t *testing.T) { func TestSetUpdatesSize(t *testing.T) { cache := NewLRUCache(100) - emptyValue := &CacheValue{0} + emptyValue := &CacheValue{} key := "key1" - cache.Set(key, emptyValue) - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0", sz) + cache.Set(key, emptyValue, 0) + if stats := cache.Stats(); stats.Size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0", stats.Size) } - someValue := &CacheValue{20} + someValue := &CacheValue{} key = "key2" - cache.Set(key, someValue) - if _, sz, _, _, _ := cache.Stats(); sz != 20 { - t.Errorf("cache.Size() = %v, expected 20", sz) + cache.Set(key, someValue, 20) + if stats := cache.Stats(); stats.Size != 20 { + t.Errorf("cache.CachedSize() = %v, expected 20", stats.Size) } } func TestSetWithOldKeyUpdatesValue(t *testing.T) { cache := NewLRUCache(100) - emptyValue := &CacheValue{0} + emptyValue := &CacheValue{} key := "key1" - cache.Set(key, emptyValue) - someValue := &CacheValue{20} - cache.Set(key, someValue) + cache.Set(key, emptyValue, 0) + someValue := &CacheValue{} + cache.Set(key, someValue, 20) v, ok := cache.Get(key) if !ok || v.(*CacheValue) != someValue { @@ -113,19 +107,19 @@ func TestSetWithOldKeyUpdatesValue(t *testing.T) { func TestSetWithOldKeyUpdatesSize(t *testing.T) { cache := NewLRUCache(100) - emptyValue := &CacheValue{0} + emptyValue := &CacheValue{} key := "key1" - cache.Set(key, emptyValue) + cache.Set(key, emptyValue, 0) - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected %v", sz, 0) + if stats := cache.Stats(); stats.Size != 0 { + t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, 0) } - someValue := &CacheValue{20} - cache.Set(key, someValue) - expected := int64(someValue.size) - if _, sz, _, _, _ := cache.Stats(); sz != expected { - t.Errorf("cache.Size() = %v, expected %v", sz, expected) + someValue := &CacheValue{} + cache.Set(key, someValue, 20) + expected := int64(20) + if stats := cache.Stats(); stats.Size != expected { + t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, expected) } } @@ -139,21 +133,21 @@ func TestGetNonExistent(t *testing.T) { func TestDelete(t *testing.T) { cache := NewLRUCache(100) - value := &CacheValue{1} + value := &CacheValue{} key := "key" - if cache.Delete(key) { + if cache.delete(key) { t.Error("Item unexpectedly already in cache.") } - cache.Set(key, value) + cache.Set(key, value, 1) - if !cache.Delete(key) { + if !cache.delete(key) { t.Error("Expected item to be in cache.") } - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0", sz) + if stats := cache.Stats(); stats.Size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0", stats.Size) } if _, ok := cache.Get(key); ok { @@ -163,14 +157,14 @@ func TestDelete(t *testing.T) { func TestClear(t *testing.T) { cache := NewLRUCache(100) - value := &CacheValue{1} + value := &CacheValue{} key := "key" - cache.Set(key, value) + cache.Set(key, value, 1) cache.Clear() - if _, sz, _, _, _ := cache.Stats(); sz != 0 { - t.Errorf("cache.Size() = %v, expected 0 after Clear()", sz) + if stats := cache.Stats(); stats.Size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0 after Clear()", stats.Size) } } @@ -178,41 +172,41 @@ func TestCapacityIsObeyed(t *testing.T) { size := int64(3) cache := NewLRUCache(100) cache.SetCapacity(size) - value := &CacheValue{1} + value := &CacheValue{} // Insert up to the cache's capacity. - cache.Set("key1", value) - cache.Set("key2", value) - cache.Set("key3", value) - if _, sz, _, _, _ := cache.Stats(); sz != size { - t.Errorf("cache.Size() = %v, expected %v", sz, size) + cache.Set("key1", value, 1) + cache.Set("key2", value, 1) + cache.Set("key3", value, 1) + if stats := cache.Stats(); stats.Size != size { + t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, size) } // Insert one more; something should be evicted to make room. - cache.Set("key4", value) - _, sz, _, evictions, _ := cache.Stats() - if sz != size { - t.Errorf("post-evict cache.Size() = %v, expected %v", sz, size) + cache.Set("key4", value, 1) + st := cache.Stats() + if st.Size != size { + t.Errorf("post-evict cache.CachedSize() = %v, expected %v", st.Size, size) } - if evictions != 1 { - t.Errorf("post-evict cache.evictions = %v, expected 1", evictions) + if st.Evictions != 1 { + t.Errorf("post-evict cache.evictions = %v, expected 1", st.Evictions) } // Check json stats - data := cache.StatsJSON() + data := st.JSON() m := make(map[string]interface{}) if err := json.Unmarshal([]byte(data), &m); err != nil { t.Errorf("cache.StatsJSON() returned bad json data: %v %v", data, err) } - if m["Size"].(float64) != float64(size) { + if m["CachedSize"].(float64) != float64(size) { t.Errorf("cache.StatsJSON() returned bad size: %v", m) } // Check various other stats - if l := cache.Length(); l != size { - t.Errorf("cache.StatsJSON() returned bad length: %v", l) + if st.Length != size { + t.Errorf("cache.StatsJSON() returned bad length: %v", st.Length) } - if s := cache.Size(); s != size { - t.Errorf("cache.StatsJSON() returned bad size: %v", s) + if st.Size != size { + t.Errorf("cache.StatsJSON() returned bad size: %v", st.Size) } if c := cache.Capacity(); c != size { t.Errorf("cache.StatsJSON() returned bad length: %v", c) @@ -220,7 +214,7 @@ func TestCapacityIsObeyed(t *testing.T) { // checks StatsJSON on nil cache = nil - if s := cache.StatsJSON(); s != "{}" { + if s := cache.Stats().JSON(); s != "{}" { t.Errorf("cache.StatsJSON() on nil object returned %v", s) } } @@ -229,9 +223,9 @@ func TestLRUIsEvicted(t *testing.T) { size := int64(3) cache := NewLRUCache(size) - cache.Set("key1", &CacheValue{1}) - cache.Set("key2", &CacheValue{1}) - cache.Set("key3", &CacheValue{1}) + cache.Set("key1", &CacheValue{}, 1) + cache.Set("key2", &CacheValue{}, 1) + cache.Set("key3", &CacheValue{}, 1) // lru: [key3, key2, key1] // Look up the elements. This will rearrange the LRU ordering. @@ -242,7 +236,7 @@ func TestLRUIsEvicted(t *testing.T) { cache.Get("key1") // lru: [key1, key2, key3] - cache.Set("key0", &CacheValue{1}) + cache.Set("key0", &CacheValue{}, 1) // lru: [key0, key1, key2] // The least recently used one should have been evicted. @@ -250,12 +244,14 @@ func TestLRUIsEvicted(t *testing.T) { t.Error("Least recently used element was not evicted.") } + st := cache.Stats() + // Check oldest - if o := cache.Oldest(); o.Before(beforeKey2) || o.After(afterKey2) { + if o := st.Oldest; o.Before(beforeKey2) || o.After(afterKey2) { t.Errorf("cache.Oldest returned an unexpected value: got %v, expected a value between %v and %v", o, beforeKey2, afterKey2) } - if e, want := cache.Evictions(), int64(1); e != want { + if e, want := st.Evictions, int64(1); e != want { t.Errorf("evictions: %d, want: %d", e, want) } } diff --git a/go/cache/perf_test.go b/go/cache/perf_test.go index b5c9a1a8b38..9873011e51b 100644 --- a/go/cache/perf_test.go +++ b/go/cache/perf_test.go @@ -20,16 +20,10 @@ import ( "testing" ) -type MyValue []byte - -func (mv MyValue) Size() int { - return cap(mv) -} - func BenchmarkGet(b *testing.B) { cache := NewLRUCache(64 * 1024 * 1024) - value := make(MyValue, 1000) - cache.Set("stuff", value) + value := make([]byte, 1000) + cache.Set("stuff", value, int64(cap(value))) for i := 0; i < b.N; i++ { val, ok := cache.Get("stuff") if !ok { diff --git a/go/pools/numbered.go b/go/pools/numbered.go index 1f88ae3d7da..32321441f2c 100644 --- a/go/pools/numbered.go +++ b/go/pools/numbered.go @@ -47,10 +47,6 @@ type unregistered struct { timeUnregistered time.Time } -func (u *unregistered) Size() int { - return 1 -} - //NewNumbered creates a new numbered func NewNumbered() *Numbered { n := &Numbered{ @@ -90,7 +86,7 @@ func (nu *Numbered) Unregister(id int64, reason string) { success := nu.unregister(id) if success { nu.recentlyUnregistered.Set( - fmt.Sprintf("%v", id), &unregistered{reason: reason, timeUnregistered: time.Now()}) + fmt.Sprintf("%v", id), &unregistered{reason: reason, timeUnregistered: time.Now()}, 1) } } diff --git a/go/sync2/consolidator.go b/go/sync2/consolidator.go index 5e7698996c9..c530be0b1dc 100644 --- a/go/sync2/consolidator.go +++ b/go/sync2/consolidator.go @@ -104,7 +104,7 @@ func (cc *ConsolidatorCache) Record(query string) { v.(*ccount).add(1) } else { c := ccount(1) - cc.Set(query, &c) + cc.Set(query, &c, 1) } } @@ -128,13 +128,6 @@ func (cc *ConsolidatorCache) Items() []ConsolidatorCacheItem { // request for the same query is already in progress. type ccount int64 -// Size always returns 1 because we use the cache only to track queries, -// independent of the number of requests waiting for them. -// This implements the cache.Value interface. -func (cc *ccount) Size() int { - return 1 -} - func (cc *ccount) add(n int64) int64 { return atomic.AddInt64((*int64)(cc), n) } diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index b23d4003de9..b21a8b7e83b 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -23,7 +23,6 @@ import ( "context" "fmt" - "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" @@ -201,7 +200,7 @@ func vtgateExecute(sql string) ([]*engine.Plan, map[string]*TabletActions, error } var plans []*engine.Plan - planCache.ForEach(func(value cache.Value) bool { + planCache.ForEach(func(value interface{}) bool { plan := value.(*engine.Plan) plan.ExecTime = 0 plans = append(plans, plan) diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index 6d347f3deb6..41456cffd53 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -238,13 +238,6 @@ func Exists(m Match, p Primitive) bool { return Find(m, p) != nil } -// Size is defined so that Plan can be given to a cache.LRUCache. -// VTGate needs to maintain a cache of plans. It uses LRUCache, which -// in turn requires its objects to define a Size function. -func (p *Plan) Size() int { - return 1 -} - //MarshalJSON serializes the plan into a JSON representation. func (p *Plan) MarshalJSON() ([]byte, error) { var instructions *PrimitiveDescription diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index a88fe6551f7..729c967f76e 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -101,7 +101,7 @@ type Executor struct { vschema *vindexes.VSchema normalize bool streamSize int - plans *cache.LRUCache + plans cache.Cache vschemaStats *VSchemaStats vm *VSchemaManager @@ -131,12 +131,18 @@ func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver e.vm.watchSrvVSchema(ctx, cell) executorOnce.Do(func() { - stats.NewGaugeFunc("QueryPlanCacheLength", "Query plan cache length", e.plans.Length) - stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", e.plans.Size) + stats.NewGaugeFunc("QueryPlanCacheLength", "Query plan cache length", func() int64 { + return e.plans.Stats().Length + }) + stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", func() int64 { + return e.plans.Stats().Size + }) stats.NewGaugeFunc("QueryPlanCacheCapacity", "Query plan cache capacity", e.plans.Capacity) - stats.NewCounterFunc("QueryPlanCacheEvictions", "Query plan cache evictions", e.plans.Evictions) + stats.NewCounterFunc("QueryPlanCacheEvictions", "Query plan cache evictions", func() int64 { + return e.plans.Stats().Evictions + }) stats.Publish("QueryPlanCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", e.plans.Oldest()) + return fmt.Sprintf("%v", e.plans.Stats().Oldest) })) http.Handle(pathQueryPlans, e) http.Handle(pathScatterStats, e) @@ -1326,7 +1332,7 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. return nil, err } if !skipQueryPlanCache && !sqlparser.SkipQueryPlanCacheDirective(statement) && sqlparser.CachePlan(statement) { - e.plans.Set(planKey, plan) + e.plans.Set(planKey, plan, plan.CachedSize(true)) } return plan, nil } @@ -1345,7 +1351,7 @@ type cacheItem struct { } func (e *Executor) debugCacheEntries() (items []cacheItem) { - e.plans.ForEach(func(value cache.Value) bool { + e.plans.ForEach(func(value interface{}) bool { plan := value.(*engine.Plan) items = append(items, cacheItem{ Key: plan.Original, @@ -1388,7 +1394,7 @@ func returnAsJSON(response http.ResponseWriter, stuff interface{}) { } // Plans returns the LRU plan cache -func (e *Executor) Plans() *cache.LRUCache { +func (e *Executor) Plans() cache.Cache { return e.plans } diff --git a/go/vt/vtgate/executor_scatter_stats.go b/go/vt/vtgate/executor_scatter_stats.go index dd321e37b67..760c18e8cc4 100644 --- a/go/vt/vtgate/executor_scatter_stats.go +++ b/go/vt/vtgate/executor_scatter_stats.go @@ -22,7 +22,6 @@ import ( "net/http" "time" - "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/proto/vtrpc" @@ -61,7 +60,7 @@ func (e *Executor) gatherScatterStats() (statsResults, error) { plans := make([]*engine.Plan, 0) routes := make([]*engine.Route, 0) // First we go over all plans and collect statistics and all query plans for scatter queries - e.plans.ForEach(func(value cache.Value) bool { + e.plans.ForEach(func(value interface{}) bool { plan := value.(*engine.Plan) scatter := engine.Find(findScatter, plan.Instructions) readOnly := !engine.Exists(isUpdating, plan.Instructions) diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go index a335d9871e0..7945b5717e2 100644 --- a/go/vt/vtgate/executor_test.go +++ b/go/vt/vtgate/executor_test.go @@ -1485,10 +1485,10 @@ func TestGetPlanUnnormalized(t *testing.T) { } } -func assertCacheSize(t *testing.T, c *cache.LRUCache, expected int) { +func assertCacheSize(t *testing.T, c cache.Cache, expected int) { t.Helper() var size int - c.ForEach(func(_ cache.Value) bool { + c.ForEach(func(_ interface{}) bool { size++ return true }) @@ -1497,7 +1497,7 @@ func assertCacheSize(t *testing.T, c *cache.LRUCache, expected int) { } } -func assertCacheContains(t *testing.T, c *cache.LRUCache, want []string) { +func assertCacheContains(t *testing.T, c cache.Cache, want []string) { t.Helper() for _, wantKey := range want { if _, ok := c.Get(wantKey); !ok { @@ -1513,9 +1513,7 @@ func TestGetPlanCacheUnnormalized(t *testing.T) { logStats1 := NewLogStats(ctx, "Test", "", nil) _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) require.NoError(t, err) - if r.plans.Size() != 0 { - t.Errorf("getPlan() expected cache to have size 0, but got: %b", r.plans.Size()) - } + assertCacheSize(t, r.plans, 0) wantSQL := query1 + " /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) @@ -1523,9 +1521,7 @@ func TestGetPlanCacheUnnormalized(t *testing.T) { logStats2 := NewLogStats(ctx, "Test", "", nil) _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) require.NoError(t, err) - if r.plans.Size() != 1 { - t.Errorf("getPlan() expected cache to have size 1, but got: %b", r.plans.Size()) - } + assertCacheSize(t, r.plans, 1) wantSQL = query1 + " /* comment 2 */" if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) @@ -1567,9 +1563,7 @@ func TestGetPlanCacheNormalized(t *testing.T) { logStats1 := NewLogStats(ctx, "Test", "", nil) _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) require.NoError(t, err) - if r.plans.Size() != 0 { - t.Errorf("getPlan() expected cache to have size 0, but got: %b", r.plans.Size()) - } + assertCacheSize(t, r.plans, 0) wantSQL := "select * from music_user_map where id = :vtg1 /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) @@ -1577,9 +1571,7 @@ func TestGetPlanCacheNormalized(t *testing.T) { logStats2 := NewLogStats(ctx, "Test", "", nil) _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) require.NoError(t, err) - if r.plans.Size() != 1 { - t.Errorf("getPlan() expected cache to have size 1, but got: %b", r.plans.Size()) - } + assertCacheSize(t, r.plans, 1) if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } diff --git a/go/vt/vtgate/queryz.go b/go/vt/vtgate/queryz.go index 587e13c6f39..b44f6dfecde 100644 --- a/go/vt/vtgate/queryz.go +++ b/go/vt/vtgate/queryz.go @@ -24,7 +24,6 @@ import ( "time" "vitess.io/vitess/go/acl" - "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/log" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/sqlparser" @@ -132,7 +131,7 @@ func queryzHandler(e *Executor, w http.ResponseWriter, r *http.Request) { }, } - e.plans.ForEach(func(value cache.Value) bool { + e.plans.ForEach(func(value interface{}) bool { plan := value.(*engine.Plan) Value := &queryzRow{ Query: logz.Wrappable(sqlparser.TruncateForUI(plan.Original)), diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index 8bf51aa3da6..0dcf43573e7 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -55,6 +55,7 @@ import ( // and track stats. type TabletPlan struct { *planbuilder.Plan + Original string Fields []*querypb.Field Rules *rules.Rules Authorized []*tableacl.ACLResult @@ -67,11 +68,6 @@ type TabletPlan struct { ErrorCount int64 } -// Size allows TabletPlan to be in cache.LRUCache. -func (*TabletPlan) Size() int { - return 1 -} - // AddStats updates the stats for the current TabletPlan. func (ep *TabletPlan) AddStats(queryCount int64, duration, mysqlTime time.Duration, rowCount, errorCount int64) { ep.mu.Lock() @@ -120,7 +116,7 @@ type QueryEngine struct { // mu protects the following fields. mu sync.RWMutex tables map[string]*schema.Table - plans *cache.LRUCache + plans cache.Cache queryRuleSources *rules.Map // Pools @@ -211,12 +207,18 @@ func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { env.Exporter().NewGaugeFunc("StreamBufferSize", "Query engine stream buffer size", qe.streamBufferSize.Get) env.Exporter().NewCounterFunc("TableACLExemptCount", "Query engine table ACL exempt count", qe.tableaclExemptCount.Get) - env.Exporter().NewGaugeFunc("QueryCacheLength", "Query engine query cache length", qe.plans.Length) - env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", qe.plans.Size) + env.Exporter().NewGaugeFunc("QueryCacheLength", "Query engine query cache length", func() int64 { + return qe.plans.Stats().Length + }) + env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", func() int64 { + return qe.plans.Stats().Size + }) env.Exporter().NewGaugeFunc("QueryCacheCapacity", "Query engine query cache capacity", qe.plans.Capacity) - env.Exporter().NewCounterFunc("QueryCacheEvictions", "Query engine query cache evictions", qe.plans.Evictions) + env.Exporter().NewCounterFunc("QueryCacheEvictions", "Query engine query cache evictions", func() int64 { + return qe.plans.Stats().Evictions + }) env.Exporter().Publish("QueryCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", qe.plans.Oldest()) + return fmt.Sprintf("%v", qe.plans.Stats().Oldest) })) qe.queryCounts = env.Exporter().NewCountersWithMultiLabels("QueryCounts", "query counts", []string{"Table", "Plan"}) qe.queryTimes = env.Exporter().NewCountersWithMultiLabels("QueryTimesNs", "query times in ns", []string{"Table", "Plan"}) @@ -305,7 +307,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats if err != nil { return nil, err } - plan := &TabletPlan{Plan: splan} + plan := &TabletPlan{Plan: splan, Original: sql} plan.Rules = qe.queryRuleSources.FilterByPlan(sql, plan.PlanID, plan.TableName().String()) plan.buildAuthorized() if plan.PlanID.IsSelect() { @@ -329,7 +331,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats return plan, nil } if !skipQueryPlanCache && !sqlparser.SkipQueryPlanCacheDirective(statement) { - qe.plans.Set(sql, plan) + qe.plans.Set(sql, plan, plan.CachedSize(true)) } return plan, nil } @@ -343,7 +345,7 @@ func (qe *QueryEngine) GetStreamPlan(sql string, isReservedConn bool) (*TabletPl if err != nil { return nil, err } - plan := &TabletPlan{Plan: splan} + plan := &TabletPlan{Plan: splan, Original: sql} plan.Rules = qe.queryRuleSources.FilterByPlan(sql, plan.PlanID, plan.TableName().String()) plan.buildAuthorized() return plan, nil @@ -440,9 +442,9 @@ func (qe *QueryEngine) handleHTTPQueryPlans(response http.ResponseWriter, reques } response.Header().Set("Content-Type", "text/plain") - qe.plans.ForEach(func(value cache.Value) bool { + qe.plans.ForEach(func(value interface{}) bool { plan := value.(*TabletPlan) - response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(plan.FullQuery.Query)))) + response.Write([]byte(fmt.Sprintf("%#v\n", sqlparser.TruncateForUI(plan.Original)))) if b, err := json.MarshalIndent(plan.Plan, "", " "); err != nil { response.Write([]byte(err.Error())) } else { @@ -460,11 +462,11 @@ func (qe *QueryEngine) handleHTTPQueryStats(response http.ResponseWriter, reques } response.Header().Set("Content-Type", "application/json; charset=utf-8") var qstats []perQueryStats - qe.plans.ForEach(func(value cache.Value) bool { + qe.plans.ForEach(func(value interface{}) bool { plan := value.(*TabletPlan) var pqstats perQueryStats - pqstats.Query = unicoded(sqlparser.TruncateForUI(plan.FullQuery.Query)) + pqstats.Query = unicoded(sqlparser.TruncateForUI(plan.Original)) pqstats.Table = plan.TableName().String() pqstats.Plan = plan.PlanID pqstats.QueryCount, pqstats.Time, pqstats.MysqlTime, pqstats.RowCount, pqstats.ErrorCount = plan.Stats() diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index 9ca7469a275..d59f0be92cf 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -17,6 +17,7 @@ limitations under the License. package tabletserver import ( + "context" "expvar" "net/http" "net/http/httptest" @@ -25,8 +26,6 @@ import ( "testing" "time" - "context" - "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/mysql/fakesqldb" @@ -137,6 +136,17 @@ func TestGetMessageStreamPlan(t *testing.T) { } } +func assertPlanCacheSize(t *testing.T, qe *QueryEngine, expected int) { + var size int + qe.plans.ForEach(func(_ interface{}) bool { + size++ + return true + }) + if size != expected { + t.Fatalf("expected query plan cache to contain %d entries, found %d", expected, size) + } +} + func TestQueryPlanCache(t *testing.T) { db := fakesqldb.New(t) defer db.Close() @@ -174,9 +184,7 @@ func TestQueryPlanCache(t *testing.T) { expvar.Do(func(kv expvar.KeyValue) { _ = kv.Value.String() }) - if qe.plans.Size() == 0 { - t.Fatalf("query plan cache should not be 0") - } + assertPlanCacheSize(t, qe, 1) qe.ClearQueryPlanCache() } @@ -206,9 +214,7 @@ func TestNoQueryPlanCache(t *testing.T) { if firstPlan == nil { t.Fatalf("plan should not be nil") } - if qe.plans.Size() != 0 { - t.Fatalf("query plan cache should be 0") - } + assertPlanCacheSize(t, qe, 0) qe.ClearQueryPlanCache() } @@ -238,9 +244,7 @@ func TestNoQueryPlanCacheDirective(t *testing.T) { if firstPlan == nil { t.Fatalf("plan should not be nil") } - if qe.plans.Size() != 0 { - t.Fatalf("query plan cache should be 0") - } + assertPlanCacheSize(t, qe, 0) qe.ClearQueryPlanCache() } diff --git a/go/vt/vttablet/tabletserver/queryz.go b/go/vt/vttablet/tabletserver/queryz.go index 168a842f737..cd170016fde 100644 --- a/go/vt/vttablet/tabletserver/queryz.go +++ b/go/vt/vttablet/tabletserver/queryz.go @@ -24,7 +24,6 @@ import ( "time" "vitess.io/vitess/go/acl" - "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/log" "vitess.io/vitess/go/vt/logz" "vitess.io/vitess/go/vt/sqlparser" @@ -141,13 +140,13 @@ func queryzHandler(qe *QueryEngine, w http.ResponseWriter, r *http.Request) { return row1.timePQ() > row2.timePQ() }, } - qe.plans.ForEach(func(value cache.Value) bool { + qe.plans.ForEach(func(value interface{}) bool { plan := value.(*TabletPlan) if plan == nil { return true } Value := &queryzRow{ - Query: logz.Wrappable(sqlparser.TruncateForUI(plan.FullQuery.Query)), + Query: logz.Wrappable(sqlparser.TruncateForUI(plan.Original)), Table: plan.TableName().String(), Plan: plan.PlanID, } diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index 556c29e1e8b..a1b9d79c90c 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -35,56 +35,56 @@ import ( func TestQueryzHandler(t *testing.T) { resp := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/schemaz", nil) - qe := newTestQueryEngine(100, 10*time.Second, true, &dbconfigs.DBConfigs{}) + qe := newTestQueryEngine(10*time.Second, true, &dbconfigs.DBConfigs{}) const query1 = "select name from test_table" plan1 := &TabletPlan{ + Original: query1, Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, - PlanID: planbuilder.PlanSelect, - FullQuery: sqlparser.BuildParsedQuery(query1), + Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, + PlanID: planbuilder.PlanSelect, }, } plan1.AddStats(10, 2*time.Second, 1*time.Second, 2, 0) - qe.plans.Set(query1, plan1) + qe.plans.Set(query1, plan1, plan1.CachedSize()) const query2 = "insert into test_table values 1" plan2 := &TabletPlan{ + Original: query2, Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, - PlanID: planbuilder.PlanDDL, - FullQuery: sqlparser.BuildParsedQuery(query2), + Table: &schema.Table{Name: sqlparser.NewTableIdent("test_table")}, + PlanID: planbuilder.PlanDDL, }, } plan2.AddStats(1, 2*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(query2, plan2) + qe.plans.Set(query2, plan2, plan2.CachedSize()) const query3 = "show tables" plan3 := &TabletPlan{ + Original: query3, Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, - PlanID: planbuilder.PlanOtherRead, - FullQuery: sqlparser.BuildParsedQuery(query3), + Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, + PlanID: planbuilder.PlanOtherRead, }, } plan3.AddStats(1, 75*time.Millisecond, 50*time.Millisecond, 1, 0) - qe.plans.Set(query3, plan3) - qe.plans.Set("", (*TabletPlan)(nil)) + qe.plans.Set(query3, plan3, plan3.CachedSize()) + qe.plans.Set("", (*TabletPlan)(nil), 1) hugeInsert := "insert into test_table values 0" for i := 1; i < 1000; i++ { hugeInsert = hugeInsert + fmt.Sprintf(", %d", i) } plan4 := &TabletPlan{ + Original: hugeInsert, Plan: &planbuilder.Plan{ - Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, - PlanID: planbuilder.PlanOtherRead, - FullQuery: sqlparser.BuildParsedQuery(hugeInsert), + Table: &schema.Table{Name: sqlparser.NewTableIdent("")}, + PlanID: planbuilder.PlanOtherRead, }, } plan4.AddStats(1, 1*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(hugeInsert, plan4) - qe.plans.Set("", (*TabletPlan)(nil)) + qe.plans.Set(hugeInsert, plan4, plan4.CachedSize()) + qe.plans.Set("", (*TabletPlan)(nil), 1) queryzHandler(qe, resp, req) body, _ := ioutil.ReadAll(resp.Body) @@ -157,6 +157,6 @@ func TestQueryzHandler(t *testing.T) { func checkQueryzHasPlan(t *testing.T, planPattern []string, plan *TabletPlan, page []byte) { matcher := regexp.MustCompile(strings.Join(planPattern, `\s*`)) if !matcher.Match(page) { - t.Fatalf("queryz page does not contain\nplan:\n%v\npattern:\n%v\npage:\n%s", plan, strings.Join(planPattern, `\s*`), string(page)) + t.Fatalf("queryz page does not contain\nplan:\n%#v\npattern:\n%v\npage:\n%s", plan, strings.Join(planPattern, `\s*`), string(page)) } } From 76bd931996a0a8bac3e5f6c32e658d72f16d6b48 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Tue, 19 Jan 2021 18:45:07 +0100 Subject: [PATCH 05/23] tools: do not cache E2E tests between runs Signed-off-by: Vicent Marti --- tools/e2e_test_runner.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/e2e_test_runner.sh b/tools/e2e_test_runner.sh index c581957a366..dc2edbf0e59 100755 --- a/tools/e2e_test_runner.sh +++ b/tools/e2e_test_runner.sh @@ -45,7 +45,7 @@ all_except_flaky_and_cluster_tests=$(echo "$packages_with_tests" | grep -vE ".+ flaky_tests=$(echo "$packages_with_tests" | grep -E ".+ .+_flaky_test\.go" | grep -vE "go/test/endtoend" | cut -d" " -f1) # Run non-flaky tests. -echo "$all_except_flaky_and_cluster_tests" | xargs go test $VT_GO_PARALLEL +echo "$all_except_flaky_and_cluster_tests" | xargs go test -count=1 $VT_GO_PARALLEL if [ $? -ne 0 ]; then echo "ERROR: Go unit tests failed. See above for errors." echo From 652a94cd6d528276fc6da4a7dce13ac417df0c01 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 20 Jan 2021 18:41:54 +0100 Subject: [PATCH 06/23] cache: configure using total memory usage The existing pattern for vttablet/vtgate cache configuration is a dangerous practice, because it lets the user configure the number of items that can be stored in the cache, as opposed to the total amount of memory (approximately) that the cache will consume. This makes tuning production systems complicated, and will skew more complex cache implementations that use size-aware eviction policies. To fix this, we're deprecating the original config settings for cache tuning, and introducing new ones where the total size of the cache is defined in BYTES as opposed to ENTRIES. To maintain backwards compatibility, if the user supplies the legacy config options with number of ENTRIES, we'll calculate an approximate total size for the cache based on the average size of a cache entry for each given cache. Signed-off-by: Vicent Marti --- go/cache/cache.go | 11 +++- go/cache/lru_cache.go | 2 + go/cache/null.go | 50 +++++++++++++++++++ go/vt/vtexplain/vtexplain_vtgate.go | 2 +- go/vt/vtgate/engine/primitive.go | 3 ++ go/vt/vtgate/executor.go | 4 +- go/vt/vtgate/executor_framework_test.go | 2 +- go/vt/vtgate/vtgate.go | 28 +++++++---- go/vt/vttablet/tabletserver/query_engine.go | 10 +++- .../tabletserver/query_engine_test.go | 20 ++++---- go/vt/vttablet/tabletserver/queryz_test.go | 8 +-- .../vttablet/tabletserver/tabletenv/config.go | 7 ++- .../tabletserver/tabletenv/config_test.go | 4 +- 13 files changed, 117 insertions(+), 34 deletions(-) create mode 100644 go/cache/null.go diff --git a/go/cache/cache.go b/go/cache/cache.go index 612b5466fa7..2bafa065bab 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -28,7 +28,7 @@ type Cache interface { Set(key string, val interface{}, valueSize int64) ForEach(callback func(interface{}) bool) - Delete(key string) bool + Delete(key string) Clear() Stats() *Stats @@ -56,3 +56,12 @@ func (s *Stats) JSON() string { } return string(buf) } + +// NewDefaultCacheImpl returns a new cache instance using the default Cache implementation +// (right now this is cache.LRUCache) +func NewDefaultCacheImpl(maxCost, _ int64) Cache { + if maxCost == 0 { + return &nullCache{} + } + return NewLRUCache(maxCost) +} diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index 5bb7c2f6170..38c8a767afb 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -25,6 +25,7 @@ package cache import ( "container/list" + "fmt" "sync" "time" ) @@ -212,6 +213,7 @@ func (lru *LRUCache) addNew(key string, value interface{}, valueSize int64) { element := lru.list.PushFront(newEntry) lru.table[key] = element lru.size += newEntry.size + fmt.Printf("CACHE: insert %d (%d / %d, %f)\n", valueSize, lru.size, lru.capacity, float64(lru.size)/float64(lru.capacity)) lru.checkCapacity() } diff --git a/go/cache/null.go b/go/cache/null.go new file mode 100644 index 00000000000..3f8a67f947c --- /dev/null +++ b/go/cache/null.go @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +// nullCache is a no-op cache that does not store items +type nullCache struct{} + +// Get never returns anything on the nullCache +func (n *nullCache) Get(_ string) (interface{}, bool) { + return nil, false +} + +// Set is a no-op in the nullCache +func (n *nullCache) Set(_ string, _ interface{}, _ int64) {} + +// ForEach iterates the nullCache, which is always empty +func (n *nullCache) ForEach(_ func(interface{}) bool) {} + +// Delete is a no-op in the nullCache +func (n *nullCache) Delete(_ string) {} + +// Clear is a no-op in the nullCache +func (n *nullCache) Clear() {} + +// Stats returns a nil stats object for the nullCache +func (n *nullCache) Stats() *Stats { + return nil +} + +// Capacity returns the capacity of the nullCache, which is always 0 +func (n *nullCache) Capacity() int64 { + return 0 +} + +// SetCapacity sets the capacity of the null cache, which is a no-op +func (n *nullCache) SetCapacity(_ int64) {} diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index b21a8b7e83b..ee3f5e80fc5 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -68,7 +68,7 @@ func initVtgateExecutor(vSchemaStr, ksShardMapStr string, opts *Options) error { vtgateSession.TargetString = opts.Target streamSize := 10 - queryPlanCacheSize := int64(10) + queryPlanCacheSize := int64(64 * 1024 * 1024) vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, queryPlanCacheSize) return nil diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index 41456cffd53..5461ce32b39 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -45,6 +45,9 @@ const ( // This is used for sending different IN clause values // to different shards. ListVarName = "__vals" + // AveragePlanSize is the average size in bytes that a cached plan takes + // when cached in memory + AveragePlanSize = 128 ) type ( diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 729c967f76e..776c90a8c8a 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -114,14 +114,14 @@ const pathScatterStats = "/debug/scatter_stats" const pathVSchema = "/debug/vschema" // NewExecutor creates a new Executor. -func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, queryPlanCacheSize int64) *Executor { +func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, queryPlanCacheSizeBytes int64) *Executor { e := &Executor{ serv: serv, cell: cell, resolver: resolver, scatterConn: resolver.scatterConn, txConn: resolver.scatterConn.txConn, - plans: cache.NewLRUCache(queryPlanCacheSize), + plans: cache.NewDefaultCacheImpl(queryPlanCacheSizeBytes, engine.AveragePlanSize), normalize: normalize, streamSize: streamSize, } diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go index fcc24f67b08..5889df4a7ae 100644 --- a/go/vt/vtgate/executor_framework_test.go +++ b/go/vt/vtgate/executor_framework_test.go @@ -304,7 +304,7 @@ var unshardedVSchema = ` const ( testBufferSize = 10 - testCacheSize = int64(10) + testCacheSize = int64(64 * 1024 * 1024) ) type DestinationAnyShardPickerFirstShard struct{} diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index 8ff57334606..f5c87a4aeae 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -41,6 +41,7 @@ import ( "vitess.io/vitess/go/vt/srvtopo" "vitess.io/vitess/go/vt/topo/topoproto" "vitess.io/vitess/go/vt/vterrors" + "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/vtgateservice" @@ -52,15 +53,16 @@ import ( ) var ( - transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") - normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") - terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") - streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") - queryPlanCacheSize = flag.Int64("gate_query_cache_size", 10000, "gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") - maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") - warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") - defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") + transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") + normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") + terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") + streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") + queryPlanCacheSize = flag.Int64("gate_query_cache_size", 0, "deprecated: gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + queryPlanCacheSizeBytes = flag.Int64("gate_query_cache_size_bytes", 64*1024*1024, "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") + maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") + warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") + defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") // TODO(deepthi): change these two vars to unexported and move to healthcheck.go when LegacyHealthcheck is removed @@ -178,8 +180,14 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) + // If the legacy queryPlanCacheSize is set, override the value of the new queryPlanCacheSizeBytes + // approximating the total size of the cache with the average size of an entry + if *queryPlanCacheSize != 0 { + *queryPlanCacheSizeBytes = *queryPlanCacheSize * engine.AveragePlanSize + } + rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSizeBytes), resolver: resolver, vsm: vsm, txConn: tc, diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index 0dcf43573e7..795ef26d4c9 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -51,6 +51,10 @@ import ( //_______________________________________________ +// AverageTabletPlanSize is the average size in bytes that a TabletPlan takes when +// cached in memory +const AverageTabletPlanSize = 256 + // TabletPlan wraps the planbuilder's exec plan to enforce additional rules // and track stats. type TabletPlan struct { @@ -161,11 +165,15 @@ type QueryEngine struct { // You must call this only once. func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { config := env.Config() + if config.QueryCacheSize != 0 { + config.QueryCacheSizeBytes = config.QueryCacheSize * AverageTabletPlanSize + } + qe := &QueryEngine{ env: env, se: se, tables: make(map[string]*schema.Table), - plans: cache.NewLRUCache(int64(config.QueryCacheSize)), + plans: cache.NewDefaultCacheImpl(int64(config.QueryCacheSizeBytes), AverageTabletPlanSize), queryRuleSources: rules.NewMap(), } diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index d59f0be92cf..66d2862ea35 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -91,7 +91,7 @@ func TestGetPlanPanicDuetoEmptyQuery(t *testing.T) { for query, result := range schematest.Queries() { db.AddQuery(query, result) } - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -111,7 +111,7 @@ func TestGetMessageStreamPlan(t *testing.T) { for query, result := range schematest.Queries() { db.AddQuery(query, result) } - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -159,14 +159,14 @@ func TestQueryPlanCache(t *testing.T) { db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + qe.SetQueryPlanCacheCap(1024) firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, false, false /* inReservedConn */) if err != nil { t.Fatal(err) @@ -199,7 +199,7 @@ func TestNoQueryPlanCache(t *testing.T) { db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -229,7 +229,7 @@ func TestNoQueryPlanCacheDirective(t *testing.T) { db.AddQuery("select /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ * from test_table_01 where 1 != 1", &sqltypes.Result{}) db.AddQuery("select /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ * from test_table_02 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 10*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(10*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -256,7 +256,7 @@ func TestStatsURL(t *testing.T) { } query := "select * from test_table_01" db.AddQuery("select * from test_table_01 where 1 != 1", &sqltypes.Result{}) - qe := newTestQueryEngine(10, 1*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(1*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() @@ -278,10 +278,10 @@ func TestStatsURL(t *testing.T) { qe.handleHTTPQueryRules(response, request) } -func newTestQueryEngine(queryCacheSize int, idleTimeout time.Duration, strict bool, dbcfgs *dbconfigs.DBConfigs) *QueryEngine { +func newTestQueryEngine(idleTimeout time.Duration, strict bool, dbcfgs *dbconfigs.DBConfigs) *QueryEngine { config := tabletenv.NewDefaultConfig() config.DB = dbcfgs - config.QueryCacheSize = queryCacheSize + config.QueryCacheSizeBytes = 1 * 1024 * 1024 config.OltpReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.OlapReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.TxPool.IdleTimeoutSeconds.Set(idleTimeout) @@ -296,7 +296,7 @@ func runConsolidatedQuery(t *testing.T, sql string) *QueryEngine { db := fakesqldb.New(t) defer db.Close() - qe := newTestQueryEngine(10, 1*time.Second, true, newDBConfigs(db)) + qe := newTestQueryEngine(1*time.Second, true, newDBConfigs(db)) qe.se.Open() qe.Open() defer qe.Close() diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index a1b9d79c90c..f61abf49b46 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -46,7 +46,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan1.AddStats(10, 2*time.Second, 1*time.Second, 2, 0) - qe.plans.Set(query1, plan1, plan1.CachedSize()) + qe.plans.Set(query1, plan1, plan1.CachedSize(true)) const query2 = "insert into test_table values 1" plan2 := &TabletPlan{ @@ -57,7 +57,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan2.AddStats(1, 2*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(query2, plan2, plan2.CachedSize()) + qe.plans.Set(query2, plan2, plan2.CachedSize(true)) const query3 = "show tables" plan3 := &TabletPlan{ @@ -68,7 +68,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan3.AddStats(1, 75*time.Millisecond, 50*time.Millisecond, 1, 0) - qe.plans.Set(query3, plan3, plan3.CachedSize()) + qe.plans.Set(query3, plan3, plan3.CachedSize(true)) qe.plans.Set("", (*TabletPlan)(nil), 1) hugeInsert := "insert into test_table values 0" @@ -83,7 +83,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan4.AddStats(1, 1*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(hugeInsert, plan4, plan4.CachedSize()) + qe.plans.Set(hugeInsert, plan4, plan4.CachedSize(true)) qe.plans.Set("", (*TabletPlan)(nil), 1) queryzHandler(qe, resp, req) diff --git a/go/vt/vttablet/tabletserver/tabletenv/config.go b/go/vt/vttablet/tabletserver/tabletenv/config.go index ce415f4e2b0..7946f2a52f3 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config.go @@ -102,7 +102,8 @@ func init() { flag.BoolVar(&deprecateAllowUnsafeDMLs, "queryserver-config-allowunsafe-dmls", false, "deprecated") flag.IntVar(¤tConfig.StreamBufferSize, "queryserver-config-stream-buffer-size", defaultConfig.StreamBufferSize, "query server stream buffer size, the maximum number of bytes sent from vttablet for each stream call. It's recommended to keep this value in sync with vtgate's stream_buffer_size.") - flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "deprecated: query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.IntVar(¤tConfig.QueryCacheSizeBytes, "queryserver-config-query-cache-size-bytes", defaultConfig.QueryCacheSizeBytes, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") SecondsVar(¤tConfig.SchemaReloadIntervalSeconds, "queryserver-config-schema-reload-time", defaultConfig.SchemaReloadIntervalSeconds, "query server schema reload time, how often vttablet reloads schemas from underlying MySQL instance in seconds. vttablet keeps table schemas in its own memory and periodically refreshes it from MySQL. This config controls the reload time.") SecondsVar(¤tConfig.Oltp.QueryTimeoutSeconds, "queryserver-config-query-timeout", defaultConfig.Oltp.QueryTimeoutSeconds, "query server query timeout (in seconds), this is the query timeout in vttablet side. If a query takes more than this timeout, it will be killed.") SecondsVar(¤tConfig.OltpReadPool.TimeoutSeconds, "queryserver-config-query-pool-timeout", defaultConfig.OltpReadPool.TimeoutSeconds, "query server query pool timeout (in seconds), it is how long vttablet waits for a connection from the query pool. If set to 0 (default) then the overall query timeout is used instead.") @@ -243,6 +244,7 @@ type TabletConfig struct { PassthroughDML bool `json:"passthroughDML,omitempty"` StreamBufferSize int `json:"streamBufferSize,omitempty"` QueryCacheSize int `json:"queryCacheSize,omitempty"` + QueryCacheSizeBytes int `json:"queryCacheSizeBytes,omitempty"` SchemaReloadIntervalSeconds Seconds `json:"schemaReloadIntervalSeconds,omitempty"` WatchReplication bool `json:"watchReplication,omitempty"` TrackSchemaVersions bool `json:"trackSchemaVersions,omitempty"` @@ -447,7 +449,8 @@ var defaultConfig = TabletConfig{ // great (the overhead makes the final packets on the wire about twice // bigger than this). StreamBufferSize: 32 * 1024, - QueryCacheSize: 5000, + QueryCacheSize: 0, + QueryCacheSizeBytes: 64 * 1024 * 1024, SchemaReloadIntervalSeconds: 30 * 60, MessagePostponeParallelism: 4, CacheResultFields: true, diff --git a/go/vt/vttablet/tabletserver/tabletenv/config_test.go b/go/vt/vttablet/tabletserver/tabletenv/config_test.go index 3930ce48a5c..6519e5bf708 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config_test.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config_test.go @@ -130,7 +130,7 @@ oltpReadPool: idleTimeoutSeconds: 1800 maxWaiters: 5000 size: 16 -queryCacheSize: 5000 +queryCacheSizeBytes: 67108864 replicationTracker: heartbeatIntervalSeconds: 0.25 mode: disable @@ -190,7 +190,7 @@ func TestFlags(t *testing.T) { MaxConcurrency: 5, }, StreamBufferSize: 32768, - QueryCacheSize: 5000, + QueryCacheSizeBytes: 64 * 1024 * 1024, SchemaReloadIntervalSeconds: 1800, TrackSchemaVersions: false, MessagePostponeParallelism: 4, From 1093c693a2852e5a02b9416b4b2fb9c54305c260 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Thu, 21 Jan 2021 11:03:29 +0100 Subject: [PATCH 07/23] cache: do not return `nil` stats Signed-off-by: Vicent Marti --- go/cache/null.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cache/null.go b/go/cache/null.go index 3f8a67f947c..0cf0e9a9902 100644 --- a/go/cache/null.go +++ b/go/cache/null.go @@ -38,7 +38,7 @@ func (n *nullCache) Clear() {} // Stats returns a nil stats object for the nullCache func (n *nullCache) Stats() *Stats { - return nil + return &Stats{} } // Capacity returns the capacity of the nullCache, which is always 0 From 2e3e0c04f6c3fd3f65ac830f15c9d04d493f5e43 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 27 Jan 2021 14:46:07 +0100 Subject: [PATCH 08/23] cache: fix flaky memory usage test Signed-off-by: Vicent Marti --- go/cache/lru_cache.go | 2 -- go/vt/vttablet/endtoend/config_test.go | 13 +++++++------ go/vt/vttablet/tabletserver/query_engine_test.go | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index 38c8a767afb..5bb7c2f6170 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -25,7 +25,6 @@ package cache import ( "container/list" - "fmt" "sync" "time" ) @@ -213,7 +212,6 @@ func (lru *LRUCache) addNew(key string, value interface{}, valueSize int64) { element := lru.list.PushFront(newEntry) lru.table[key] = element lru.size += newEntry.size - fmt.Printf("CACHE: insert %d (%d / %d, %f)\n", valueSize, lru.size, lru.capacity, float64(lru.size)/float64(lru.capacity)) lru.checkCapacity() } diff --git a/go/vt/vttablet/endtoend/config_test.go b/go/vt/vttablet/endtoend/config_test.go index 6690c58e41b..fde3881972e 100644 --- a/go/vt/vttablet/endtoend/config_test.go +++ b/go/vt/vttablet/endtoend/config_test.go @@ -176,11 +176,12 @@ func TestConsolidatorReplicasOnly(t *testing.T) { } func TestQueryPlanCache(t *testing.T) { + const cachedPlanSize = 2275 //sleep to avoid race between SchemaChanged event clearing out the plans cache which breaks this test time.Sleep(1 * time.Second) defer framework.Server.SetQueryPlanCacheCap(framework.Server.QueryPlanCacheCap()) - framework.Server.SetQueryPlanCacheCap(1) + framework.Server.SetQueryPlanCacheCap(cachedPlanSize) bindVars := map[string]*querypb.BindVariable{ "ival1": sqltypes.Int64BindVariable(1), @@ -191,18 +192,18 @@ func TestQueryPlanCache(t *testing.T) { _, _ = client.Execute("select * from vitess_test where intval=:ival2", bindVars) vend := framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 1) - verifyIntValue(t, vend, "QueryCacheSize", 1) - verifyIntValue(t, vend, "QueryCacheCapacity", 1) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize) + verifyIntValue(t, vend, "QueryCacheCapacity", cachedPlanSize) - framework.Server.SetQueryPlanCacheCap(10) + framework.Server.SetQueryPlanCacheCap(64 * 1024) _, _ = client.Execute("select * from vitess_test where intval=:ival1", bindVars) vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 2) - verifyIntValue(t, vend, "QueryCacheSize", 2) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2) _, _ = client.Execute("select * from vitess_test where intval=1", bindVars) vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 3) - verifyIntValue(t, vend, "QueryCacheSize", 3) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2+2254) } func TestMaxResultSize(t *testing.T) { diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index 66d2862ea35..cc0655d7b6e 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -206,7 +206,7 @@ func TestNoQueryPlanCache(t *testing.T) { ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + qe.SetQueryPlanCacheCap(1024) firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, true, false /* inReservedConn */) if err != nil { t.Fatal(err) @@ -236,7 +236,7 @@ func TestNoQueryPlanCacheDirective(t *testing.T) { ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1) + qe.SetQueryPlanCacheCap(1024) firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, false, false /* inReservedConn */) if err != nil { t.Fatal(err) From d6a13f10baa8fb703402a998ce61211947956e49 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Tue, 19 Jan 2021 17:47:53 +0100 Subject: [PATCH 09/23] cache: switch to a new implementation based on Ristretto Signed-off-by: Vicent Marti --- go.mod | 2 - go.sum | 27 + go/cache/cache.go | 43 +- go/cache/lru_cache.go | 43 +- go/cache/lru_cache_test.go | 94 +-- go/cache/null.go | 23 +- go/cache/ristretto.go | 22 + go/cache/ristretto/bloom/bbloom.go | 210 ++++++ go/cache/ristretto/bloom/bbloom_test.go | 114 +++ go/cache/ristretto/cache.go | 694 +++++++++++++++++ go/cache/ristretto/cache_test.go | 786 ++++++++++++++++++++ go/cache/ristretto/policy.go | 417 +++++++++++ go/cache/ristretto/policy_test.go | 259 +++++++ go/cache/ristretto/ring.go | 91 +++ go/cache/ristretto/ring_test.go | 70 ++ go/cache/ristretto/sketch.go | 155 ++++ go/cache/ristretto/sketch_test.go | 85 +++ go/cache/ristretto/store.go | 280 +++++++ go/cache/ristretto/store_test.go | 207 ++++++ go/cache/ristretto/ttl.go | 147 ++++ go/hack/runtime.go | 45 ++ go/hack/runtime.s | 0 go/vt/vtgate/executor.go | 15 +- go/vt/vtgate/executor_test.go | 106 ++- go/vt/vtgate/queryz_test.go | 3 + go/vt/vttablet/tabletserver/query_engine.go | 17 +- go/vt/vttablet/tabletserver/queryz_test.go | 3 + 27 files changed, 3747 insertions(+), 211 deletions(-) create mode 100644 go/cache/ristretto.go create mode 100644 go/cache/ristretto/bloom/bbloom.go create mode 100644 go/cache/ristretto/bloom/bbloom_test.go create mode 100644 go/cache/ristretto/cache.go create mode 100644 go/cache/ristretto/cache_test.go create mode 100644 go/cache/ristretto/policy.go create mode 100644 go/cache/ristretto/policy_test.go create mode 100644 go/cache/ristretto/ring.go create mode 100644 go/cache/ristretto/ring_test.go create mode 100644 go/cache/ristretto/sketch.go create mode 100644 go/cache/ristretto/sketch_test.go create mode 100644 go/cache/ristretto/store.go create mode 100644 go/cache/ristretto/store_test.go create mode 100644 go/cache/ristretto/ttl.go create mode 100644 go/hack/runtime.go create mode 100644 go/hack/runtime.s diff --git a/go.mod b/go.mod index fa07aafe241..547a624d226 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,6 @@ require ( github.com/hashicorp/go-msgpack v0.5.5 github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/hashicorp/serf v0.9.2 // indirect github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 @@ -54,7 +53,6 @@ require ( github.com/klauspost/compress v1.4.1 // indirect github.com/klauspost/cpuid v1.2.0 // indirect github.com/klauspost/pgzip v1.2.4 - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/krishicks/yaml-patch v0.0.10 github.com/magiconair/properties v1.8.1 github.com/martini-contrib/auth v0.0.0-20150219114609-fa62c19b7ae8 diff --git a/go.sum b/go.sum index 41cc2b77507..deccc4d563e 100644 --- a/go.sum +++ b/go.sum @@ -147,9 +147,11 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbp github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/corpix/uarand v0.1.1 h1:RMr1TWc9F4n5jiPDzFHtmaUXLKLNUFK0SgCLo4BhX/U= github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU= +github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= @@ -160,14 +162,18 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= +github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -188,6 +194,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -274,6 +281,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/google/go-github/v27 v27.0.4/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -363,9 +371,12 @@ github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2I github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -424,6 +435,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -451,6 +463,7 @@ github.com/martini-contrib/gzip v0.0.0-20151124214156-6c035326b43f h1:wVDxEVZP1e github.com/martini-contrib/gzip v0.0.0-20151124214156-6c035326b43f/go.mod h1:jhUB0rZB2TPWqy0yGugKRRictO591eSO7If7O4MfCaA= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= +github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -499,6 +512,7 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3 h1:f/MjBEBDLttYCGfRaKBbKSRVF5aV2O6fnBpzknuE3jU= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcloughlin/avo v0.0.0-20201105074841-5d2f697d268f/go.mod h1:6aKT4zZIrpGqB3RpFU14ByCSSyKY6LfJz4J/JJChHfI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -545,6 +559,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pires/go-proxyproto v0.0.0-20191211124218-517ecdf5bb2b h1:JPLdtNmpXbWytipbGwYz7zXZzlQNASEiFw5aGAM75us= @@ -611,6 +626,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sjmudd/stopwatch v0.0.0-20170613150411-f380bf8a9be1 h1:acClJNSOjUrAUKW+ZneCZymCFDWtSaJG5YQl8FoOlyI= github.com/sjmudd/stopwatch v0.0.0-20170613150411-f380bf8a9be1/go.mod h1:Pgf1sZ2KrHK8vdRTV5UHGp80LT7HMUKuNAiKC402abY= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -629,6 +646,7 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -637,6 +655,7 @@ github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -668,6 +687,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.0/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= @@ -697,6 +717,7 @@ go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -704,6 +725,8 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -816,6 +839,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -868,6 +892,8 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a h1:TYqOq/v+Ri5aADpldxXOj6PmvcPMOJbLjdALzZDQT2M= golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -982,6 +1008,7 @@ modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03 modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= diff --git a/go/cache/cache.go b/go/cache/cache.go index 2bafa065bab..b7ff081f0e3 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -16,52 +16,29 @@ limitations under the License. package cache -import ( - "encoding/json" - "time" -) - // Cache is a generic interface type for a data structure that keeps recently used // objects in memory and evicts them when it becomes full. type Cache interface { Get(key string) (interface{}, bool) - Set(key string, val interface{}, valueSize int64) + Set(key string, val interface{}, valueSize int64) bool ForEach(callback func(interface{}) bool) Delete(key string) Clear() + Wait() - Stats() *Stats - Capacity() int64 + Len() int + Evictions() int64 + UsedCapacity() int64 + MaxCapacity() int64 SetCapacity(int64) } -// Stats are the internal statistics about a live Cache that can be queried at runtime -type Stats struct { - Length int64 `json:"Length"` - Size int64 `json:"CachedSize"` - Capacity int64 `json:"Capacity"` - Evictions int64 `json:"Evictions"` - Oldest time.Time `json:"OldestAccess"` -} - -// JSON returns the serialized statistics in JSON form -func (s *Stats) JSON() string { - if s == nil { - return "{}" - } - buf, err := json.Marshal(s) - if err != nil { - panic("cache.Stats failed to serialize (should never happen)") - } - return string(buf) -} - -// NewDefaultCacheImpl returns a new cache instance using the default Cache implementation -// (right now this is cache.LRUCache) -func NewDefaultCacheImpl(maxCost, _ int64) Cache { +// NewDefaultCacheImpl returns the default cache implementation for Vitess, which at the moment +// is based on Ristretto +func NewDefaultCacheImpl(maxCost, averageItem int64) Cache { if maxCost == 0 { return &nullCache{} } - return NewLRUCache(maxCost) + return NewRistrettoCache(maxCost, averageItem) } diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index 5bb7c2f6170..e5578eef509 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -84,7 +84,7 @@ func (lru *LRUCache) Get(key string) (v interface{}, ok bool) { } // Set sets a value in the cache. -func (lru *LRUCache) Set(key string, value interface{}, valueSize int64) { +func (lru *LRUCache) Set(key string, value interface{}, valueSize int64) bool { lru.mu.Lock() defer lru.mu.Unlock() @@ -93,6 +93,8 @@ func (lru *LRUCache) Set(key string, value interface{}, valueSize int64) { } else { lru.addNew(key, value, valueSize) } + // the LRU cache cannot fail to insert items; it always returns true + return true } // Delete removes an entry from the cache, and returns if the entry existed. @@ -126,6 +128,13 @@ func (lru *LRUCache) Clear() { lru.size = 0 } +// Len returns the size of the cache (in entries) +func (lru *LRUCache) Len() int { + lru.mu.Lock() + defer lru.mu.Unlock() + return lru.list.Len() +} + // SetCapacity will set the capacity of the cache. If the capacity is // smaller, and the current cache size exceed that capacity, the cache // will be shrank. @@ -137,32 +146,26 @@ func (lru *LRUCache) SetCapacity(capacity int64) { lru.checkCapacity() } -// Stats returns a few stats on the cache. -func (lru *LRUCache) Stats() *Stats { - if lru == nil { - return nil - } +// Wait is a no-op in the LRU cache +func (lru *LRUCache) Wait() {} +// UsedCapacity returns the size of the cache (in bytes) +func (lru *LRUCache) UsedCapacity() int64 { + return lru.size +} + +// MaxCapacity returns the cache maximum capacity. +func (lru *LRUCache) MaxCapacity() int64 { lru.mu.Lock() defer lru.mu.Unlock() - - stats := &Stats{ - Length: int64(lru.list.Len()), - Size: lru.size, - Capacity: lru.capacity, - Evictions: lru.evictions, - } - if lastElem := lru.list.Back(); lastElem != nil { - stats.Oldest = lastElem.Value.(*entry).timeAccessed - } - return stats + return lru.capacity } -// Capacity returns the cache maximum capacity. -func (lru *LRUCache) Capacity() int64 { +// Evictions returns the number of evictions +func (lru *LRUCache) Evictions() int64 { lru.mu.Lock() defer lru.mu.Unlock() - return lru.capacity + return lru.evictions } // ForEach yields all the values for the cache, ordered from most recently diff --git a/go/cache/lru_cache_test.go b/go/cache/lru_cache_test.go index 966672c1efc..ed339af576d 100644 --- a/go/cache/lru_cache_test.go +++ b/go/cache/lru_cache_test.go @@ -17,27 +17,24 @@ limitations under the License. package cache import ( - "encoding/json" "testing" - "time" ) type CacheValue struct{} func TestInitialState(t *testing.T) { cache := NewLRUCache(5) - stats := cache.Stats() - if stats.Length != 0 { - t.Errorf("length = %v, want 0", stats.Length) + if cache.Len() != 0 { + t.Errorf("length = %v, want 0", cache.Len()) } - if stats.Size != 0 { - t.Errorf("size = %v, want 0", stats.Size) + if cache.UsedCapacity() != 0 { + t.Errorf("size = %v, want 0", cache.UsedCapacity()) } - if stats.Capacity != 5 { - t.Errorf("capacity = %v, want 5", stats.Capacity) + if cache.MaxCapacity() != 5 { + t.Errorf("capacity = %v, want 5", cache.MaxCapacity()) } - if stats.Evictions != 0 { - t.Errorf("evictions = %v, want 0", stats.Evictions) + if cache.Evictions() != 0 { + t.Errorf("evictions = %v, want 0", cache.Evictions()) } } @@ -80,14 +77,14 @@ func TestSetUpdatesSize(t *testing.T) { emptyValue := &CacheValue{} key := "key1" cache.Set(key, emptyValue, 0) - if stats := cache.Stats(); stats.Size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0", stats.Size) + if size := cache.UsedCapacity(); size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0", size) } someValue := &CacheValue{} key = "key2" cache.Set(key, someValue, 20) - if stats := cache.Stats(); stats.Size != 20 { - t.Errorf("cache.CachedSize() = %v, expected 20", stats.Size) + if size := cache.UsedCapacity(); size != 20 { + t.Errorf("cache.CachedSize() = %v, expected 20", size) } } @@ -111,15 +108,15 @@ func TestSetWithOldKeyUpdatesSize(t *testing.T) { key := "key1" cache.Set(key, emptyValue, 0) - if stats := cache.Stats(); stats.Size != 0 { - t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, 0) + if size := cache.UsedCapacity(); size != 0 { + t.Errorf("cache.CachedSize() = %v, expected %v", size, 0) } someValue := &CacheValue{} cache.Set(key, someValue, 20) expected := int64(20) - if stats := cache.Stats(); stats.Size != expected { - t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, expected) + if size := cache.UsedCapacity(); size != expected { + t.Errorf("cache.CachedSize() = %v, expected %v", size, expected) } } @@ -146,8 +143,8 @@ func TestDelete(t *testing.T) { t.Error("Expected item to be in cache.") } - if stats := cache.Stats(); stats.Size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0", stats.Size) + if size := cache.UsedCapacity(); size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0", size) } if _, ok := cache.Get(key); ok { @@ -163,8 +160,8 @@ func TestClear(t *testing.T) { cache.Set(key, value, 1) cache.Clear() - if stats := cache.Stats(); stats.Size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0 after Clear()", stats.Size) + if size := cache.UsedCapacity(); size != 0 { + t.Errorf("cache.CachedSize() = %v, expected 0 after Clear()", size) } } @@ -178,44 +175,20 @@ func TestCapacityIsObeyed(t *testing.T) { cache.Set("key1", value, 1) cache.Set("key2", value, 1) cache.Set("key3", value, 1) - if stats := cache.Stats(); stats.Size != size { - t.Errorf("cache.CachedSize() = %v, expected %v", stats.Size, size) + if usedCap := cache.UsedCapacity(); usedCap != size { + t.Errorf("cache.CachedSize() = %v, expected %v", usedCap, size) } // Insert one more; something should be evicted to make room. cache.Set("key4", value, 1) - st := cache.Stats() - if st.Size != size { - t.Errorf("post-evict cache.CachedSize() = %v, expected %v", st.Size, size) + if cache.UsedCapacity() != size { + t.Errorf("post-evict cache.CachedSize() = %v, expected %v", cache.UsedCapacity(), size) } - if st.Evictions != 1 { - t.Errorf("post-evict cache.evictions = %v, expected 1", st.Evictions) + if cache.Evictions() != 1 { + t.Errorf("post-evict cache.evictions = %v, expected 1", cache.Evictions()) } - - // Check json stats - data := st.JSON() - m := make(map[string]interface{}) - if err := json.Unmarshal([]byte(data), &m); err != nil { - t.Errorf("cache.StatsJSON() returned bad json data: %v %v", data, err) - } - if m["CachedSize"].(float64) != float64(size) { - t.Errorf("cache.StatsJSON() returned bad size: %v", m) - } - // Check various other stats - if st.Length != size { - t.Errorf("cache.StatsJSON() returned bad length: %v", st.Length) - } - if st.Size != size { - t.Errorf("cache.StatsJSON() returned bad size: %v", st.Size) - } - if c := cache.Capacity(); c != size { - t.Errorf("cache.StatsJSON() returned bad length: %v", c) - } - - // checks StatsJSON on nil - cache = nil - if s := cache.Stats().JSON(); s != "{}" { - t.Errorf("cache.StatsJSON() on nil object returned %v", s) + if cache.Len() != int(size) { + t.Errorf("cache.StatsJSON() returned bad length: %v", cache.Len()) } } @@ -230,9 +203,7 @@ func TestLRUIsEvicted(t *testing.T) { // Look up the elements. This will rearrange the LRU ordering. cache.Get("key3") - beforeKey2 := time.Now() cache.Get("key2") - afterKey2 := time.Now() cache.Get("key1") // lru: [key1, key2, key3] @@ -244,14 +215,7 @@ func TestLRUIsEvicted(t *testing.T) { t.Error("Least recently used element was not evicted.") } - st := cache.Stats() - - // Check oldest - if o := st.Oldest; o.Before(beforeKey2) || o.After(afterKey2) { - t.Errorf("cache.Oldest returned an unexpected value: got %v, expected a value between %v and %v", o, beforeKey2, afterKey2) - } - - if e, want := st.Evictions, int64(1); e != want { + if e, want := cache.Evictions(), int64(1); e != want { t.Errorf("evictions: %d, want: %d", e, want) } } diff --git a/go/cache/null.go b/go/cache/null.go index 0cf0e9a9902..87341f1fd4e 100644 --- a/go/cache/null.go +++ b/go/cache/null.go @@ -25,7 +25,9 @@ func (n *nullCache) Get(_ string) (interface{}, bool) { } // Set is a no-op in the nullCache -func (n *nullCache) Set(_ string, _ interface{}, _ int64) {} +func (n *nullCache) Set(_ string, _ interface{}, _ int64) bool { + return false +} // ForEach iterates the nullCache, which is always empty func (n *nullCache) ForEach(_ func(interface{}) bool) {} @@ -36,15 +38,26 @@ func (n *nullCache) Delete(_ string) {} // Clear is a no-op in the nullCache func (n *nullCache) Clear() {} -// Stats returns a nil stats object for the nullCache -func (n *nullCache) Stats() *Stats { - return &Stats{} +// Wait is a no-op in the nullcache +func (n *nullCache) Wait() {} + +func (n *nullCache) Len() int { + return 0 +} + +// Capacity returns the capacity of the nullCache, which is always 0 +func (n *nullCache) UsedCapacity() int64 { + return 0 } // Capacity returns the capacity of the nullCache, which is always 0 -func (n *nullCache) Capacity() int64 { +func (n *nullCache) MaxCapacity() int64 { return 0 } // SetCapacity sets the capacity of the null cache, which is a no-op func (n *nullCache) SetCapacity(_ int64) {} + +func (n *nullCache) Evictions() int64 { + return 0 +} diff --git a/go/cache/ristretto.go b/go/cache/ristretto.go new file mode 100644 index 00000000000..3e3218df30c --- /dev/null +++ b/go/cache/ristretto.go @@ -0,0 +1,22 @@ +package cache + +import ( + "vitess.io/vitess/go/cache/ristretto" +) + +var _ Cache = &ristretto.Cache{} + +// NewRistrettoCache returns a Cache implementation based on Ristretto +func NewRistrettoCache(maxCost, averageItemSize int64) *ristretto.Cache { + config := ristretto.Config{ + NumCounters: (maxCost / averageItemSize) * 10, + MaxCost: maxCost, + BufferItems: 64, + Metrics: true, + } + cache, err := ristretto.NewCache(&config) + if err != nil { + panic(err) + } + return cache +} diff --git a/go/cache/ristretto/bloom/bbloom.go b/go/cache/ristretto/bloom/bbloom.go new file mode 100644 index 00000000000..586adec9cb6 --- /dev/null +++ b/go/cache/ristretto/bloom/bbloom.go @@ -0,0 +1,210 @@ +// The MIT License (MIT) +// Copyright (c) 2014 Andreas Briese, eduToolbox@Bri-C GmbH, Sarstedt + +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package bloom + +import ( + "bytes" + "encoding/json" + "log" + "math" + "unsafe" +) + +// helper +var mask = []uint8{1, 2, 4, 8, 16, 32, 64, 128} + +func getSize(ui64 uint64) (size uint64, exponent uint64) { + if ui64 < uint64(512) { + ui64 = uint64(512) + } + size = uint64(1) + for size < ui64 { + size <<= 1 + exponent++ + } + return size, exponent +} + +func calcSizeByWrongPositives(numEntries, wrongs float64) (uint64, uint64) { + size := -1 * numEntries * math.Log(wrongs) / math.Pow(float64(0.69314718056), 2) + locs := math.Ceil(float64(0.69314718056) * size / numEntries) + return uint64(size), uint64(locs) +} + +// NewBloomFilter returns a new bloomfilter. +func NewBloomFilter(params ...float64) (bloomfilter *Bloom) { + var entries, locs uint64 + if len(params) == 2 { + if params[1] < 1 { + entries, locs = calcSizeByWrongPositives(params[0], params[1]) + } else { + entries, locs = uint64(params[0]), uint64(params[1]) + } + } else { + log.Fatal("usage: New(float64(number_of_entries), float64(number_of_hashlocations))" + + " i.e. New(float64(1000), float64(3)) or New(float64(number_of_entries)," + + " float64(number_of_hashlocations)) i.e. New(float64(1000), float64(0.03))") + } + size, exponent := getSize(entries) + bloomfilter = &Bloom{ + sizeExp: exponent, + size: size - 1, + setLocs: locs, + shift: 64 - exponent, + } + bloomfilter.Size(size) + return bloomfilter +} + +// Bloom filter +type Bloom struct { + bitset []uint64 + ElemNum uint64 + sizeExp uint64 + size uint64 + setLocs uint64 + shift uint64 +} + +// <--- http://www.cse.yorku.ca/~oz/hash.html +// modified Berkeley DB Hash (32bit) +// hash is casted to l, h = 16bit fragments +// func (bl Bloom) absdbm(b *[]byte) (l, h uint64) { +// hash := uint64(len(*b)) +// for _, c := range *b { +// hash = uint64(c) + (hash << 6) + (hash << bl.sizeExp) - hash +// } +// h = hash >> bl.shift +// l = hash << bl.shift >> bl.shift +// return l, h +// } + +// Add adds hash of a key to the bloomfilter. +func (bl *Bloom) Add(hash uint64) { + h := hash >> bl.shift + l := hash << bl.shift >> bl.shift + for i := uint64(0); i < bl.setLocs; i++ { + bl.Set((h + i*l) & bl.size) + bl.ElemNum++ + } +} + +// Has checks if bit(s) for entry hash is/are set, +// returns true if the hash was added to the Bloom Filter. +func (bl Bloom) Has(hash uint64) bool { + h := hash >> bl.shift + l := hash << bl.shift >> bl.shift + for i := uint64(0); i < bl.setLocs; i++ { + if !bl.IsSet((h + i*l) & bl.size) { + return false + } + } + return true +} + +// AddIfNotHas only Adds hash, if it's not present in the bloomfilter. +// Returns true if hash was added. +// Returns false if hash was already registered in the bloomfilter. +func (bl *Bloom) AddIfNotHas(hash uint64) bool { + if bl.Has(hash) { + return false + } + bl.Add(hash) + return true +} + +// TotalSize returns the total size of the bloom filter. +func (bl *Bloom) TotalSize() int { + // The bl struct has 5 members and each one is 8 byte. The bitset is a + // uint64 byte slice. + return len(bl.bitset)*8 + 5*8 +} + +// Size makes Bloom filter with as bitset of size sz. +func (bl *Bloom) Size(sz uint64) { + bl.bitset = make([]uint64, sz>>6) +} + +// Clear resets the Bloom filter. +func (bl *Bloom) Clear() { + for i := range bl.bitset { + bl.bitset[i] = 0 + } +} + +// Set sets the bit[idx] of bitset. +func (bl *Bloom) Set(idx uint64) { + ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[idx>>6])) + uintptr((idx%64)>>3)) + *(*uint8)(ptr) |= mask[idx%8] +} + +// IsSet checks if bit[idx] of bitset is set, returns true/false. +func (bl *Bloom) IsSet(idx uint64) bool { + ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[idx>>6])) + uintptr((idx%64)>>3)) + r := ((*(*uint8)(ptr)) >> (idx % 8)) & 1 + return r == 1 +} + +// bloomJSONImExport +// Im/Export structure used by JSONMarshal / JSONUnmarshal +type bloomJSONImExport struct { + FilterSet []byte + SetLocs uint64 +} + +// NewWithBoolset takes a []byte slice and number of locs per entry, +// returns the bloomfilter with a bitset populated according to the input []byte. +func newWithBoolset(bs *[]byte, locs uint64) *Bloom { + bloomfilter := NewBloomFilter(float64(len(*bs)<<3), float64(locs)) + for i, b := range *bs { + *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&bloomfilter.bitset[0])) + uintptr(i))) = b + } + return bloomfilter +} + +// JSONUnmarshal takes JSON-Object (type bloomJSONImExport) as []bytes +// returns bloom32 / bloom64 object. +func JSONUnmarshal(dbData []byte) (*Bloom, error) { + bloomImEx := bloomJSONImExport{} + if err := json.Unmarshal(dbData, &bloomImEx); err != nil { + return nil, err + } + buf := bytes.NewBuffer(bloomImEx.FilterSet) + bs := buf.Bytes() + bf := newWithBoolset(&bs, bloomImEx.SetLocs) + return bf, nil +} + +// JSONMarshal returns JSON-object (type bloomJSONImExport) as []byte. +func (bl Bloom) JSONMarshal() []byte { + bloomImEx := bloomJSONImExport{} + bloomImEx.SetLocs = bl.setLocs + bloomImEx.FilterSet = make([]byte, len(bl.bitset)<<3) + for i := range bloomImEx.FilterSet { + bloomImEx.FilterSet[i] = *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&bl.bitset[0])) + + uintptr(i))) + } + data, err := json.Marshal(bloomImEx) + if err != nil { + log.Fatal("json.Marshal failed: ", err) + } + return data +} diff --git a/go/cache/ristretto/bloom/bbloom_test.go b/go/cache/ristretto/bloom/bbloom_test.go new file mode 100644 index 00000000000..ac0cb9c9104 --- /dev/null +++ b/go/cache/ristretto/bloom/bbloom_test.go @@ -0,0 +1,114 @@ +package bloom + +import ( + "crypto/rand" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "vitess.io/vitess/go/hack" +) + +var ( + wordlist1 [][]byte + n = 1 << 16 + bf *Bloom +) + +func TestMain(m *testing.M) { + wordlist1 = make([][]byte, n) + for i := range wordlist1 { + b := make([]byte, 32) + rand.Read(b) + wordlist1[i] = b + } + fmt.Println("\n###############\nbbloom_test.go") + fmt.Print("Benchmarks relate to 2**16 OP. --> output/65536 op/ns\n###############\n\n") + + os.Exit(m.Run()) +} + +func TestM_NumberOfWrongs(t *testing.T) { + bf = NewBloomFilter(float64(n*10), float64(7)) + + cnt := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf.AddIfNotHas(hash) { + cnt++ + } + } + fmt.Printf("Bloomfilter New(7* 2**16, 7) (-> size=%v bit): \n Check for 'false positives': %v wrong positive 'Has' results on 2**16 entries => %v %%\n", len(bf.bitset)<<6, cnt, float64(cnt)/float64(n)) + +} + +func TestM_JSON(t *testing.T) { + const shallBe = int(1 << 16) + + bf = NewBloomFilter(float64(n*10), float64(7)) + + cnt := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf.AddIfNotHas(hash) { + cnt++ + } + } + + jsonm := bf.JSONMarshal() + + // create new bloomfilter from bloomfilter's JSON representation + bf2, err := JSONUnmarshal(jsonm) + require.NoError(t, err) + + cnt2 := 0 + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + if !bf2.AddIfNotHas(hash) { + cnt2++ + } + } + require.Equal(t, shallBe, cnt2) +} + +func BenchmarkM_New(b *testing.B) { + for r := 0; r < b.N; r++ { + _ = NewBloomFilter(float64(n*10), float64(7)) + } +} + +func BenchmarkM_Clear(b *testing.B) { + bf = NewBloomFilter(float64(n*10), float64(7)) + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Add(hash) + } + b.ResetTimer() + for r := 0; r < b.N; r++ { + bf.Clear() + } +} + +func BenchmarkM_Add(b *testing.B) { + bf = NewBloomFilter(float64(n*10), float64(7)) + b.ResetTimer() + for r := 0; r < b.N; r++ { + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Add(hash) + } + } + +} + +func BenchmarkM_Has(b *testing.B) { + b.ResetTimer() + for r := 0; r < b.N; r++ { + for i := range wordlist1 { + hash := hack.RuntimeMemhash(wordlist1[i], 0) + bf.Has(hash) + } + } +} diff --git a/go/cache/ristretto/cache.go b/go/cache/ristretto/cache.go new file mode 100644 index 00000000000..6238559f605 --- /dev/null +++ b/go/cache/ristretto/cache.go @@ -0,0 +1,694 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ristretto is a fast, fixed size, in-memory cache with a dual focus on +// throughput and hit ratio performance. You can easily add Ristretto to an +// existing system and keep the most valuable data where you need it. +package ristretto + +import ( + "bytes" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + "unsafe" + + "vitess.io/vitess/go/hack" +) + +var ( + // TODO: find the optimal value for this or make it configurable + setBufSize = 32 * 1024 +) + +func defaultStringHash(key string) (uint64, uint64) { + const Seed1 = uint64(0x1122334455667788) + const Seed2 = uint64(0x8877665544332211) + return hack.RuntimeStrhash(key, Seed1), hack.RuntimeStrhash(key, Seed2) +} + +type itemCallback func(*Item) + +const itemSize = int64(unsafe.Sizeof(storeItem{})) + +// Cache is a thread-safe implementation of a hashmap with a TinyLFU admission +// policy and a Sampled LFU eviction policy. You can use the same Cache instance +// from as many goroutines as you want. +type Cache struct { + // store is the central concurrent hashmap where key-value items are stored. + store store + // policy determines what gets let in to the cache and what gets kicked out. + policy policy + // getBuf is a custom ring buffer implementation that gets pushed to when + // keys are read. + getBuf *ringBuffer + // setBuf is a buffer allowing us to batch/drop Sets during times of high + // contention. + setBuf chan *Item + // onEvict is called for item evictions. + onEvict itemCallback + // onReject is called when an item is rejected via admission policy. + onReject itemCallback + // onExit is called whenever a value goes out of scope from the cache. + onExit (func(interface{})) + // KeyToHash function is used to customize the key hashing algorithm. + // Each key will be hashed using the provided function. If keyToHash value + // is not set, the default keyToHash function is used. + keyToHash func(string) (uint64, uint64) + // stop is used to stop the processItems goroutine. + stop chan struct{} + // indicates whether cache is closed. + isClosed bool + // cost calculates cost from a value. + cost func(value interface{}) int64 + // ignoreInternalCost dictates whether to ignore the cost of internally storing + // the item in the cost calculation. + ignoreInternalCost bool + // cleanupTicker is used to periodically check for entries whose TTL has passed. + cleanupTicker *time.Ticker + // Metrics contains a running log of important statistics like hits, misses, + // and dropped items. + Metrics *Metrics +} + +// Config is passed to NewCache for creating new Cache instances. +type Config struct { + // NumCounters determines the number of counters (keys) to keep that hold + // access frequency information. It's generally a good idea to have more + // counters than the max cache capacity, as this will improve eviction + // accuracy and subsequent hit ratios. + // + // For example, if you expect your cache to hold 1,000,000 items when full, + // NumCounters should be 10,000,000 (10x). Each counter takes up 4 bits, so + // keeping 10,000,000 counters would require 5MB of memory. + NumCounters int64 + // MaxCost can be considered as the cache capacity, in whatever units you + // choose to use. + // + // For example, if you want the cache to have a max capacity of 100MB, you + // would set MaxCost to 100,000,000 and pass an item's number of bytes as + // the `cost` parameter for calls to Set. If new items are accepted, the + // eviction process will take care of making room for the new item and not + // overflowing the MaxCost value. + MaxCost int64 + // BufferItems determines the size of Get buffers. + // + // Unless you have a rare use case, using `64` as the BufferItems value + // results in good performance. + BufferItems int64 + // Metrics determines whether cache statistics are kept during the cache's + // lifetime. There *is* some overhead to keeping statistics, so you should + // only set this flag to true when testing or throughput performance isn't a + // major factor. + Metrics bool + // OnEvict is called for every eviction and passes the hashed key, value, + // and cost to the function. + OnEvict func(item *Item) + // OnReject is called for every rejection done via the policy. + OnReject func(item *Item) + // OnExit is called whenever a value is removed from cache. This can be + // used to do manual memory deallocation. Would also be called on eviction + // and rejection of the value. + OnExit func(val interface{}) + // KeyToHash function is used to customize the key hashing algorithm. + // Each key will be hashed using the provided function. If keyToHash value + // is not set, the default keyToHash function is used. + KeyToHash func(string) (uint64, uint64) + // Cost evaluates a value and outputs a corresponding cost. This function + // is ran after Set is called for a new item or an item update with a cost + // param of 0. + Cost func(value interface{}) int64 + // IgnoreInternalCost set to true indicates to the cache that the cost of + // internally storing the value should be ignored. This is useful when the + // cost passed to set is not using bytes as units. Keep in mind that setting + // this to true will increase the memory usage. + IgnoreInternalCost bool +} + +type itemFlag byte + +const ( + itemNew itemFlag = iota + itemDelete + itemUpdate +) + +// Item is passed to setBuf so items can eventually be added to the cache. +type Item struct { + flag itemFlag + Key uint64 + Conflict uint64 + Value interface{} + Cost int64 + Expiration int64 + wg *sync.WaitGroup +} + +// NewCache returns a new Cache instance and any configuration errors, if any. +func NewCache(config *Config) (*Cache, error) { + switch { + case config.NumCounters == 0: + return nil, errors.New("NumCounters can't be zero") + case config.MaxCost == 0: + return nil, errors.New("Capacity can't be zero") + case config.BufferItems == 0: + return nil, errors.New("BufferItems can't be zero") + } + policy := newPolicy(config.NumCounters, config.MaxCost) + cache := &Cache{ + store: newStore(), + policy: policy, + getBuf: newRingBuffer(policy, config.BufferItems), + setBuf: make(chan *Item, setBufSize), + keyToHash: config.KeyToHash, + stop: make(chan struct{}), + cost: config.Cost, + ignoreInternalCost: config.IgnoreInternalCost, + cleanupTicker: time.NewTicker(time.Duration(bucketDurationSecs) * time.Second / 2), + } + cache.onExit = func(val interface{}) { + if config.OnExit != nil && val != nil { + config.OnExit(val) + } + } + cache.onEvict = func(item *Item) { + if config.OnEvict != nil { + config.OnEvict(item) + } + cache.onExit(item.Value) + } + cache.onReject = func(item *Item) { + if config.OnReject != nil { + config.OnReject(item) + } + cache.onExit(item.Value) + } + if cache.keyToHash == nil { + cache.keyToHash = defaultStringHash + } + if config.Metrics { + cache.collectMetrics() + } + // NOTE: benchmarks seem to show that performance decreases the more + // goroutines we have running cache.processItems(), so 1 should + // usually be sufficient + go cache.processItems() + return cache, nil +} + +// Wait blocks until all the current cache operations have been processed in the background +func (c *Cache) Wait() { + if c == nil || c.isClosed { + return + } + wg := &sync.WaitGroup{} + wg.Add(1) + c.setBuf <- &Item{wg: wg} + wg.Wait() +} + +// Get returns the value (if any) and a boolean representing whether the +// value was found or not. The value can be nil and the boolean can be true at +// the same time. +func (c *Cache) Get(key string) (interface{}, bool) { + if c == nil || c.isClosed { + return nil, false + } + keyHash, conflictHash := c.keyToHash(key) + c.getBuf.Push(keyHash) + value, ok := c.store.Get(keyHash, conflictHash) + if ok { + c.Metrics.add(hit, keyHash, 1) + } else { + c.Metrics.add(miss, keyHash, 1) + } + return value, ok +} + +// Set attempts to add the key-value item to the cache. If it returns false, +// then the Set was dropped and the key-value item isn't added to the cache. If +// it returns true, there's still a chance it could be dropped by the policy if +// its determined that the key-value item isn't worth keeping, but otherwise the +// item will be added and other items will be evicted in order to make room. +// +// To dynamically evaluate the items cost using the Config.Coster function, set +// the cost parameter to 0 and Coster will be ran when needed in order to find +// the items true cost. +func (c *Cache) Set(key string, value interface{}, cost int64) bool { + return c.SetWithTTL(key, value, cost, 0*time.Second) +} + +// SetWithTTL works like Set but adds a key-value pair to the cache that will expire +// after the specified TTL (time to live) has passed. A zero value means the value never +// expires, which is identical to calling Set. A negative value is a no-op and the value +// is discarded. +func (c *Cache) SetWithTTL(key string, value interface{}, cost int64, ttl time.Duration) bool { + if c == nil || c.isClosed { + return false + } + + var expiration int64 + switch { + case ttl == 0: + // No expiration. + break + case ttl < 0: + // Treat this a a no-op. + return false + default: + expiration = time.Now().Add(ttl).Unix() + } + + keyHash, conflictHash := c.keyToHash(key) + i := &Item{ + flag: itemNew, + Key: keyHash, + Conflict: conflictHash, + Value: value, + Cost: cost, + Expiration: expiration, + } + // cost is eventually updated. The expiration must also be immediately updated + // to prevent items from being prematurely removed from the map. + if prev, ok := c.store.Update(i); ok { + c.onExit(prev) + i.flag = itemUpdate + } + // Attempt to send item to policy. + select { + case c.setBuf <- i: + return true + default: + if i.flag == itemUpdate { + // Return true if this was an update operation since we've already + // updated the store. For all the other operations (set/delete), we + // return false which means the item was not inserted. + return true + } + c.Metrics.add(dropSets, keyHash, 1) + return false + } +} + +// Delete deletes the key-value item from the cache if it exists. +func (c *Cache) Delete(key string) { + if c == nil || c.isClosed { + return + } + keyHash, conflictHash := c.keyToHash(key) + // Delete immediately. + _, prev := c.store.Del(keyHash, conflictHash) + c.onExit(prev) + // If we've set an item, it would be applied slightly later. + // So we must push the same item to `setBuf` with the deletion flag. + // This ensures that if a set is followed by a delete, it will be + // applied in the correct order. + c.setBuf <- &Item{ + flag: itemDelete, + Key: keyHash, + Conflict: conflictHash, + } +} + +// Close stops all goroutines and closes all channels. +func (c *Cache) Close() { + if c == nil || c.isClosed { + return + } + c.Clear() + + // Block until processItems goroutine is returned. + c.stop <- struct{}{} + close(c.stop) + close(c.setBuf) + c.policy.Close() + c.isClosed = true +} + +// Clear empties the hashmap and zeroes all policy counters. Note that this is +// not an atomic operation (but that shouldn't be a problem as it's assumed that +// Set/Get calls won't be occurring until after this). +func (c *Cache) Clear() { + if c == nil || c.isClosed { + return + } + // Block until processItems goroutine is returned. + c.stop <- struct{}{} + + // Clear out the setBuf channel. +loop: + for { + select { + case i := <-c.setBuf: + if i.flag != itemUpdate { + // In itemUpdate, the value is already set in the store. So, no need to call + // onEvict here. + c.onEvict(i) + } + default: + break loop + } + } + + // Clear value hashmap and policy data. + c.policy.Clear() + c.store.Clear(c.onEvict) + // Only reset metrics if they're enabled. + if c.Metrics != nil { + c.Metrics.Clear() + } + // Restart processItems goroutine. + go c.processItems() +} + +// Len returns the size of the cache (in entries) +func (c *Cache) Len() int { + if c == nil { + return 0 + } + return c.store.Len() +} + +// UsedCapacity returns the size of the cache (in bytes) +func (c *Cache) UsedCapacity() int64 { + if c == nil { + return 0 + } + return c.policy.Used() +} + +// MaxCapacity returns the max cost of the cache (in bytes) +func (c *Cache) MaxCapacity() int64 { + if c == nil { + return 0 + } + return c.policy.MaxCost() +} + +// SetCapacity updates the maxCost of an existing cache. +func (c *Cache) SetCapacity(maxCost int64) { + if c == nil { + return + } + c.policy.UpdateMaxCost(maxCost) +} + +// Evictions returns the number of evictions +func (c *Cache) Evictions() int64 { + // TODO + return 0 +} + +// ForEach yields all the values currently stored in the cache to the given callback. +// The callback may return `false` to stop the iteration early. +func (c *Cache) ForEach(forEach func(interface{}) bool) { + if c == nil { + return + } + c.store.ForEach(forEach) +} + +// processItems is ran by goroutines processing the Set buffer. +func (c *Cache) processItems() { + startTs := make(map[uint64]time.Time) + numToKeep := 100000 // TODO: Make this configurable via options. + + trackAdmission := func(key uint64) { + if c.Metrics == nil { + return + } + startTs[key] = time.Now() + if len(startTs) > numToKeep { + for k := range startTs { + if len(startTs) <= numToKeep { + break + } + delete(startTs, k) + } + } + } + onEvict := func(i *Item) { + delete(startTs, i.Key) + if c.onEvict != nil { + c.onEvict(i) + } + } + + for { + select { + case i := <-c.setBuf: + if i.wg != nil { + i.wg.Done() + continue + } + // Calculate item cost value if new or update. + if i.Cost == 0 && c.cost != nil && i.flag != itemDelete { + i.Cost = c.cost(i.Value) + } + if !c.ignoreInternalCost { + // Add the cost of internally storing the object. + i.Cost += itemSize + } + + switch i.flag { + case itemNew: + victims, added := c.policy.Add(i.Key, i.Cost) + if added { + c.store.Set(i) + c.Metrics.add(keyAdd, i.Key, 1) + trackAdmission(i.Key) + } else { + c.onReject(i) + } + for _, victim := range victims { + victim.Conflict, victim.Value = c.store.Del(victim.Key, 0) + onEvict(victim) + } + + case itemUpdate: + c.policy.Update(i.Key, i.Cost) + + case itemDelete: + c.policy.Del(i.Key) // Deals with metrics updates. + _, val := c.store.Del(i.Key, i.Conflict) + c.onExit(val) + } + case <-c.cleanupTicker.C: + c.store.Cleanup(c.policy, onEvict) + case <-c.stop: + return + } + } +} + +// collectMetrics just creates a new *Metrics instance and adds the pointers +// to the cache and policy instances. +func (c *Cache) collectMetrics() { + c.Metrics = newMetrics() + c.policy.CollectMetrics(c.Metrics) +} + +type metricType int + +const ( + // The following 2 keep track of hits and misses. + hit = iota + miss + // The following 3 keep track of number of keys added, updated and evicted. + keyAdd + keyUpdate + keyEvict + // The following 2 keep track of cost of keys added and evicted. + costAdd + costEvict + // The following keep track of how many sets were dropped or rejected later. + dropSets + rejectSets + // The following 2 keep track of how many gets were kept and dropped on the + // floor. + dropGets + keepGets + // This should be the final enum. Other enums should be set before this. + doNotUse +) + +func stringFor(t metricType) string { + switch t { + case hit: + return "hit" + case miss: + return "miss" + case keyAdd: + return "keys-added" + case keyUpdate: + return "keys-updated" + case keyEvict: + return "keys-evicted" + case costAdd: + return "cost-added" + case costEvict: + return "cost-evicted" + case dropSets: + return "sets-dropped" + case rejectSets: + return "sets-rejected" // by policy. + case dropGets: + return "gets-dropped" + case keepGets: + return "gets-kept" + default: + return "unidentified" + } +} + +// Metrics is a snapshot of performance statistics for the lifetime of a cache instance. +type Metrics struct { + all [doNotUse][]*uint64 +} + +func newMetrics() *Metrics { + s := &Metrics{} + for i := 0; i < doNotUse; i++ { + s.all[i] = make([]*uint64, 256) + slice := s.all[i] + for j := range slice { + slice[j] = new(uint64) + } + } + return s +} + +func (p *Metrics) add(t metricType, hash, delta uint64) { + if p == nil { + return + } + valp := p.all[t] + // Avoid false sharing by padding at least 64 bytes of space between two + // atomic counters which would be incremented. + idx := (hash % 25) * 10 + atomic.AddUint64(valp[idx], delta) +} + +func (p *Metrics) get(t metricType) uint64 { + if p == nil { + return 0 + } + valp := p.all[t] + var total uint64 + for i := range valp { + total += atomic.LoadUint64(valp[i]) + } + return total +} + +// Hits is the number of Get calls where a value was found for the corresponding key. +func (p *Metrics) Hits() uint64 { + return p.get(hit) +} + +// Misses is the number of Get calls where a value was not found for the corresponding key. +func (p *Metrics) Misses() uint64 { + return p.get(miss) +} + +// KeysAdded is the total number of Set calls where a new key-value item was added. +func (p *Metrics) KeysAdded() uint64 { + return p.get(keyAdd) +} + +// KeysUpdated is the total number of Set calls where the value was updated. +func (p *Metrics) KeysUpdated() uint64 { + return p.get(keyUpdate) +} + +// KeysEvicted is the total number of keys evicted. +func (p *Metrics) KeysEvicted() uint64 { + return p.get(keyEvict) +} + +// CostAdded is the sum of costs that have been added (successful Set calls). +func (p *Metrics) CostAdded() uint64 { + return p.get(costAdd) +} + +// CostEvicted is the sum of all costs that have been evicted. +func (p *Metrics) CostEvicted() uint64 { + return p.get(costEvict) +} + +// SetsDropped is the number of Set calls that don't make it into internal +// buffers (due to contention or some other reason). +func (p *Metrics) SetsDropped() uint64 { + return p.get(dropSets) +} + +// SetsRejected is the number of Set calls rejected by the policy (TinyLFU). +func (p *Metrics) SetsRejected() uint64 { + return p.get(rejectSets) +} + +// GetsDropped is the number of Get counter increments that are dropped +// internally. +func (p *Metrics) GetsDropped() uint64 { + return p.get(dropGets) +} + +// GetsKept is the number of Get counter increments that are kept. +func (p *Metrics) GetsKept() uint64 { + return p.get(keepGets) +} + +// Ratio is the number of Hits over all accesses (Hits + Misses). This is the +// percentage of successful Get calls. +func (p *Metrics) Ratio() float64 { + if p == nil { + return 0.0 + } + hits, misses := p.get(hit), p.get(miss) + if hits == 0 && misses == 0 { + return 0.0 + } + return float64(hits) / float64(hits+misses) +} + +// Clear resets all the metrics. +func (p *Metrics) Clear() { + if p == nil { + return + } + for i := 0; i < doNotUse; i++ { + for j := range p.all[i] { + atomic.StoreUint64(p.all[i][j], 0) + } + } +} + +// String returns a string representation of the metrics. +func (p *Metrics) String() string { + if p == nil { + return "" + } + var buf bytes.Buffer + for i := 0; i < doNotUse; i++ { + t := metricType(i) + fmt.Fprintf(&buf, "%s: %d ", stringFor(t), p.get(t)) + } + fmt.Fprintf(&buf, "gets-total: %d ", p.get(hit)+p.get(miss)) + fmt.Fprintf(&buf, "hit-ratio: %.2f", p.Ratio()) + return buf.String() +} diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go new file mode 100644 index 00000000000..35c6fc0e806 --- /dev/null +++ b/go/cache/ristretto/cache_test.go @@ -0,0 +1,786 @@ +package ristretto + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var wait = time.Millisecond * 10 + +func TestCacheKeyToHash(t *testing.T) { + keyToHashCount := 0 + c, err := NewCache(&Config{ + NumCounters: 10, + MaxCost: 1000, + BufferItems: 64, + IgnoreInternalCost: true, + KeyToHash: func(key string) (uint64, uint64) { + keyToHashCount++ + return defaultStringHash(key) + }, + }) + require.NoError(t, err) + if c.Set("1", 1, 1) { + time.Sleep(wait) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + c.Delete("1") + } + require.Equal(t, 3, keyToHashCount) +} + +func TestCacheMaxCost(t *testing.T) { + charset := "abcdefghijklmnopqrstuvwxyz0123456789" + key := func() string { + k := make([]byte, 2) + for i := range k { + k[i] = charset[rand.Intn(len(charset))] + } + return string(k) + } + c, err := NewCache(&Config{ + NumCounters: 12960, // 36^2 * 10 + MaxCost: 1e6, // 1mb + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + stop := make(chan struct{}, 8) + for i := 0; i < 8; i++ { + go func() { + for { + select { + case <-stop: + return + default: + time.Sleep(time.Millisecond) + + k := key() + if _, ok := c.Get(k); !ok { + val := "" + if rand.Intn(100) < 10 { + val = "test" + } else { + val = strings.Repeat("a", 1000) + } + c.Set(key(), val, int64(2+len(val))) + } + } + } + }() + } + for i := 0; i < 20; i++ { + time.Sleep(time.Second) + cacheCost := c.Metrics.CostAdded() - c.Metrics.CostEvicted() + t.Logf("total cache cost: %d\n", cacheCost) + require.True(t, float64(cacheCost) <= float64(1e6*1.05)) + } + for i := 0; i < 8; i++ { + stop <- struct{}{} + } +} + +func TestUpdateMaxCost(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 10, + MaxCost: 10, + BufferItems: 64, + }) + require.NoError(t, err) + require.Equal(t, int64(10), c.MaxCapacity()) + require.True(t, c.Set("1", 1, 1)) + time.Sleep(wait) + _, ok := c.Get("1") + // Set is rejected because the cost of the entry is too high + // when accounting for the internal cost of storing the entry. + require.False(t, ok) + + // Update the max cost of the cache and retry. + c.SetCapacity(1000) + require.Equal(t, int64(1000), c.MaxCapacity()) + require.True(t, c.Set("1", 1, 1)) + time.Sleep(wait) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + c.Delete("1") +} + +func TestNewCache(t *testing.T) { + _, err := NewCache(&Config{ + NumCounters: 0, + }) + require.Error(t, err) + + _, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 0, + }) + require.Error(t, err) + + _, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 0, + }) + require.Error(t, err) + + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + require.NotNil(t, c) +} + +func TestNilCache(t *testing.T) { + var c *Cache + val, ok := c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + require.False(t, c.Set("1", 1, 1)) + c.Delete("1") + c.Clear() + c.Close() +} + +func TestMultipleClose(t *testing.T) { + var c *Cache + c.Close() + + var err error + c, err = NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + c.Close() + c.Close() +} + +func TestSetAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + c.Close() + require.False(t, c.Set("1", 1, 1)) +} + +func TestClearAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + c.Close() + c.Clear() +} + +func TestGetAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + require.True(t, c.Set("1", 1, 1)) + c.Close() + + _, ok := c.Get("2") + require.False(t, ok) +} + +func TestDelAfterClose(t *testing.T) { + c, err := newTestCache() + require.NoError(t, err) + require.NotNil(t, c) + + require.True(t, c.Set("1", 1, 1)) + c.Close() + + c.Delete("1") +} + +func TestCacheProcessItems(t *testing.T) { + m := &sync.Mutex{} + evicted := make(map[uint64]struct{}) + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + IgnoreInternalCost: true, + Cost: func(value interface{}) int64 { + return int64(value.(int)) + }, + OnEvict: func(item *Item) { + m.Lock() + defer m.Unlock() + evicted[item.Key] = struct{}{} + }, + }) + require.NoError(t, err) + + var key uint64 + var conflict uint64 + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 1, + Cost: 0, + } + time.Sleep(wait) + require.True(t, c.policy.Has(key)) + require.Equal(t, int64(1), c.policy.Cost(key)) + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemUpdate, + Key: key, + Conflict: conflict, + Value: 2, + Cost: 0, + } + time.Sleep(wait) + require.Equal(t, int64(2), c.policy.Cost(key)) + + key, conflict = defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemDelete, + Key: key, + Conflict: conflict, + } + time.Sleep(wait) + key, conflict = defaultStringHash("1") + val, ok := c.store.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + require.False(t, c.policy.Has(1)) + + key, conflict = defaultStringHash("2") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 2, + Cost: 3, + } + key, conflict = defaultStringHash("3") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 3, + } + key, conflict = defaultStringHash("4") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 3, + } + key, conflict = defaultStringHash("5") + c.setBuf <- &Item{ + flag: itemNew, + Key: key, + Conflict: conflict, + Value: 3, + Cost: 5, + } + time.Sleep(wait) + m.Lock() + require.NotEqual(t, 0, len(evicted)) + m.Unlock() + + defer func() { + require.NotNil(t, recover()) + }() + c.Close() + c.setBuf <- &Item{flag: itemNew} +} + +func TestCacheGet(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + IgnoreInternalCost: true, + Metrics: true, + }) + require.NoError(t, err) + + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + c.store.Set(&i) + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + + val, ok = c.Get("2") + require.False(t, ok) + require.Nil(t, val) + + // 0.5 and not 1.0 because we tried Getting each item twice + require.Equal(t, 0.5, c.Metrics.Ratio()) + + c = nil + val, ok = c.Get("0") + require.False(t, ok) + require.Nil(t, val) +} + +// retrySet calls SetWithTTL until the item is accepted by the cache. +func retrySet(t *testing.T, c *Cache, key string, value int, cost int64, ttl time.Duration) { + for { + if set := c.SetWithTTL(key, value, cost, ttl); !set { + time.Sleep(wait) + continue + } + + time.Sleep(wait) + val, ok := c.Get(key) + require.True(t, ok) + require.NotNil(t, val) + require.Equal(t, value, val.(int)) + return + } +} + +func TestCacheSet(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + retrySet(t, c, "1", 1, 1, 0) + + c.Set("1", 2, 2) + val, ok := c.store.Get(defaultStringHash("1")) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + c.stop <- struct{}{} + for i := 0; i < setBufSize; i++ { + key, conflict := defaultStringHash("1") + c.setBuf <- &Item{ + flag: itemUpdate, + Key: key, + Conflict: conflict, + Value: 1, + Cost: 1, + } + } + require.False(t, c.Set("2", 2, 1)) + require.Equal(t, uint64(1), c.Metrics.SetsDropped()) + close(c.setBuf) + close(c.stop) + + c = nil + require.False(t, c.Set("1", 1, 1)) +} + +func TestCacheInternalCost(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + // Get should return false because the cache's cost is too small to store the item + // when accounting for the internal cost. + c.SetWithTTL("1", 1, 1, 0) + time.Sleep(wait) + _, ok := c.Get("1") + require.False(t, ok) +} + +func TestRecacheWithTTL(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + + require.NoError(t, err) + + // Set initial value for key = 1 + insert := c.SetWithTTL("1", 1, 1, 5*time.Second) + require.True(t, insert) + time.Sleep(2 * time.Second) + + // Get value from cache for key = 1 + val, ok := c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + require.Equal(t, 1, val) + + // Wait for expiration + time.Sleep(5 * time.Second) + + // The cached value for key = 1 should be gone + val, ok = c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + // Set new value for key = 1 + insert = c.SetWithTTL("1", 2, 1, 5*time.Second) + require.True(t, insert) + time.Sleep(2 * time.Second) + + // Get value from cache for key = 1 + val, ok = c.Get("1") + require.True(t, ok) + require.NotNil(t, val) + require.Equal(t, 2, val) +} + +func TestCacheSetWithTTL(t *testing.T) { + m := &sync.Mutex{} + evicted := make(map[uint64]struct{}) + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + OnEvict: func(item *Item) { + m.Lock() + defer m.Unlock() + evicted[item.Key] = struct{}{} + }, + }) + require.NoError(t, err) + + retrySet(t, c, "1", 1, 1, time.Second) + + // Sleep to make sure the item has expired after execution resumes. + time.Sleep(2 * time.Second) + val, ok := c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + // Sleep to ensure that the bucket where the item was stored has been cleared + // from the expiraton map. + time.Sleep(5 * time.Second) + m.Lock() + require.Equal(t, 1, len(evicted)) + evk, _ := defaultStringHash("1") + _, ok = evicted[evk] + require.True(t, ok) + m.Unlock() + + // Verify that expiration times are overwritten. + retrySet(t, c, "2", 1, 1, time.Second) + retrySet(t, c, "2", 2, 1, 100*time.Second) + time.Sleep(3 * time.Second) + val, ok = c.Get("2") + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + // Verify that entries with no expiration are overwritten. + retrySet(t, c, "3", 1, 1, 0) + retrySet(t, c, "3", 2, 1, time.Second) + time.Sleep(3 * time.Second) + val, ok = c.Get("3") + require.False(t, ok) + require.Nil(t, val) +} + +func TestCacheDel(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + }) + require.NoError(t, err) + + c.Set("1", 1, 1) + c.Delete("1") + // The deletes and sets are pushed through the setbuf. It might be possible + // that the delete is not processed before the following get is called. So + // wait for a millisecond for things to be processed. + time.Sleep(time.Millisecond) + val, ok := c.Get("1") + require.False(t, ok) + require.Nil(t, val) + + c = nil + defer func() { + require.Nil(t, recover()) + }() + c.Delete("1") +} + +func TestCacheDelWithTTL(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + }) + require.NoError(t, err) + retrySet(t, c, "3", 1, 1, 10*time.Second) + time.Sleep(1 * time.Second) + // Delete the item + c.Delete("3") + // Ensure the key is deleted. + val, ok := c.Get("3") + require.False(t, ok) + require.Nil(t, val) +} + +func TestCacheClear(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + for i := 0; i < 10; i++ { + c.Set(strconv.Itoa(i), i, 1) + } + time.Sleep(wait) + require.Equal(t, uint64(10), c.Metrics.KeysAdded()) + + c.Clear() + require.Equal(t, uint64(0), c.Metrics.KeysAdded()) + + for i := 0; i < 10; i++ { + val, ok := c.Get(strconv.Itoa(i)) + require.False(t, ok) + require.Nil(t, val) + } +} + +func TestCacheMetrics(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + IgnoreInternalCost: true, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + for i := 0; i < 10; i++ { + c.Set(strconv.Itoa(i), i, 1) + } + time.Sleep(wait) + m := c.Metrics + require.Equal(t, uint64(10), m.KeysAdded()) +} + +func TestMetrics(t *testing.T) { + newMetrics() +} + +func TestNilMetrics(t *testing.T) { + var m *Metrics + for _, f := range []func() uint64{ + m.Hits, + m.Misses, + m.KeysAdded, + m.KeysEvicted, + m.CostEvicted, + m.SetsDropped, + m.SetsRejected, + m.GetsDropped, + m.GetsKept, + } { + require.Equal(t, uint64(0), f()) + } +} + +func TestMetricsAddGet(t *testing.T) { + m := newMetrics() + m.add(hit, 1, 1) + m.add(hit, 2, 2) + m.add(hit, 3, 3) + require.Equal(t, uint64(6), m.Hits()) + + m = nil + m.add(hit, 1, 1) + require.Equal(t, uint64(0), m.Hits()) +} + +func TestMetricsRatio(t *testing.T) { + m := newMetrics() + require.Equal(t, float64(0), m.Ratio()) + + m.add(hit, 1, 1) + m.add(hit, 2, 2) + m.add(miss, 1, 1) + m.add(miss, 2, 2) + require.Equal(t, 0.5, m.Ratio()) + + m = nil + require.Equal(t, float64(0), m.Ratio()) +} + +func TestMetricsString(t *testing.T) { + m := newMetrics() + m.add(hit, 1, 1) + m.add(miss, 1, 1) + m.add(keyAdd, 1, 1) + m.add(keyUpdate, 1, 1) + m.add(keyEvict, 1, 1) + m.add(costAdd, 1, 1) + m.add(costEvict, 1, 1) + m.add(dropSets, 1, 1) + m.add(rejectSets, 1, 1) + m.add(dropGets, 1, 1) + m.add(keepGets, 1, 1) + require.Equal(t, uint64(1), m.Hits()) + require.Equal(t, uint64(1), m.Misses()) + require.Equal(t, 0.5, m.Ratio()) + require.Equal(t, uint64(1), m.KeysAdded()) + require.Equal(t, uint64(1), m.KeysUpdated()) + require.Equal(t, uint64(1), m.KeysEvicted()) + require.Equal(t, uint64(1), m.CostAdded()) + require.Equal(t, uint64(1), m.CostEvicted()) + require.Equal(t, uint64(1), m.SetsDropped()) + require.Equal(t, uint64(1), m.SetsRejected()) + require.Equal(t, uint64(1), m.GetsDropped()) + require.Equal(t, uint64(1), m.GetsKept()) + + require.NotEqual(t, 0, len(m.String())) + + m = nil + require.Equal(t, 0, len(m.String())) + + require.Equal(t, "unidentified", stringFor(doNotUse)) +} + +func TestCacheMetricsClear(t *testing.T) { + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) + require.NoError(t, err) + + c.Set("1", 1, 1) + stop := make(chan struct{}) + go func() { + for { + select { + case <-stop: + return + default: + c.Get("1") + } + } + }() + time.Sleep(wait) + c.Clear() + stop <- struct{}{} + c.Metrics = nil + c.Metrics.Clear() +} + +func init() { + // Set bucketSizeSecs to 1 to avoid waiting too much during the tests. + bucketDurationSecs = 1 +} + +// Regression test for bug https://github.com/dgraph-io/ristretto/issues/167 +func TestDropUpdates(t *testing.T) { + originalSetBugSize := setBufSize + defer func() { setBufSize = originalSetBugSize }() + + test := func() { + // dropppedMap stores the items dropped from the cache. + droppedMap := make(map[int]struct{}) + lastEvictedSet := int64(-1) + + var err error + handler := func(_ interface{}, value interface{}) { + v := value.(string) + lastEvictedSet, err = strconv.ParseInt(string(v), 10, 32) + require.NoError(t, err) + + _, ok := droppedMap[int(lastEvictedSet)] + if ok { + panic(fmt.Sprintf("val = %+v was dropped but it got evicted. Dropped items: %+v\n", + lastEvictedSet, droppedMap)) + } + } + + // This is important. The race condition shows up only when the setBuf + // is full and that's why we reduce the buf size here. The test will + // try to fill up the setbuf to it's capacity and then perform an + // update on a key. + setBufSize = 10 + + c, err := NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + OnEvict: func(item *Item) { + handler(nil, item.Value) + }, + }) + require.NoError(t, err) + + for i := 0; i < 5*setBufSize; i++ { + v := fmt.Sprintf("%0100d", i) + // We're updating the same key. + if !c.Set("0", v, 1) { + // The race condition doesn't show up without this sleep. + time.Sleep(time.Microsecond) + droppedMap[i] = struct{}{} + } + } + // Wait for all the items to be processed. + time.Sleep(time.Millisecond) + // This will cause eviction from the cache. + require.True(t, c.Set("1", nil, 10)) + c.Close() + } + + // Run the test 100 times since it's not reliable. + for i := 0; i < 100; i++ { + test() + } +} + +func newTestCache() (*Cache, error) { + return NewCache(&Config{ + NumCounters: 100, + MaxCost: 10, + BufferItems: 64, + Metrics: true, + }) +} diff --git a/go/cache/ristretto/policy.go b/go/cache/ristretto/policy.go new file mode 100644 index 00000000000..aa3c0b8c26b --- /dev/null +++ b/go/cache/ristretto/policy.go @@ -0,0 +1,417 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "math" + "sync" + "sync/atomic" + + "vitess.io/vitess/go/cache/ristretto/bloom" +) + +const ( + // lfuSample is the number of items to sample when looking at eviction + // candidates. 5 seems to be the most optimal number [citation needed]. + lfuSample = 5 +) + +// policy is the interface encapsulating eviction/admission behavior. +// +// TODO: remove this interface and just rename defaultPolicy to policy, as we +// are probably only going to use/implement/maintain one policy. +type policy interface { + ringConsumer + // Add attempts to Add the key-cost pair to the Policy. It returns a slice + // of evicted keys and a bool denoting whether or not the key-cost pair + // was added. If it returns true, the key should be stored in cache. + Add(uint64, int64) ([]*Item, bool) + // Has returns true if the key exists in the Policy. + Has(uint64) bool + // Del deletes the key from the Policy. + Del(uint64) + // Cap returns the amount of used capacity. + Used() int64 + // Close stops all goroutines and closes all channels. + Close() + // Update updates the cost value for the key. + Update(uint64, int64) + // Cost returns the cost value of a key or -1 if missing. + Cost(uint64) int64 + // Optionally, set stats object to track how policy is performing. + CollectMetrics(*Metrics) + // Clear zeroes out all counters and clears hashmaps. + Clear() + // MaxCost returns the current max cost of the cache policy. + MaxCost() int64 + // UpdateMaxCost updates the max cost of the cache policy. + UpdateMaxCost(int64) +} + +func newPolicy(numCounters, maxCost int64) policy { + return newDefaultPolicy(numCounters, maxCost) +} + +type defaultPolicy struct { + sync.Mutex + admit *tinyLFU + evict *sampledLFU + itemsCh chan []uint64 + stop chan struct{} + isClosed bool + metrics *Metrics +} + +func newDefaultPolicy(numCounters, maxCost int64) *defaultPolicy { + p := &defaultPolicy{ + admit: newTinyLFU(numCounters), + evict: newSampledLFU(maxCost), + itemsCh: make(chan []uint64, 3), + stop: make(chan struct{}), + } + go p.processItems() + return p +} + +func (p *defaultPolicy) CollectMetrics(metrics *Metrics) { + p.metrics = metrics + p.evict.metrics = metrics +} + +type policyPair struct { + key uint64 + cost int64 +} + +func (p *defaultPolicy) processItems() { + for { + select { + case items := <-p.itemsCh: + p.Lock() + p.admit.Push(items) + p.Unlock() + case <-p.stop: + return + } + } +} + +func (p *defaultPolicy) Push(keys []uint64) bool { + if p.isClosed { + return false + } + + if len(keys) == 0 { + return true + } + + select { + case p.itemsCh <- keys: + p.metrics.add(keepGets, keys[0], uint64(len(keys))) + return true + default: + p.metrics.add(dropGets, keys[0], uint64(len(keys))) + return false + } +} + +// Add decides whether the item with the given key and cost should be accepted by +// the policy. It returns the list of victims that have been evicted and a boolean +// indicating whether the incoming item should be accepted. +func (p *defaultPolicy) Add(key uint64, cost int64) ([]*Item, bool) { + p.Lock() + defer p.Unlock() + + // Cannot add an item bigger than entire cache. + if cost > p.evict.getMaxCost() { + return nil, false + } + + // No need to go any further if the item is already in the cache. + if has := p.evict.updateIfHas(key, cost); has { + // An update does not count as an addition, so return false. + return nil, false + } + + // If the execution reaches this point, the key doesn't exist in the cache. + // Calculate the remaining room in the cache (usually bytes). + room := p.evict.roomLeft(cost) + if room >= 0 { + // There's enough room in the cache to store the new item without + // overflowing. Do that now and stop here. + p.evict.add(key, cost) + p.metrics.add(costAdd, key, uint64(cost)) + return nil, true + } + + // incHits is the hit count for the incoming item. + incHits := p.admit.Estimate(key) + // sample is the eviction candidate pool to be filled via random sampling. + // TODO: perhaps we should use a min heap here. Right now our time + // complexity is N for finding the min. Min heap should bring it down to + // O(lg N). + sample := make([]*policyPair, 0, lfuSample) + // As items are evicted they will be appended to victims. + victims := make([]*Item, 0) + + // Delete victims until there's enough space or a minKey is found that has + // more hits than incoming item. + for ; room < 0; room = p.evict.roomLeft(cost) { + // Fill up empty slots in sample. + sample = p.evict.fillSample(sample) + + // Find minimally used item in sample. + minKey, minHits, minID, minCost := uint64(0), int64(math.MaxInt64), 0, int64(0) + for i, pair := range sample { + // Look up hit count for sample key. + if hits := p.admit.Estimate(pair.key); hits < minHits { + minKey, minHits, minID, minCost = pair.key, hits, i, pair.cost + } + } + + // If the incoming item isn't worth keeping in the policy, reject. + if incHits < minHits { + p.metrics.add(rejectSets, key, 1) + return victims, false + } + + // Delete the victim from metadata. + p.evict.del(minKey) + + // Delete the victim from sample. + sample[minID] = sample[len(sample)-1] + sample = sample[:len(sample)-1] + // Store victim in evicted victims slice. + victims = append(victims, &Item{ + Key: minKey, + Conflict: 0, + Cost: minCost, + }) + } + + p.evict.add(key, cost) + p.metrics.add(costAdd, key, uint64(cost)) + return victims, true +} + +func (p *defaultPolicy) Has(key uint64) bool { + p.Lock() + _, exists := p.evict.keyCosts[key] + p.Unlock() + return exists +} + +func (p *defaultPolicy) Del(key uint64) { + p.Lock() + p.evict.del(key) + p.Unlock() +} + +func (p *defaultPolicy) Used() int64 { + p.Lock() + used := p.evict.used + p.Unlock() + return used +} + +func (p *defaultPolicy) Update(key uint64, cost int64) { + p.Lock() + p.evict.updateIfHas(key, cost) + p.Unlock() +} + +func (p *defaultPolicy) Cost(key uint64) int64 { + p.Lock() + if cost, found := p.evict.keyCosts[key]; found { + p.Unlock() + return cost + } + p.Unlock() + return -1 +} + +func (p *defaultPolicy) Clear() { + p.Lock() + p.admit.clear() + p.evict.clear() + p.Unlock() +} + +func (p *defaultPolicy) Close() { + if p.isClosed { + return + } + + // Block until the p.processItems goroutine returns. + p.stop <- struct{}{} + close(p.stop) + close(p.itemsCh) + p.isClosed = true +} + +func (p *defaultPolicy) MaxCost() int64 { + if p == nil || p.evict == nil { + return 0 + } + return p.evict.getMaxCost() +} + +func (p *defaultPolicy) UpdateMaxCost(maxCost int64) { + if p == nil || p.evict == nil { + return + } + p.evict.updateMaxCost(maxCost) +} + +// sampledLFU is an eviction helper storing key-cost pairs. +type sampledLFU struct { + keyCosts map[uint64]int64 + maxCost int64 + used int64 + metrics *Metrics +} + +func newSampledLFU(maxCost int64) *sampledLFU { + return &sampledLFU{ + keyCosts: make(map[uint64]int64), + maxCost: maxCost, + } +} + +func (p *sampledLFU) getMaxCost() int64 { + return atomic.LoadInt64(&p.maxCost) +} + +func (p *sampledLFU) updateMaxCost(maxCost int64) { + atomic.StoreInt64(&p.maxCost, maxCost) +} + +func (p *sampledLFU) roomLeft(cost int64) int64 { + return p.getMaxCost() - (p.used + cost) +} + +func (p *sampledLFU) fillSample(in []*policyPair) []*policyPair { + if len(in) >= lfuSample { + return in + } + for key, cost := range p.keyCosts { + in = append(in, &policyPair{key, cost}) + if len(in) >= lfuSample { + return in + } + } + return in +} + +func (p *sampledLFU) del(key uint64) { + cost, ok := p.keyCosts[key] + if !ok { + return + } + p.used -= cost + delete(p.keyCosts, key) + p.metrics.add(costEvict, key, uint64(cost)) + p.metrics.add(keyEvict, key, 1) +} + +func (p *sampledLFU) add(key uint64, cost int64) { + p.keyCosts[key] = cost + p.used += cost +} + +func (p *sampledLFU) updateIfHas(key uint64, cost int64) bool { + if prev, found := p.keyCosts[key]; found { + // Update the cost of an existing key, but don't worry about evicting. + // Evictions will be handled the next time a new item is added. + p.metrics.add(keyUpdate, key, 1) + if prev > cost { + diff := prev - cost + p.metrics.add(costAdd, key, ^uint64(uint64(diff)-1)) + } else if cost > prev { + diff := cost - prev + p.metrics.add(costAdd, key, uint64(diff)) + } + p.used += cost - prev + p.keyCosts[key] = cost + return true + } + return false +} + +func (p *sampledLFU) clear() { + p.used = 0 + p.keyCosts = make(map[uint64]int64) +} + +// tinyLFU is an admission helper that keeps track of access frequency using +// tiny (4-bit) counters in the form of a count-min sketch. +// tinyLFU is NOT thread safe. +type tinyLFU struct { + freq *cmSketch + door *bloom.Bloom + incrs int64 + resetAt int64 +} + +func newTinyLFU(numCounters int64) *tinyLFU { + return &tinyLFU{ + freq: newCmSketch(numCounters), + door: bloom.NewBloomFilter(float64(numCounters), 0.01), + resetAt: numCounters, + } +} + +func (p *tinyLFU) Push(keys []uint64) { + for _, key := range keys { + p.Increment(key) + } +} + +func (p *tinyLFU) Estimate(key uint64) int64 { + hits := p.freq.Estimate(key) + if p.door.Has(key) { + hits++ + } + return hits +} + +func (p *tinyLFU) Increment(key uint64) { + // Flip doorkeeper bit if not already done. + if added := p.door.AddIfNotHas(key); !added { + // Increment count-min counter if doorkeeper bit is already set. + p.freq.Increment(key) + } + p.incrs++ + if p.incrs >= p.resetAt { + p.reset() + } +} + +func (p *tinyLFU) reset() { + // Zero out incrs. + p.incrs = 0 + // clears doorkeeper bits + p.door.Clear() + // halves count-min counters + p.freq.Reset() +} + +func (p *tinyLFU) clear() { + p.incrs = 0 + p.door.Clear() + p.freq.Clear() +} diff --git a/go/cache/ristretto/policy_test.go b/go/cache/ristretto/policy_test.go new file mode 100644 index 00000000000..5e5df1ac1af --- /dev/null +++ b/go/cache/ristretto/policy_test.go @@ -0,0 +1,259 @@ +package ristretto + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestPolicy(t *testing.T) { + defer func() { + require.Nil(t, recover()) + }() + newPolicy(100, 10) +} + +func TestPolicyMetrics(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.CollectMetrics(newMetrics()) + require.NotNil(t, p.metrics) + require.NotNil(t, p.evict.metrics) +} + +func TestPolicyProcessItems(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.itemsCh <- []uint64{1, 2, 2} + time.Sleep(wait) + p.Lock() + require.Equal(t, int64(2), p.admit.Estimate(2)) + require.Equal(t, int64(1), p.admit.Estimate(1)) + p.Unlock() + + p.stop <- struct{}{} + p.itemsCh <- []uint64{3, 3, 3} + time.Sleep(wait) + p.Lock() + require.Equal(t, int64(0), p.admit.Estimate(3)) + p.Unlock() +} + +func TestPolicyPush(t *testing.T) { + p := newDefaultPolicy(100, 10) + require.True(t, p.Push([]uint64{})) + + keepCount := 0 + for i := 0; i < 10; i++ { + if p.Push([]uint64{1, 2, 3, 4, 5}) { + keepCount++ + } + } + require.NotEqual(t, 0, keepCount) +} + +func TestPolicyAdd(t *testing.T) { + p := newDefaultPolicy(1000, 100) + if victims, added := p.Add(1, 101); victims != nil || added { + t.Fatal("can't add an item bigger than entire cache") + } + p.Lock() + p.evict.add(1, 1) + p.admit.Increment(1) + p.admit.Increment(2) + p.admit.Increment(3) + p.Unlock() + + victims, added := p.Add(1, 1) + require.Nil(t, victims) + require.False(t, added) + + victims, added = p.Add(2, 20) + require.Nil(t, victims) + require.True(t, added) + + victims, added = p.Add(3, 90) + require.NotNil(t, victims) + require.True(t, added) + + victims, added = p.Add(4, 20) + require.NotNil(t, victims) + require.False(t, added) +} + +func TestPolicyHas(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + require.True(t, p.Has(1)) + require.False(t, p.Has(2)) +} + +func TestPolicyDel(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Del(1) + p.Del(2) + require.False(t, p.Has(1)) + require.False(t, p.Has(2)) +} + +func TestPolicyCap(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + require.Equal(t, int64(9), p.MaxCost()-p.Used()) +} + +func TestPolicyUpdate(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Update(1, 2) + p.Lock() + require.Equal(t, int64(2), p.evict.keyCosts[1]) + p.Unlock() +} + +func TestPolicyCost(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 2) + require.Equal(t, int64(2), p.Cost(1)) + require.Equal(t, int64(-1), p.Cost(2)) +} + +func TestPolicyClear(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Add(2, 2) + p.Add(3, 3) + p.Clear() + require.Equal(t, int64(10), p.MaxCost()-p.Used()) + require.False(t, p.Has(1)) + require.False(t, p.Has(2)) + require.False(t, p.Has(3)) +} + +func TestPolicyClose(t *testing.T) { + defer func() { + require.NotNil(t, recover()) + }() + + p := newDefaultPolicy(100, 10) + p.Add(1, 1) + p.Close() + p.itemsCh <- []uint64{1} +} + +func TestPushAfterClose(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Close() + require.False(t, p.Push([]uint64{1, 2})) +} + +func TestAddAfterClose(t *testing.T) { + p := newDefaultPolicy(100, 10) + p.Close() + p.Add(1, 1) +} + +func TestSampledLFUAdd(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.add(3, 1) + require.Equal(t, int64(4), e.used) + require.Equal(t, int64(2), e.keyCosts[2]) +} + +func TestSampledLFUDel(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.del(2) + require.Equal(t, int64(1), e.used) + _, ok := e.keyCosts[2] + require.False(t, ok) + e.del(4) +} + +func TestSampledLFUUpdate(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + require.True(t, e.updateIfHas(1, 2)) + require.Equal(t, int64(2), e.used) + require.False(t, e.updateIfHas(2, 2)) +} + +func TestSampledLFUClear(t *testing.T) { + e := newSampledLFU(4) + e.add(1, 1) + e.add(2, 2) + e.add(3, 1) + e.clear() + require.Equal(t, 0, len(e.keyCosts)) + require.Equal(t, int64(0), e.used) +} + +func TestSampledLFURoom(t *testing.T) { + e := newSampledLFU(16) + e.add(1, 1) + e.add(2, 2) + e.add(3, 3) + require.Equal(t, int64(6), e.roomLeft(4)) +} + +func TestSampledLFUSample(t *testing.T) { + e := newSampledLFU(16) + e.add(4, 4) + e.add(5, 5) + sample := e.fillSample([]*policyPair{ + {1, 1}, + {2, 2}, + {3, 3}, + }) + k := sample[len(sample)-1].key + require.Equal(t, 5, len(sample)) + require.NotEqual(t, 1, k) + require.NotEqual(t, 2, k) + require.NotEqual(t, 3, k) + require.Equal(t, len(sample), len(e.fillSample(sample))) + e.del(5) + sample = e.fillSample(sample[:len(sample)-2]) + require.Equal(t, 4, len(sample)) +} + +func TestTinyLFUIncrement(t *testing.T) { + a := newTinyLFU(4) + a.Increment(1) + a.Increment(1) + a.Increment(1) + require.True(t, a.door.Has(1)) + require.Equal(t, int64(2), a.freq.Estimate(1)) + + a.Increment(1) + require.False(t, a.door.Has(1)) + require.Equal(t, int64(1), a.freq.Estimate(1)) +} + +func TestTinyLFUEstimate(t *testing.T) { + a := newTinyLFU(8) + a.Increment(1) + a.Increment(1) + a.Increment(1) + require.Equal(t, int64(3), a.Estimate(1)) + require.Equal(t, int64(0), a.Estimate(2)) +} + +func TestTinyLFUPush(t *testing.T) { + a := newTinyLFU(16) + a.Push([]uint64{1, 2, 2, 3, 3, 3}) + require.Equal(t, int64(1), a.Estimate(1)) + require.Equal(t, int64(2), a.Estimate(2)) + require.Equal(t, int64(3), a.Estimate(3)) + require.Equal(t, int64(6), a.incrs) +} + +func TestTinyLFUClear(t *testing.T) { + a := newTinyLFU(16) + a.Push([]uint64{1, 3, 3, 3}) + a.clear() + require.Equal(t, int64(0), a.incrs) + require.Equal(t, int64(0), a.Estimate(3)) +} diff --git a/go/cache/ristretto/ring.go b/go/cache/ristretto/ring.go new file mode 100644 index 00000000000..5dbed4cc59c --- /dev/null +++ b/go/cache/ristretto/ring.go @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" +) + +// ringConsumer is the user-defined object responsible for receiving and +// processing items in batches when buffers are drained. +type ringConsumer interface { + Push([]uint64) bool +} + +// ringStripe is a singular ring buffer that is not concurrent safe. +type ringStripe struct { + cons ringConsumer + data []uint64 + capa int +} + +func newRingStripe(cons ringConsumer, capa int64) *ringStripe { + return &ringStripe{ + cons: cons, + data: make([]uint64, 0, capa), + capa: int(capa), + } +} + +// Push appends an item in the ring buffer and drains (copies items and +// sends to Consumer) if full. +func (s *ringStripe) Push(item uint64) { + s.data = append(s.data, item) + // Decide if the ring buffer should be drained. + if len(s.data) >= s.capa { + // Send elements to consumer and create a new ring stripe. + if s.cons.Push(s.data) { + s.data = make([]uint64, 0, s.capa) + } else { + s.data = s.data[:0] + } + } +} + +// ringBuffer stores multiple buffers (stripes) and distributes Pushed items +// between them to lower contention. +// +// This implements the "batching" process described in the BP-Wrapper paper +// (section III part A). +type ringBuffer struct { + pool *sync.Pool +} + +// newRingBuffer returns a striped ring buffer. The Consumer in ringConfig will +// be called when individual stripes are full and need to drain their elements. +func newRingBuffer(cons ringConsumer, capa int64) *ringBuffer { + // LOSSY buffers use a very simple sync.Pool for concurrently reusing + // stripes. We do lose some stripes due to GC (unheld items in sync.Pool + // are cleared), but the performance gains generally outweigh the small + // percentage of elements lost. The performance primarily comes from + // low-level runtime functions used in the standard library that aren't + // available to us (such as runtime_procPin()). + return &ringBuffer{ + pool: &sync.Pool{ + New: func() interface{} { return newRingStripe(cons, capa) }, + }, + } +} + +// Push adds an element to one of the internal stripes and possibly drains if +// the stripe becomes full. +func (b *ringBuffer) Push(item uint64) { + // Reuse or create a new stripe. + stripe := b.pool.Get().(*ringStripe) + stripe.Push(item) + b.pool.Put(stripe) +} diff --git a/go/cache/ristretto/ring_test.go b/go/cache/ristretto/ring_test.go new file mode 100644 index 00000000000..1c729fc3260 --- /dev/null +++ b/go/cache/ristretto/ring_test.go @@ -0,0 +1,70 @@ +package ristretto + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +type testConsumer struct { + push func([]uint64) + save bool +} + +func (c *testConsumer) Push(items []uint64) bool { + if c.save { + c.push(items) + return true + } + return false +} + +func TestRingDrain(t *testing.T) { + drains := 0 + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + drains++ + }, + save: true, + }, 1) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + require.Equal(t, 100, drains, "buffers shouldn't be dropped with BufferItems == 1") +} + +func TestRingReset(t *testing.T) { + drains := 0 + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + drains++ + }, + save: false, + }, 4) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + require.Equal(t, 0, drains, "testConsumer shouldn't be draining") +} + +func TestRingConsumer(t *testing.T) { + mu := &sync.Mutex{} + drainItems := make(map[uint64]struct{}) + r := newRingBuffer(&testConsumer{ + push: func(items []uint64) { + mu.Lock() + defer mu.Unlock() + for i := range items { + drainItems[items[i]] = struct{}{} + } + }, + save: true, + }, 4) + for i := 0; i < 100; i++ { + r.Push(uint64(i)) + } + l := len(drainItems) + require.NotEqual(t, 0, l) + require.True(t, l <= 100) +} diff --git a/go/cache/ristretto/sketch.go b/go/cache/ristretto/sketch.go new file mode 100644 index 00000000000..f12add3aed5 --- /dev/null +++ b/go/cache/ristretto/sketch.go @@ -0,0 +1,155 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ristretto includes multiple probabalistic data structures needed for +// admission/eviction metadata. Most are Counting Bloom Filter variations, but +// a caching-specific feature that is also required is a "freshness" mechanism, +// which basically serves as a "lifetime" process. This freshness mechanism +// was described in the original TinyLFU paper [1], but other mechanisms may +// be better suited for certain data distributions. +// +// [1]: https://arxiv.org/abs/1512.00727 +package ristretto + +import ( + "fmt" + "math/rand" + "time" +) + +// cmSketch is a Count-Min sketch implementation with 4-bit counters, heavily +// based on Damian Gryski's CM4 [1]. +// +// [1]: https://github.com/dgryski/go-tinylfu/blob/master/cm4.go +type cmSketch struct { + rows [cmDepth]cmRow + seed [cmDepth]uint64 + mask uint64 +} + +const ( + // cmDepth is the number of counter copies to store (think of it as rows). + cmDepth = 4 +) + +func newCmSketch(numCounters int64) *cmSketch { + if numCounters == 0 { + panic("cmSketch: bad numCounters") + } + // Get the next power of 2 for better cache performance. + numCounters = next2Power(numCounters) + sketch := &cmSketch{mask: uint64(numCounters - 1)} + // Initialize rows of counters and seeds. + source := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := 0; i < cmDepth; i++ { + sketch.seed[i] = source.Uint64() + sketch.rows[i] = newCmRow(numCounters) + } + return sketch +} + +// Increment increments the count(ers) for the specified key. +func (s *cmSketch) Increment(hashed uint64) { + for i := range s.rows { + s.rows[i].increment((hashed ^ s.seed[i]) & s.mask) + } +} + +// Estimate returns the value of the specified key. +func (s *cmSketch) Estimate(hashed uint64) int64 { + min := byte(255) + for i := range s.rows { + val := s.rows[i].get((hashed ^ s.seed[i]) & s.mask) + if val < min { + min = val + } + } + return int64(min) +} + +// Reset halves all counter values. +func (s *cmSketch) Reset() { + for _, r := range s.rows { + r.reset() + } +} + +// Clear zeroes all counters. +func (s *cmSketch) Clear() { + for _, r := range s.rows { + r.clear() + } +} + +// cmRow is a row of bytes, with each byte holding two counters. +type cmRow []byte + +func newCmRow(numCounters int64) cmRow { + return make(cmRow, numCounters/2) +} + +func (r cmRow) get(n uint64) byte { + return byte(r[n/2]>>((n&1)*4)) & 0x0f +} + +func (r cmRow) increment(n uint64) { + // Index of the counter. + i := n / 2 + // Shift distance (even 0, odd 4). + s := (n & 1) * 4 + // Counter value. + v := (r[i] >> s) & 0x0f + // Only increment if not max value (overflow wrap is bad for LFU). + if v < 15 { + r[i] += 1 << s + } +} + +func (r cmRow) reset() { + // Halve each counter. + for i := range r { + r[i] = (r[i] >> 1) & 0x77 + } +} + +func (r cmRow) clear() { + // Zero each counter. + for i := range r { + r[i] = 0 + } +} + +func (r cmRow) string() string { + s := "" + for i := uint64(0); i < uint64(len(r)*2); i++ { + s += fmt.Sprintf("%02d ", (r[(i/2)]>>((i&1)*4))&0x0f) + } + s = s[:len(s)-1] + return s +} + +// next2Power rounds x up to the next power of 2, if it's not already one. +func next2Power(x int64) int64 { + x-- + x |= x >> 1 + x |= x >> 2 + x |= x >> 4 + x |= x >> 8 + x |= x >> 16 + x |= x >> 32 + x++ + return x +} diff --git a/go/cache/ristretto/sketch_test.go b/go/cache/ristretto/sketch_test.go new file mode 100644 index 00000000000..62142901fbd --- /dev/null +++ b/go/cache/ristretto/sketch_test.go @@ -0,0 +1,85 @@ +package ristretto + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSketch(t *testing.T) { + defer func() { + require.NotNil(t, recover()) + }() + + s := newCmSketch(5) + require.Equal(t, uint64(7), s.mask) + newCmSketch(0) +} + +func TestSketchIncrement(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(5) + s.Increment(9) + for i := 0; i < cmDepth; i++ { + if s.rows[i].string() != s.rows[0].string() { + break + } + require.False(t, i == cmDepth-1, "identical rows, bad seeding") + } +} + +func TestSketchEstimate(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(1) + require.Equal(t, int64(2), s.Estimate(1)) + require.Equal(t, int64(0), s.Estimate(0)) +} + +func TestSketchReset(t *testing.T) { + s := newCmSketch(16) + s.Increment(1) + s.Increment(1) + s.Increment(1) + s.Increment(1) + s.Reset() + require.Equal(t, int64(2), s.Estimate(1)) +} + +func TestSketchClear(t *testing.T) { + s := newCmSketch(16) + for i := 0; i < 16; i++ { + s.Increment(uint64(i)) + } + s.Clear() + for i := 0; i < 16; i++ { + require.Equal(t, int64(0), s.Estimate(uint64(i))) + } +} + +func TestNext2Power(t *testing.T) { + sz := 12 << 30 + szf := float64(sz) * 0.01 + val := int64(szf) + t.Logf("szf = %.2f val = %d\n", szf, val) + pow := next2Power(val) + t.Logf("pow = %d. mult 4 = %d\n", pow, pow*4) +} + +func BenchmarkSketchIncrement(b *testing.B) { + s := newCmSketch(16) + b.SetBytes(1) + for n := 0; n < b.N; n++ { + s.Increment(1) + } +} + +func BenchmarkSketchEstimate(b *testing.B) { + s := newCmSketch(16) + s.Increment(1) + b.SetBytes(1) + for n := 0; n < b.N; n++ { + s.Estimate(1) + } +} diff --git a/go/cache/ristretto/store.go b/go/cache/ristretto/store.go new file mode 100644 index 00000000000..c45f96718d7 --- /dev/null +++ b/go/cache/ristretto/store.go @@ -0,0 +1,280 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" + "time" +) + +// TODO: Do we need this to be a separate struct from Item? +type storeItem struct { + key uint64 + conflict uint64 + value interface{} + expiration int64 +} + +// store is the interface fulfilled by all hash map implementations in this +// file. Some hash map implementations are better suited for certain data +// distributions than others, so this allows us to abstract that out for use +// in Ristretto. +// +// Every store is safe for concurrent usage. +type store interface { + // Get returns the value associated with the key parameter. + Get(uint64, uint64) (interface{}, bool) + // Expiration returns the expiration time for this key. + Expiration(uint64) int64 + // Set adds the key-value pair to the Map or updates the value if it's + // already present. The key-value pair is passed as a pointer to an + // item object. + Set(*Item) + // Del deletes the key-value pair from the Map. + Del(uint64, uint64) (uint64, interface{}) + // Update attempts to update the key with a new value and returns true if + // successful. + Update(*Item) (interface{}, bool) + // Cleanup removes items that have an expired TTL. + Cleanup(policy policy, onEvict itemCallback) + // Clear clears all contents of the store. + Clear(onEvict itemCallback) + // ForEach yields all the values in the store + ForEach(forEach func(interface{}) bool) + // Len returns the number of entries in the store + Len() int +} + +// newStore returns the default store implementation. +func newStore() store { + return newShardedMap() +} + +const numShards uint64 = 256 + +type shardedMap struct { + shards []*lockedMap + expiryMap *expirationMap +} + +func newShardedMap() *shardedMap { + sm := &shardedMap{ + shards: make([]*lockedMap, int(numShards)), + expiryMap: newExpirationMap(), + } + for i := range sm.shards { + sm.shards[i] = newLockedMap(sm.expiryMap) + } + return sm +} + +func (sm *shardedMap) Get(key, conflict uint64) (interface{}, bool) { + return sm.shards[key%numShards].get(key, conflict) +} + +func (sm *shardedMap) Expiration(key uint64) int64 { + return sm.shards[key%numShards].Expiration(key) +} + +func (sm *shardedMap) Set(i *Item) { + if i == nil { + // If item is nil make this Set a no-op. + return + } + + sm.shards[i.Key%numShards].Set(i) +} + +func (sm *shardedMap) Del(key, conflict uint64) (uint64, interface{}) { + return sm.shards[key%numShards].Del(key, conflict) +} + +func (sm *shardedMap) Update(newItem *Item) (interface{}, bool) { + return sm.shards[newItem.Key%numShards].Update(newItem) +} + +func (sm *shardedMap) Cleanup(policy policy, onEvict itemCallback) { + sm.expiryMap.cleanup(sm, policy, onEvict) +} + +func (sm *shardedMap) ForEach(forEach func(interface{}) bool) { + for _, shard := range sm.shards { + if !shard.foreach(forEach) { + break + } + } +} + +func (sm *shardedMap) Len() int { + l := 0 + for _, shard := range sm.shards { + l += shard.Len() + } + return l +} + +func (sm *shardedMap) Clear(onEvict itemCallback) { + for i := uint64(0); i < numShards; i++ { + sm.shards[i].Clear(onEvict) + } +} + +type lockedMap struct { + sync.RWMutex + data map[uint64]storeItem + em *expirationMap +} + +func newLockedMap(em *expirationMap) *lockedMap { + return &lockedMap{ + data: make(map[uint64]storeItem), + em: em, + } +} + +func (m *lockedMap) get(key, conflict uint64) (interface{}, bool) { + m.RLock() + item, ok := m.data[key] + m.RUnlock() + if !ok { + return nil, false + } + if conflict != 0 && (conflict != item.conflict) { + return nil, false + } + + // Handle expired items. + if item.expiration != 0 && time.Now().Unix() > item.expiration { + return nil, false + } + return item.value, true +} + +func (m *lockedMap) Expiration(key uint64) int64 { + m.RLock() + defer m.RUnlock() + return m.data[key].expiration +} + +func (m *lockedMap) Set(i *Item) { + if i == nil { + // If the item is nil make this Set a no-op. + return + } + + m.Lock() + defer m.Unlock() + item, ok := m.data[i.Key] + + if ok { + // The item existed already. We need to check the conflict key and reject the + // update if they do not match. Only after that the expiration map is updated. + if i.Conflict != 0 && (i.Conflict != item.conflict) { + return + } + m.em.update(i.Key, i.Conflict, item.expiration, i.Expiration) + } else { + // The value is not in the map already. There's no need to return anything. + // Simply add the expiration map. + m.em.add(i.Key, i.Conflict, i.Expiration) + } + + m.data[i.Key] = storeItem{ + key: i.Key, + conflict: i.Conflict, + value: i.Value, + expiration: i.Expiration, + } +} + +func (m *lockedMap) Del(key, conflict uint64) (uint64, interface{}) { + m.Lock() + item, ok := m.data[key] + if !ok { + m.Unlock() + return 0, nil + } + if conflict != 0 && (conflict != item.conflict) { + m.Unlock() + return 0, nil + } + + if item.expiration != 0 { + m.em.del(key, item.expiration) + } + + delete(m.data, key) + m.Unlock() + return item.conflict, item.value +} + +func (m *lockedMap) Update(newItem *Item) (interface{}, bool) { + m.Lock() + item, ok := m.data[newItem.Key] + if !ok { + m.Unlock() + return nil, false + } + if newItem.Conflict != 0 && (newItem.Conflict != item.conflict) { + m.Unlock() + return nil, false + } + + m.em.update(newItem.Key, newItem.Conflict, item.expiration, newItem.Expiration) + m.data[newItem.Key] = storeItem{ + key: newItem.Key, + conflict: newItem.Conflict, + value: newItem.Value, + expiration: newItem.Expiration, + } + + m.Unlock() + return item.value, true +} + +func (m *lockedMap) Len() int { + m.RLock() + l := len(m.data) + m.RUnlock() + return l +} + +func (m *lockedMap) Clear(onEvict itemCallback) { + m.Lock() + i := &Item{} + if onEvict != nil { + for _, si := range m.data { + i.Key = si.key + i.Conflict = si.conflict + i.Value = si.value + onEvict(i) + } + } + m.data = make(map[uint64]storeItem) + m.Unlock() +} + +func (m *lockedMap) foreach(forEach func(interface{}) bool) bool { + m.RLock() + defer m.RUnlock() + for _, si := range m.data { + if !forEach(si.value) { + return false + } + } + return true +} diff --git a/go/cache/ristretto/store_test.go b/go/cache/ristretto/store_test.go new file mode 100644 index 00000000000..7f9aa052b3f --- /dev/null +++ b/go/cache/ristretto/store_test.go @@ -0,0 +1,207 @@ +package ristretto + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStoreSetGet(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + s.Set(&i) + val, ok := s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + i.Value = 3 + s.Set(&i) + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 3, val.(int)) + + key, conflict = defaultStringHash("2") + i = Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + s.Set(&i) + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) +} + +func TestStoreDel(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + s.Del(key, conflict) + val, ok := s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + + s.Del(2, 0) +} + +func TestStoreClear(t *testing.T) { + s := newStore() + for i := 0; i < 1000; i++ { + key, conflict := defaultStringHash(strconv.Itoa(i)) + it := Item{ + Key: key, + Conflict: conflict, + Value: i, + } + s.Set(&it) + } + s.Clear(nil) + for i := 0; i < 1000; i++ { + key, conflict := defaultStringHash(strconv.Itoa(i)) + val, ok := s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) + } +} + +func TestStoreUpdate(t *testing.T) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + i.Value = 2 + _, ok := s.Update(&i) + require.True(t, ok) + + val, ok := s.Get(key, conflict) + require.True(t, ok) + require.NotNil(t, val) + + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 2, val.(int)) + + i.Value = 3 + _, ok = s.Update(&i) + require.True(t, ok) + + val, ok = s.Get(key, conflict) + require.True(t, ok) + require.Equal(t, 3, val.(int)) + + key, conflict = defaultStringHash("2") + i = Item{ + Key: key, + Conflict: conflict, + Value: 2, + } + _, ok = s.Update(&i) + require.False(t, ok) + val, ok = s.Get(key, conflict) + require.False(t, ok) + require.Nil(t, val) +} + +func TestStoreCollision(t *testing.T) { + s := newShardedMap() + s.shards[1].Lock() + s.shards[1].data[1] = storeItem{ + key: 1, + conflict: 0, + value: 1, + } + s.shards[1].Unlock() + val, ok := s.Get(1, 1) + require.False(t, ok) + require.Nil(t, val) + + i := Item{ + Key: 1, + Conflict: 1, + Value: 2, + } + s.Set(&i) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotEqual(t, 2, val.(int)) + + _, ok = s.Update(&i) + require.False(t, ok) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotEqual(t, 2, val.(int)) + + s.Del(1, 1) + val, ok = s.Get(1, 0) + require.True(t, ok) + require.NotNil(t, val) +} + +func BenchmarkStoreGet(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + s.Get(key, conflict) + } + }) +} + +func BenchmarkStoreSet(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + } + }) +} + +func BenchmarkStoreUpdate(b *testing.B) { + s := newStore() + key, conflict := defaultStringHash("1") + i := Item{ + Key: key, + Conflict: conflict, + Value: 1, + } + s.Set(&i) + b.SetBytes(1) + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + s.Update(&Item{ + Key: key, + Conflict: conflict, + Value: 2, + }) + } + }) +} diff --git a/go/cache/ristretto/ttl.go b/go/cache/ristretto/ttl.go new file mode 100644 index 00000000000..40a91bc1e51 --- /dev/null +++ b/go/cache/ristretto/ttl.go @@ -0,0 +1,147 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ristretto + +import ( + "sync" + "time" +) + +var ( + // TODO: find the optimal value or make it configurable. + bucketDurationSecs = int64(5) +) + +func storageBucket(t int64) int64 { + return (t / bucketDurationSecs) + 1 +} + +func cleanupBucket(t int64) int64 { + // The bucket to cleanup is always behind the storage bucket by one so that + // no elements in that bucket (which might not have expired yet) are deleted. + return storageBucket(t) - 1 +} + +// bucket type is a map of key to conflict. +type bucket map[uint64]uint64 + +// expirationMap is a map of bucket number to the corresponding bucket. +type expirationMap struct { + sync.RWMutex + buckets map[int64]bucket +} + +func newExpirationMap() *expirationMap { + return &expirationMap{ + buckets: make(map[int64]bucket), + } +} + +func (m *expirationMap) add(key, conflict uint64, expiration int64) { + if m == nil { + return + } + + // Items that don't expire don't need to be in the expiration map. + if expiration == 0 { + return + } + + bucketNum := storageBucket(expiration) + m.Lock() + defer m.Unlock() + + b, ok := m.buckets[bucketNum] + if !ok { + b = make(bucket) + m.buckets[bucketNum] = b + } + b[key] = conflict +} + +func (m *expirationMap) update(key, conflict uint64, oldExpTime, newExpTime int64) { + if m == nil { + return + } + + m.Lock() + defer m.Unlock() + + oldBucketNum := storageBucket(oldExpTime) + oldBucket, ok := m.buckets[oldBucketNum] + if ok { + delete(oldBucket, key) + } + + newBucketNum := storageBucket(newExpTime) + newBucket, ok := m.buckets[newBucketNum] + if !ok { + newBucket = make(bucket) + m.buckets[newBucketNum] = newBucket + } + newBucket[key] = conflict +} + +func (m *expirationMap) del(key uint64, expiration int64) { + if m == nil { + return + } + + bucketNum := storageBucket(expiration) + m.Lock() + defer m.Unlock() + _, ok := m.buckets[bucketNum] + if !ok { + return + } + delete(m.buckets[bucketNum], key) +} + +// cleanup removes all the items in the bucket that was just completed. It deletes +// those items from the store, and calls the onEvict function on those items. +// This function is meant to be called periodically. +func (m *expirationMap) cleanup(store store, policy policy, onEvict itemCallback) { + if m == nil { + return + } + + m.Lock() + now := time.Now().Unix() + bucketNum := cleanupBucket(now) + keys := m.buckets[bucketNum] + delete(m.buckets, bucketNum) + m.Unlock() + + for key, conflict := range keys { + // Sanity check. Verify that the store agrees that this key is expired. + if store.Expiration(key) > now { + continue + } + + cost := policy.Cost(key) + policy.Del(key) + _, value := store.Del(key, conflict) + + if onEvict != nil { + onEvict(&Item{Key: key, + Conflict: conflict, + Value: value, + Cost: cost, + }) + } + } +} diff --git a/go/hack/runtime.go b/go/hack/runtime.go new file mode 100644 index 00000000000..418050dc5f9 --- /dev/null +++ b/go/hack/runtime.go @@ -0,0 +1,45 @@ +/* +Copyright 2019 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hack + +import ( + "reflect" + "unsafe" +) + +//go:noescape +//go:linkname memhash runtime.memhash +func memhash(p unsafe.Pointer, h, s uintptr) uintptr + +//go:noescape +//go:linkname strhash runtime.strhash +func strhash(p unsafe.Pointer, h uintptr) uintptr + +// RuntimeMemhash provides access to the Go runtime's default hash function for arbitrary bytes. +// This is an optimal hash function which takes an input seed and is potentially implemented in hardware +// for most architectures. This is the same hash function that the language's `map` uses. +func RuntimeMemhash(b []byte, seed uint64) uint64 { + pstring := (*reflect.SliceHeader)(unsafe.Pointer(&b)) + return uint64(memhash(unsafe.Pointer(pstring.Data), uintptr(seed), uintptr(pstring.Len))) +} + +// RuntimeStrhash provides access to the Go runtime's default hash function for strings. +// This is an optimal hash function which takes an input seed and is potentially implemented in hardware +// for most architectures. This is the same hash function that the language's `map` uses. +func RuntimeStrhash(str string, seed uint64) uint64 { + return uint64(strhash(unsafe.Pointer(&str), uintptr(seed))) +} diff --git a/go/hack/runtime.s b/go/hack/runtime.s new file mode 100644 index 00000000000..e69de29bb2d diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 776c90a8c8a..70d16d4abc0 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -132,18 +132,11 @@ func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver executorOnce.Do(func() { stats.NewGaugeFunc("QueryPlanCacheLength", "Query plan cache length", func() int64 { - return e.plans.Stats().Length + return int64(e.plans.Len()) }) - stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", func() int64 { - return e.plans.Stats().Size - }) - stats.NewGaugeFunc("QueryPlanCacheCapacity", "Query plan cache capacity", e.plans.Capacity) - stats.NewCounterFunc("QueryPlanCacheEvictions", "Query plan cache evictions", func() int64 { - return e.plans.Stats().Evictions - }) - stats.Publish("QueryPlanCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", e.plans.Stats().Oldest) - })) + stats.NewGaugeFunc("QueryPlanCacheSize", "Query plan cache size", e.plans.UsedCapacity) + stats.NewGaugeFunc("QueryPlanCacheCapacity", "Query plan cache capacity", e.plans.MaxCapacity) + stats.NewCounterFunc("QueryPlanCacheEvictions", "Query plan cache evictions", e.plans.Evictions) http.Handle(pathQueryPlans, e) http.Handle(pathScatterStats, e) http.Handle(pathVSchema, e) diff --git a/go/vt/vtgate/executor_test.go b/go/vt/vtgate/executor_test.go index 7945b5717e2..d95106db15f 100644 --- a/go/vt/vtgate/executor_test.go +++ b/go/vt/vtgate/executor_test.go @@ -31,6 +31,7 @@ import ( "vitess.io/vitess/go/cache" "vitess.io/vitess/go/test/utils" + "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/topo" @@ -1438,18 +1439,14 @@ func TestGetPlanUnnormalized(t *testing.T) { emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - logStats1 := NewLogStats(ctx, "Test", "", nil) query1 := "select * from music_user_map where id = 1" - plan1, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + plan1, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) wantSQL := query1 + " /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - plan2, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats2) - require.NoError(t, err) + plan2, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan2 { t.Errorf("getPlan(query1): plans must be equal: %p %p", plan1, plan2) } @@ -1460,18 +1457,14 @@ func TestGetPlanUnnormalized(t *testing.T) { if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } - logStats3 := NewLogStats(ctx, "Test", "", nil) - plan3, err := r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats3) - require.NoError(t, err) + plan3, logStats3 := getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan1 == plan3 { t.Errorf("getPlan(query1, ks): plans must not be equal: %p %p", plan1, plan3) } if logStats3.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats3.SQL) } - logStats4 := NewLogStats(ctx, "Test", "", nil) - plan4, err := r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats4) - require.NoError(t, err) + plan4, logStats4 := getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) if plan3 != plan4 { t.Errorf("getPlan(query1, ks): plans must be equal: %p %p", plan3, plan4) } @@ -1506,22 +1499,32 @@ func assertCacheContains(t *testing.T, c cache.Cache, want []string) { } } +func getPlanCached(t *testing.T, e *Executor, vcursor *vcursorImpl, sql string, comments sqlparser.MarginComments, bindVars map[string]*querypb.BindVariable, skipQueryPlanCache bool) (*engine.Plan, *LogStats) { + logStats := NewLogStats(ctx, "Test", "", nil) + plan, err := e.getPlan(vcursor, sql, comments, bindVars, skipQueryPlanCache, logStats) + require.NoError(t, err) + + // Wait for cache to settle + e.plans.Wait() + return plan, logStats +} + func TestGetPlanCacheUnnormalized(t *testing.T) { r, _, _, _ := createLegacyExecutorEnv() emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 := "select * from music_user_map where id = 1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) - require.NoError(t, err) + + _, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true) assertCacheSize(t, r.plans, 0) + wantSQL := query1 + " /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) - require.NoError(t, err) + + _, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 1) + wantSQL = query1 + " /* comment 2 */" if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) @@ -1532,26 +1535,21 @@ func TestGetPlanCacheUnnormalized(t *testing.T) { unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 = "insert /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ into user(id) values (1), (2)" - logStats1 = NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 2) } @@ -1559,18 +1557,16 @@ func TestGetPlanCacheNormalized(t *testing.T) { r, _, _, _ := createLegacyExecutorEnv() r.normalize = true emptyvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) + query1 := "select * from music_user_map where id = 1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - _, err := r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */, logStats1) - require.NoError(t, err) + _, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, true /* skipQueryPlanCache */) assertCacheSize(t, r.plans, 0) wantSQL := "select * from music_user_map where id = :vtg1 /* comment */" if logStats1.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats1.SQL) } - logStats2 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */, logStats2) - require.NoError(t, err) + + _, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false /* skipQueryPlanCache */) assertCacheSize(t, r.plans, 1) if logStats2.SQL != wantSQL { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) @@ -1582,26 +1578,21 @@ func TestGetPlanCacheNormalized(t *testing.T) { unshardedvc, _ := newVCursorImpl(ctx, NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "@unknown"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) query1 = "insert /*vt+ SKIP_QUERY_PLAN_CACHE=1 */ into user(id) values (1), (2)" - logStats1 = NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 0) query1 = "insert into user(id) values (1), (2)" - _, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 1) // the target string will be resolved and become part of the plan cache key, which adds a new entry ksIDVc1, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[deadbeef]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, ksIDVc1, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 2) // the target string will be resolved and become part of the plan cache key, as it's an unsharded ks, it will be the same entry as above ksIDVc2, _ := newVCursorImpl(context.Background(), NewSafeSession(&vtgatepb.Session{TargetString: KsTestUnsharded + "[beefdead]"}), makeComments(""), r, nil, r.vm, r.VSchema(), r.resolver.resolver, nil) - _, err = r.getPlan(ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) + getPlanCached(t, r, ksIDVc2, query1, makeComments(" /* comment */"), map[string]*querypb.BindVariable{}, false) assertCacheSize(t, r.plans, 2) } @@ -1614,12 +1605,10 @@ func TestGetPlanNormalized(t *testing.T) { query1 := "select * from music_user_map where id = 1" query2 := "select * from music_user_map where id = 2" normalized := "select * from music_user_map where id = :vtg1" - logStats1 := NewLogStats(ctx, "Test", "", nil) - plan1, err := r.getPlan(emptyvc, query1, makeComments(" /* comment 1 */"), map[string]*querypb.BindVariable{}, false, logStats1) - require.NoError(t, err) - logStats2 := NewLogStats(ctx, "Test", "", nil) - plan2, err := r.getPlan(emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false, logStats2) - require.NoError(t, err) + + plan1, logStats1 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 1 */"), map[string]*querypb.BindVariable{}, false) + plan2, logStats2 := getPlanCached(t, r, emptyvc, query1, makeComments(" /* comment 2 */"), map[string]*querypb.BindVariable{}, false) + if plan1 != plan2 { t.Errorf("getPlan(query1): plans must be equal: %p %p", plan1, plan2) } @@ -1637,9 +1626,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats2.SQL) } - logStats3 := NewLogStats(ctx, "Test", "", nil) - plan3, err := r.getPlan(emptyvc, query2, makeComments(" /* comment 3 */"), map[string]*querypb.BindVariable{}, false, logStats3) - require.NoError(t, err) + plan3, logStats3 := getPlanCached(t, r, emptyvc, query2, makeComments(" /* comment 3 */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan3 { t.Errorf("getPlan(query2): plans must be equal: %p %p", plan1, plan3) } @@ -1648,9 +1635,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats3.SQL) } - logStats4 := NewLogStats(ctx, "Test", "", nil) - plan4, err := r.getPlan(emptyvc, normalized, makeComments(" /* comment 4 */"), map[string]*querypb.BindVariable{}, false, logStats4) - require.NoError(t, err) + plan4, logStats4 := getPlanCached(t, r, emptyvc, normalized, makeComments(" /* comment 4 */"), map[string]*querypb.BindVariable{}, false) if plan1 != plan4 { t.Errorf("getPlan(normalized): plans must be equal: %p %p", plan1, plan4) } @@ -1659,9 +1644,8 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats4.SQL) } - logStats5 := NewLogStats(ctx, "Test", "", nil) - plan3, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment 5 */"), map[string]*querypb.BindVariable{}, false, logStats5) - require.NoError(t, err) + var logStats5 *LogStats + plan3, logStats5 = getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment 5 */"), map[string]*querypb.BindVariable{}, false) if plan1 == plan3 { t.Errorf("getPlan(query1, ks): plans must not be equal: %p %p", plan1, plan3) } @@ -1670,9 +1654,7 @@ func TestGetPlanNormalized(t *testing.T) { t.Errorf("logstats sql want \"%s\" got \"%s\"", wantSQL, logStats5.SQL) } - logStats6 := NewLogStats(ctx, "Test", "", nil) - plan4, err = r.getPlan(unshardedvc, query1, makeComments(" /* comment 6 */"), map[string]*querypb.BindVariable{}, false, logStats6) - require.NoError(t, err) + plan4, _ = getPlanCached(t, r, unshardedvc, query1, makeComments(" /* comment 6 */"), map[string]*querypb.BindVariable{}, false) if plan3 != plan4 { t.Errorf("getPlan(query1, ks): plans must be equal: %p %p", plan3, plan4) } @@ -1682,9 +1664,7 @@ func TestGetPlanNormalized(t *testing.T) { } assertCacheContains(t, r.plans, want) - // Errors - logStats7 := NewLogStats(ctx, "Test", "", nil) - _, err = r.getPlan(emptyvc, "syntax", makeComments(""), map[string]*querypb.BindVariable{}, false, logStats7) + _, err := r.getPlan(emptyvc, "syntax", makeComments(""), map[string]*querypb.BindVariable{}, false, nil) wantErr := "syntax error at position 7 near 'syntax'" if err == nil || err.Error() != wantErr { t.Errorf("getPlan(syntax): %v, want %s", err, wantErr) diff --git a/go/vt/vtgate/queryz_test.go b/go/vt/vtgate/queryz_test.go index c82dd3f9b76..ee25534b0e8 100644 --- a/go/vt/vtgate/queryz_test.go +++ b/go/vt/vtgate/queryz_test.go @@ -43,6 +43,7 @@ func TestQueryzHandler(t *testing.T) { sql := "select id from user where id = 1" _, err := executorExec(executor, sql, nil) require.NoError(t, err) + executor.plans.Wait() result, ok := executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") @@ -54,6 +55,7 @@ func TestQueryzHandler(t *testing.T) { sql = "select id from user" _, err = executorExec(executor, sql, nil) require.NoError(t, err) + executor.plans.Wait() result, ok = executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") @@ -67,6 +69,7 @@ func TestQueryzHandler(t *testing.T) { "name": sqltypes.BytesBindVariable([]byte("myname")), }) require.NoError(t, err) + executor.plans.Wait() result, ok = executor.plans.Get("@master:" + sql) if !ok { t.Fatalf("couldn't get plan from cache") diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index 795ef26d4c9..9f3b6efcefb 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -216,18 +216,11 @@ func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { env.Exporter().NewCounterFunc("TableACLExemptCount", "Query engine table ACL exempt count", qe.tableaclExemptCount.Get) env.Exporter().NewGaugeFunc("QueryCacheLength", "Query engine query cache length", func() int64 { - return qe.plans.Stats().Length + return int64(qe.plans.Len()) }) - env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", func() int64 { - return qe.plans.Stats().Size - }) - env.Exporter().NewGaugeFunc("QueryCacheCapacity", "Query engine query cache capacity", qe.plans.Capacity) - env.Exporter().NewCounterFunc("QueryCacheEvictions", "Query engine query cache evictions", func() int64 { - return qe.plans.Stats().Evictions - }) - env.Exporter().Publish("QueryCacheOldest", stats.StringFunc(func() string { - return fmt.Sprintf("%v", qe.plans.Stats().Oldest) - })) + env.Exporter().NewGaugeFunc("QueryCacheSize", "Query engine query cache size", qe.plans.UsedCapacity) + env.Exporter().NewGaugeFunc("QueryCacheCapacity", "Query engine query cache capacity", qe.plans.MaxCapacity) + env.Exporter().NewCounterFunc("QueryCacheEvictions", "Query engine query cache evictions", qe.plans.Evictions) qe.queryCounts = env.Exporter().NewCountersWithMultiLabels("QueryCounts", "query counts", []string{"Table", "Plan"}) qe.queryTimes = env.Exporter().NewCountersWithMultiLabels("QueryTimesNs", "query times in ns", []string{"Table", "Plan"}) qe.queryRowCounts = env.Exporter().NewCountersWithMultiLabels("QueryRowCounts", "query row counts", []string{"Table", "Plan"}) @@ -419,7 +412,7 @@ func (qe *QueryEngine) SetQueryPlanCacheCap(size int) { // QueryPlanCacheCap returns the capacity of the query cache. func (qe *QueryEngine) QueryPlanCacheCap() int { - return int(qe.plans.Capacity()) + return int(qe.plans.MaxCapacity()) } // AddStats adds the given stats for the planName.tableName diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index f61abf49b46..7c7a9bce754 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -86,6 +86,9 @@ func TestQueryzHandler(t *testing.T) { qe.plans.Set(hugeInsert, plan4, plan4.CachedSize(true)) qe.plans.Set("", (*TabletPlan)(nil), 1) + // Wait for cache to settle + qe.plans.Wait() + queryzHandler(qe, resp, req) body, _ := ioutil.ReadAll(resp.Body) planPattern1 := []string{ From 1cade52f041ecbf85525093af2e60dacb47e6915 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Mon, 1 Feb 2021 15:37:35 +0100 Subject: [PATCH 10/23] endtoend: fix test values Signed-off-by: Vicent Marti --- go/vt/vttablet/endtoend/config_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/go/vt/vttablet/endtoend/config_test.go b/go/vt/vttablet/endtoend/config_test.go index fde3881972e..33fa23d771c 100644 --- a/go/vt/vttablet/endtoend/config_test.go +++ b/go/vt/vttablet/endtoend/config_test.go @@ -176,7 +176,10 @@ func TestConsolidatorReplicasOnly(t *testing.T) { } func TestQueryPlanCache(t *testing.T) { - const cachedPlanSize = 2275 + const cacheItemSize = 40 + const cachedPlanSize = 2275 + cacheItemSize + const cachePlanSize2 = 2254 + cacheItemSize + //sleep to avoid race between SchemaChanged event clearing out the plans cache which breaks this test time.Sleep(1 * time.Second) @@ -190,6 +193,8 @@ func TestQueryPlanCache(t *testing.T) { client := framework.NewClient() _, _ = client.Execute("select * from vitess_test where intval=:ival1", bindVars) _, _ = client.Execute("select * from vitess_test where intval=:ival2", bindVars) + time.Sleep(100 * time.Millisecond) + vend := framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 1) verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize) @@ -197,13 +202,17 @@ func TestQueryPlanCache(t *testing.T) { framework.Server.SetQueryPlanCacheCap(64 * 1024) _, _ = client.Execute("select * from vitess_test where intval=:ival1", bindVars) + time.Sleep(100 * time.Millisecond) + vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 2) verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2) _, _ = client.Execute("select * from vitess_test where intval=1", bindVars) + time.Sleep(100 * time.Millisecond) + vend = framework.DebugVars() verifyIntValue(t, vend, "QueryCacheLength", 3) - verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2+2254) + verifyIntValue(t, vend, "QueryCacheSize", cachedPlanSize*2+cachePlanSize2) } func TestMaxResultSize(t *testing.T) { From 5f2a6129a37b9a6f66f1a5bdba70bd60a992bdeb Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Mon, 1 Feb 2021 15:45:03 +0100 Subject: [PATCH 11/23] plan builder: use standard V3 planner Signed-off-by: Vicent Marti --- go/vt/vtgate/planbuilder/plan_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index 0e7cbf6cff3..ecdc25ebf76 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -623,7 +623,7 @@ func BenchmarkSelectVsDML(b *testing.B) { vschema := &vschemaWrapper{ v: loadSchema(b, "schema_test.json"), sysVarEnabled: true, - version: V4, + version: V3, } var dmlCases []testCase @@ -650,11 +650,11 @@ func BenchmarkSelectVsDML(b *testing.B) { }) b.Run("DML (random sample, N=32)", func(b *testing.B) { - benchmarkPlanner(b, V4, dmlCases[:32], vschema) + benchmarkPlanner(b, V3, dmlCases[:32], vschema) }) b.Run("Select (random sample, N=32)", func(b *testing.B) { - benchmarkPlanner(b, V4, selectCases[:32], vschema) + benchmarkPlanner(b, V3, selectCases[:32], vschema) }) } From 4c35cfa7c9e59db0a238db49071fc2cfccf62fb9 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Mon, 1 Feb 2021 17:11:53 +0100 Subject: [PATCH 12/23] cache: make unit test more reliable Signed-off-by: Vicent Marti --- go/cache/ristretto/cache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go index 35c6fc0e806..e253f41c3ed 100644 --- a/go/cache/ristretto/cache_test.go +++ b/go/cache/ristretto/cache_test.go @@ -764,7 +764,7 @@ func TestDropUpdates(t *testing.T) { } } // Wait for all the items to be processed. - time.Sleep(time.Millisecond) + c.Wait() // This will cause eviction from the cache. require.True(t, c.Set("1", nil, 10)) c.Close() From 70c8038649067ba086211d4b212e7b9b674eb9df Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Tue, 2 Feb 2021 11:26:59 +0100 Subject: [PATCH 13/23] cache: speed up clearing large caches Signed-off-by: Vicent Marti --- go/cache/ristretto/policy.go | 30 +++++++++++++---------- go/vt/vtgate/executor_vschema_ddl_test.go | 8 +++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/go/cache/ristretto/policy.go b/go/cache/ristretto/policy.go index aa3c0b8c26b..b20b903309c 100644 --- a/go/cache/ristretto/policy.go +++ b/go/cache/ristretto/policy.go @@ -68,20 +68,24 @@ func newPolicy(numCounters, maxCost int64) policy { type defaultPolicy struct { sync.Mutex - admit *tinyLFU - evict *sampledLFU - itemsCh chan []uint64 - stop chan struct{} - isClosed bool - metrics *Metrics + admit *tinyLFU + evict *sampledLFU + itemsCh chan []uint64 + stop chan struct{} + isClosed bool + metrics *Metrics + numCounters int64 + maxCost int64 } func newDefaultPolicy(numCounters, maxCost int64) *defaultPolicy { p := &defaultPolicy{ - admit: newTinyLFU(numCounters), - evict: newSampledLFU(maxCost), - itemsCh: make(chan []uint64, 3), - stop: make(chan struct{}), + admit: newTinyLFU(numCounters), + evict: newSampledLFU(maxCost), + itemsCh: make(chan []uint64, 3), + stop: make(chan struct{}), + numCounters: numCounters, + maxCost: maxCost, } go p.processItems() return p @@ -246,8 +250,8 @@ func (p *defaultPolicy) Cost(key uint64) int64 { func (p *defaultPolicy) Clear() { p.Lock() - p.admit.clear() - p.evict.clear() + p.admit = newTinyLFU(p.numCounters) + p.evict = newSampledLFU(p.maxCost) p.Unlock() } @@ -412,6 +416,6 @@ func (p *tinyLFU) reset() { func (p *tinyLFU) clear() { p.incrs = 0 - p.door.Clear() p.freq.Clear() + p.door.Clear() } diff --git a/go/vt/vtgate/executor_vschema_ddl_test.go b/go/vt/vtgate/executor_vschema_ddl_test.go index b6346761cbe..2e974c47cdb 100644 --- a/go/vt/vtgate/executor_vschema_ddl_test.go +++ b/go/vt/vtgate/executor_vschema_ddl_test.go @@ -58,14 +58,14 @@ func waitForVindex(t *testing.T, ks, name string, watch chan *vschemapb.SrvVSche t.Errorf("vschema was not updated as expected") } - // Wait up to 10ms until the vindex manager gets notified of the update + // Wait up to 100ms until the vindex manager gets notified of the update for i := 0; i < 10; i++ { vschema := executor.vm.GetCurrentSrvVschema() vindex, ok := vschema.Keyspaces[ks].Vindexes[name] if ok { return vschema, vindex } - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) } t.Fatalf("updated vschema did not contain %s", name) @@ -75,7 +75,7 @@ func waitForVindex(t *testing.T, ks, name string, watch chan *vschemapb.SrvVSche func waitForVschemaTables(t *testing.T, ks string, tables []string, executor *Executor) *vschemapb.SrvVSchema { t.Helper() - // Wait up to 10ms until the vindex manager gets notified of the update + // Wait up to 100ms until the vindex manager gets notified of the update for i := 0; i < 10; i++ { vschema := executor.vm.GetCurrentSrvVschema() gotTables := []string{} @@ -87,7 +87,7 @@ func waitForVschemaTables(t *testing.T, ks string, tables []string, executor *Ex if reflect.DeepEqual(tables, gotTables) { return vschema } - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) } t.Fatalf("updated vschema did not contain tables %v", tables) From f681e00146a99692e2f745a0e63d0a17d930ba4d Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Tue, 2 Feb 2021 17:49:17 +0100 Subject: [PATCH 14/23] cache: make the cache implementation swappable Signed-off-by: Vicent Marti --- go/cache/cache.go | 75 +++++++- go/cache/lru_cache.go | 17 +- go/cache/lru_cache_test.go | 165 +++++++++--------- go/cache/null.go | 2 +- go/cache/perf_test.go | 6 +- go/cache/ristretto.go | 3 +- go/cache/ristretto/cache.go | 4 +- go/cache/ristretto/cache_test.go | 34 ++-- go/pools/numbered.go | 8 +- go/sync2/consolidator.go | 6 +- go/vt/vtexplain/vtexplain_vtgate.go | 4 +- go/vt/vtgate/engine/primitive.go | 2 +- go/vt/vtgate/executor.go | 6 +- go/vt/vtgate/executor_framework_test.go | 8 +- go/vt/vtgate/executor_scatter_stats_test.go | 2 + go/vt/vtgate/executor_select_test.go | 23 +-- go/vt/vtgate/executor_stream_test.go | 3 +- go/vt/vtgate/vtgate.go | 38 ++-- go/vt/vttablet/endtoend/config_test.go | 16 +- go/vt/vttablet/tabletserver/query_engine.go | 12 +- .../tabletserver/query_engine_test.go | 1 - go/vt/vttablet/tabletserver/queryz_test.go | 12 +- .../vttablet/tabletserver/tabletenv/config.go | 9 +- .../tabletserver/tabletenv/config_test.go | 6 +- 24 files changed, 283 insertions(+), 179 deletions(-) diff --git a/go/cache/cache.go b/go/cache/cache.go index b7ff081f0e3..eaf51f43498 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -16,11 +16,26 @@ limitations under the License. package cache +// DefaultCacheSize is the default size for a Vitess cache instance. +// If this value is specified in BYTES, Vitess will use a LFU-based cache that keeps track of the total +// memory usage of all cached entries accurately. If this value is specified in ENTRIES, Vitess will +// use the legacy LRU cache implementation which only tracks the amount of entries being stored. +// Changing this value affects: +// - the default values for CLI arguments in VTGate +// - the default values for the config files in VTTablet +// - the default values for the test caches used in integration and end-to-end tests +// Regardless of the default value used here, the user can always override Vitess' configuration to +// force a specific cache type (e.g. when passing a value in ENTRIES to vtgate, the service will use +// a LRU cache). +const DefaultCacheSize = SizeInBytes(64 * 1024 * 1024) + +// const DefaultCacheSize = SizeInEntries(10000) + // Cache is a generic interface type for a data structure that keeps recently used // objects in memory and evicts them when it becomes full. type Cache interface { Get(key string) (interface{}, bool) - Set(key string, val interface{}, valueSize int64) bool + Set(key string, val interface{}) bool ForEach(callback func(interface{}) bool) Delete(key string) @@ -34,11 +49,59 @@ type Cache interface { SetCapacity(int64) } -// NewDefaultCacheImpl returns the default cache implementation for Vitess, which at the moment -// is based on Ristretto -func NewDefaultCacheImpl(maxCost, averageItem int64) Cache { - if maxCost == 0 { +type cachedObject interface { + CachedSize(alloc bool) int64 +} + +// NewDefaultCacheImpl returns the default cache implementation for Vitess. If the given capacity +// is given in bytes, the implementation will be LFU-based and keep track of the total memory usage +// for the cache. If the implementation is given in entries, the legacy LRU implementation will be used, +// keeping track +func NewDefaultCacheImpl(capacity Capacity, averageItemSize int64) Cache { + switch { + case capacity == nil || (capacity.Entries() == 0 && capacity.Bytes() == 0): return &nullCache{} + + case capacity.Bytes() != 0: + return NewRistrettoCache(capacity.Bytes(), averageItemSize, func(val interface{}) int64 { + return val.(cachedObject).CachedSize(true) + }) + + default: + return NewLRUCache(capacity.Entries(), func(_ interface{}) int64 { + return 1 + }) } - return NewRistrettoCache(maxCost, averageItem) +} + +// Capacity is the interface implemented by numeric types that define a cache's capacity +type Capacity interface { + Bytes() int64 + Entries() int64 +} + +// SizeInBytes is a Capacity that measures the total size of the cache in Bytes +type SizeInBytes int64 + +// Bytes returns the size of the cache in Bytes +func (s SizeInBytes) Bytes() int64 { + return int64(s) +} + +// Entries returns 0 because this Capacity measures the cache size in Bytes +func (s SizeInBytes) Entries() int64 { + return 0 +} + +// SizeInEntries is a Capacity that measures the total size of the cache in Entries +type SizeInEntries int64 + +// Bytes returns 0 because this Capacity measures the cache size in Entries +func (s SizeInEntries) Bytes() int64 { + return 0 +} + +// Entries returns the size of the cache in Entries +func (s SizeInEntries) Entries() int64 { + return int64(s) } diff --git a/go/cache/lru_cache.go b/go/cache/lru_cache.go index e5578eef509..9175f942e94 100644 --- a/go/cache/lru_cache.go +++ b/go/cache/lru_cache.go @@ -41,6 +41,7 @@ type LRUCache struct { // list & table contain *entry objects. list *list.List table map[string]*list.Element + cost func(interface{}) int64 size int64 capacity int64 @@ -61,11 +62,12 @@ type entry struct { } // NewLRUCache creates a new empty cache with the given capacity. -func NewLRUCache(capacity int64) *LRUCache { +func NewLRUCache(capacity int64, cost func(interface{}) int64) *LRUCache { return &LRUCache{ list: list.New(), table: make(map[string]*list.Element), capacity: capacity, + cost: cost, } } @@ -84,14 +86,14 @@ func (lru *LRUCache) Get(key string) (v interface{}, ok bool) { } // Set sets a value in the cache. -func (lru *LRUCache) Set(key string, value interface{}, valueSize int64) bool { +func (lru *LRUCache) Set(key string, value interface{}) bool { lru.mu.Lock() defer lru.mu.Unlock() if element := lru.table[key]; element != nil { - lru.updateInplace(element, value, valueSize) + lru.updateInplace(element, value) } else { - lru.addNew(key, value, valueSize) + lru.addNew(key, value) } // the LRU cache cannot fail to insert items; it always returns true return true @@ -196,7 +198,8 @@ func (lru *LRUCache) Items() []Item { return items } -func (lru *LRUCache) updateInplace(element *list.Element, value interface{}, valueSize int64) { +func (lru *LRUCache) updateInplace(element *list.Element, value interface{}) { + valueSize := lru.cost(value) sizeDiff := valueSize - element.Value.(*entry).size element.Value.(*entry).value = value element.Value.(*entry).size = valueSize @@ -210,8 +213,8 @@ func (lru *LRUCache) moveToFront(element *list.Element) { element.Value.(*entry).timeAccessed = time.Now() } -func (lru *LRUCache) addNew(key string, value interface{}, valueSize int64) { - newEntry := &entry{key, value, valueSize, time.Now()} +func (lru *LRUCache) addNew(key string, value interface{}) { + newEntry := &entry{key, value, lru.cost(value), time.Now()} element := lru.list.PushFront(newEntry) lru.table[key] = element lru.size += newEntry.size diff --git a/go/cache/lru_cache_test.go b/go/cache/lru_cache_test.go index ed339af576d..152ac17ab6f 100644 --- a/go/cache/lru_cache_test.go +++ b/go/cache/lru_cache_test.go @@ -20,29 +20,36 @@ import ( "testing" ) -type CacheValue struct{} +type CacheValue struct { + size int64 +} + +func cacheValueSize(val interface{}) int64 { + return val.(*CacheValue).size +} func TestInitialState(t *testing.T) { - cache := NewLRUCache(5) - if cache.Len() != 0 { - t.Errorf("length = %v, want 0", cache.Len()) + cache := NewLRUCache(5, cacheValueSize) + l, sz, c, e := cache.Len(), cache.UsedCapacity(), cache.MaxCapacity(), cache.Evictions() + if l != 0 { + t.Errorf("length = %v, want 0", l) } - if cache.UsedCapacity() != 0 { - t.Errorf("size = %v, want 0", cache.UsedCapacity()) + if sz != 0 { + t.Errorf("size = %v, want 0", sz) } - if cache.MaxCapacity() != 5 { - t.Errorf("capacity = %v, want 5", cache.MaxCapacity()) + if c != 5 { + t.Errorf("capacity = %v, want 5", c) } - if cache.Evictions() != 0 { - t.Errorf("evictions = %v, want 0", cache.Evictions()) + if e != 0 { + t.Errorf("evictions = %v, want 0", c) } } func TestSetInsertsValue(t *testing.T) { - cache := NewLRUCache(100) - data := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + data := &CacheValue{0} key := "key" - cache.Set(key, data, 0) + cache.Set(key, data) v, ok := cache.Get(key) if !ok || v.(*CacheValue) != data { @@ -56,10 +63,10 @@ func TestSetInsertsValue(t *testing.T) { } func TestGetValueWithMultipleTypes(t *testing.T) { - cache := NewLRUCache(100) - data := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + data := &CacheValue{0} key := "key" - cache.Set(key, data, 0) + cache.Set(key, data) v, ok := cache.Get("key") if !ok || v.(*CacheValue) != data { @@ -73,28 +80,28 @@ func TestGetValueWithMultipleTypes(t *testing.T) { } func TestSetUpdatesSize(t *testing.T) { - cache := NewLRUCache(100) - emptyValue := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + emptyValue := &CacheValue{0} key := "key1" - cache.Set(key, emptyValue, 0) - if size := cache.UsedCapacity(); size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0", size) + cache.Set(key, emptyValue) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0", sz) } - someValue := &CacheValue{} + someValue := &CacheValue{20} key = "key2" - cache.Set(key, someValue, 20) - if size := cache.UsedCapacity(); size != 20 { - t.Errorf("cache.CachedSize() = %v, expected 20", size) + cache.Set(key, someValue) + if sz := cache.UsedCapacity(); sz != 20 { + t.Errorf("cache.UsedCapacity() = %v, expected 20", sz) } } func TestSetWithOldKeyUpdatesValue(t *testing.T) { - cache := NewLRUCache(100) - emptyValue := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + emptyValue := &CacheValue{0} key := "key1" - cache.Set(key, emptyValue, 0) - someValue := &CacheValue{} - cache.Set(key, someValue, 20) + cache.Set(key, emptyValue) + someValue := &CacheValue{20} + cache.Set(key, someValue) v, ok := cache.Get(key) if !ok || v.(*CacheValue) != someValue { @@ -103,25 +110,25 @@ func TestSetWithOldKeyUpdatesValue(t *testing.T) { } func TestSetWithOldKeyUpdatesSize(t *testing.T) { - cache := NewLRUCache(100) - emptyValue := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + emptyValue := &CacheValue{0} key := "key1" - cache.Set(key, emptyValue, 0) + cache.Set(key, emptyValue) - if size := cache.UsedCapacity(); size != 0 { - t.Errorf("cache.CachedSize() = %v, expected %v", size, 0) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, 0) } - someValue := &CacheValue{} - cache.Set(key, someValue, 20) - expected := int64(20) - if size := cache.UsedCapacity(); size != expected { - t.Errorf("cache.CachedSize() = %v, expected %v", size, expected) + someValue := &CacheValue{20} + cache.Set(key, someValue) + expected := int64(someValue.size) + if sz := cache.UsedCapacity(); sz != expected { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, expected) } } func TestGetNonExistent(t *testing.T) { - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) if _, ok := cache.Get("notthere"); ok { t.Error("Cache returned a notthere value after no inserts.") @@ -129,22 +136,16 @@ func TestGetNonExistent(t *testing.T) { } func TestDelete(t *testing.T) { - cache := NewLRUCache(100) - value := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + value := &CacheValue{1} key := "key" - if cache.delete(key) { - t.Error("Item unexpectedly already in cache.") - } - - cache.Set(key, value, 1) + cache.Delete(key) + cache.Set(key, value) + cache.Delete(key) - if !cache.delete(key) { - t.Error("Expected item to be in cache.") - } - - if size := cache.UsedCapacity(); size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0", size) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0", sz) } if _, ok := cache.Get(key); ok { @@ -153,52 +154,60 @@ func TestDelete(t *testing.T) { } func TestClear(t *testing.T) { - cache := NewLRUCache(100) - value := &CacheValue{} + cache := NewLRUCache(100, cacheValueSize) + value := &CacheValue{1} key := "key" - cache.Set(key, value, 1) + cache.Set(key, value) cache.Clear() - if size := cache.UsedCapacity(); size != 0 { - t.Errorf("cache.CachedSize() = %v, expected 0 after Clear()", size) + if sz := cache.UsedCapacity(); sz != 0 { + t.Errorf("cache.UsedCapacity() = %v, expected 0 after Clear()", sz) } } func TestCapacityIsObeyed(t *testing.T) { size := int64(3) - cache := NewLRUCache(100) + cache := NewLRUCache(100, cacheValueSize) cache.SetCapacity(size) - value := &CacheValue{} + value := &CacheValue{1} // Insert up to the cache's capacity. - cache.Set("key1", value, 1) - cache.Set("key2", value, 1) - cache.Set("key3", value, 1) - if usedCap := cache.UsedCapacity(); usedCap != size { - t.Errorf("cache.CachedSize() = %v, expected %v", usedCap, size) + cache.Set("key1", value) + cache.Set("key2", value) + cache.Set("key3", value) + if sz := cache.UsedCapacity(); sz != size { + t.Errorf("cache.UsedCapacity() = %v, expected %v", sz, size) } // Insert one more; something should be evicted to make room. - cache.Set("key4", value, 1) - if cache.UsedCapacity() != size { - t.Errorf("post-evict cache.CachedSize() = %v, expected %v", cache.UsedCapacity(), size) + cache.Set("key4", value) + sz, evictions := cache.UsedCapacity(), cache.Evictions() + if sz != size { + t.Errorf("post-evict cache.UsedCapacity() = %v, expected %v", sz, size) } - if cache.Evictions() != 1 { - t.Errorf("post-evict cache.evictions = %v, expected 1", cache.Evictions()) + if evictions != 1 { + t.Errorf("post-evict cache.Evictions() = %v, expected 1", evictions) } + // Check various other stats - if cache.Len() != int(size) { - t.Errorf("cache.StatsJSON() returned bad length: %v", cache.Len()) + if l := cache.Len(); int64(l) != size { + t.Errorf("cache.Len() returned bad length: %v", l) + } + if s := cache.UsedCapacity(); s != size { + t.Errorf("cache.UsedCapacity() returned bad size: %v", s) + } + if c := cache.MaxCapacity(); c != size { + t.Errorf("cache.UsedCapacity() returned bad length: %v", c) } } func TestLRUIsEvicted(t *testing.T) { size := int64(3) - cache := NewLRUCache(size) + cache := NewLRUCache(size, cacheValueSize) - cache.Set("key1", &CacheValue{}, 1) - cache.Set("key2", &CacheValue{}, 1) - cache.Set("key3", &CacheValue{}, 1) + cache.Set("key1", &CacheValue{1}) + cache.Set("key2", &CacheValue{1}) + cache.Set("key3", &CacheValue{1}) // lru: [key3, key2, key1] // Look up the elements. This will rearrange the LRU ordering. @@ -207,7 +216,7 @@ func TestLRUIsEvicted(t *testing.T) { cache.Get("key1") // lru: [key1, key2, key3] - cache.Set("key0", &CacheValue{}, 1) + cache.Set("key0", &CacheValue{1}) // lru: [key0, key1, key2] // The least recently used one should have been evicted. diff --git a/go/cache/null.go b/go/cache/null.go index 87341f1fd4e..5ef0f13a8c7 100644 --- a/go/cache/null.go +++ b/go/cache/null.go @@ -25,7 +25,7 @@ func (n *nullCache) Get(_ string) (interface{}, bool) { } // Set is a no-op in the nullCache -func (n *nullCache) Set(_ string, _ interface{}, _ int64) bool { +func (n *nullCache) Set(_ string, _ interface{}) bool { return false } diff --git a/go/cache/perf_test.go b/go/cache/perf_test.go index 9873011e51b..95546f66c06 100644 --- a/go/cache/perf_test.go +++ b/go/cache/perf_test.go @@ -21,9 +21,11 @@ import ( ) func BenchmarkGet(b *testing.B) { - cache := NewLRUCache(64 * 1024 * 1024) + cache := NewLRUCache(64*1024*1024, func(val interface{}) int64 { + return int64(cap(val.([]byte))) + }) value := make([]byte, 1000) - cache.Set("stuff", value, int64(cap(value))) + cache.Set("stuff", value) for i := 0; i < b.N; i++ { val, ok := cache.Get("stuff") if !ok { diff --git a/go/cache/ristretto.go b/go/cache/ristretto.go index 3e3218df30c..396a63f23e3 100644 --- a/go/cache/ristretto.go +++ b/go/cache/ristretto.go @@ -7,12 +7,13 @@ import ( var _ Cache = &ristretto.Cache{} // NewRistrettoCache returns a Cache implementation based on Ristretto -func NewRistrettoCache(maxCost, averageItemSize int64) *ristretto.Cache { +func NewRistrettoCache(maxCost, averageItemSize int64, cost func(interface{}) int64) *ristretto.Cache { config := ristretto.Config{ NumCounters: (maxCost / averageItemSize) * 10, MaxCost: maxCost, BufferItems: 64, Metrics: true, + Cost: cost, } cache, err := ristretto.NewCache(&config) if err != nil { diff --git a/go/cache/ristretto/cache.go b/go/cache/ristretto/cache.go index 6238559f605..5f31aa09054 100644 --- a/go/cache/ristretto/cache.go +++ b/go/cache/ristretto/cache.go @@ -249,8 +249,8 @@ func (c *Cache) Get(key string) (interface{}, bool) { // To dynamically evaluate the items cost using the Config.Coster function, set // the cost parameter to 0 and Coster will be ran when needed in order to find // the items true cost. -func (c *Cache) Set(key string, value interface{}, cost int64) bool { - return c.SetWithTTL(key, value, cost, 0*time.Second) +func (c *Cache) Set(key string, value interface{}) bool { + return c.SetWithTTL(key, value, 0, 0*time.Second) } // SetWithTTL works like Set but adds a key-value pair to the cache that will expire diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go index e253f41c3ed..a54e5fa8cdc 100644 --- a/go/cache/ristretto/cache_test.go +++ b/go/cache/ristretto/cache_test.go @@ -27,7 +27,7 @@ func TestCacheKeyToHash(t *testing.T) { }, }) require.NoError(t, err) - if c.Set("1", 1, 1) { + if c.SetWithTTL("1", 1, 1, 0) { time.Sleep(wait) val, ok := c.Get("1") require.True(t, ok) @@ -71,7 +71,7 @@ func TestCacheMaxCost(t *testing.T) { } else { val = strings.Repeat("a", 1000) } - c.Set(key(), val, int64(2+len(val))) + c.SetWithTTL(key(), val, int64(2+len(val)), 0) } } } @@ -96,7 +96,7 @@ func TestUpdateMaxCost(t *testing.T) { }) require.NoError(t, err) require.Equal(t, int64(10), c.MaxCapacity()) - require.True(t, c.Set("1", 1, 1)) + require.True(t, c.SetWithTTL("1", 1, 1, 0)) time.Sleep(wait) _, ok := c.Get("1") // Set is rejected because the cost of the entry is too high @@ -106,7 +106,7 @@ func TestUpdateMaxCost(t *testing.T) { // Update the max cost of the cache and retry. c.SetCapacity(1000) require.Equal(t, int64(1000), c.MaxCapacity()) - require.True(t, c.Set("1", 1, 1)) + require.True(t, c.SetWithTTL("1", 1, 1, 0)) time.Sleep(wait) val, ok := c.Get("1") require.True(t, ok) @@ -149,7 +149,7 @@ func TestNilCache(t *testing.T) { require.False(t, ok) require.Nil(t, val) - require.False(t, c.Set("1", 1, 1)) + require.False(t, c.SetWithTTL("1", 1, 1, 0)) c.Delete("1") c.Clear() c.Close() @@ -177,7 +177,7 @@ func TestSetAfterClose(t *testing.T) { require.NotNil(t, c) c.Close() - require.False(t, c.Set("1", 1, 1)) + require.False(t, c.SetWithTTL("1", 1, 1, 0)) } func TestClearAfterClose(t *testing.T) { @@ -194,7 +194,7 @@ func TestGetAfterClose(t *testing.T) { require.NoError(t, err) require.NotNil(t, c) - require.True(t, c.Set("1", 1, 1)) + require.True(t, c.SetWithTTL("1", 1, 1, 0)) c.Close() _, ok := c.Get("2") @@ -206,7 +206,7 @@ func TestDelAfterClose(t *testing.T) { require.NoError(t, err) require.NotNil(t, c) - require.True(t, c.Set("1", 1, 1)) + require.True(t, c.SetWithTTL("1", 1, 1, 0)) c.Close() c.Delete("1") @@ -377,7 +377,7 @@ func TestCacheSet(t *testing.T) { retrySet(t, c, "1", 1, 1, 0) - c.Set("1", 2, 2) + c.SetWithTTL("1", 2, 2, 0) val, ok := c.store.Get(defaultStringHash("1")) require.True(t, ok) require.Equal(t, 2, val.(int)) @@ -393,13 +393,13 @@ func TestCacheSet(t *testing.T) { Cost: 1, } } - require.False(t, c.Set("2", 2, 1)) + require.False(t, c.SetWithTTL("2", 2, 1, 0)) require.Equal(t, uint64(1), c.Metrics.SetsDropped()) close(c.setBuf) close(c.stop) c = nil - require.False(t, c.Set("1", 1, 1)) + require.False(t, c.SetWithTTL("1", 1, 1, 0)) } func TestCacheInternalCost(t *testing.T) { @@ -521,7 +521,7 @@ func TestCacheDel(t *testing.T) { }) require.NoError(t, err) - c.Set("1", 1, 1) + c.SetWithTTL("1", 1, 1, 0) c.Delete("1") // The deletes and sets are pushed through the setbuf. It might be possible // that the delete is not processed before the following get is called. So @@ -567,7 +567,7 @@ func TestCacheClear(t *testing.T) { require.NoError(t, err) for i := 0; i < 10; i++ { - c.Set(strconv.Itoa(i), i, 1) + c.SetWithTTL(strconv.Itoa(i), i, 1, 0) } time.Sleep(wait) require.Equal(t, uint64(10), c.Metrics.KeysAdded()) @@ -593,7 +593,7 @@ func TestCacheMetrics(t *testing.T) { require.NoError(t, err) for i := 0; i < 10; i++ { - c.Set(strconv.Itoa(i), i, 1) + c.SetWithTTL(strconv.Itoa(i), i, 1, 0) } time.Sleep(wait) m := c.Metrics @@ -690,7 +690,7 @@ func TestCacheMetricsClear(t *testing.T) { }) require.NoError(t, err) - c.Set("1", 1, 1) + c.SetWithTTL("1", 1, 1, 0) stop := make(chan struct{}) go func() { for { @@ -757,7 +757,7 @@ func TestDropUpdates(t *testing.T) { for i := 0; i < 5*setBufSize; i++ { v := fmt.Sprintf("%0100d", i) // We're updating the same key. - if !c.Set("0", v, 1) { + if !c.SetWithTTL("0", v, 1, 0) { // The race condition doesn't show up without this sleep. time.Sleep(time.Microsecond) droppedMap[i] = struct{}{} @@ -766,7 +766,7 @@ func TestDropUpdates(t *testing.T) { // Wait for all the items to be processed. c.Wait() // This will cause eviction from the cache. - require.True(t, c.Set("1", nil, 10)) + require.True(t, c.SetWithTTL("1", nil, 10, 0)) c.Close() } diff --git a/go/pools/numbered.go b/go/pools/numbered.go index 32321441f2c..04cc5807d55 100644 --- a/go/pools/numbered.go +++ b/go/pools/numbered.go @@ -50,8 +50,10 @@ type unregistered struct { //NewNumbered creates a new numbered func NewNumbered() *Numbered { n := &Numbered{ - resources: make(map[int64]*numberedWrapper), - recentlyUnregistered: cache.NewLRUCache(1000), + resources: make(map[int64]*numberedWrapper), + recentlyUnregistered: cache.NewLRUCache(1000, func(_ interface{}) int64 { + return 1 + }), } n.empty = sync.NewCond(&n.mu) return n @@ -86,7 +88,7 @@ func (nu *Numbered) Unregister(id int64, reason string) { success := nu.unregister(id) if success { nu.recentlyUnregistered.Set( - fmt.Sprintf("%v", id), &unregistered{reason: reason, timeUnregistered: time.Now()}, 1) + fmt.Sprintf("%v", id), &unregistered{reason: reason, timeUnregistered: time.Now()}) } } diff --git a/go/sync2/consolidator.go b/go/sync2/consolidator.go index c530be0b1dc..d0515615763 100644 --- a/go/sync2/consolidator.go +++ b/go/sync2/consolidator.go @@ -94,7 +94,9 @@ type ConsolidatorCache struct { // NewConsolidatorCache creates a new cache with the given capacity. func NewConsolidatorCache(capacity int64) *ConsolidatorCache { - return &ConsolidatorCache{cache.NewLRUCache(capacity)} + return &ConsolidatorCache{cache.NewLRUCache(capacity, func(_ interface{}) int64 { + return 1 + })} } // Record increments the count for "query" by 1. @@ -104,7 +106,7 @@ func (cc *ConsolidatorCache) Record(query string) { v.(*ccount).add(1) } else { c := ccount(1) - cc.Set(query, &c, 1) + cc.Set(query, &c) } } diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index ee3f5e80fc5..bb4c888cdd6 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -23,6 +23,7 @@ import ( "context" "fmt" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" @@ -68,8 +69,7 @@ func initVtgateExecutor(vSchemaStr, ksShardMapStr string, opts *Options) error { vtgateSession.TargetString = opts.Target streamSize := 10 - queryPlanCacheSize := int64(64 * 1024 * 1024) - vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, queryPlanCacheSize) + vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, cache.DefaultCacheSize) return nil } diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index 5461ce32b39..27b96cd4451 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -47,7 +47,7 @@ const ( ListVarName = "__vals" // AveragePlanSize is the average size in bytes that a cached plan takes // when cached in memory - AveragePlanSize = 128 + AveragePlanSize = 2500 ) type ( diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 70d16d4abc0..14713ed400c 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -114,14 +114,14 @@ const pathScatterStats = "/debug/scatter_stats" const pathVSchema = "/debug/vschema" // NewExecutor creates a new Executor. -func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, queryPlanCacheSizeBytes int64) *Executor { +func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, cacheSize cache.Capacity) *Executor { e := &Executor{ serv: serv, cell: cell, resolver: resolver, scatterConn: resolver.scatterConn, txConn: resolver.scatterConn.txConn, - plans: cache.NewDefaultCacheImpl(queryPlanCacheSizeBytes, engine.AveragePlanSize), + plans: cache.NewDefaultCacheImpl(cacheSize, engine.AveragePlanSize), normalize: normalize, streamSize: streamSize, } @@ -1325,7 +1325,7 @@ func (e *Executor) getPlan(vcursor *vcursorImpl, sql string, comments sqlparser. return nil, err } if !skipQueryPlanCache && !sqlparser.SkipQueryPlanCacheDirective(statement) && sqlparser.CachePlan(statement) { - e.plans.Set(planKey, plan, plan.CachedSize(true)) + e.plans.Set(planKey, plan) } return plan, nil } diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go index 5889df4a7ae..6fe50822480 100644 --- a/go/vt/vtgate/executor_framework_test.go +++ b/go/vt/vtgate/executor_framework_test.go @@ -30,6 +30,7 @@ import ( "context" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/vt/discovery" @@ -304,7 +305,6 @@ var unshardedVSchema = ` const ( testBufferSize = 10 - testCacheSize = int64(64 * 1024 * 1024) ) type DestinationAnyShardPickerFirstShard struct{} @@ -398,7 +398,7 @@ func createLegacyExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandb bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -433,7 +433,7 @@ func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -453,7 +453,7 @@ func createCustomExecutor(vschema string) (executor *Executor, sbc1, sbc2, sbclo sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_MASTER, true, 1, nil) getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) return executor, sbc1, sbc2, sbclookup } diff --git a/go/vt/vtgate/executor_scatter_stats_test.go b/go/vt/vtgate/executor_scatter_stats_test.go index f1b18a6c080..12a591299cf 100644 --- a/go/vt/vtgate/executor_scatter_stats_test.go +++ b/go/vt/vtgate/executor_scatter_stats_test.go @@ -68,6 +68,8 @@ func TestScatterStatsHttpWriting(t *testing.T) { _, err = executor.Execute(context.Background(), "TestExecutorResultsExceeded", session, query4, nil) require.NoError(t, err) + executor.plans.Wait() + recorder := httptest.NewRecorder() executor.WriteScatterStats(recorder) diff --git a/go/vt/vtgate/executor_select_test.go b/go/vt/vtgate/executor_select_test.go index 79d92a4f50a..d51424e981e 100644 --- a/go/vt/vtgate/executor_select_test.go +++ b/go/vt/vtgate/executor_select_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/test/utils" "github.com/stretchr/testify/assert" @@ -1085,7 +1086,7 @@ func TestSelectScatter(t *testing.T) { sbc := hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1119,7 +1120,7 @@ func TestSelectScatterPartial(t *testing.T) { conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1176,7 +1177,7 @@ func TestStreamSelectScatter(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) sql := "select id from user" result, err := executorStream(executor, sql) @@ -1230,7 +1231,7 @@ func TestSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col1, col2 from user order by col2 desc" gotResult, err := executorExec(executor, query, nil) @@ -1301,7 +1302,7 @@ func TestSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col1, textcol from user order by textcol desc" gotResult, err := executorExec(executor, query, nil) @@ -1367,7 +1368,7 @@ func TestStreamSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select id, col from user order by col desc" gotResult, err := executorStream(executor, query) @@ -1429,7 +1430,7 @@ func TestStreamSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select id, textcol from user order by textcol desc" gotResult, err := executorStream(executor, query) @@ -1491,7 +1492,7 @@ func TestSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col, sum(foo) from user group by col" gotResult, err := executorExec(executor, query, nil) @@ -1554,7 +1555,7 @@ func TestStreamSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col, sum(foo) from user group by col" gotResult, err := executorStream(executor, query) @@ -1617,7 +1618,7 @@ func TestSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorExec(executor, query, nil) @@ -1689,7 +1690,7 @@ func TestStreamSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorStream(executor, query) diff --git a/go/vt/vtgate/executor_stream_test.go b/go/vt/vtgate/executor_stream_test.go index c4351b79581..2b2ea2277d9 100644 --- a/go/vt/vtgate/executor_stream_test.go +++ b/go/vt/vtgate/executor_stream_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/vt/discovery" querypb "vitess.io/vitess/go/vt/proto/query" @@ -59,7 +60,7 @@ func TestStreamSQLSharded(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, testCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) sql := "stream * from sharded_user_msgs" result, err := executorStreamMessages(executor, sql) diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index f5c87a4aeae..cfbb5a42f0b 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -29,6 +29,7 @@ import ( "context" "vitess.io/vitess/go/acl" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/stats" "vitess.io/vitess/go/tb" @@ -41,7 +42,6 @@ import ( "vitess.io/vitess/go/vt/srvtopo" "vitess.io/vitess/go/vt/topo/topoproto" "vitess.io/vitess/go/vt/vterrors" - "vitess.io/vitess/go/vt/vtgate/engine" "vitess.io/vitess/go/vt/vtgate/vtgateservice" @@ -53,16 +53,16 @@ import ( ) var ( - transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") - normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") - terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") - streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") - queryPlanCacheSize = flag.Int64("gate_query_cache_size", 0, "deprecated: gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - queryPlanCacheSizeBytes = flag.Int64("gate_query_cache_size_bytes", 64*1024*1024, "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") - maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") - warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") - defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") + transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") + normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") + terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") + streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") + queryPlanCacheSize = flag.Int64("gate_query_cache_size", cache.DefaultCacheSize.Entries(), "deprecated: gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + lfuQueryPlanCacheSizeBytes = flag.Int64("lfu_gate_query_cache_size_bytes", cache.DefaultCacheSize.Bytes(), "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") + maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") + warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") + defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") // TODO(deepthi): change these two vars to unexported and move to healthcheck.go when LegacyHealthcheck is removed @@ -180,14 +180,13 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) - // If the legacy queryPlanCacheSize is set, override the value of the new queryPlanCacheSizeBytes - // approximating the total size of the cache with the average size of an entry - if *queryPlanCacheSize != 0 { - *queryPlanCacheSizeBytes = *queryPlanCacheSize * engine.AveragePlanSize + var cacheSize cache.Capacity = cache.SizeInEntries(*queryPlanCacheSize) + if *lfuQueryPlanCacheSizeBytes != 0 { + cacheSize = cache.SizeInBytes(*lfuQueryPlanCacheSizeBytes) } rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSizeBytes), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), resolver: resolver, vsm: vsm, txConn: tc, @@ -505,8 +504,13 @@ func LegacyInit(ctx context.Context, hc discovery.LegacyHealthCheck, serv srvtop resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) + var cacheSize cache.Capacity = cache.SizeInEntries(*queryPlanCacheSize) + if *lfuQueryPlanCacheSizeBytes != 0 { + cacheSize = cache.SizeInBytes(*lfuQueryPlanCacheSizeBytes) + } + rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, *queryPlanCacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), resolver: resolver, vsm: vsm, txConn: tc, diff --git a/go/vt/vttablet/endtoend/config_test.go b/go/vt/vttablet/endtoend/config_test.go index 33fa23d771c..256ccaf1090 100644 --- a/go/vt/vttablet/endtoend/config_test.go +++ b/go/vt/vttablet/endtoend/config_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/sqltypes" querypb "vitess.io/vitess/go/vt/proto/query" topodatapb "vitess.io/vitess/go/vt/proto/topodata" @@ -176,9 +177,18 @@ func TestConsolidatorReplicasOnly(t *testing.T) { } func TestQueryPlanCache(t *testing.T) { - const cacheItemSize = 40 - const cachedPlanSize = 2275 + cacheItemSize - const cachePlanSize2 = 2254 + cacheItemSize + if cache.DefaultCacheSize.Bytes() != 0 { + const cacheItemSize = 40 + const cachedPlanSize = 2275 + cacheItemSize + const cachePlanSize2 = 2254 + cacheItemSize + testQueryPlanCache(t, cachedPlanSize, cachePlanSize2) + } else { + testQueryPlanCache(t, 1, 1) + } +} + +func testQueryPlanCache(t *testing.T, cachedPlanSize, cachePlanSize2 int) { + t.Helper() //sleep to avoid race between SchemaChanged event clearing out the plans cache which breaks this test time.Sleep(1 * time.Second) diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index 9f3b6efcefb..c0f46cdfe3c 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -53,7 +53,7 @@ import ( // AverageTabletPlanSize is the average size in bytes that a TabletPlan takes when // cached in memory -const AverageTabletPlanSize = 256 +const AverageTabletPlanSize = 4000 // TabletPlan wraps the planbuilder's exec plan to enforce additional rules // and track stats. @@ -165,15 +165,17 @@ type QueryEngine struct { // You must call this only once. func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { config := env.Config() - if config.QueryCacheSize != 0 { - config.QueryCacheSizeBytes = config.QueryCacheSize * AverageTabletPlanSize + + var cacheSize cache.Capacity = cache.SizeInEntries(config.QueryCacheSize) + if config.LFUQueryCacheSizeBytes != 0 { + cacheSize = cache.SizeInBytes(config.LFUQueryCacheSizeBytes) } qe := &QueryEngine{ env: env, se: se, tables: make(map[string]*schema.Table), - plans: cache.NewDefaultCacheImpl(int64(config.QueryCacheSizeBytes), AverageTabletPlanSize), + plans: cache.NewDefaultCacheImpl(cacheSize, AverageTabletPlanSize), queryRuleSources: rules.NewMap(), } @@ -332,7 +334,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats return plan, nil } if !skipQueryPlanCache && !sqlparser.SkipQueryPlanCacheDirective(statement) { - qe.plans.Set(sql, plan, plan.CachedSize(true)) + qe.plans.Set(sql, plan) } return plan, nil } diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index cc0655d7b6e..d597aa9475c 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -281,7 +281,6 @@ func TestStatsURL(t *testing.T) { func newTestQueryEngine(idleTimeout time.Duration, strict bool, dbcfgs *dbconfigs.DBConfigs) *QueryEngine { config := tabletenv.NewDefaultConfig() config.DB = dbcfgs - config.QueryCacheSizeBytes = 1 * 1024 * 1024 config.OltpReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.OlapReadPool.IdleTimeoutSeconds.Set(idleTimeout) config.TxPool.IdleTimeoutSeconds.Set(idleTimeout) diff --git a/go/vt/vttablet/tabletserver/queryz_test.go b/go/vt/vttablet/tabletserver/queryz_test.go index 7c7a9bce754..f61d3648cbc 100644 --- a/go/vt/vttablet/tabletserver/queryz_test.go +++ b/go/vt/vttablet/tabletserver/queryz_test.go @@ -46,7 +46,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan1.AddStats(10, 2*time.Second, 1*time.Second, 2, 0) - qe.plans.Set(query1, plan1, plan1.CachedSize(true)) + qe.plans.Set(query1, plan1) const query2 = "insert into test_table values 1" plan2 := &TabletPlan{ @@ -57,7 +57,7 @@ func TestQueryzHandler(t *testing.T) { }, } plan2.AddStats(1, 2*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(query2, plan2, plan2.CachedSize(true)) + qe.plans.Set(query2, plan2) const query3 = "show tables" plan3 := &TabletPlan{ @@ -68,8 +68,8 @@ func TestQueryzHandler(t *testing.T) { }, } plan3.AddStats(1, 75*time.Millisecond, 50*time.Millisecond, 1, 0) - qe.plans.Set(query3, plan3, plan3.CachedSize(true)) - qe.plans.Set("", (*TabletPlan)(nil), 1) + qe.plans.Set(query3, plan3) + qe.plans.Set("", (*TabletPlan)(nil)) hugeInsert := "insert into test_table values 0" for i := 1; i < 1000; i++ { @@ -83,8 +83,8 @@ func TestQueryzHandler(t *testing.T) { }, } plan4.AddStats(1, 1*time.Millisecond, 1*time.Millisecond, 1, 0) - qe.plans.Set(hugeInsert, plan4, plan4.CachedSize(true)) - qe.plans.Set("", (*TabletPlan)(nil), 1) + qe.plans.Set(hugeInsert, plan4) + qe.plans.Set("", (*TabletPlan)(nil)) // Wait for cache to settle qe.plans.Wait() diff --git a/go/vt/vttablet/tabletserver/tabletenv/config.go b/go/vt/vttablet/tabletserver/tabletenv/config.go index 7946f2a52f3..8fa7a9a0d69 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config.go @@ -24,6 +24,7 @@ import ( "github.com/golang/protobuf/proto" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/flagutil" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/vt/dbconfigs" @@ -103,7 +104,7 @@ func init() { flag.IntVar(¤tConfig.StreamBufferSize, "queryserver-config-stream-buffer-size", defaultConfig.StreamBufferSize, "query server stream buffer size, the maximum number of bytes sent from vttablet for each stream call. It's recommended to keep this value in sync with vtgate's stream_buffer_size.") flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "deprecated: query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - flag.IntVar(¤tConfig.QueryCacheSizeBytes, "queryserver-config-query-cache-size-bytes", defaultConfig.QueryCacheSizeBytes, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.Int64Var(¤tConfig.LFUQueryCacheSizeBytes, "queryserver-config-query-cache-size-bytes", defaultConfig.LFUQueryCacheSizeBytes, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") SecondsVar(¤tConfig.SchemaReloadIntervalSeconds, "queryserver-config-schema-reload-time", defaultConfig.SchemaReloadIntervalSeconds, "query server schema reload time, how often vttablet reloads schemas from underlying MySQL instance in seconds. vttablet keeps table schemas in its own memory and periodically refreshes it from MySQL. This config controls the reload time.") SecondsVar(¤tConfig.Oltp.QueryTimeoutSeconds, "queryserver-config-query-timeout", defaultConfig.Oltp.QueryTimeoutSeconds, "query server query timeout (in seconds), this is the query timeout in vttablet side. If a query takes more than this timeout, it will be killed.") SecondsVar(¤tConfig.OltpReadPool.TimeoutSeconds, "queryserver-config-query-pool-timeout", defaultConfig.OltpReadPool.TimeoutSeconds, "query server query pool timeout (in seconds), it is how long vttablet waits for a connection from the query pool. If set to 0 (default) then the overall query timeout is used instead.") @@ -244,7 +245,7 @@ type TabletConfig struct { PassthroughDML bool `json:"passthroughDML,omitempty"` StreamBufferSize int `json:"streamBufferSize,omitempty"` QueryCacheSize int `json:"queryCacheSize,omitempty"` - QueryCacheSizeBytes int `json:"queryCacheSizeBytes,omitempty"` + LFUQueryCacheSizeBytes int64 `json:"lfuQueryCacheSizeBytes,omitempty"` SchemaReloadIntervalSeconds Seconds `json:"schemaReloadIntervalSeconds,omitempty"` WatchReplication bool `json:"watchReplication,omitempty"` TrackSchemaVersions bool `json:"trackSchemaVersions,omitempty"` @@ -449,8 +450,8 @@ var defaultConfig = TabletConfig{ // great (the overhead makes the final packets on the wire about twice // bigger than this). StreamBufferSize: 32 * 1024, - QueryCacheSize: 0, - QueryCacheSizeBytes: 64 * 1024 * 1024, + QueryCacheSize: int(cache.DefaultCacheSize.Entries()), + LFUQueryCacheSizeBytes: cache.DefaultCacheSize.Bytes(), SchemaReloadIntervalSeconds: 30 * 60, MessagePostponeParallelism: 4, CacheResultFields: true, diff --git a/go/vt/vttablet/tabletserver/tabletenv/config_test.go b/go/vt/vttablet/tabletserver/tabletenv/config_test.go index 6519e5bf708..aecfbc18e25 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config_test.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/vt/dbconfigs" "vitess.io/vitess/go/yaml2" ) @@ -118,6 +119,7 @@ hotRowProtection: maxGlobalQueueSize: 1000 maxQueueSize: 20 mode: disable +lfuQueryCacheSizeBytes: 67108864 messagePostponeParallelism: 4 olapReadPool: idleTimeoutSeconds: 1800 @@ -130,7 +132,6 @@ oltpReadPool: idleTimeoutSeconds: 1800 maxWaiters: 5000 size: 16 -queryCacheSizeBytes: 67108864 replicationTracker: heartbeatIntervalSeconds: 0.25 mode: disable @@ -190,7 +191,8 @@ func TestFlags(t *testing.T) { MaxConcurrency: 5, }, StreamBufferSize: 32768, - QueryCacheSizeBytes: 64 * 1024 * 1024, + QueryCacheSize: int(cache.DefaultCacheSize.Entries()), + LFUQueryCacheSizeBytes: cache.DefaultCacheSize.Bytes(), SchemaReloadIntervalSeconds: 1800, TrackSchemaVersions: false, MessagePostponeParallelism: 4, From 7c0c56f3a8aee31ee2db10f9747bde806753a676 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 3 Feb 2021 11:40:02 +0100 Subject: [PATCH 15/23] cache: fix DropUpdates test Signed-off-by: Vicent Marti --- go/cache/ristretto/cache_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go index a54e5fa8cdc..0d71246f69e 100644 --- a/go/cache/ristretto/cache_test.go +++ b/go/cache/ristretto/cache_test.go @@ -749,7 +749,9 @@ func TestDropUpdates(t *testing.T) { BufferItems: 64, Metrics: true, OnEvict: func(item *Item) { - handler(nil, item.Value) + if item.Value != nil { + handler(nil, item.Value) + } }, }) require.NoError(t, err) From f7ad521c679026a3154dece7b75e5ef395aa2253 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 3 Feb 2021 14:58:15 +0100 Subject: [PATCH 16/23] cache: use the legacy LRU cache by default Signed-off-by: Vicent Marti --- go/cache/cache.go | 4 ++-- go/vt/vttablet/tabletserver/query_engine_test.go | 7 ++++++- go/vt/vttablet/tabletserver/tabletenv/config_test.go | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/go/cache/cache.go b/go/cache/cache.go index eaf51f43498..fae67d4d762 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -27,9 +27,9 @@ package cache // Regardless of the default value used here, the user can always override Vitess' configuration to // force a specific cache type (e.g. when passing a value in ENTRIES to vtgate, the service will use // a LRU cache). -const DefaultCacheSize = SizeInBytes(64 * 1024 * 1024) +const DefaultCacheSize = SizeInEntries(5000) -// const DefaultCacheSize = SizeInEntries(10000) +// const DefaultCacheSize = SizeInBytes(64 * 1024 * 1024) // Cache is a generic interface type for a data structure that keeps recently used // objects in memory and evicts them when it becomes full. diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index d597aa9475c..cdd6893dced 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -26,6 +26,7 @@ import ( "testing" "time" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/streamlog" "vitess.io/vitess/go/mysql/fakesqldb" @@ -166,7 +167,11 @@ func TestQueryPlanCache(t *testing.T) { ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - qe.SetQueryPlanCacheCap(1024) + if cache.DefaultCacheSize.Bytes() != 0 { + qe.SetQueryPlanCacheCap(1024) + } else { + qe.SetQueryPlanCacheCap(1) + } firstPlan, err := qe.GetPlan(ctx, logStats, firstQuery, false, false /* inReservedConn */) if err != nil { t.Fatal(err) diff --git a/go/vt/vttablet/tabletserver/tabletenv/config_test.go b/go/vt/vttablet/tabletserver/tabletenv/config_test.go index aecfbc18e25..2fadf975929 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config_test.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config_test.go @@ -119,7 +119,6 @@ hotRowProtection: maxGlobalQueueSize: 1000 maxQueueSize: 20 mode: disable -lfuQueryCacheSizeBytes: 67108864 messagePostponeParallelism: 4 olapReadPool: idleTimeoutSeconds: 1800 @@ -132,6 +131,7 @@ oltpReadPool: idleTimeoutSeconds: 1800 maxWaiters: 5000 size: 16 +queryCacheSize: 5000 replicationTracker: heartbeatIntervalSeconds: 0.25 mode: disable From 9fe7cc031a805ef50e14c8034f697fb5bf70c018 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Wed, 3 Feb 2021 17:36:17 +0100 Subject: [PATCH 17/23] cache: remove more unused features Signed-off-by: Vicent Marti --- go/cache/ristretto/cache.go | 64 ++++------- go/cache/ristretto/cache_test.go | 178 +++++++----------------------- go/cache/ristretto/policy.go | 1 + go/cache/ristretto/policy_test.go | 17 +++ go/cache/ristretto/ring.go | 1 + go/cache/ristretto/ring_test.go | 17 +++ go/cache/ristretto/sketch.go | 1 + go/cache/ristretto/sketch_test.go | 17 +++ go/cache/ristretto/store.go | 68 +++--------- go/cache/ristretto/store_test.go | 17 +++ go/cache/ristretto/ttl.go | 147 ------------------------ 11 files changed, 147 insertions(+), 381 deletions(-) delete mode 100644 go/cache/ristretto/ttl.go diff --git a/go/cache/ristretto/cache.go b/go/cache/ristretto/cache.go index 5f31aa09054..62f086f69c2 100644 --- a/go/cache/ristretto/cache.go +++ b/go/cache/ristretto/cache.go @@ -1,5 +1,6 @@ /* * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +66,7 @@ type Cache struct { // onReject is called when an item is rejected via admission policy. onReject itemCallback // onExit is called whenever a value goes out of scope from the cache. - onExit (func(interface{})) + onExit func(interface{}) // KeyToHash function is used to customize the key hashing algorithm. // Each key will be hashed using the provided function. If keyToHash value // is not set, the default keyToHash function is used. @@ -79,8 +80,6 @@ type Cache struct { // ignoreInternalCost dictates whether to ignore the cost of internally storing // the item in the cost calculation. ignoreInternalCost bool - // cleanupTicker is used to periodically check for entries whose TTL has passed. - cleanupTicker *time.Ticker // Metrics contains a running log of important statistics like hits, misses, // and dropped items. Metrics *Metrics @@ -150,13 +149,12 @@ const ( // Item is passed to setBuf so items can eventually be added to the cache. type Item struct { - flag itemFlag - Key uint64 - Conflict uint64 - Value interface{} - Cost int64 - Expiration int64 - wg *sync.WaitGroup + flag itemFlag + Key uint64 + Conflict uint64 + Value interface{} + Cost int64 + wg *sync.WaitGroup } // NewCache returns a new Cache instance and any configuration errors, if any. @@ -179,7 +177,6 @@ func NewCache(config *Config) (*Cache, error) { stop: make(chan struct{}), cost: config.Cost, ignoreInternalCost: config.IgnoreInternalCost, - cleanupTicker: time.NewTicker(time.Duration(bucketDurationSecs) * time.Second / 2), } cache.onExit = func(val interface{}) { if config.OnExit != nil && val != nil { @@ -246,42 +243,26 @@ func (c *Cache) Get(key string) (interface{}, bool) { // its determined that the key-value item isn't worth keeping, but otherwise the // item will be added and other items will be evicted in order to make room. // -// To dynamically evaluate the items cost using the Config.Coster function, set -// the cost parameter to 0 and Coster will be ran when needed in order to find -// the items true cost. +// The cost of the entry will be evaluated lazily by the cache's Cost function. func (c *Cache) Set(key string, value interface{}) bool { - return c.SetWithTTL(key, value, 0, 0*time.Second) + return c.SetWithCost(key, value, 0) } -// SetWithTTL works like Set but adds a key-value pair to the cache that will expire -// after the specified TTL (time to live) has passed. A zero value means the value never -// expires, which is identical to calling Set. A negative value is a no-op and the value -// is discarded. -func (c *Cache) SetWithTTL(key string, value interface{}, cost int64, ttl time.Duration) bool { +// SetWithCost works like Set but adds a key-value pair to the cache with a specific +// cost. The built-in Cost function will not be called to evaluate the object's cost +// and instead the given value will be used. +func (c *Cache) SetWithCost(key string, value interface{}, cost int64) bool { if c == nil || c.isClosed { return false } - var expiration int64 - switch { - case ttl == 0: - // No expiration. - break - case ttl < 0: - // Treat this a a no-op. - return false - default: - expiration = time.Now().Add(ttl).Unix() - } - keyHash, conflictHash := c.keyToHash(key) i := &Item{ - flag: itemNew, - Key: keyHash, - Conflict: conflictHash, - Value: value, - Cost: cost, - Expiration: expiration, + flag: itemNew, + Key: keyHash, + Conflict: conflictHash, + Value: value, + Cost: cost, } // cost is eventually updated. The expiration must also be immediately updated // to prevent items from being prematurely removed from the map. @@ -411,7 +392,10 @@ func (c *Cache) SetCapacity(maxCost int64) { // Evictions returns the number of evictions func (c *Cache) Evictions() int64 { // TODO - return 0 + if c == nil || c.Metrics == nil { + return 0 + } + return int64(c.Metrics.KeysEvicted()) } // ForEach yields all the values currently stored in the cache to the given callback. @@ -488,8 +472,6 @@ func (c *Cache) processItems() { _, val := c.store.Del(i.Key, i.Conflict) c.onExit(val) } - case <-c.cleanupTicker.C: - c.store.Cleanup(c.policy, onEvict) case <-c.stop: return } diff --git a/go/cache/ristretto/cache_test.go b/go/cache/ristretto/cache_test.go index 0d71246f69e..a070c6f785a 100644 --- a/go/cache/ristretto/cache_test.go +++ b/go/cache/ristretto/cache_test.go @@ -1,3 +1,20 @@ +/* + * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ristretto import ( @@ -27,7 +44,7 @@ func TestCacheKeyToHash(t *testing.T) { }, }) require.NoError(t, err) - if c.SetWithTTL("1", 1, 1, 0) { + if c.SetWithCost("1", 1, 1) { time.Sleep(wait) val, ok := c.Get("1") require.True(t, ok) @@ -71,7 +88,7 @@ func TestCacheMaxCost(t *testing.T) { } else { val = strings.Repeat("a", 1000) } - c.SetWithTTL(key(), val, int64(2+len(val)), 0) + c.SetWithCost(key(), val, int64(2+len(val))) } } } @@ -96,7 +113,7 @@ func TestUpdateMaxCost(t *testing.T) { }) require.NoError(t, err) require.Equal(t, int64(10), c.MaxCapacity()) - require.True(t, c.SetWithTTL("1", 1, 1, 0)) + require.True(t, c.SetWithCost("1", 1, 1)) time.Sleep(wait) _, ok := c.Get("1") // Set is rejected because the cost of the entry is too high @@ -106,7 +123,7 @@ func TestUpdateMaxCost(t *testing.T) { // Update the max cost of the cache and retry. c.SetCapacity(1000) require.Equal(t, int64(1000), c.MaxCapacity()) - require.True(t, c.SetWithTTL("1", 1, 1, 0)) + require.True(t, c.SetWithCost("1", 1, 1)) time.Sleep(wait) val, ok := c.Get("1") require.True(t, ok) @@ -149,7 +166,7 @@ func TestNilCache(t *testing.T) { require.False(t, ok) require.Nil(t, val) - require.False(t, c.SetWithTTL("1", 1, 1, 0)) + require.False(t, c.SetWithCost("1", 1, 1)) c.Delete("1") c.Clear() c.Close() @@ -177,7 +194,7 @@ func TestSetAfterClose(t *testing.T) { require.NotNil(t, c) c.Close() - require.False(t, c.SetWithTTL("1", 1, 1, 0)) + require.False(t, c.SetWithCost("1", 1, 1)) } func TestClearAfterClose(t *testing.T) { @@ -194,7 +211,7 @@ func TestGetAfterClose(t *testing.T) { require.NoError(t, err) require.NotNil(t, c) - require.True(t, c.SetWithTTL("1", 1, 1, 0)) + require.True(t, c.SetWithCost("1", 1, 1)) c.Close() _, ok := c.Get("2") @@ -206,7 +223,7 @@ func TestDelAfterClose(t *testing.T) { require.NoError(t, err) require.NotNil(t, c) - require.True(t, c.SetWithTTL("1", 1, 1, 0)) + require.True(t, c.SetWithCost("1", 1, 1)) c.Close() c.Delete("1") @@ -348,10 +365,10 @@ func TestCacheGet(t *testing.T) { require.Nil(t, val) } -// retrySet calls SetWithTTL until the item is accepted by the cache. -func retrySet(t *testing.T, c *Cache, key string, value int, cost int64, ttl time.Duration) { +// retrySet calls SetWithCost until the item is accepted by the cache. +func retrySet(t *testing.T, c *Cache, key string, value int, cost int64) { for { - if set := c.SetWithTTL(key, value, cost, ttl); !set { + if set := c.SetWithCost(key, value, cost); !set { time.Sleep(wait) continue } @@ -375,9 +392,9 @@ func TestCacheSet(t *testing.T) { }) require.NoError(t, err) - retrySet(t, c, "1", 1, 1, 0) + retrySet(t, c, "1", 1, 1) - c.SetWithTTL("1", 2, 2, 0) + c.SetWithCost("1", 2, 2) val, ok := c.store.Get(defaultStringHash("1")) require.True(t, ok) require.Equal(t, 2, val.(int)) @@ -393,13 +410,13 @@ func TestCacheSet(t *testing.T) { Cost: 1, } } - require.False(t, c.SetWithTTL("2", 2, 1, 0)) + require.False(t, c.SetWithCost("2", 2, 1)) require.Equal(t, uint64(1), c.Metrics.SetsDropped()) close(c.setBuf) close(c.stop) c = nil - require.False(t, c.SetWithTTL("1", 1, 1, 0)) + require.False(t, c.SetWithCost("1", 1, 1)) } func TestCacheInternalCost(t *testing.T) { @@ -413,106 +430,12 @@ func TestCacheInternalCost(t *testing.T) { // Get should return false because the cache's cost is too small to store the item // when accounting for the internal cost. - c.SetWithTTL("1", 1, 1, 0) + c.SetWithCost("1", 1, 1) time.Sleep(wait) _, ok := c.Get("1") require.False(t, ok) } -func TestRecacheWithTTL(t *testing.T) { - c, err := NewCache(&Config{ - NumCounters: 100, - MaxCost: 10, - IgnoreInternalCost: true, - BufferItems: 64, - Metrics: true, - }) - - require.NoError(t, err) - - // Set initial value for key = 1 - insert := c.SetWithTTL("1", 1, 1, 5*time.Second) - require.True(t, insert) - time.Sleep(2 * time.Second) - - // Get value from cache for key = 1 - val, ok := c.Get("1") - require.True(t, ok) - require.NotNil(t, val) - require.Equal(t, 1, val) - - // Wait for expiration - time.Sleep(5 * time.Second) - - // The cached value for key = 1 should be gone - val, ok = c.Get("1") - require.False(t, ok) - require.Nil(t, val) - - // Set new value for key = 1 - insert = c.SetWithTTL("1", 2, 1, 5*time.Second) - require.True(t, insert) - time.Sleep(2 * time.Second) - - // Get value from cache for key = 1 - val, ok = c.Get("1") - require.True(t, ok) - require.NotNil(t, val) - require.Equal(t, 2, val) -} - -func TestCacheSetWithTTL(t *testing.T) { - m := &sync.Mutex{} - evicted := make(map[uint64]struct{}) - c, err := NewCache(&Config{ - NumCounters: 100, - MaxCost: 10, - IgnoreInternalCost: true, - BufferItems: 64, - Metrics: true, - OnEvict: func(item *Item) { - m.Lock() - defer m.Unlock() - evicted[item.Key] = struct{}{} - }, - }) - require.NoError(t, err) - - retrySet(t, c, "1", 1, 1, time.Second) - - // Sleep to make sure the item has expired after execution resumes. - time.Sleep(2 * time.Second) - val, ok := c.Get("1") - require.False(t, ok) - require.Nil(t, val) - - // Sleep to ensure that the bucket where the item was stored has been cleared - // from the expiraton map. - time.Sleep(5 * time.Second) - m.Lock() - require.Equal(t, 1, len(evicted)) - evk, _ := defaultStringHash("1") - _, ok = evicted[evk] - require.True(t, ok) - m.Unlock() - - // Verify that expiration times are overwritten. - retrySet(t, c, "2", 1, 1, time.Second) - retrySet(t, c, "2", 2, 1, 100*time.Second) - time.Sleep(3 * time.Second) - val, ok = c.Get("2") - require.True(t, ok) - require.Equal(t, 2, val.(int)) - - // Verify that entries with no expiration are overwritten. - retrySet(t, c, "3", 1, 1, 0) - retrySet(t, c, "3", 2, 1, time.Second) - time.Sleep(3 * time.Second) - val, ok = c.Get("3") - require.False(t, ok) - require.Nil(t, val) -} - func TestCacheDel(t *testing.T) { c, err := NewCache(&Config{ NumCounters: 100, @@ -521,7 +444,7 @@ func TestCacheDel(t *testing.T) { }) require.NoError(t, err) - c.SetWithTTL("1", 1, 1, 0) + c.SetWithCost("1", 1, 1) c.Delete("1") // The deletes and sets are pushed through the setbuf. It might be possible // that the delete is not processed before the following get is called. So @@ -538,24 +461,6 @@ func TestCacheDel(t *testing.T) { c.Delete("1") } -func TestCacheDelWithTTL(t *testing.T) { - c, err := NewCache(&Config{ - NumCounters: 100, - MaxCost: 10, - IgnoreInternalCost: true, - BufferItems: 64, - }) - require.NoError(t, err) - retrySet(t, c, "3", 1, 1, 10*time.Second) - time.Sleep(1 * time.Second) - // Delete the item - c.Delete("3") - // Ensure the key is deleted. - val, ok := c.Get("3") - require.False(t, ok) - require.Nil(t, val) -} - func TestCacheClear(t *testing.T) { c, err := NewCache(&Config{ NumCounters: 100, @@ -567,7 +472,7 @@ func TestCacheClear(t *testing.T) { require.NoError(t, err) for i := 0; i < 10; i++ { - c.SetWithTTL(strconv.Itoa(i), i, 1, 0) + c.SetWithCost(strconv.Itoa(i), i, 1) } time.Sleep(wait) require.Equal(t, uint64(10), c.Metrics.KeysAdded()) @@ -593,7 +498,7 @@ func TestCacheMetrics(t *testing.T) { require.NoError(t, err) for i := 0; i < 10; i++ { - c.SetWithTTL(strconv.Itoa(i), i, 1, 0) + c.SetWithCost(strconv.Itoa(i), i, 1) } time.Sleep(wait) m := c.Metrics @@ -690,7 +595,7 @@ func TestCacheMetricsClear(t *testing.T) { }) require.NoError(t, err) - c.SetWithTTL("1", 1, 1, 0) + c.SetWithCost("1", 1, 1) stop := make(chan struct{}) go func() { for { @@ -709,11 +614,6 @@ func TestCacheMetricsClear(t *testing.T) { c.Metrics.Clear() } -func init() { - // Set bucketSizeSecs to 1 to avoid waiting too much during the tests. - bucketDurationSecs = 1 -} - // Regression test for bug https://github.com/dgraph-io/ristretto/issues/167 func TestDropUpdates(t *testing.T) { originalSetBugSize := setBufSize @@ -759,7 +659,7 @@ func TestDropUpdates(t *testing.T) { for i := 0; i < 5*setBufSize; i++ { v := fmt.Sprintf("%0100d", i) // We're updating the same key. - if !c.SetWithTTL("0", v, 1, 0) { + if !c.SetWithCost("0", v, 1) { // The race condition doesn't show up without this sleep. time.Sleep(time.Microsecond) droppedMap[i] = struct{}{} @@ -768,7 +668,7 @@ func TestDropUpdates(t *testing.T) { // Wait for all the items to be processed. c.Wait() // This will cause eviction from the cache. - require.True(t, c.SetWithTTL("1", nil, 10, 0)) + require.True(t, c.SetWithCost("1", nil, 10)) c.Close() } diff --git a/go/cache/ristretto/policy.go b/go/cache/ristretto/policy.go index b20b903309c..9ebf0b38d72 100644 --- a/go/cache/ristretto/policy.go +++ b/go/cache/ristretto/policy.go @@ -1,5 +1,6 @@ /* * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/go/cache/ristretto/policy_test.go b/go/cache/ristretto/policy_test.go index 5e5df1ac1af..c864b6c74d0 100644 --- a/go/cache/ristretto/policy_test.go +++ b/go/cache/ristretto/policy_test.go @@ -1,3 +1,20 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ristretto import ( diff --git a/go/cache/ristretto/ring.go b/go/cache/ristretto/ring.go index 5dbed4cc59c..afc2c1559f8 100644 --- a/go/cache/ristretto/ring.go +++ b/go/cache/ristretto/ring.go @@ -1,5 +1,6 @@ /* * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/go/cache/ristretto/ring_test.go b/go/cache/ristretto/ring_test.go index 1c729fc3260..0dbe962ccc6 100644 --- a/go/cache/ristretto/ring_test.go +++ b/go/cache/ristretto/ring_test.go @@ -1,3 +1,20 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ristretto import ( diff --git a/go/cache/ristretto/sketch.go b/go/cache/ristretto/sketch.go index f12add3aed5..ce0504a2a83 100644 --- a/go/cache/ristretto/sketch.go +++ b/go/cache/ristretto/sketch.go @@ -1,5 +1,6 @@ /* * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/go/cache/ristretto/sketch_test.go b/go/cache/ristretto/sketch_test.go index 62142901fbd..f0d523df559 100644 --- a/go/cache/ristretto/sketch_test.go +++ b/go/cache/ristretto/sketch_test.go @@ -1,3 +1,20 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ristretto import ( diff --git a/go/cache/ristretto/store.go b/go/cache/ristretto/store.go index c45f96718d7..44e5ad8b147 100644 --- a/go/cache/ristretto/store.go +++ b/go/cache/ristretto/store.go @@ -1,5 +1,6 @@ /* * Copyright 2019 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +19,13 @@ package ristretto import ( "sync" - "time" ) // TODO: Do we need this to be a separate struct from Item? type storeItem struct { - key uint64 - conflict uint64 - value interface{} - expiration int64 + key uint64 + conflict uint64 + value interface{} } // store is the interface fulfilled by all hash map implementations in this @@ -38,8 +37,6 @@ type storeItem struct { type store interface { // Get returns the value associated with the key parameter. Get(uint64, uint64) (interface{}, bool) - // Expiration returns the expiration time for this key. - Expiration(uint64) int64 // Set adds the key-value pair to the Map or updates the value if it's // already present. The key-value pair is passed as a pointer to an // item object. @@ -49,8 +46,6 @@ type store interface { // Update attempts to update the key with a new value and returns true if // successful. Update(*Item) (interface{}, bool) - // Cleanup removes items that have an expired TTL. - Cleanup(policy policy, onEvict itemCallback) // Clear clears all contents of the store. Clear(onEvict itemCallback) // ForEach yields all the values in the store @@ -67,17 +62,15 @@ func newStore() store { const numShards uint64 = 256 type shardedMap struct { - shards []*lockedMap - expiryMap *expirationMap + shards []*lockedMap } func newShardedMap() *shardedMap { sm := &shardedMap{ - shards: make([]*lockedMap, int(numShards)), - expiryMap: newExpirationMap(), + shards: make([]*lockedMap, int(numShards)), } for i := range sm.shards { - sm.shards[i] = newLockedMap(sm.expiryMap) + sm.shards[i] = newLockedMap() } return sm } @@ -86,10 +79,6 @@ func (sm *shardedMap) Get(key, conflict uint64) (interface{}, bool) { return sm.shards[key%numShards].get(key, conflict) } -func (sm *shardedMap) Expiration(key uint64) int64 { - return sm.shards[key%numShards].Expiration(key) -} - func (sm *shardedMap) Set(i *Item) { if i == nil { // If item is nil make this Set a no-op. @@ -107,10 +96,6 @@ func (sm *shardedMap) Update(newItem *Item) (interface{}, bool) { return sm.shards[newItem.Key%numShards].Update(newItem) } -func (sm *shardedMap) Cleanup(policy policy, onEvict itemCallback) { - sm.expiryMap.cleanup(sm, policy, onEvict) -} - func (sm *shardedMap) ForEach(forEach func(interface{}) bool) { for _, shard := range sm.shards { if !shard.foreach(forEach) { @@ -136,13 +121,11 @@ func (sm *shardedMap) Clear(onEvict itemCallback) { type lockedMap struct { sync.RWMutex data map[uint64]storeItem - em *expirationMap } -func newLockedMap(em *expirationMap) *lockedMap { +func newLockedMap() *lockedMap { return &lockedMap{ data: make(map[uint64]storeItem), - em: em, } } @@ -156,20 +139,9 @@ func (m *lockedMap) get(key, conflict uint64) (interface{}, bool) { if conflict != 0 && (conflict != item.conflict) { return nil, false } - - // Handle expired items. - if item.expiration != 0 && time.Now().Unix() > item.expiration { - return nil, false - } return item.value, true } -func (m *lockedMap) Expiration(key uint64) int64 { - m.RLock() - defer m.RUnlock() - return m.data[key].expiration -} - func (m *lockedMap) Set(i *Item) { if i == nil { // If the item is nil make this Set a no-op. @@ -186,18 +158,12 @@ func (m *lockedMap) Set(i *Item) { if i.Conflict != 0 && (i.Conflict != item.conflict) { return } - m.em.update(i.Key, i.Conflict, item.expiration, i.Expiration) - } else { - // The value is not in the map already. There's no need to return anything. - // Simply add the expiration map. - m.em.add(i.Key, i.Conflict, i.Expiration) } m.data[i.Key] = storeItem{ - key: i.Key, - conflict: i.Conflict, - value: i.Value, - expiration: i.Expiration, + key: i.Key, + conflict: i.Conflict, + value: i.Value, } } @@ -213,10 +179,6 @@ func (m *lockedMap) Del(key, conflict uint64) (uint64, interface{}) { return 0, nil } - if item.expiration != 0 { - m.em.del(key, item.expiration) - } - delete(m.data, key) m.Unlock() return item.conflict, item.value @@ -234,12 +196,10 @@ func (m *lockedMap) Update(newItem *Item) (interface{}, bool) { return nil, false } - m.em.update(newItem.Key, newItem.Conflict, item.expiration, newItem.Expiration) m.data[newItem.Key] = storeItem{ - key: newItem.Key, - conflict: newItem.Conflict, - value: newItem.Value, - expiration: newItem.Expiration, + key: newItem.Key, + conflict: newItem.Conflict, + value: newItem.Value, } m.Unlock() diff --git a/go/cache/ristretto/store_test.go b/go/cache/ristretto/store_test.go index 7f9aa052b3f..54634736a72 100644 --- a/go/cache/ristretto/store_test.go +++ b/go/cache/ristretto/store_test.go @@ -1,3 +1,20 @@ +/* + * Copyright 2020 Dgraph Labs, Inc. and Contributors + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package ristretto import ( diff --git a/go/cache/ristretto/ttl.go b/go/cache/ristretto/ttl.go deleted file mode 100644 index 40a91bc1e51..00000000000 --- a/go/cache/ristretto/ttl.go +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2020 Dgraph Labs, Inc. and Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ristretto - -import ( - "sync" - "time" -) - -var ( - // TODO: find the optimal value or make it configurable. - bucketDurationSecs = int64(5) -) - -func storageBucket(t int64) int64 { - return (t / bucketDurationSecs) + 1 -} - -func cleanupBucket(t int64) int64 { - // The bucket to cleanup is always behind the storage bucket by one so that - // no elements in that bucket (which might not have expired yet) are deleted. - return storageBucket(t) - 1 -} - -// bucket type is a map of key to conflict. -type bucket map[uint64]uint64 - -// expirationMap is a map of bucket number to the corresponding bucket. -type expirationMap struct { - sync.RWMutex - buckets map[int64]bucket -} - -func newExpirationMap() *expirationMap { - return &expirationMap{ - buckets: make(map[int64]bucket), - } -} - -func (m *expirationMap) add(key, conflict uint64, expiration int64) { - if m == nil { - return - } - - // Items that don't expire don't need to be in the expiration map. - if expiration == 0 { - return - } - - bucketNum := storageBucket(expiration) - m.Lock() - defer m.Unlock() - - b, ok := m.buckets[bucketNum] - if !ok { - b = make(bucket) - m.buckets[bucketNum] = b - } - b[key] = conflict -} - -func (m *expirationMap) update(key, conflict uint64, oldExpTime, newExpTime int64) { - if m == nil { - return - } - - m.Lock() - defer m.Unlock() - - oldBucketNum := storageBucket(oldExpTime) - oldBucket, ok := m.buckets[oldBucketNum] - if ok { - delete(oldBucket, key) - } - - newBucketNum := storageBucket(newExpTime) - newBucket, ok := m.buckets[newBucketNum] - if !ok { - newBucket = make(bucket) - m.buckets[newBucketNum] = newBucket - } - newBucket[key] = conflict -} - -func (m *expirationMap) del(key uint64, expiration int64) { - if m == nil { - return - } - - bucketNum := storageBucket(expiration) - m.Lock() - defer m.Unlock() - _, ok := m.buckets[bucketNum] - if !ok { - return - } - delete(m.buckets[bucketNum], key) -} - -// cleanup removes all the items in the bucket that was just completed. It deletes -// those items from the store, and calls the onEvict function on those items. -// This function is meant to be called periodically. -func (m *expirationMap) cleanup(store store, policy policy, onEvict itemCallback) { - if m == nil { - return - } - - m.Lock() - now := time.Now().Unix() - bucketNum := cleanupBucket(now) - keys := m.buckets[bucketNum] - delete(m.buckets, bucketNum) - m.Unlock() - - for key, conflict := range keys { - // Sanity check. Verify that the store agrees that this key is expired. - if store.Expiration(key) > now { - continue - } - - cost := policy.Cost(key) - policy.Del(key) - _, value := store.Del(key, conflict) - - if onEvict != nil { - onEvict(&Item{Key: key, - Conflict: conflict, - Value: value, - Cost: cost, - }) - } - } -} From a3feeb8eca2311d0cd04e756c18971ddb33475c8 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Thu, 4 Feb 2021 11:02:28 +0100 Subject: [PATCH 18/23] cache: handle default arguments for both cache types Signed-off-by: Vicent Marti --- go/cache/cache.go | 25 +++++++++++++++++++++ go/vt/vtgate/vtgate.go | 12 ++-------- go/vt/vttablet/tabletserver/query_engine.go | 6 +---- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/go/cache/cache.go b/go/cache/cache.go index fae67d4d762..0404446178e 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -29,6 +29,31 @@ package cache // a LRU cache). const DefaultCacheSize = SizeInEntries(5000) +// GuessCapacity returns a Capacity value for a cache instance based on the defaults in Vitess +// and the options passed by the user +func GuessCapacity(inEntries, inBytes int64) Capacity { + switch { + // If the default cache has a byte capacity, only override it if the user has explicitly + // passed a capacity in entries + case DefaultCacheSize.Bytes() != 0: + if inEntries != 0 { + return SizeInEntries(inEntries) + } + return SizeInBytes(inBytes) + + // If the default cache has capacity in entries, only override it if the user has explicitly + // passed a capacity in bytes + case DefaultCacheSize.Entries() != 0: + if inBytes != 0 { + return SizeInBytes(inBytes) + } + return SizeInEntries(inEntries) + + default: + panic("DefaultCacheSize is not initialized") + } +} + // const DefaultCacheSize = SizeInBytes(64 * 1024 * 1024) // Cache is a generic interface type for a data structure that keeps recently used diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index cfbb5a42f0b..69dc9c68295 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -179,11 +179,7 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) - - var cacheSize cache.Capacity = cache.SizeInEntries(*queryPlanCacheSize) - if *lfuQueryPlanCacheSizeBytes != 0 { - cacheSize = cache.SizeInBytes(*lfuQueryPlanCacheSizeBytes) - } + cacheSize := cache.GuessCapacity(*queryPlanCacheSize, *lfuQueryPlanCacheSizeBytes) rpcVTGate = &VTGate{ executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), @@ -503,11 +499,7 @@ func LegacyInit(ctx context.Context, hc discovery.LegacyHealthCheck, serv srvtop srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) - - var cacheSize cache.Capacity = cache.SizeInEntries(*queryPlanCacheSize) - if *lfuQueryPlanCacheSizeBytes != 0 { - cacheSize = cache.SizeInBytes(*lfuQueryPlanCacheSizeBytes) - } + cacheSize := cache.GuessCapacity(*queryPlanCacheSize, *lfuQueryPlanCacheSizeBytes) rpcVTGate = &VTGate{ executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index c0f46cdfe3c..70a9fe893c7 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -165,11 +165,7 @@ type QueryEngine struct { // You must call this only once. func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { config := env.Config() - - var cacheSize cache.Capacity = cache.SizeInEntries(config.QueryCacheSize) - if config.LFUQueryCacheSizeBytes != 0 { - cacheSize = cache.SizeInBytes(config.LFUQueryCacheSizeBytes) - } + cacheSize := cache.GuessCapacity(int64(config.QueryCacheSize), config.LFUQueryCacheSizeBytes) qe := &QueryEngine{ env: env, From 15dae44bae7037911e1e5f330d9084221c85f3c6 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 5 Feb 2021 11:09:19 +0100 Subject: [PATCH 19/23] hack: document empty Goassembly file Signed-off-by: Vicent Marti --- go/hack/runtime.s | 3 +++ 1 file changed, 3 insertions(+) diff --git a/go/hack/runtime.s b/go/hack/runtime.s index e69de29bb2d..ac00d502ab5 100644 --- a/go/hack/runtime.s +++ b/go/hack/runtime.s @@ -0,0 +1,3 @@ +// DO NOT REMOVE: this empty goassembly file forces the Go compiler to perform +// external linking on the sibling `runtime.go`, so that the symbols declared in that +// file become properly resolved From a8b242d34225d7da9dfd75e1b560a9ef3ca33e57 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 5 Feb 2021 12:16:29 +0100 Subject: [PATCH 20/23] cache: allow configuring both entries & size Signed-off-by: Vicent Marti --- go/cache/cache.go | 95 +++++-------------- go/cache/ristretto.go | 9 +- go/vt/vtexplain/vtexplain_vtgate.go | 2 +- go/vt/vtgate/engine/primitive.go | 3 - go/vt/vtgate/executor.go | 4 +- go/vt/vtgate/executor_framework_test.go | 6 +- go/vt/vtgate/executor_select_test.go | 22 ++--- go/vt/vtgate/executor_stream_test.go | 2 +- go/vt/vtgate/vtgate.go | 37 +++++--- go/vt/vttablet/tabletserver/query_engine.go | 13 +-- .../vttablet/tabletserver/tabletenv/config.go | 13 ++- .../tabletserver/tabletenv/config_test.go | 6 +- 12 files changed, 89 insertions(+), 123 deletions(-) diff --git a/go/cache/cache.go b/go/cache/cache.go index 0404446178e..ef88de07602 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -16,46 +16,6 @@ limitations under the License. package cache -// DefaultCacheSize is the default size for a Vitess cache instance. -// If this value is specified in BYTES, Vitess will use a LFU-based cache that keeps track of the total -// memory usage of all cached entries accurately. If this value is specified in ENTRIES, Vitess will -// use the legacy LRU cache implementation which only tracks the amount of entries being stored. -// Changing this value affects: -// - the default values for CLI arguments in VTGate -// - the default values for the config files in VTTablet -// - the default values for the test caches used in integration and end-to-end tests -// Regardless of the default value used here, the user can always override Vitess' configuration to -// force a specific cache type (e.g. when passing a value in ENTRIES to vtgate, the service will use -// a LRU cache). -const DefaultCacheSize = SizeInEntries(5000) - -// GuessCapacity returns a Capacity value for a cache instance based on the defaults in Vitess -// and the options passed by the user -func GuessCapacity(inEntries, inBytes int64) Capacity { - switch { - // If the default cache has a byte capacity, only override it if the user has explicitly - // passed a capacity in entries - case DefaultCacheSize.Bytes() != 0: - if inEntries != 0 { - return SizeInEntries(inEntries) - } - return SizeInBytes(inBytes) - - // If the default cache has capacity in entries, only override it if the user has explicitly - // passed a capacity in bytes - case DefaultCacheSize.Entries() != 0: - if inBytes != 0 { - return SizeInBytes(inBytes) - } - return SizeInEntries(inEntries) - - default: - panic("DefaultCacheSize is not initialized") - } -} - -// const DefaultCacheSize = SizeInBytes(64 * 1024 * 1024) - // Cache is a generic interface type for a data structure that keeps recently used // objects in memory and evicts them when it becomes full. type Cache interface { @@ -65,6 +25,10 @@ type Cache interface { Delete(key string) Clear() + + // Wait waits for all pending operations on the cache to settle. Since cache writes + // are asynchronous, a write may not be immediately accessible unless the user + // manually calls Wait. Wait() Len() int @@ -82,51 +46,36 @@ type cachedObject interface { // is given in bytes, the implementation will be LFU-based and keep track of the total memory usage // for the cache. If the implementation is given in entries, the legacy LRU implementation will be used, // keeping track -func NewDefaultCacheImpl(capacity Capacity, averageItemSize int64) Cache { +func NewDefaultCacheImpl(cfg *Config) Cache { switch { - case capacity == nil || (capacity.Entries() == 0 && capacity.Bytes() == 0): + case cfg == nil || (cfg.MaxEntries == 0 && cfg.MaxMemoryUsage == 0): return &nullCache{} - case capacity.Bytes() != 0: - return NewRistrettoCache(capacity.Bytes(), averageItemSize, func(val interface{}) int64 { + case cfg.LFU: + return NewRistrettoCache(cfg.MaxEntries, cfg.MaxMemoryUsage, func(val interface{}) int64 { return val.(cachedObject).CachedSize(true) }) default: - return NewLRUCache(capacity.Entries(), func(_ interface{}) int64 { + return NewLRUCache(cfg.MaxEntries, func(_ interface{}) int64 { return 1 }) } } -// Capacity is the interface implemented by numeric types that define a cache's capacity -type Capacity interface { - Bytes() int64 - Entries() int64 -} - -// SizeInBytes is a Capacity that measures the total size of the cache in Bytes -type SizeInBytes int64 - -// Bytes returns the size of the cache in Bytes -func (s SizeInBytes) Bytes() int64 { - return int64(s) -} - -// Entries returns 0 because this Capacity measures the cache size in Bytes -func (s SizeInBytes) Entries() int64 { - return 0 -} - -// SizeInEntries is a Capacity that measures the total size of the cache in Entries -type SizeInEntries int64 - -// Bytes returns 0 because this Capacity measures the cache size in Entries -func (s SizeInEntries) Bytes() int64 { - return 0 +// Config is the configuration options for a cache instance +type Config struct { + // MaxEntries is the estimated amount of entries that the cache will hold at capacity + MaxEntries int64 + // MaxMemoryUsage is the maximum amount of memory the cache can handle + MaxMemoryUsage int64 + // LFU toggles whether to use a new cache implementation with a TinyLFU admission policy + LFU bool } -// Entries returns the size of the cache in Entries -func (s SizeInEntries) Entries() int64 { - return int64(s) +// DefaultConfig is the default configuration for a cache instance in Vitess +var DefaultConfig = &Config{ + MaxEntries: 5000, + MaxMemoryUsage: 32 * 1024 * 1024, + LFU: false, } diff --git a/go/cache/ristretto.go b/go/cache/ristretto.go index 396a63f23e3..29eb52fe692 100644 --- a/go/cache/ristretto.go +++ b/go/cache/ristretto.go @@ -7,9 +7,14 @@ import ( var _ Cache = &ristretto.Cache{} // NewRistrettoCache returns a Cache implementation based on Ristretto -func NewRistrettoCache(maxCost, averageItemSize int64, cost func(interface{}) int64) *ristretto.Cache { +func NewRistrettoCache(maxEntries, maxCost int64, cost func(interface{}) int64) *ristretto.Cache { + // The TinyLFU paper recommends to allocate 10x times the max entries amount as counters + // for the admission policy; since our caches are small and we're very interested on admission + // accuracy, we're a bit more greedy than 10x + const CounterRatio = 12 + config := ristretto.Config{ - NumCounters: (maxCost / averageItemSize) * 10, + NumCounters: maxEntries * CounterRatio, MaxCost: maxCost, BufferItems: 64, Metrics: true, diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index bb4c888cdd6..5696ead4d20 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -69,7 +69,7 @@ func initVtgateExecutor(vSchemaStr, ksShardMapStr string, opts *Options) error { vtgateSession.TargetString = opts.Target streamSize := 10 - vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, cache.DefaultCacheSize) + vtgateExecutor = vtgate.NewExecutor(context.Background(), explainTopo, vtexplainCell, resolver, opts.Normalize, streamSize, cache.DefaultConfig) return nil } diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go index a1746707ba5..467255943f9 100644 --- a/go/vt/vtgate/engine/primitive.go +++ b/go/vt/vtgate/engine/primitive.go @@ -45,9 +45,6 @@ const ( // This is used for sending different IN clause values // to different shards. ListVarName = "__vals" - // AveragePlanSize is the average size in bytes that a cached plan takes - // when cached in memory - AveragePlanSize = 2500 ) type ( diff --git a/go/vt/vtgate/executor.go b/go/vt/vtgate/executor.go index 07ff11949d2..cac28af3eff 100644 --- a/go/vt/vtgate/executor.go +++ b/go/vt/vtgate/executor.go @@ -114,14 +114,14 @@ const pathScatterStats = "/debug/scatter_stats" const pathVSchema = "/debug/vschema" // NewExecutor creates a new Executor. -func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, cacheSize cache.Capacity) *Executor { +func NewExecutor(ctx context.Context, serv srvtopo.Server, cell string, resolver *Resolver, normalize bool, streamSize int, cacheCfg *cache.Config) *Executor { e := &Executor{ serv: serv, cell: cell, resolver: resolver, scatterConn: resolver.scatterConn, txConn: resolver.scatterConn.txConn, - plans: cache.NewDefaultCacheImpl(cacheSize, engine.AveragePlanSize), + plans: cache.NewDefaultCacheImpl(cacheCfg), normalize: normalize, streamSize: streamSize, } diff --git a/go/vt/vtgate/executor_framework_test.go b/go/vt/vtgate/executor_framework_test.go index 6fe50822480..49ab3aa6caa 100644 --- a/go/vt/vtgate/executor_framework_test.go +++ b/go/vt/vtgate/executor_framework_test.go @@ -398,7 +398,7 @@ func createLegacyExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandb bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -433,7 +433,7 @@ func createExecutorEnv() (executor *Executor, sbc1, sbc2, sbclookup *sandboxconn bad.VSchema = badVSchema getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) key.AnyShardPicker = DestinationAnyShardPickerFirstShard{} return executor, sbc1, sbc2, sbclookup @@ -453,7 +453,7 @@ func createCustomExecutor(vschema string) (executor *Executor, sbc1, sbc2, sbclo sbclookup = hc.AddTestTablet(cell, "0", 1, KsTestUnsharded, "0", topodatapb.TabletType_MASTER, true, 1, nil) getSandbox(KsTestUnsharded).VSchema = unshardedVSchema - executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor = NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) return executor, sbc1, sbc2, sbclookup } diff --git a/go/vt/vtgate/executor_select_test.go b/go/vt/vtgate/executor_select_test.go index 509825ea3ea..75a9ef5bcce 100644 --- a/go/vt/vtgate/executor_select_test.go +++ b/go/vt/vtgate/executor_select_test.go @@ -1026,7 +1026,7 @@ func TestSelectScatter(t *testing.T) { sbc := hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1058,7 +1058,7 @@ func TestSelectScatterPartial(t *testing.T) { conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) logChan := QueryLogger.Subscribe("Test") defer QueryLogger.Unsubscribe(logChan) @@ -1115,7 +1115,7 @@ func TestStreamSelectScatter(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) sql := "select id from user" result, err := executorStream(executor, sql) @@ -1168,7 +1168,7 @@ func TestSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc" gotResult, err := executorExec(executor, query, nil) @@ -1233,7 +1233,7 @@ func TestSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, textcol from user order by textcol desc" gotResult, err := executorExec(executor, query, nil) @@ -1293,7 +1293,7 @@ func TestStreamSelectScatterOrderBy(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select id, col from user order by col desc" gotResult, err := executorStream(executor, query) @@ -1350,7 +1350,7 @@ func TestStreamSelectScatterOrderByVarChar(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select id, textcol from user order by textcol desc" gotResult, err := executorStream(executor, query) @@ -1407,7 +1407,7 @@ func TestSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col, sum(foo) from user group by col" gotResult, err := executorExec(executor, query, nil) @@ -1464,7 +1464,7 @@ func TestStreamSelectScatterAggregate(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col, sum(foo) from user group by col" gotResult, err := executorStream(executor, query) @@ -1522,7 +1522,7 @@ func TestSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorExec(executor, query, nil) @@ -1588,7 +1588,7 @@ func TestStreamSelectScatterLimit(t *testing.T) { }}) conns = append(conns, sbc) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) query := "select col1, col2 from user order by col2 desc limit 3" gotResult, err := executorStream(executor, query) diff --git a/go/vt/vtgate/executor_stream_test.go b/go/vt/vtgate/executor_stream_test.go index 2b2ea2277d9..c07ae62a0a7 100644 --- a/go/vt/vtgate/executor_stream_test.go +++ b/go/vt/vtgate/executor_stream_test.go @@ -60,7 +60,7 @@ func TestStreamSQLSharded(t *testing.T) { for _, shard := range shards { _ = hc.AddTestTablet(cell, shard, 1, "TestExecutor", shard, topodatapb.TabletType_MASTER, true, 1, nil) } - executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultCacheSize) + executor := NewExecutor(context.Background(), serv, cell, resolver, false, testBufferSize, cache.DefaultConfig) sql := "stream * from sharded_user_msgs" result, err := executorStreamMessages(executor, sql) diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go index 0e3f21f4059..32fbb701a24 100644 --- a/go/vt/vtgate/vtgate.go +++ b/go/vt/vtgate/vtgate.go @@ -53,16 +53,17 @@ import ( ) var ( - transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") - normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") - terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") - streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") - queryPlanCacheSize = flag.Int64("gate_query_cache_size", cache.DefaultCacheSize.Entries(), "deprecated: gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - lfuQueryPlanCacheSizeBytes = flag.Int64("lfu_gate_query_cache_size_bytes", cache.DefaultCacheSize.Bytes(), "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") - maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") - warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") - defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") + transactionMode = flag.String("transaction_mode", "MULTI", "SINGLE: disallow multi-db transactions, MULTI: allow multi-db transactions with best effort commit, TWOPC: allow multi-db transactions with 2pc commit") + normalizeQueries = flag.Bool("normalize_queries", true, "Rewrite queries with bind vars. Turn this off if the app itself sends normalized queries with bind vars.") + terseErrors = flag.Bool("vtgate-config-terse-errors", false, "prevent bind vars from escaping in returned errors") + streamBufferSize = flag.Int("stream_buffer_size", 32*1024, "the number of bytes sent from vtgate for each stream call. It's recommended to keep this value in sync with vttablet's query-server-config-stream-buffer-size.") + queryPlanCacheSize = flag.Int64("gate_query_cache_size", cache.DefaultConfig.MaxEntries, "gate server query cache size, maximum number of queries to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a cache. This config controls the expected amount of unique entries in the cache.") + queryPlanCacheMemory = flag.Int64("gate_query_cache_memory", cache.DefaultConfig.MaxMemoryUsage, "gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + queryPlanCacheLFU = flag.Bool("gate_query_cache_lfu", cache.DefaultConfig.LFU, "gate server cache algorithm. when set to true, a new cache algorithm based on a TinyLFU admission policy will be used to improve cache behavior and prevent pollution from sparse queries") + _ = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell") + maxMemoryRows = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.") + warnMemoryRows = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.") + defaultDDLStrategy = flag.String("ddl_strategy", string(schema.DDLStrategyDirect), "Set default strategy for DDL statements. Override with @@ddl_strategy session variable") // TODO(deepthi): change these two vars to unexported and move to healthcheck.go when LegacyHealthcheck is removed @@ -180,10 +181,14 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) - cacheSize := cache.GuessCapacity(*queryPlanCacheSize, *lfuQueryPlanCacheSizeBytes) + cacheCfg := &cache.Config{ + MaxEntries: *queryPlanCacheSize, + MaxMemoryUsage: *queryPlanCacheMemory, + LFU: *queryPlanCacheLFU, + } rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheCfg), resolver: resolver, vsm: vsm, txConn: tc, @@ -506,10 +511,14 @@ func LegacyInit(ctx context.Context, hc discovery.LegacyHealthCheck, serv srvtop srvResolver := srvtopo.NewResolver(serv, gw, cell) resolver := NewResolver(srvResolver, serv, cell, sc) vsm := newVStreamManager(srvResolver, serv, cell) - cacheSize := cache.GuessCapacity(*queryPlanCacheSize, *lfuQueryPlanCacheSizeBytes) + cacheCfg := &cache.Config{ + MaxEntries: *queryPlanCacheSize, + MaxMemoryUsage: *queryPlanCacheMemory, + LFU: *queryPlanCacheLFU, + } rpcVTGate = &VTGate{ - executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheSize), + executor: NewExecutor(ctx, serv, cell, resolver, *normalizeQueries, *streamBufferSize, cacheCfg), resolver: resolver, vsm: vsm, txConn: tc, diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index f135f28e301..7ea07cd40b3 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -51,10 +51,6 @@ import ( //_______________________________________________ -// AverageTabletPlanSize is the average size in bytes that a TabletPlan takes when -// cached in memory -const AverageTabletPlanSize = 4000 - // TabletPlan wraps the planbuilder's exec plan to enforce additional rules // and track stats. type TabletPlan struct { @@ -168,13 +164,17 @@ type QueryEngine struct { // You must call this only once. func NewQueryEngine(env tabletenv.Env, se *schema.Engine) *QueryEngine { config := env.Config() - cacheSize := cache.GuessCapacity(int64(config.QueryCacheSize), config.LFUQueryCacheSizeBytes) + cacheCfg := &cache.Config{ + MaxEntries: int64(config.QueryCacheSize), + MaxMemoryUsage: config.QueryCacheMemory, + LFU: config.QueryCacheLFU, + } qe := &QueryEngine{ env: env, se: se, tables: make(map[string]*schema.Table), - plans: cache.NewDefaultCacheImpl(cacheSize, AverageTabletPlanSize), + plans: cache.NewDefaultCacheImpl(cacheCfg), queryRuleSources: rules.NewMap(), } @@ -290,6 +290,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats defer span.Finish() if plan := qe.getQuery(sql); plan != nil { + logStats.Cached = true return plan, nil } diff --git a/go/vt/vttablet/tabletserver/tabletenv/config.go b/go/vt/vttablet/tabletserver/tabletenv/config.go index 8fa7a9a0d69..113126c157d 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config.go @@ -103,8 +103,9 @@ func init() { flag.BoolVar(&deprecateAllowUnsafeDMLs, "queryserver-config-allowunsafe-dmls", false, "deprecated") flag.IntVar(¤tConfig.StreamBufferSize, "queryserver-config-stream-buffer-size", defaultConfig.StreamBufferSize, "query server stream buffer size, the maximum number of bytes sent from vttablet for each stream call. It's recommended to keep this value in sync with vtgate's stream_buffer_size.") - flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "deprecated: query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") - flag.Int64Var(¤tConfig.LFUQueryCacheSizeBytes, "queryserver-config-query-cache-size-bytes", defaultConfig.LFUQueryCacheSizeBytes, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.IntVar(¤tConfig.QueryCacheSize, "queryserver-config-query-cache-size", defaultConfig.QueryCacheSize, "query server query cache size, maximum number of queries to be cached. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.Int64Var(¤tConfig.QueryCacheMemory, "queryserver-config-query-cache-memory", defaultConfig.QueryCacheMemory, "query server query cache size in bytes, maximum amount of memory to be used for caching. vttablet analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache.") + flag.BoolVar(¤tConfig.QueryCacheLFU, "queryserver-config-query-cache-lfu", defaultConfig.QueryCacheLFU, "query server cache algorithm. when set to true, a new cache algorithm based on a TinyLFU admission policy will be used to improve cache behavior and prevent pollution from sparse queries") SecondsVar(¤tConfig.SchemaReloadIntervalSeconds, "queryserver-config-schema-reload-time", defaultConfig.SchemaReloadIntervalSeconds, "query server schema reload time, how often vttablet reloads schemas from underlying MySQL instance in seconds. vttablet keeps table schemas in its own memory and periodically refreshes it from MySQL. This config controls the reload time.") SecondsVar(¤tConfig.Oltp.QueryTimeoutSeconds, "queryserver-config-query-timeout", defaultConfig.Oltp.QueryTimeoutSeconds, "query server query timeout (in seconds), this is the query timeout in vttablet side. If a query takes more than this timeout, it will be killed.") SecondsVar(¤tConfig.OltpReadPool.TimeoutSeconds, "queryserver-config-query-pool-timeout", defaultConfig.OltpReadPool.TimeoutSeconds, "query server query pool timeout (in seconds), it is how long vttablet waits for a connection from the query pool. If set to 0 (default) then the overall query timeout is used instead.") @@ -245,7 +246,8 @@ type TabletConfig struct { PassthroughDML bool `json:"passthroughDML,omitempty"` StreamBufferSize int `json:"streamBufferSize,omitempty"` QueryCacheSize int `json:"queryCacheSize,omitempty"` - LFUQueryCacheSizeBytes int64 `json:"lfuQueryCacheSizeBytes,omitempty"` + QueryCacheMemory int64 `json:"queryCacheMemory,omitempty"` + QueryCacheLFU bool `json:"queryCacheLFU,omitempty"` SchemaReloadIntervalSeconds Seconds `json:"schemaReloadIntervalSeconds,omitempty"` WatchReplication bool `json:"watchReplication,omitempty"` TrackSchemaVersions bool `json:"trackSchemaVersions,omitempty"` @@ -450,8 +452,9 @@ var defaultConfig = TabletConfig{ // great (the overhead makes the final packets on the wire about twice // bigger than this). StreamBufferSize: 32 * 1024, - QueryCacheSize: int(cache.DefaultCacheSize.Entries()), - LFUQueryCacheSizeBytes: cache.DefaultCacheSize.Bytes(), + QueryCacheSize: int(cache.DefaultConfig.MaxEntries), + QueryCacheMemory: cache.DefaultConfig.MaxMemoryUsage, + QueryCacheLFU: cache.DefaultConfig.LFU, SchemaReloadIntervalSeconds: 30 * 60, MessagePostponeParallelism: 4, CacheResultFields: true, diff --git a/go/vt/vttablet/tabletserver/tabletenv/config_test.go b/go/vt/vttablet/tabletserver/tabletenv/config_test.go index 2fadf975929..76d9bd6ac17 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/config_test.go +++ b/go/vt/vttablet/tabletserver/tabletenv/config_test.go @@ -131,6 +131,7 @@ oltpReadPool: idleTimeoutSeconds: 1800 maxWaiters: 5000 size: 16 +queryCacheMemory: 33554432 queryCacheSize: 5000 replicationTracker: heartbeatIntervalSeconds: 0.25 @@ -191,8 +192,9 @@ func TestFlags(t *testing.T) { MaxConcurrency: 5, }, StreamBufferSize: 32768, - QueryCacheSize: int(cache.DefaultCacheSize.Entries()), - LFUQueryCacheSizeBytes: cache.DefaultCacheSize.Bytes(), + QueryCacheSize: int(cache.DefaultConfig.MaxEntries), + QueryCacheMemory: cache.DefaultConfig.MaxMemoryUsage, + QueryCacheLFU: cache.DefaultConfig.LFU, SchemaReloadIntervalSeconds: 1800, TrackSchemaVersions: false, MessagePostponeParallelism: 4, From 37db725cc386b0c7f2eb00bfe0302aa7b34fb7f2 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 5 Feb 2021 12:17:29 +0100 Subject: [PATCH 21/23] hack: update license header Signed-off-by: Vicent Marti --- go/hack/runtime.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/hack/runtime.go b/go/hack/runtime.go index 418050dc5f9..c7355769307 100644 --- a/go/hack/runtime.go +++ b/go/hack/runtime.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Vitess Authors. +Copyright 2021 The Vitess Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 915e019952334c54fec3b0ac8786dc70a51d7145 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 5 Feb 2021 12:21:29 +0100 Subject: [PATCH 22/23] query_engine: add test for cache pollution Signed-off-by: Vicent Marti --- go/vt/vttablet/tabletserver/query_engine.go | 2 +- .../tabletserver/query_engine_test.go | 144 +++++++++++++++++- .../tabletserver/tabletenv/logstats.go | 1 + 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/go/vt/vttablet/tabletserver/query_engine.go b/go/vt/vttablet/tabletserver/query_engine.go index 7ea07cd40b3..67b6d4c68da 100644 --- a/go/vt/vttablet/tabletserver/query_engine.go +++ b/go/vt/vttablet/tabletserver/query_engine.go @@ -290,7 +290,7 @@ func (qe *QueryEngine) GetPlan(ctx context.Context, logStats *tabletenv.LogStats defer span.Finish() if plan := qe.getQuery(sql); plan != nil { - logStats.Cached = true + logStats.CachedPlan = true return plan, nil } diff --git a/go/vt/vttablet/tabletserver/query_engine_test.go b/go/vt/vttablet/tabletserver/query_engine_test.go index cdd6893dced..029fc363e31 100644 --- a/go/vt/vttablet/tabletserver/query_engine_test.go +++ b/go/vt/vttablet/tabletserver/query_engine_test.go @@ -19,13 +19,21 @@ package tabletserver import ( "context" "expvar" + "fmt" + "math/rand" "net/http" "net/http/httptest" + "os" + "path" "reflect" "strings" + "sync" + "sync/atomic" "testing" "time" + "github.com/stretchr/testify/require" + "vitess.io/vitess/go/cache" "vitess.io/vitess/go/streamlog" @@ -167,7 +175,7 @@ func TestQueryPlanCache(t *testing.T) { ctx := context.Background() logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") - if cache.DefaultCacheSize.Bytes() != 0 { + if cache.DefaultConfig.LFU { qe.SetQueryPlanCacheCap(1024) } else { qe.SetQueryPlanCacheCap(1) @@ -354,3 +362,137 @@ func TestConsolidationsUIRedaction(t *testing.T) { t.Fatalf("Response missing redacted consolidated query: %v %v", redactedSQL, redactedResponse.Body.String()) } } + +func TestPlanCachePollution(t *testing.T) { + plotPath := os.Getenv("CACHE_PLOT_PATH") + if plotPath == "" { + t.Skipf("CACHE_PLOT_PATH not set") + } + + const NormalQueries = 500000 + const PollutingQueries = NormalQueries / 2 + + db := fakesqldb.New(t) + defer db.Close() + + for query, result := range schematest.Queries() { + db.AddQuery(query, result) + } + + db.AddQueryPattern(".*", &sqltypes.Result{}) + + dbcfgs := newDBConfigs(db) + config := tabletenv.NewDefaultConfig() + config.DB = dbcfgs + // config.LFUQueryCacheSizeBytes = 3 * 1024 * 1024 + + env := tabletenv.NewEnv(config, "TabletServerTest") + se := schema.NewEngine(env) + qe := NewQueryEngine(env, se) + + se.InitDBConfig(dbcfgs.DbaWithDB()) + se.Open() + + qe.Open() + defer qe.Close() + + type Stats struct { + queries uint64 + cached uint64 + interval time.Duration + } + + var stats1, stats2 Stats + var wg sync.WaitGroup + + go func() { + cacheMode := "lru" + if config.QueryCacheLFU { + cacheMode = "lfu" + } + + out, err := os.Create(path.Join(plotPath, + fmt.Sprintf("cache_plot_%d_%d_%s.dat", + config.QueryCacheSize, config.QueryCacheMemory, cacheMode, + )), + ) + require.NoError(t, err) + defer out.Close() + + var last1 uint64 + var last2 uint64 + + for range time.Tick(100 * time.Millisecond) { + var avg1, avg2 time.Duration + + if stats1.queries-last1 > 0 { + avg1 = stats1.interval / time.Duration(stats1.queries-last1) + } + if stats2.queries-last2 > 0 { + avg2 = stats2.interval / time.Duration(stats2.queries-last2) + } + + stats1.interval = 0 + last1 = stats1.queries + stats2.interval = 0 + last2 = stats2.queries + + cacheUsed, cacheCap := qe.plans.UsedCapacity(), qe.plans.MaxCapacity() + + t.Logf("%d queries (%f hit rate), cache %d / %d (%f usage), %v %v", + stats1.queries+stats2.queries, + float64(stats1.cached)/float64(stats1.queries), + cacheUsed, cacheCap, + float64(cacheUsed)/float64(cacheCap), avg1, avg2) + + if out != nil { + fmt.Fprintf(out, "%d %f %f %f %f %d %d\n", + stats1.queries+stats2.queries, + float64(stats1.queries)/float64(NormalQueries), + float64(stats2.queries)/float64(PollutingQueries), + float64(stats1.cached)/float64(stats1.queries), + float64(cacheUsed)/float64(cacheCap), + avg1.Microseconds(), + avg2.Microseconds(), + ) + } + } + }() + + runner := func(totalQueries uint64, stats *Stats, sample func() string) { + for i := uint64(0); i < totalQueries; i++ { + ctx := context.Background() + logStats := tabletenv.NewLogStats(ctx, "GetPlanStats") + query := sample() + + start := time.Now() + _, err := qe.GetPlan(ctx, logStats, query, false, false /* inReservedConn */) + require.NoErrorf(t, err, "bad query: %s", query) + stats.interval += time.Since(start) + + atomic.AddUint64(&stats.queries, 1) + if logStats.CachedPlan { + atomic.AddUint64(&stats.cached, 1) + } + } + } + + wg.Add(2) + + go func() { + defer wg.Done() + runner(NormalQueries, &stats1, func() string { + return fmt.Sprintf("SELECT (a, b, c) FROM test_table_%d", rand.Intn(5000)) + }) + }() + + go func() { + defer wg.Done() + time.Sleep(500 * time.Millisecond) + runner(PollutingQueries, &stats2, func() string { + return fmt.Sprintf("INSERT INTO test_table_00 VALUES (1, 2, 3, %d)", rand.Int()) + }) + }() + + wg.Wait() +} diff --git a/go/vt/vttablet/tabletserver/tabletenv/logstats.go b/go/vt/vttablet/tabletserver/tabletenv/logstats.go index 5e63bd89db0..6fc131d74fa 100644 --- a/go/vt/vttablet/tabletserver/tabletenv/logstats.go +++ b/go/vt/vttablet/tabletserver/tabletenv/logstats.go @@ -61,6 +61,7 @@ type LogStats struct { TransactionID int64 ReservedID int64 Error error + CachedPlan bool } // NewLogStats constructs a new LogStats with supplied Method and ctx From 431da53b678ec904d047e33b2d7cd3f76d8e6e42 Mon Sep 17 00:00:00 2001 From: Vicent Marti Date: Fri, 5 Feb 2021 12:26:59 +0100 Subject: [PATCH 23/23] cache: review feedback Signed-off-by: Vicent Marti --- go/cache/cache.go | 7 +++---- go/vt/vtgate/planbuilder/plan_test.go | 4 +--- go/vt/vttablet/endtoend/config_test.go | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/go/cache/cache.go b/go/cache/cache.go index ef88de07602..3f81cec50e7 100644 --- a/go/cache/cache.go +++ b/go/cache/cache.go @@ -42,10 +42,9 @@ type cachedObject interface { CachedSize(alloc bool) int64 } -// NewDefaultCacheImpl returns the default cache implementation for Vitess. If the given capacity -// is given in bytes, the implementation will be LFU-based and keep track of the total memory usage -// for the cache. If the implementation is given in entries, the legacy LRU implementation will be used, -// keeping track +// NewDefaultCacheImpl returns the default cache implementation for Vitess. The options in the +// Config struct control the memory and entry limits for the cache, and the underlying cache +// implementation. func NewDefaultCacheImpl(cfg *Config) Cache { switch { case cfg == nil || (cfg.MaxEntries == 0 && cfg.MaxMemoryUsage == 0): diff --git a/go/vt/vtgate/planbuilder/plan_test.go b/go/vt/vtgate/planbuilder/plan_test.go index ecdc25ebf76..da769cf8231 100644 --- a/go/vt/vtgate/planbuilder/plan_test.go +++ b/go/vt/vtgate/planbuilder/plan_test.go @@ -630,9 +630,7 @@ func BenchmarkSelectVsDML(b *testing.B) { var selectCases []testCase for tc := range iterateExecFile("dml_cases.txt") { - if tc.output2ndPlanner != "" { - dmlCases = append(dmlCases, tc) - } + dmlCases = append(dmlCases, tc) } for tc := range iterateExecFile("select_cases.txt") { diff --git a/go/vt/vttablet/endtoend/config_test.go b/go/vt/vttablet/endtoend/config_test.go index 256ccaf1090..9848fd3cb26 100644 --- a/go/vt/vttablet/endtoend/config_test.go +++ b/go/vt/vttablet/endtoend/config_test.go @@ -177,7 +177,7 @@ func TestConsolidatorReplicasOnly(t *testing.T) { } func TestQueryPlanCache(t *testing.T) { - if cache.DefaultCacheSize.Bytes() != 0 { + if cache.DefaultConfig.LFU { const cacheItemSize = 40 const cachedPlanSize = 2275 + cacheItemSize const cachePlanSize2 = 2254 + cacheItemSize