featurebase/querycontext/txstore.go
Seebs 10edb62b83 initial implementation of RBF backend
This provides us with most of the existing Tx interface, split
across QueryRead and QueryWrite. The functions not included here
are the ones that are used *only* for anti-entropy (ForEach
and ForEachRange).

We add additional testing to verify that TxStores are getting
closed correctly, to go with cleaning up the test directories they're
made in.

We also introduce some test wrappers that can automatically
fail tests on error, so tests don't need to be full of error
checks.

Also, now that I'm starting to think more about the flow of
writing tests using QueryScope, we add the missing "full
database" scope option, and make the Add methods return
their operand so (1) you can chain them, (2) you can use
the AddIndex(...) inline in a NewWriteQueryContext.

Also addressed a plausible performance concern in shardList,
and some comments that were stale or incorrect.

The test coverage here is skimpy on the actual RBF-calling
functions because those are trivial. We do, however, significantly
expand coverage in the random write requests, which are now
a mix of random writes and random reads, and add test cases
that at least hit a lot of the error checks once.

The Error() method is changed to be like (testing.T).Error(),
taking ...interface{} and using fmt.Sprint on them.

There's also some minor tweaks such as making the visualizations
more consistent, testing visualization generation on two kinds
of keysplitter, and so on.

(cherry picked from commit 7b434cd11c)
2022-11-15 11:32:03 -08:00

177 lines
6.3 KiB
Go

