mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-29 13:52:23 +00:00
When doing tests, we create a ton of one-off clusters. This turns out to be expensive and slow. Fixing it is surprisingly hard. Fundamentally: If we're sharing clusters, we need to use different indexes for each test, to avoid clashes. This changes index names. As a side-effect, this reorders many partition-based things, like the order keys are returned in. Thus, to fix this, we change a lot of tests to no longer depend on the *order* in which strings are returned. Having done that, we can also discard the ModHasher behavior, since that only existed to allow us to reliably predict partitioning. The basic design is as follows: Instead of a cluster being a []*Command, a "shareable" cluster is now a []*Command plus some flags, and a "cluster" is a pointer to a possibly-shared cluster, plus a link to the specific test using this specific cluster, and correspondingly, its test name suitably coerced to be a valid index name prefix. The "test.Cluster" object now has methods to allow retrieving an index name, and also implemnts fmt.Formatter to let you use, e.g., `%i` with it in Sprintf to get "the index name, plus an i". (This works for everything but %p and %T.) This allows us to consistently rework all the many things that use index names in a persistent way. We also have `MustUnshared` and `MustRunUnsharedCluster` methods which allow us to specify that a given test needs its own cluster for some reason. For instance, the tests that want to run backups need their own isolated cluster, and the tests that want to close or reopen nodes need their own cluster because a reopened cluster won't have working GRPC for some reason. On "closing" a shared cluster (actually the test-specific wrapper that reflects a given sharing), we delete any indexes starting with that test's index name prefix. Otherwise, the huge pile of open indexes prevents `go test -race` from working on MacOS, where we run out of address space too quickly. This is fairly enormous but most of the individual changes are fairly trivial things like replacing the string "i" with "c.Idx()". We also tweaked a test that failed for me a couple of times to not depend on sort order.
258 lines
7.9 KiB
Go
258 lines
7.9 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package stats_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
pilosa "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/featurebasedb/featurebase/v3/stats"
|
|
"github.com/featurebasedb/featurebase/v3/test"
|
|
)
|
|
|
|
// TestMultiStatClient_Expvar run the multistat client with exp var
|
|
// since the EXPVAR data is stored in a global we should run these in one test function
|
|
func TestMultiStatClient_Expvar(t *testing.T) {
|
|
hldr := test.MustOpenHolder(t)
|
|
defer hldr.Close()
|
|
|
|
c := stats.NewExpvarStatsClient()
|
|
ms := make(stats.MultiStatsClient, 1)
|
|
ms[0] = c
|
|
hldr.Stats = ms
|
|
|
|
hldr.SetBit("d", "f", 0, 0)
|
|
hldr.SetBit("d", "f", 0, 1)
|
|
hldr.SetBit("d", "f", 0, pilosa.ShardWidth)
|
|
hldr.SetBit("d", "f", 0, pilosa.ShardWidth+2)
|
|
hldr.ClearBit("d", "f", 0, 1)
|
|
|
|
indexStats := fmt.Sprintf(`{"%s": %d, "%s": %d}`, pilosa.MetricClearBit, 1, pilosa.MetricSetBit, 4)
|
|
|
|
if stats.Expvar.String() != `{"index:d": `+indexStats+`}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
hldr.Stats.CountWithCustomTags("cc", 1, 1.0, []string{"foo:bar"})
|
|
if stats.Expvar.String() != `{"cc": 1, "index:d": `+indexStats+`}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
// Gauge creates a unique key, subsequent Gauge calls will overwrite
|
|
hldr.Stats.Gauge("g", 5, 1.0)
|
|
hldr.Stats.Gauge("g", 8, 1.0)
|
|
if stats.Expvar.String() != `{"cc": 1, "g": 8, "index:d": `+indexStats+`}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
// Set creates a unique key, subsequent sets will overwrite
|
|
hldr.Stats.Set("s", "4", 1.0)
|
|
hldr.Stats.Set("s", "7", 1.0)
|
|
if stats.Expvar.String() != `{"cc": 1, "g": 8, "index:d": `+indexStats+`, "s": "7"}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
// Record timing duration and a uniquely Set key/value
|
|
dur, _ := time.ParseDuration("123us")
|
|
hldr.Stats.Timing("tt", dur, 1.0)
|
|
if stats.Expvar.String() != `{"cc": 1, "g": 8, "index:d": `+indexStats+`, "s": "7", "tt": 123µs}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
// Expvar histogram is implemented as a gauge
|
|
hldr.Stats.Histogram("hh", 3, 1.0)
|
|
if stats.Expvar.String() != `{"cc": 1, "g": 8, "hh": 3, "index:d": `+indexStats+`, "s": "7", "tt": 123µs}` {
|
|
t.Fatalf("unexpected expvar : %s", stats.Expvar.String())
|
|
}
|
|
|
|
// Expvar should ignore earlier set tags from setbit
|
|
if hldr.Stats.Tags() != nil {
|
|
t.Fatalf("unexpected tag")
|
|
}
|
|
}
|
|
|
|
func TestStatsCount_TopN(t *testing.T) {
|
|
c := test.MustRunCluster(t, 1)
|
|
defer c.Close()
|
|
hldr := test.Holder{Holder: c.GetNode(0).Server.Holder()}
|
|
|
|
// Execute query.
|
|
called := false
|
|
hldr.Holder.Stats = &MockStats{
|
|
mockCountWithTags: func(name string, value int64, rate float64, tags []string) {
|
|
if name != "query_topn_total" {
|
|
t.Errorf("Expected query_topn_total, Results %s", name)
|
|
}
|
|
|
|
if tags[0] != "index:d" {
|
|
t.Errorf("Expected index, Results %s", tags[0])
|
|
}
|
|
|
|
called = true
|
|
},
|
|
}
|
|
|
|
hldr.SetBit("d", "f", 0, 0)
|
|
hldr.SetBit("d", "f", 0, 1)
|
|
hldr.SetBit("d", "f", 0, pilosa.ShardWidth)
|
|
hldr.SetBit("d", "f", 0, pilosa.ShardWidth+2)
|
|
|
|
if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "d", Query: `TopN(field=f, n=2)`}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !called {
|
|
t.Error("CountWithCustomTags name isn't called")
|
|
}
|
|
}
|
|
|
|
func TestStatsCount_Bitmap(t *testing.T) {
|
|
// Cluster has to be unhsared because we're mocking the stats which writes
|
|
// to a holder in use by other tests.
|
|
c := test.MustRunUnsharedCluster(t, 1)
|
|
defer c.Close()
|
|
hldr := test.Holder{Holder: c.GetNode(0).Server.Holder()}
|
|
|
|
hldr.SetBit("d", "f", 0, 0)
|
|
hldr.SetBit("d", "f", 0, 1)
|
|
called := false
|
|
hldr.Holder.Stats = &MockStats{
|
|
mockCountWithTags: func(name string, value int64, rate float64, tags []string) {
|
|
if name != pilosa.MetricRow {
|
|
t.Errorf("Expected %s, Results %s", pilosa.MetricRow, name)
|
|
}
|
|
|
|
if tags[0] != "index:d" {
|
|
t.Errorf("Expected index, Results %s", tags[0])
|
|
}
|
|
|
|
called = true
|
|
},
|
|
}
|
|
if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "d", Query: `Row(f=0)`}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !called {
|
|
t.Error("CountWithCustomTags name isn't called")
|
|
}
|
|
}
|
|
|
|
func TestStatsCount_APICalls(t *testing.T) {
|
|
// We can't share a cluster when we're modifying its stats counter.
|
|
cluster := test.MustRunUnsharedCluster(t, 1)
|
|
defer cluster.Close()
|
|
cmd := cluster.GetNode(0)
|
|
h := cmd.Handler.(*pilosa.Handler).Handler
|
|
holder := cmd.Server.Holder()
|
|
hldr := test.Holder{Holder: holder}
|
|
|
|
t.Run("create index", func(t *testing.T) {
|
|
called := false
|
|
hldr.Stats = &MockStats{
|
|
mockCount: func(name string, value int64, rate float64) {
|
|
if name != pilosa.MetricCreateIndex {
|
|
t.Errorf("Expected %v, Results %s", pilosa.MetricCreateIndex, name)
|
|
}
|
|
called = true
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i", strings.NewReader("")))
|
|
if !called {
|
|
t.Error("Count isn't called")
|
|
}
|
|
})
|
|
|
|
t.Run("create field", func(t *testing.T) {
|
|
called := false
|
|
hldr.Stats = &MockStats{
|
|
mockCountWithTags: func(name string, value int64, rate float64, index []string) {
|
|
if name != pilosa.MetricCreateField {
|
|
t.Errorf("Expected %v, Results %s", pilosa.MetricCreateField, name)
|
|
}
|
|
if index[0] != "index:i" {
|
|
t.Errorf("Expected index:i, Results %s", index)
|
|
}
|
|
|
|
called = true
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i/field/f", strings.NewReader("")))
|
|
if !called {
|
|
t.Error("Count isn't called")
|
|
}
|
|
})
|
|
|
|
t.Run("delete field", func(t *testing.T) {
|
|
called := false
|
|
hldr.Stats = &MockStats{
|
|
mockCountWithTags: func(name string, value int64, rate float64, index []string) {
|
|
if name != pilosa.MetricDeleteField {
|
|
t.Errorf("Expected %v, Results %s", pilosa.MetricDeleteField, name)
|
|
}
|
|
if index[0] != "index:i" {
|
|
t.Errorf("Expected index:i, Results %s", index)
|
|
}
|
|
|
|
called = true
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/i/field/f", strings.NewReader("")))
|
|
if !called {
|
|
t.Error("Count isn't called")
|
|
}
|
|
})
|
|
|
|
t.Run("delete index", func(t *testing.T) {
|
|
called := false
|
|
hldr.Stats = &MockStats{
|
|
mockCount: func(name string, value int64, rate float64) {
|
|
if name != pilosa.MetricDeleteIndex {
|
|
t.Errorf("Expected %v, Results %s", pilosa.MetricDeleteIndex, name)
|
|
}
|
|
|
|
called = true
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, test.MustNewHTTPRequest("DELETE", "/index/i", strings.NewReader("")))
|
|
if !called {
|
|
t.Error("Count isn't called")
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
type MockStats struct {
|
|
mockCount func(name string, value int64, rate float64)
|
|
mockCountWithTags func(name string, value int64, rate float64, tags []string)
|
|
}
|
|
|
|
func (s *MockStats) Count(name string, value int64, rate float64) {
|
|
if s.mockCount != nil {
|
|
s.mockCount(name, value, rate)
|
|
}
|
|
}
|
|
|
|
func (s *MockStats) CountWithCustomTags(name string, value int64, rate float64, tags []string) {
|
|
if s.mockCountWithTags != nil {
|
|
s.mockCountWithTags(name, value, rate, tags)
|
|
}
|
|
}
|
|
|
|
func (c *MockStats) Tags() []string { return nil }
|
|
func (c *MockStats) WithTags(tags ...string) stats.StatsClient { return c }
|
|
func (c *MockStats) Gauge(name string, value float64, rate float64) {}
|
|
func (c *MockStats) Histogram(name string, value float64, rate float64) {}
|
|
func (c *MockStats) Set(name string, value string, rate float64) {}
|
|
func (c *MockStats) Timing(name string, value time.Duration, rate float64) {}
|
|
func (c *MockStats) SetLogger(logger logger.Logger) {}
|
|
func (c *MockStats) Open() {}
|
|
func (c *MockStats) Close() error { return nil }
|