mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-05 16:15:56 +00:00
This is a lot more complex than it sounds like it will be. We shut down the cache flush when a holder is closed, but if you're deleting an index, we don't check for that, and can have a cache flush still creating cache files in an index which could conceivably result in os.RemoteAll() failing. This shouldn't happen often, but it's happened at least once. To address this, first, we make sure that every tier of this operation bails as quickly as it can after the thing it's working on closes. Second, we retry RemoveAll. Unfortunately, some things get reopened, so we have to handle that, have mutexes covering the access to the channel, and so on. Also, some things were getting double-closed, which was previously harmless but could now fail. So, first, catch all the existing double-closes and remove them, second, make the double-close fail with an error. Note that virtually none of the tests check for errors on close. This passes tests and should be unable to hit the original problem. Unfortunately, it's unreasonably hard to check that, because it requires an incredible coincidence of timing on the delete aligning with a cache flush.
332 lines
9.3 KiB
Go
332 lines
9.3 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pilosa_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
pilosa "github.com/molecula/featurebase/v3"
|
|
"github.com/molecula/featurebase/v3/disco"
|
|
"github.com/molecula/featurebase/v3/pql"
|
|
"github.com/molecula/featurebase/v3/test"
|
|
"github.com/molecula/featurebase/v3/testhook"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ShardWidth is a helper reference to use when testing.
|
|
const ShardWidth = pilosa.ShardWidth
|
|
|
|
// Ensure index can open and retrieve a field.
|
|
func TestIndex_CreateFieldIfNotExists(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field.
|
|
f, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeDefault())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if f == nil {
|
|
t.Fatal("expected field")
|
|
}
|
|
|
|
// Retrieve existing field.
|
|
other, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeDefault())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if f.Field != other.Field {
|
|
t.Fatal("field mismatch")
|
|
}
|
|
|
|
if f.Field != index.Field("f") {
|
|
t.Fatal("field mismatch")
|
|
}
|
|
}
|
|
|
|
func TestIndex_CreateField(t *testing.T) {
|
|
// Ensure time quantum can be set appropriately on a new field.
|
|
t.Run("TimeQuantum", func(t *testing.T) {
|
|
t.Run("Explicit", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field with explicit quantum.
|
|
f, err := index.CreateField("f", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMDH"), "0"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if q := f.TimeQuantum(); q != pilosa.TimeQuantum("YMDH") {
|
|
t.Fatalf("unexpected field time quantum: %s", q)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Ensure time quantum can be set appropriately on a new field.
|
|
t.Run("TimeQuantumNoStandardView", func(t *testing.T) {
|
|
t.Run("Explicit", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field with explicit quantum with no standard view
|
|
f, err := index.CreateField("f", pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YMDH"), "0", true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if q := f.TimeQuantum(); q != pilosa.TimeQuantum("YMDH") {
|
|
t.Fatalf("unexpected field time quantum: %s", q)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Ensure field can include range columns.
|
|
t.Run("BSIFields", func(t *testing.T) {
|
|
t.Run("Int", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field with schema and verify it exists.
|
|
if f, err := index.CreateField("f", pilosa.OptFieldTypeInt(-990, 1000)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !reflect.DeepEqual(f.Type(), pilosa.FieldTypeInt) {
|
|
t.Fatalf("unexpected type: %#v", f.Type())
|
|
}
|
|
|
|
// Reopen the index & verify the fields are loaded.
|
|
if err := index.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
} else if f := index.Field("f"); !reflect.DeepEqual(f.Type(), pilosa.FieldTypeInt) {
|
|
t.Fatalf("unexpected type after reopen: %#v", f.Type())
|
|
}
|
|
})
|
|
|
|
t.Run("Timestamp", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field with schema and verify it exists.
|
|
if f, err := index.CreateField("f", pilosa.OptFieldTypeTimestamp(pilosa.DefaultEpoch, pilosa.TimeUnitSeconds)); err != nil {
|
|
t.Fatal(err)
|
|
} else if !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
|
|
t.Fatalf("unexpected type: %#v", f.Type())
|
|
}
|
|
|
|
// Reopen the index & verify the fields are loaded.
|
|
if err := index.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
} else if f := index.Field("f"); !reflect.DeepEqual(f.Type(), pilosa.FieldTypeTimestamp) {
|
|
t.Fatalf("unexpected type after reopen: %#v", f.Type())
|
|
}
|
|
})
|
|
|
|
// TODO: These errors don't apply here. Instead, we need these tests
|
|
// on field creation FieldOptions validation.
|
|
/*
|
|
t.Run("ErrRangeCacheAllowed", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
CacheType: pilosa.CacheTypeRanked,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("BSIFieldsWithCacheTypeNone", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
CacheType: pilosa.CacheTypeNone,
|
|
CacheSize: uint32(5),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrFieldFieldsAllowed", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: pilosa.FieldTypeInt},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrFieldNameRequired", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "", Type: pilosa.FieldTypeInt},
|
|
},
|
|
}); err != pilosa.ErrFieldNameRequired {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrInvalidFieldType", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: "bad_type"},
|
|
},
|
|
}); err != pilosa.ErrInvalidFieldType {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("ErrInvalidBSIGroupRange", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
if _, err := index.CreateField("f", pilosa.FieldOptions{
|
|
Fields: []*pilosa.Field{
|
|
{Name: "field0", Type: pilosa.FieldTypeInt, Min: 100, Max: 50},
|
|
},
|
|
}); err != pilosa.ErrInvalidBSIGroupRange {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
*/
|
|
})
|
|
|
|
t.Run("WithKeys", func(t *testing.T) {
|
|
// Don't allow an int field to be created with keys=true
|
|
t.Run("IntField", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
_, err := index.CreateField("f", pilosa.OptFieldTypeInt(-1, 1), pilosa.OptFieldKeys())
|
|
if errors.Cause(err) != pilosa.ErrIntFieldWithKeys {
|
|
t.Fatal("int field cannot be created with keys=true")
|
|
}
|
|
})
|
|
|
|
// Don't allow a decimal field to be created with keys=true
|
|
t.Run("DecimalField", func(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
_, err := index.CreateField("f", pilosa.OptFieldTypeDecimal(1, pql.Decimal{Value: -1}, pql.Decimal{Value: 1}), pilosa.OptFieldKeys())
|
|
if errors.Cause(err) != pilosa.ErrDecimalFieldWithKeys {
|
|
t.Fatal("decimal field cannot be created with keys=true")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// Ensure index can delete a field.
|
|
func TestIndex_DeleteField(t *testing.T) {
|
|
index := test.MustOpenIndex(t)
|
|
|
|
// Create field.
|
|
if _, err := index.CreateFieldIfNotExists("f", pilosa.OptFieldTypeDefault()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Delete field & verify it's gone.
|
|
if err := index.DeleteField("f"); err != nil {
|
|
t.Fatal(err)
|
|
} else if index.Field("f") != nil {
|
|
t.Fatal("expected nil field")
|
|
}
|
|
|
|
// Delete again to make sure it errors.
|
|
err := index.DeleteField("f")
|
|
if !isNotFoundError(err) {
|
|
t.Fatalf("expected 'field not found' error, got: %#v", err)
|
|
}
|
|
}
|
|
|
|
// Ensure index can validate its name.
|
|
func TestIndex_InvalidName(t *testing.T) {
|
|
path, err := testhook.TempDir(t, "pilosa-index-")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
index, err := pilosa.NewIndex(pilosa.NewHolder(path, mustHolderConfig()), path, "ABC")
|
|
if err == nil {
|
|
t.Fatalf("should have gotten an error on index name with caps")
|
|
}
|
|
if index != nil {
|
|
t.Fatalf("unexpected index name %v", index)
|
|
}
|
|
}
|
|
|
|
func isNotFoundError(err error) bool {
|
|
root := errors.Cause(err)
|
|
_, ok := root.(pilosa.NotFoundError)
|
|
return ok
|
|
}
|
|
|
|
// Ensure that after node/cluster restart, deleting and recreating a field
|
|
// does not cause a deadlock
|
|
// This is a regression test after a customer experienced the same deadlock.
|
|
// For details, check out https://molecula.atlassian.net/browse/CORE-919
|
|
func TestIndex_RecreateFieldOnRestart(t *testing.T) {
|
|
c := test.MustRunCluster(t, 1)
|
|
defer c.Close()
|
|
|
|
// create index
|
|
indexName := fmt.Sprintf("idx_%d", rand.Uint64())
|
|
holder := c.GetHolder(0)
|
|
_, err := holder.CreateIndex(indexName, pilosa.IndexOptions{
|
|
Keys: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// create field
|
|
fieldName := fmt.Sprintf("field_%d", rand.Uint64())
|
|
_, err = c.GetNode(0).API.CreateField(context.Background(), indexName, fieldName,
|
|
pilosa.OptFieldTypeDefault())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// set value
|
|
_, err = c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{
|
|
Index: indexName,
|
|
Query: fmt.Sprintf(`Set(1, %s=1)`, fieldName),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// restart node
|
|
node := c.GetNode(0)
|
|
if err := node.Reopen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.AwaitState(disco.ClusterStateNormal, 10*time.Second); err != nil {
|
|
t.Fatalf("restarting cluster: %v", err)
|
|
}
|
|
|
|
// delete field
|
|
err = c.GetNode(0).API.DeleteField(context.Background(), indexName, fieldName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// recreate field
|
|
errCh := make(chan error)
|
|
go func() {
|
|
_, err := c.GetNode(0).API.CreateField(context.Background(), indexName,
|
|
fieldName, pilosa.OptFieldTypeDefault())
|
|
errCh <- err
|
|
}()
|
|
select {
|
|
case <-time.After(10 * time.Second):
|
|
// We have to use os.Exit here instead of t.Fatal or panic since
|
|
// on panic, deferred statements are still ran. Given that
|
|
// we have deferred cluster.Close(), it deadlocks on the same
|
|
// issue this test is, well, is testing on.
|
|
// With os.Exit, the process exits at that point without running the
|
|
// deferred actions. This is more of a work-around fix to make the
|
|
// test meaningful on timeout.
|
|
t.Logf("recreating field took too long")
|
|
os.Exit(1)
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
}
|