// Copyright 2022 Molecula Corp (DBA FeatureBase). All rights reserved.
package querycontext
import (
"context"
"fmt"
"path/filepath"
"strings"
)
// TxStore represents a transactional database backend, mapping
// {index,field,view,shard} tuples to combinations of specific on-disk
// databases and keys to use with them to find related data.
//
// Each TxStore implements the KeySplitter interface, possibly by
// having a KeySplitter embedded in it. The KeySplitter used with the
// TxStore determines which database to use (the database key) and
// which part of that database to use (the fragment key) to access
// a given fragment.
type TxStore interface {
KeySplitter
// NewQueryContext yields a new query context which is read-only.
NewQueryContext(context.Context) (QueryContext, error)
// NewWriteQueryContext yields a new query context which can
// write to the things in the given QueryScope
NewWriteQueryContext(context.Context, QueryScope) (QueryContext, error)
// Close attempts to shut down. It can fail if there are still
// open transactions.
Close() error
}
// verify that rbfTxStore implements this interface
var _ TxStore = &rbfTxStore{}
// dbKey is an identifier which can distinguish backend databases.
type dbKey string
// fragKey is an identifier which can be used to tell a backend database
// which data to operate on.
type fragKey string
// KeySplitter knows how to convert fragment identifiers
// (index/field/view/shard) into database backends. This is handled
// by the unexported `keys` method. The exported method, Scope,
// produces a QueryScope which follows corresponding rules.
//
// Imagine that you have two QueryScopes A and B from the same TxStore,
// and two fragment identifiers such that A.Allowed(i1, f1, v1, s1)
// and B.Allowed(i2, f2, v2, s2) are both true. If the keys method
// produces the same database key for these two fragment identifiers,
// then the two query scopes are said to overlap. It doesn't matter
// whether the whole fragment identifiers are identical.
type KeySplitter interface {
// keys yields two strings that between them denote the
// index/field/view/shard provided. When two sets of parameters
// yield the same dbKey result, that means that they share a
// backing database, and thus that they must share a backing
// database transaction. The fragKey result is used by operations
// within the database backend.
keys(IndexName, FieldName, ViewName, ShardID) (dbKey, fragKey)
// dbPath yields a filesystem-friendly string that corresponds to dbKey.
// possibly it is identical to dbKey, but you might want a terse dbKey
// like "i/0" and a longer path like "indexes/i/shards/0".
dbPath(dbKey) (string, error)
// Scope() yields a new scope which is aware of this KeySplitter
// and will give correct results for Overlap calls.
Scope() QueryScope
}
var _ KeySplitter = &indexShardKeySplitter{}
var _ KeySplitter = &fieldShardKeySplitter{}
var _ KeySplitter = &flexibleKeySplitter{}
// indexShardKeySplitter splits the database by index,shard pairs
// (the default for RBF).
type indexShardKeySplitter struct{}
func (*indexShardKeySplitter) keys(index IndexName, field FieldName, view ViewName, shard ShardID) (dbKey, fragKey) {
d := dbKey(fmt.Sprintf("%s/%08x", index, shard))
t := fragKey(fmt.Sprintf("%s:%s", field, view))
return d, t
}
func (*indexShardKeySplitter) Scope() QueryScope {
return &indexShardQueryScope{}
}
func (*indexShardKeySplitter) dbPath(dbk dbKey) (string, error) {
slash := strings.IndexByte(string(dbk), '/')
if slash == -1 {
return "", fmt.Errorf("malformed dbKey %q", dbk)
}
paths := [4]string{"indexes", "", "shards", ""}
paths[1] = string(dbk)[:slash]
paths[3] = string(dbk)[slash+1:]
return filepath.Join(paths[:]...), nil
}
// fieldShardKeySplitter splits the database by field,shard pairs.
type fieldShardKeySplitter struct{}
func (*fieldShardKeySplitter) keys(index IndexName, field FieldName, view ViewName, shard ShardID) (dbKey, fragKey) {
d := dbKey(fmt.Sprintf("%s/%s/%08x", index, field, shard))
t := fragKey(view)
return d, t
}
func (*fieldShardKeySplitter) dbPath(dbk dbKey) (string, error) {
slash := strings.IndexByte(string(dbk), '/')
if slash == -1 {
return "", fmt.Errorf("malformed dbKey %q", dbk)
}
paths := [6]string{"indexes", "", "fields", "", "shards", ""}
paths[1] = string(dbk)[:slash]
paths[3] = string(dbk)[slash+1:]
slash = strings.IndexByte(paths[3], '/')
if slash == -1 {
return "", fmt.Errorf("malformed dbKey %q", dbk)
}
// note order: have to grab the tail of paths[3] before cutting it off
paths[5] = paths[3][slash+1:]
paths[3] = paths[3][:slash]
return filepath.Join(paths[:]...), nil
}
func (*fieldShardKeySplitter) Scope() QueryScope {
// a flexibleQueryScope without a flexibleKeySplitter always
// splits keys by field.
return &flexibleQueryScope{}
}
// flexibleKeySplitter splits the database into index,shard pairs,
// except for a subset of indexes which get split by field. this
// exists primarily to explore the API space. Do not alter the splitIndexes
// map after initial creation, it will produce inconsistent results.
type flexibleKeySplitter struct {
splitIndexes map[IndexName]struct{}
}
// NewFlexibleKeySplitter creates a KeySplitter which uses index/shard
// splits by default, but splits things into fields if they're in the
// indexes provided. This design is experimental, and should be considered
// pre-deprecated for production use for now.
func NewFlexibleKeySplitter(indexes ...IndexName) *flexibleKeySplitter {
splitIndexes := make(map[IndexName]struct{}, len(indexes))
for _, index := range indexes {
splitIndexes[index] = struct{}{}
}
return &flexibleKeySplitter{splitIndexes: splitIndexes}
}
func (f *flexibleKeySplitter) keys(index IndexName, field FieldName, view ViewName, shard ShardID) (dbKey, fragKey) {
if _, ok := f.splitIndexes[index]; ok {
return (&fieldShardKeySplitter{}).keys(index, field, view, shard)
}
return (&indexShardKeySplitter{}).keys(index, field, view, shard)
}
func (f *flexibleKeySplitter) dbPath(dbk dbKey) (string, error) {
slash := strings.IndexByte(string(dbk), '/')
if slash == -1 {
return "", fmt.Errorf("malformed dbKey %q", dbk)
}
if _, ok := f.splitIndexes[IndexName(dbk)[:slash]]; ok {
return (&fieldShardKeySplitter{}).dbPath(dbk)
}
return (&indexShardKeySplitter{}).dbPath(dbk)
}
func (f *flexibleKeySplitter) Scope() QueryScope {
return &flexibleQueryScope{splitter: f}
}