mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
remove a bunch of roaring backend stuff
snapshotQueue, op tracking, roaring-only tests
This commit is contained in:
parent
fa4855c887
commit
69c00a92ad
26 changed files with 197 additions and 2069 deletions
12
Makefile
12
Makefile
|
|
@ -79,9 +79,6 @@ testvsub-race:
|
|||
cd ..; \
|
||||
done
|
||||
|
||||
tour:
|
||||
./tournament.sh
|
||||
|
||||
bench:
|
||||
$(GO) test ./... -bench=. -run=NoneZ -timeout=127m $(TESTFLAGS)
|
||||
|
||||
|
|
@ -349,14 +346,5 @@ install-gometalinter:
|
|||
GO111MODULE=off gometalinter --install
|
||||
GO111MODULE=off $(GO) get github.com/remyoudompheng/go-misc/deadcode
|
||||
|
||||
test-txstore-rbf:
|
||||
PILOSA_STORAGE_BACKEND=rbf $(MAKE) testv-race
|
||||
|
||||
# WARNING: This feature is no longer being tested regularly in CI. The test is
|
||||
# very slow and very expensive, and we're not sure it actually provides useful
|
||||
# information now.
|
||||
test-txstore-rbf_bolt:
|
||||
PILOSA_STORAGE_BACKEND=rbf_bolt $(MAKE) testv-race
|
||||
|
||||
test-external-lookup:
|
||||
$(GO) test . -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) -run ^TestExternalLookup$$ -externalLookupDSN $(EXTERNAL_LOOKUP_DSN)
|
||||
|
|
|
|||
|
|
@ -75,13 +75,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
|
|||
flags.IntVar(&srv.Config.Profile.BlockRate, "profile.block-rate", srv.Config.Profile.BlockRate, "Sampling rate for goroutine blocking profiler. One sample per <rate> ns.")
|
||||
flags.IntVar(&srv.Config.Profile.MutexFraction, "profile.mutex-fraction", srv.Config.Profile.MutexFraction, "Sampling fraction for mutex contention profiling. Sample 1/<rate> of events.")
|
||||
|
||||
// Storage
|
||||
// Note: the default for --storage.backend must be kept "" empty string.
|
||||
// Otherwise we cannot detect and honor the PILOSA_STORAGE_BACKEND env var
|
||||
// over-ride.
|
||||
// TODO: the comment above was carried over from the PILOSA_TXSRC flag, but
|
||||
// we should confirm that this still applies.
|
||||
flags.StringVar(&srv.Config.Storage.Backend, "storage.backend", storage.DefaultBackend, fmt.Sprintf("transaction/storage to use: one of roaring or rbf. The default is: %v. The env var PILOSA_STORAGE_BACKEND is over-ridden by --storage.backend option on the command line.", storage.DefaultBackend))
|
||||
flags.StringVar(&srv.Config.Storage.Backend, "storage.backend", storage.DefaultBackend, fmt.Sprintf("transaction/storage to use: 'rbf' is only supported value.", storage.DefaultBackend))
|
||||
flags.BoolVar(&srv.Config.Storage.FsyncEnabled, "storage.fsync", true, "enable fsync fully safe flush-to-disk")
|
||||
|
||||
// RBF specific flags. See pilosa/rbf/cfg/cfg.go for definitions.
|
||||
|
|
|
|||
55
dbshard.go
55
dbshard.go
|
|
@ -67,9 +67,8 @@ type DBShard struct {
|
|||
Shard uint64
|
||||
Open bool
|
||||
|
||||
typ txtype
|
||||
styp string
|
||||
hasRoaring bool // if either of the types is roaringTxn
|
||||
typ txtype
|
||||
styp string
|
||||
|
||||
W DBWrapper
|
||||
ParentDBIndex *DBIndex
|
||||
|
|
@ -131,8 +130,7 @@ type DBPerShard struct {
|
|||
// Easily see how many we have.
|
||||
Flatmap map[flatkey]*DBShard
|
||||
|
||||
typ txtype
|
||||
hasRoaring bool
|
||||
typ txtype
|
||||
|
||||
txf *TxFactory
|
||||
holder *Holder
|
||||
|
|
@ -269,11 +267,6 @@ func (txf *TxFactory) NewDBPerShard(typ txtype, holderDir string, holder *Holder
|
|||
vprint.PanicOn("must have holder.cfg.RBFConfig and holder.cfg.StorageConfig set here")
|
||||
}
|
||||
|
||||
hasRoaring := false
|
||||
if typ == roaringTxn {
|
||||
hasRoaring = true
|
||||
}
|
||||
|
||||
d = &DBPerShard{
|
||||
typ: typ,
|
||||
HolderDir: holderDir,
|
||||
|
|
@ -281,7 +274,6 @@ func (txf *TxFactory) NewDBPerShard(typ txtype, holderDir string, holder *Holder
|
|||
dbh: NewDBHolder(),
|
||||
Flatmap: make(map[flatkey]*DBShard),
|
||||
txf: txf,
|
||||
hasRoaring: hasRoaring,
|
||||
index2shards: newIndex2Shards(),
|
||||
StorageConfig: holder.cfg.StorageConfig,
|
||||
RBFConfig: holder.cfg.RBFConfig,
|
||||
|
|
@ -407,10 +399,7 @@ func (per *DBPerShard) unprotectedGetDBShard(index string, shard uint64, idx *In
|
|||
}
|
||||
dbs, ok = dbi.Shard[shard]
|
||||
if dbs != nil && dbs.closed {
|
||||
// roaring txn are nil/fake anyway. Don't freak out.
|
||||
if per.typ != roaringTxn {
|
||||
vprint.PanicOn(fmt.Sprintf("cannot retain closed dbs across holder ReOpen dbs='%p'; per.typ='%v'", dbs, per.typ))
|
||||
}
|
||||
vprint.PanicOn(fmt.Sprintf("cannot retain closed dbs across holder ReOpen dbs='%p'; per.typ='%v'", dbs, per.typ))
|
||||
}
|
||||
if !ok {
|
||||
dbs = &DBShard{
|
||||
|
|
@ -421,7 +410,6 @@ func (per *DBPerShard) unprotectedGetDBShard(index string, shard uint64, idx *In
|
|||
HolderPath: per.HolderDir,
|
||||
idx: idx,
|
||||
per: per,
|
||||
hasRoaring: per.hasRoaring,
|
||||
}
|
||||
dbs.styp = per.typ.String()
|
||||
dbi.Shard[shard] = dbs
|
||||
|
|
@ -430,8 +418,6 @@ func (per *DBPerShard) unprotectedGetDBShard(index string, shard uint64, idx *In
|
|||
if !dbs.Open {
|
||||
var registry DBRegistry
|
||||
switch dbs.typ {
|
||||
case roaringTxn:
|
||||
registry = globalRoaringReg
|
||||
case rbfTxn:
|
||||
registry = globalRbfDBReg
|
||||
registry.(*rbfDBRegistrar).SetRBFConfig(per.RBFConfig)
|
||||
|
|
@ -470,8 +456,6 @@ func (f *TxFactory) GetShardsForIndex(idx *Index, roaringViewPath string, requir
|
|||
return f.dbPerShard.TypedDBPerShardGetShardsForIndex(f.typ, idx, roaringViewPath, requireData)
|
||||
}
|
||||
|
||||
// if roaringViewPath is "" then for ty == roaringTxn we go to disk to discover
|
||||
// all the view paths under idx for type ty.
|
||||
// requireData means open the database file and verify that at least one key is set.
|
||||
// The returned sliceOfShards should not be modified. We will cache it for subsequent
|
||||
// queries.
|
||||
|
|
@ -485,14 +469,6 @@ func (per *DBPerShard) TypedDBPerShardGetShardsForIndex(ty txtype, idx *Index, r
|
|||
per.Mu.Lock()
|
||||
defer per.Mu.Unlock()
|
||||
|
||||
if ty == roaringTxn && roaringViewPath != "" {
|
||||
shardMap, err := roaringMapOfShards(roaringViewPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return shardMap, nil
|
||||
}
|
||||
|
||||
i2ss := per.index2shards
|
||||
|
||||
ss, ok := i2ss[idx.name]
|
||||
|
|
@ -507,27 +483,6 @@ func (per *DBPerShard) TypedDBPerShardGetShardsForIndex(ty txtype, idx *Index, r
|
|||
|
||||
// Upon return, cache the setOfShards value and reuse it next time
|
||||
|
||||
if ty == roaringTxn {
|
||||
// INVAR: roaringViewPath == "", because the other case is
|
||||
// handled above.
|
||||
fields := idx.Fields()
|
||||
for _, field := range fields {
|
||||
for _, view := range field.views() {
|
||||
shardMap, err := roaringMapOfShards(view.path)
|
||||
if err != nil {
|
||||
return nil,
|
||||
errors.Wrap(err, fmt.Sprintf(
|
||||
"TypedDBPerShardGetLocalShardsForIndex roaringTxn view.path='%v'", view.path))
|
||||
}
|
||||
for shard := range shardMap {
|
||||
setOfShards.add(shard)
|
||||
}
|
||||
}
|
||||
}
|
||||
return setOfShards.CloneMaybe(), nil
|
||||
}
|
||||
// INVAR: not-roaring.
|
||||
|
||||
path := per.prefixForType(idx, ty)
|
||||
|
||||
ignoreEmpty := false
|
||||
|
|
@ -730,8 +685,6 @@ func (per *DBPerShard) GetFieldView2ShardsMapForIndex(idx *Index) (vs *FieldView
|
|||
ty := per.typ
|
||||
|
||||
switch ty {
|
||||
case roaringTxn:
|
||||
return roaringGetFieldView2Shards(idx)
|
||||
default:
|
||||
vs = NewFieldView2Shards()
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func Test_DBPerShard_GetShardsForIndex_LocalOnly(t *testing.T) {
|
|||
v2s.addViewShardSet(txkey.FieldView{Field: field, View: "standard"}, stdShardSet)
|
||||
}
|
||||
|
||||
for _, src := range []string{"roaring", "rbf"} {
|
||||
for _, src := range []string{"rbf"} {
|
||||
cfg := mustHolderConfig()
|
||||
cfg.StorageConfig.Backend = src
|
||||
holder := NewHolder(tmpdir, cfg)
|
||||
|
|
@ -82,7 +82,6 @@ func Test_DBPerShard_GetShardsForIndex_LocalOnly(t *testing.T) {
|
|||
idx, err = NewIndex(holder, filepath.Join(tmpdir, index), index)
|
||||
PanicOn(err)
|
||||
}
|
||||
estd := "rick/fields/_exists/views/standard"
|
||||
std := "rick/fields/f/views/standard"
|
||||
|
||||
shards, err := holder.txf.GetShardsForIndex(idx, tmpdir+sep+std, false)
|
||||
|
|
@ -93,65 +92,23 @@ func Test_DBPerShard_GetShardsForIndex_LocalOnly(t *testing.T) {
|
|||
panic(fmt.Sprintf("missing shard=%v from shards='%#v'", shard, shards))
|
||||
}
|
||||
}
|
||||
if src == "roaring" {
|
||||
// check estd too
|
||||
shards, err = holder.txf.GetShardsForIndex(idx, tmpdir+sep+estd, false)
|
||||
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
|
||||
tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: shard})
|
||||
fvs, err := tx.GetSortedFieldViewList(idx, shard)
|
||||
PanicOn(err)
|
||||
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
|
||||
if !shards[shard] {
|
||||
panic(fmt.Sprintf("missing shard=%v from shards='%#v'", shard, shards))
|
||||
}
|
||||
// expect these same two field/views for all 6 shards
|
||||
expect0 := txkey.FieldView{Field: "_exists", View: "standard"}
|
||||
expect1 := txkey.FieldView{Field: "f", View: "standard"}
|
||||
if len(fvs) != 2 {
|
||||
panic(fmt.Sprintf("fvs should be len 2, got '%#v' (%s)", fvs, src))
|
||||
}
|
||||
|
||||
// check GetSortedFieldViewList() and roaringGetFieldView2Shards()
|
||||
vs, err := roaringGetFieldView2Shards(idx)
|
||||
PanicOn(err)
|
||||
|
||||
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
|
||||
tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: shard})
|
||||
fvs, err := tx.GetSortedFieldViewList(idx, shard)
|
||||
PanicOn(err)
|
||||
// expect these same two field/views for all 6 shards
|
||||
expect0 := txkey.FieldView{Field: "_exists", View: "standard"}
|
||||
expect1 := txkey.FieldView{Field: "f", View: "standard"}
|
||||
if len(fvs) != 2 {
|
||||
panic(fmt.Sprintf("fvs should be len 2, got '%#v' (%s)", fvs, src))
|
||||
}
|
||||
if fvs[0] != expect0 {
|
||||
panic(fmt.Sprintf("expected fvs[0]='%#v', but got '%#v'", expect0, fvs[0]))
|
||||
}
|
||||
if fvs[1] != expect1 {
|
||||
panic(fmt.Sprintf("expected fvs[1]='%#v', but got '%#v'", expect1, fvs[1]))
|
||||
}
|
||||
|
||||
for _, fv := range fvs {
|
||||
if !vs.has(fv.Field, fv.View, shard) {
|
||||
panic(fmt.Sprintf("vs did not contain fv='%#v' for shard %v", fv, shard))
|
||||
}
|
||||
}
|
||||
tx.Rollback()
|
||||
if fvs[0] != expect0 {
|
||||
panic(fmt.Sprintf("expected fvs[0]='%#v', but got '%#v'", expect0, fvs[0]))
|
||||
}
|
||||
} else {
|
||||
// non-roaring: rbf
|
||||
|
||||
for _, shard := range []uint64{93, 223, 221, 215, 219, 217} {
|
||||
tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: shard})
|
||||
fvs, err := tx.GetSortedFieldViewList(idx, shard)
|
||||
PanicOn(err)
|
||||
// expect these same two field/views for all 6 shards
|
||||
expect0 := txkey.FieldView{Field: "_exists", View: "standard"}
|
||||
expect1 := txkey.FieldView{Field: "f", View: "standard"}
|
||||
if len(fvs) != 2 {
|
||||
panic(fmt.Sprintf("fvs should be len 2, got '%#v' (%s)", fvs, src))
|
||||
}
|
||||
if fvs[0] != expect0 {
|
||||
panic(fmt.Sprintf("expected fvs[0]='%#v', but got '%#v'", expect0, fvs[0]))
|
||||
}
|
||||
if fvs[1] != expect1 {
|
||||
panic(fmt.Sprintf("expected fvs[1]='%#v', but got '%#v'", expect1, fvs[1]))
|
||||
}
|
||||
tx.Rollback()
|
||||
if fvs[1] != expect1 {
|
||||
panic(fmt.Sprintf("expected fvs[1]='%#v', but got '%#v'", expect1, fvs[1]))
|
||||
}
|
||||
tx.Rollback()
|
||||
}
|
||||
holder.Close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import (
|
|||
"github.com/molecula/featurebase/v3/pql"
|
||||
"github.com/molecula/featurebase/v3/proto"
|
||||
"github.com/molecula/featurebase/v3/server"
|
||||
"github.com/molecula/featurebase/v3/storage"
|
||||
"github.com/molecula/featurebase/v3/test"
|
||||
"github.com/molecula/featurebase/v3/testhook"
|
||||
. "github.com/molecula/featurebase/v3/vprint" // nolint:staticcheck
|
||||
|
|
@ -1277,15 +1276,6 @@ func TestExecutor_Execute_Count(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func roaringOnlyTest(t *testing.T) {
|
||||
src := pilosa.CurrentBackend()
|
||||
if src == pilosa.RoaringTxn || (storage.DefaultBackend == pilosa.RoaringTxn && src == "") {
|
||||
// okay to run, we are under roaring only
|
||||
} else {
|
||||
t.Skip("skip for everything but roaring")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure a set query can be executed.
|
||||
func TestExecutor_Execute_Set(t *testing.T) {
|
||||
t.Run("RowIDColumnID", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -247,7 +247,6 @@ func NewTestField(t testing.TB, opts FieldOption) *TestField {
|
|||
}
|
||||
|
||||
cfg := DefaultHolderConfig()
|
||||
cfg.StorageConfig.Backend = CurrentBackendOrDefault()
|
||||
cfg.StorageConfig.FsyncEnabled = false
|
||||
cfg.RBFConfig.FsyncEnabled = false
|
||||
h := NewHolder(path, cfg)
|
||||
|
|
|
|||
287
fragment.go
287
fragment.go
|
|
@ -3,7 +3,6 @@ package pilosa
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"container/heap"
|
||||
"context"
|
||||
|
|
@ -16,7 +15,6 @@ import (
|
|||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -59,18 +57,12 @@ const (
|
|||
// width of roaring containers is 2^16
|
||||
containerWidth = 1 << 16
|
||||
|
||||
// snapshotExt is the file extension used for an in-process snapshot.
|
||||
snapshotExt = ".snapshotting"
|
||||
|
||||
// cacheExt is the file extension for persisted cache ids.
|
||||
cacheExt = ".cache"
|
||||
|
||||
// HashBlockSize is the number of rows in a merkle hash block.
|
||||
HashBlockSize = 100
|
||||
|
||||
// defaultFragmentMaxOpN is the default value for Fragment.MaxOpN.
|
||||
defaultFragmentMaxOpN = 10000
|
||||
|
||||
// Row ids used for boolean fields.
|
||||
falseRowID = uint64(0)
|
||||
trueRowID = uint64(1)
|
||||
|
|
@ -132,22 +124,11 @@ type fragment struct {
|
|||
// idx cached to avoid repeatedly looking it up everywhere.
|
||||
idx *Index
|
||||
|
||||
// parent holder, used to find snapshot queue, etc.
|
||||
// parent holder
|
||||
holder *Holder
|
||||
|
||||
// debugging tool: addresses of current and previous maps
|
||||
prevdata, currdata struct{ from, to uintptr }
|
||||
|
||||
// File-backed storage
|
||||
flags byte // user-defined flags passed to roaring
|
||||
storage *roaring.Bitmap
|
||||
opN int // number of ops since snapshot (may be approximate for imports)
|
||||
ops int // number of higher-level operations, as opposed to bit changes
|
||||
snapshotPending bool // set to true when requesting a snapshot, set to false after snapshot completes
|
||||
snapshotCond sync.Cond
|
||||
snapshotErr error // error yielded by the last snapshot operation
|
||||
snapshotStamp time.Time // timestamp of last snapshot
|
||||
open bool // is this fragment actually open?
|
||||
storage *roaring.Bitmap
|
||||
|
||||
// Cache for row counts.
|
||||
CacheType string // passed in by field
|
||||
|
|
@ -164,11 +145,6 @@ type fragment struct {
|
|||
// Cached checksums for each block.
|
||||
checksums map[int][]byte
|
||||
|
||||
// Number of operations performed before performing a snapshot.
|
||||
// This limits the size of fragments on the heap and flushes them to disk
|
||||
// so that they can be mmapped and heap utilization can be kept low.
|
||||
MaxOpN int
|
||||
|
||||
// Logger used for out-of-band log entries.
|
||||
Logger logger.Logger
|
||||
|
||||
|
|
@ -194,18 +170,15 @@ func newFragment(holder *Holder, spec fragSpec, shard uint64, flags byte) *fragm
|
|||
fieldstr: spec.fieldstr,
|
||||
fld: spec.field,
|
||||
shard: shard,
|
||||
flags: flags,
|
||||
idx: idx,
|
||||
|
||||
CacheType: DefaultCacheType,
|
||||
CacheSize: DefaultCacheSize,
|
||||
|
||||
holder: holder,
|
||||
MaxOpN: defaultFragmentMaxOpN,
|
||||
|
||||
stats: stats.NopStatsClient,
|
||||
}
|
||||
f.snapshotCond = sync.Cond{L: &f.mu}
|
||||
return f
|
||||
}
|
||||
|
||||
|
|
@ -259,12 +232,6 @@ func (f *fragment) Open() error {
|
|||
defer f.mu.Unlock()
|
||||
|
||||
if err := func() error {
|
||||
// Initialize storage in a function so we can close if anything goes wrong.
|
||||
f.holder.Logger.Debugf("open storage for index/field/view/fragment: %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
|
||||
if err := f.openStorage(true); err != nil {
|
||||
return errors.Wrap(err, "opening storage")
|
||||
}
|
||||
|
||||
// Fill cache with rows persisted to disk.
|
||||
f.holder.Logger.Debugf("open cache for index/field/view/fragment: %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
|
||||
if err := f.openCache(); err != nil {
|
||||
|
|
@ -278,24 +245,12 @@ func (f *fragment) Open() error {
|
|||
f.close()
|
||||
return err
|
||||
}
|
||||
f.open = true
|
||||
|
||||
_ = testhook.Opened(f.holder.Auditor, f, nil)
|
||||
f.holder.Logger.Debugf("successfully opened index/field/view/fragment: %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
|
||||
return nil
|
||||
}
|
||||
|
||||
// openStorage opens the storage bitmap. Does nothing in RBF-world and will be removed soon.
|
||||
func (f *fragment) openStorage(unmarshalData bool) error {
|
||||
if !f.idx.NeedsSnapshot() {
|
||||
f.currdata = struct{ from, to uintptr }{}
|
||||
f.prevdata = f.currdata
|
||||
return nil // openStorage becomes a noop under RBF, Badger, etc.
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCache initializes the cache from row ids persisted to disk.
|
||||
func (f *fragment) openCache() error {
|
||||
// Determine cache type from field name.
|
||||
|
|
@ -351,12 +306,6 @@ func (f *fragment) Close() error {
|
|||
defer func() {
|
||||
_ = testhook.Closed(f.holder.Auditor, f, nil)
|
||||
}()
|
||||
for f.snapshotPending {
|
||||
f.snapshotCond.Wait()
|
||||
}
|
||||
// Note: snapshots won't progress on a closed fragment, so we
|
||||
// wait until after a possible pending snapshot to close.
|
||||
f.open = false
|
||||
return f.close()
|
||||
}
|
||||
|
||||
|
|
@ -367,28 +316,12 @@ func (f *fragment) close() error {
|
|||
return errors.Wrap(err, "flushing cache")
|
||||
}
|
||||
|
||||
// Close underlying storage.
|
||||
if err := f.closeStorage(); err != nil {
|
||||
f.holder.Logger.Errorf("fragment: error closing storage: err=%s, path=%s", err, f.path())
|
||||
return errors.Wrap(err, "closing storage")
|
||||
}
|
||||
|
||||
// Remove checksums.
|
||||
f.checksums = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeStorage is essentially a no-op and will go away soon.
|
||||
func (f *fragment) closeStorage() error {
|
||||
// opN is determined by how many bit set/clear operations are in the storage
|
||||
// write log, so once the storage is closed it should be 0. Opening new
|
||||
// storage will set opN appropriately.
|
||||
f.opN = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mutexCheck checks for any entries in fragment which violate the mutex
|
||||
// property of having only one value set for a given column ID.
|
||||
func (f *fragment) mutexCheck(tx Tx, details bool, limit int) (map[uint64][]uint64, error) {
|
||||
|
|
@ -511,9 +444,6 @@ func (f *fragment) unprotectedSetBit(tx Tx, rowID, columnID uint64) (changed boo
|
|||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
|
||||
// Increment number of operations until snapshot is required.
|
||||
f.incrementOpN(1)
|
||||
|
||||
// If we're using a cache, update it. Otherwise skip the
|
||||
// possibly-expensive count operation.
|
||||
if f.CacheType != CacheTypeNone {
|
||||
|
|
@ -563,9 +493,6 @@ func (f *fragment) unprotectedClearBit(tx Tx, rowID, columnID uint64) (changed b
|
|||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
|
||||
// Increment number of operations until snapshot is required.
|
||||
f.incrementOpN(1)
|
||||
|
||||
// If we're using a cache, update it. Otherwise skip the
|
||||
// possibly-expensive count operation.
|
||||
if f.CacheType != CacheTypeNone {
|
||||
|
|
@ -632,8 +559,6 @@ func (f *fragment) unprotectedSetRow(tx Tx, row *Row, rowID uint64) (changed boo
|
|||
}
|
||||
}
|
||||
|
||||
// Snapshot storage.
|
||||
f.holder.SnapshotQueue.Enqueue(f)
|
||||
f.stats.Count("setRow", 1, 1.0)
|
||||
|
||||
return changed, nil
|
||||
|
|
@ -672,9 +597,6 @@ func (f *fragment) unprotectedClearRow(tx Tx, rowID uint64) (changed bool, err e
|
|||
// Clear the row in cache.
|
||||
f.cache.Add(rowID, 0)
|
||||
|
||||
// Snapshot storage.
|
||||
f.holder.SnapshotQueue.Enqueue(f)
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
|
|
@ -1929,7 +1851,7 @@ func (f *fragment) mergeBlock(tx Tx, id int, data []pairSet) (sets, clears []pai
|
|||
return sets[1:], clears[1:], err
|
||||
}
|
||||
|
||||
// bulkImport bulk imports a set of bits and then snapshots the storage.
|
||||
// bulkImport bulk imports a set of bits.
|
||||
// The cache is updated to reflect the new data.
|
||||
func (f *fragment) bulkImport(tx Tx, rowIDs, columnIDs []uint64, options *ImportOptions) error {
|
||||
// Verify that there are an equal number of row ids and column ids.
|
||||
|
|
@ -2142,68 +2064,48 @@ func (p parallelSlices) Swap(i, j int) {
|
|||
// snapshot of the fragment or just do in-memory updates while appending
|
||||
// operations to the op log.
|
||||
func (f *fragment) importPositions(tx Tx, set, clear []uint64, rowSet map[uint64]struct{}) error {
|
||||
//tx.AddN()
|
||||
doFunc := func() error {
|
||||
if len(set) > 0 {
|
||||
f.stats.Count(MetricImportingN, int64(len(set)), 1)
|
||||
if len(set) > 0 {
|
||||
f.stats.Count(MetricImportingN, int64(len(set)), 1)
|
||||
|
||||
// TODO benchmark Add/RemoveN behavior with sorted/unsorted positions
|
||||
changedN, err := tx.Add(f.index(), f.field(), f.view(), f.shard, set...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding positions")
|
||||
}
|
||||
f.stats.Count(MetricImportedN, int64(changedN), 1)
|
||||
f.incrementOpN(changedN)
|
||||
// TODO benchmark Add/RemoveN behavior with sorted/unsorted positions
|
||||
changedN, err := tx.Add(f.index(), f.field(), f.view(), f.shard, set...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "adding positions")
|
||||
}
|
||||
f.stats.Count(MetricImportedN, int64(changedN), 1)
|
||||
}
|
||||
|
||||
if len(clear) > 0 {
|
||||
f.stats.Count(MetricClearingN, int64(len(clear)), 1)
|
||||
changedN, err := tx.Remove(f.index(), f.field(), f.view(), f.shard, clear...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "clearing positions")
|
||||
}
|
||||
f.stats.Count(MetricClearedN, int64(changedN), 1)
|
||||
f.incrementOpN(changedN)
|
||||
if len(clear) > 0 {
|
||||
f.stats.Count(MetricClearingN, int64(len(clear)), 1)
|
||||
changedN, err := tx.Remove(f.index(), f.field(), f.view(), f.shard, clear...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "clearing positions")
|
||||
}
|
||||
f.stats.Count(MetricClearedN, int64(changedN), 1)
|
||||
}
|
||||
|
||||
// Update cache counts for all affected rows.
|
||||
for rowID := range rowSet {
|
||||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
|
||||
if f.CacheType != CacheTypeNone {
|
||||
start := rowID * ShardWidth
|
||||
end := (rowID + 1) * ShardWidth
|
||||
|
||||
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, start, end)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CountRange")
|
||||
}
|
||||
|
||||
f.cache.BulkAdd(rowID, n)
|
||||
}
|
||||
}
|
||||
// Update cache counts for all affected rows.
|
||||
for rowID := range rowSet {
|
||||
// Invalidate block checksum.
|
||||
delete(f.checksums, int(rowID/HashBlockSize))
|
||||
|
||||
if f.CacheType != CacheTypeNone {
|
||||
f.cache.Invalidate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := doFunc()
|
||||
if err != nil && f.storage != nil {
|
||||
// we got an error. it's possible that the error indicates that something went wrong.
|
||||
mappedIn, mappedOut, unmappedIn, errs, e2 := f.storage.SanityCheckMapping(f.currdata.from, f.currdata.to)
|
||||
if errs != 0 {
|
||||
f.holder.Logger.Errorf("transaction failed on %s. storage has %d mapped in range, %d mapped out of range, %d unmapped in range, %d errors total, last %v",
|
||||
f.path(), mappedIn, mappedOut, unmappedIn, errs, e2)
|
||||
if f.prevdata.from != f.currdata.from {
|
||||
mappedIn, mappedOut, unmappedIn, errs, e2 = f.storage.SanityCheckMapping(f.prevdata.from, f.prevdata.to)
|
||||
f.holder.Logger.Errorf("with previous map, storage would have %d mapped in range, %d mapped out of range, %d unmapped in range, %d errors total, last %v",
|
||||
mappedIn, mappedOut, unmappedIn, errs, e2)
|
||||
start := rowID * ShardWidth
|
||||
end := (rowID + 1) * ShardWidth
|
||||
|
||||
n, err := tx.CountRange(f.index(), f.field(), f.view(), f.shard, start, end)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CountRange")
|
||||
}
|
||||
|
||||
f.cache.BulkAdd(rowID, n)
|
||||
}
|
||||
}
|
||||
return err
|
||||
|
||||
if f.CacheType != CacheTypeNone {
|
||||
f.cache.Invalidate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sliceDifference removes everything from original that's found in remove,
|
||||
|
|
@ -2503,125 +2405,6 @@ func (f *fragment) importRoaringOverwrite(ctx context.Context, tx Tx, data []byt
|
|||
return f.importRoaring(ctx, tx, data, false)
|
||||
}
|
||||
|
||||
// incrementOpN increase the operation count by one.
|
||||
// If the count exceeds the maximum allowed then a snapshot is performed.
|
||||
func (f *fragment) incrementOpN(changed int) {
|
||||
if changed <= 0 {
|
||||
return
|
||||
}
|
||||
// don't count opN or ops if our index doesn't want snapshots
|
||||
if !f.idx.NeedsSnapshot() {
|
||||
return
|
||||
}
|
||||
f.opN += changed
|
||||
f.ops++
|
||||
if f.opN > f.MaxOpN {
|
||||
f.holder.SnapshotQueue.Enqueue(f)
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot writes the storage bitmap to disk and reopens it. This may
|
||||
// coexist with existing background-queue snapshotting; it does not remove
|
||||
// things from the queue. You probably don't want to do this; use
|
||||
// the snapshotQueue's Enqueue/Await.
|
||||
func (f *fragment) Snapshot() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.snapshot()
|
||||
}
|
||||
|
||||
func track(start time.Time, message string, stats stats.StatsClient, logger logger.Logger) {
|
||||
elapsed := time.Since(start)
|
||||
logger.Debugf("%s took %s", message, elapsed)
|
||||
stats.Timing(MetricSnapshotDurationSeconds, elapsed, 1.0)
|
||||
}
|
||||
|
||||
// snapshot does the actual snapshot operation. it does not check or care
|
||||
// about f.snapshotPending.
|
||||
func (f *fragment) snapshot() (err error) {
|
||||
if !f.idx.NeedsSnapshot() {
|
||||
return nil
|
||||
}
|
||||
if !f.open {
|
||||
return errors.New("snapshot request on closed fragment")
|
||||
}
|
||||
wouldPanic := debug.SetPanicOnFault(true)
|
||||
defer func() {
|
||||
debug.SetPanicOnFault(wouldPanic)
|
||||
if r := recover(); r != nil {
|
||||
if e2, ok := r.(error); ok {
|
||||
err = e2
|
||||
// special case: if we caught a page fault, we diagnose that directly. sadly,
|
||||
// we can't see the actual values that were used to generate this, probably.
|
||||
if e2.Error() == "runtime error: invalid memory address or nil pointer dereference" {
|
||||
mappedIn, mappedOut, unmappedIn, errs, _ := f.storage.SanityCheckMapping(f.currdata.from, f.currdata.to)
|
||||
f.holder.Logger.Errorf("transaction failed on %s. storage has %d mapped in range, %d mapped out of range, %d unmapped in range, %d errors total",
|
||||
f.path(), mappedIn, mappedOut, unmappedIn, errs)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("non-error PanicOn: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
_, err = unprotectedWriteToFragment(f, f.storage)
|
||||
if err == nil {
|
||||
f.snapshotStamp = time.Now()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// unprotectedWriteToFragment writes the fragment f with bm as the data. It is unprotected, and
|
||||
// f.mu must be locked when calling it.
|
||||
func unprotectedWriteToFragment(f *fragment, bm *roaring.Bitmap) (n int64, err error) { // nolint: interfacer
|
||||
completeMessage := fmt.Sprintf("fragment: snapshot complete %s/%s/%s/%d", f.index(), f.field(), f.view(), f.shard)
|
||||
start := time.Now()
|
||||
defer track(start, completeMessage, f.stats, f.holder.Logger)
|
||||
|
||||
// Create a temporary file to snapshot to.
|
||||
snapshotPath := f.path() + snapshotExt
|
||||
file, err := os.Create(snapshotPath)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("create snapshot file: %s", err)
|
||||
}
|
||||
// No deferred close, because we want to close it sooner than the
|
||||
// end of this function.
|
||||
|
||||
// Write storage to snapshot.
|
||||
bw := bufio.NewWriter(file)
|
||||
if n, err = bm.WriteTo(bw); err != nil {
|
||||
file.Close()
|
||||
return n, fmt.Errorf("snapshot write to: %s", err)
|
||||
}
|
||||
|
||||
if err := bw.Flush(); err != nil {
|
||||
file.Close()
|
||||
return n, fmt.Errorf("flush: %s", err)
|
||||
}
|
||||
|
||||
// we close the file here so we don't still have it open when trying
|
||||
// to open it in a moment.
|
||||
file.Close()
|
||||
|
||||
// Move snapshot to data file location.
|
||||
if err := os.Rename(snapshotPath, f.path()); err != nil {
|
||||
return n, fmt.Errorf("rename snapshot: %s", err)
|
||||
}
|
||||
|
||||
// if we reloaded from the file, we'd end up with this bitmap
|
||||
// as our storage. so... let's use this bitmap. as our storage.
|
||||
f.storage = bm
|
||||
|
||||
// Reopen storage.
|
||||
if err := f.openStorage(false); err != nil {
|
||||
return n, fmt.Errorf("open storage: %s", err)
|
||||
}
|
||||
|
||||
// Reset operation count.
|
||||
f.opN = 0
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// RecalculateCache rebuilds the cache regardless of invalidate time delay.
|
||||
func (f *fragment) RecalculateCache() {
|
||||
f.mu.Lock()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
28
holder.go
28
holder.go
|
|
@ -85,8 +85,7 @@ type Holder struct {
|
|||
// The interval at which the cached row ids are persisted to disk.
|
||||
cacheFlushInterval time.Duration
|
||||
|
||||
Logger logger.Logger
|
||||
SnapshotQueue SnapshotQueue
|
||||
Logger logger.Logger
|
||||
|
||||
// Instantiates new translation stores
|
||||
OpenTranslateStore OpenTranslateStoreFunc
|
||||
|
|
@ -271,8 +270,6 @@ func NewHolder(path string, cfg *HolderConfig) *Holder {
|
|||
Logger: cfg.Logger,
|
||||
Opts: HolderOpts{StorageBackend: cfg.StorageConfig.Backend},
|
||||
|
||||
SnapshotQueue: defaultSnapshotQueue,
|
||||
|
||||
Auditor: NewAuditor(),
|
||||
|
||||
path: path,
|
||||
|
|
@ -734,16 +731,15 @@ func (h *Holder) maybeSpool(msg Message) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Activate runs the background tasks relevant to keeping a holder in a stable
|
||||
// state, such as scanning it for needed snapshots, or flushing caches. This
|
||||
// is separate from opening because, while a server would nearly always want
|
||||
// to do this, other use cases (like consistency checks of a data directory)
|
||||
// Activate runs the background tasks relevant to keeping a holder in
|
||||
// a stable state, such as flushing caches. This is separate from
|
||||
// opening because, while a server would nearly always want to do
|
||||
// this, other use cases (like consistency checks of a data directory)
|
||||
// need to avoid it even getting started.
|
||||
func (h *Holder) Activate() {
|
||||
// Periodically flush cache.
|
||||
h.wg.Add(2)
|
||||
h.wg.Add(1)
|
||||
go func() { defer h.wg.Done(); h.monitorCacheFlush() }()
|
||||
go func() { defer h.wg.Done(); h.SnapshotQueue.ScanHolder(h, h.closing) }()
|
||||
}
|
||||
|
||||
// checkForeignIndex is a check before applying a foreign
|
||||
|
|
@ -791,7 +787,6 @@ func (h *Holder) Close() error {
|
|||
// Notify goroutines of closing and wait for completion.
|
||||
close(h.closing)
|
||||
h.wg.Wait()
|
||||
|
||||
for _, index := range h.Indexes() {
|
||||
if err := index.Close(); err != nil {
|
||||
return errors.Wrap(err, "closing index")
|
||||
|
|
@ -809,10 +804,6 @@ func (h *Holder) Close() error {
|
|||
h.opened.mu.Lock()
|
||||
h.opened.ch = make(chan struct{})
|
||||
h.opened.mu.Unlock()
|
||||
if h.SnapshotQueue != nil {
|
||||
h.SnapshotQueue.Stop()
|
||||
h.SnapshotQueue = nil
|
||||
}
|
||||
|
||||
if h.lookupDB != nil {
|
||||
err := h.lookupDB.Close()
|
||||
|
|
@ -827,13 +818,6 @@ func (h *Holder) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (h *Holder) NeedsSnapshot() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
return h.txf.NeedsSnapshot()
|
||||
}
|
||||
|
||||
// HasData returns true if Holder contains at least one index.
|
||||
// This is used to determine if the rebalancing of data is necessary
|
||||
// when a node joins the cluster.
|
||||
|
|
|
|||
|
|
@ -174,17 +174,9 @@ func TestHolderOperatorCancel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// mustHolderConfig is meant to help minimize the number of places in the code
|
||||
// where we're reading the PILOSA_STORAGE_BACKEND environment variable for
|
||||
// testing purposes. Ideally we would handle this differently, but this is a
|
||||
// first attempt at improving things. Note: the actual os.Getenv() call was
|
||||
// moved to the CurrentBackend() function.
|
||||
// mustHolderConfig sets up a default holder config for tests.
|
||||
func mustHolderConfig() *HolderConfig {
|
||||
cfg := DefaultHolderConfig()
|
||||
if backend := CurrentBackend(); backend != "" {
|
||||
_ = MustBackendToTxtype(backend)
|
||||
cfg.StorageConfig.Backend = backend
|
||||
}
|
||||
cfg.StorageConfig.FsyncEnabled = false
|
||||
cfg.RBFConfig.FsyncEnabled = false
|
||||
cfg.Schemator = disco.InMemSchemator
|
||||
|
|
|
|||
111
holder_test.go
111
holder_test.go
|
|
@ -5,13 +5,12 @@ import (
|
|||
"context"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/molecula/featurebase/v3"
|
||||
pilosa "github.com/molecula/featurebase/v3"
|
||||
"github.com/molecula/featurebase/v3/disco"
|
||||
"github.com/molecula/featurebase/v3/pql"
|
||||
"github.com/molecula/featurebase/v3/test"
|
||||
|
|
@ -21,10 +20,7 @@ import (
|
|||
// mustHolderConfig provides a default test-friendly holder config.
|
||||
func mustHolderConfig() *pilosa.HolderConfig {
|
||||
cfg := pilosa.DefaultHolderConfig()
|
||||
if backend := pilosa.CurrentBackend(); backend != "" {
|
||||
_ = pilosa.MustBackendToTxtype(backend)
|
||||
cfg.StorageConfig.Backend = backend
|
||||
}
|
||||
cfg.StorageConfig.Backend = "rbf"
|
||||
cfg.StorageConfig.FsyncEnabled = false
|
||||
cfg.RBFConfig.FsyncEnabled = false
|
||||
cfg.Schemator = disco.InMemSchemator
|
||||
|
|
@ -55,109 +51,6 @@ func TestHolder_Open(t *testing.T) {
|
|||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrFragmentStoragePermission", func(t *testing.T) {
|
||||
roaringOnlyTest(t)
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("Skipping permissions test since user is root.")
|
||||
}
|
||||
h := test.MustOpenHolder(t)
|
||||
defer h.Close()
|
||||
|
||||
var idx *pilosa.Index
|
||||
var err error
|
||||
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var shard uint64
|
||||
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
|
||||
defer tx.Rollback()
|
||||
|
||||
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := tx.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := h.Holder.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.Chmod(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Chmod(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 0644)
|
||||
}()
|
||||
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "permission denied") {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrFragmentStorageCorrupt", func(t *testing.T) {
|
||||
roaringOnlyTest(t)
|
||||
|
||||
h := test.MustOpenHolder(t)
|
||||
defer h.Close()
|
||||
|
||||
var idx *pilosa.Index
|
||||
var err error
|
||||
if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var shard uint64
|
||||
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := tx.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := h.Holder.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.Truncate(filepath.Join(h.Path(), "foo", "bar", "views", "standard", "fragments", "0"), 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := h.Reopen(); err == nil || !strings.Contains(err.Error(), "open fragment: shard=0, err=opening storage: unmarshal storage") {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrFragmentStorageRecoverable", func(t *testing.T) {
|
||||
roaringOnlyTest(t)
|
||||
|
||||
h := test.MustOpenHolder(t)
|
||||
defer h.Close()
|
||||
|
||||
idx, err := h.CreateIndex("foo", pilosa.IndexOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var shard uint64
|
||||
tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard})
|
||||
defer tx.Rollback()
|
||||
|
||||
if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := field.SetBit(tx, 0, 0, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := tx.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := h.Holder.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.Truncate(filepath.Join(h.IndexesPath(), "foo", "bar", "views", "standard", "fragments", "0"), 20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := h.Reopen(); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ForeignIndex", func(t *testing.T) {
|
||||
t.Run("ErrForeignIndexNotFound", func(t *testing.T) {
|
||||
h := test.MustOpenHolder(t)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import (
|
|||
"github.com/molecula/featurebase/v3/logger"
|
||||
"github.com/molecula/featurebase/v3/pql"
|
||||
"github.com/molecula/featurebase/v3/rbf"
|
||||
"github.com/molecula/featurebase/v3/storage"
|
||||
"github.com/molecula/featurebase/v3/topology"
|
||||
"github.com/molecula/featurebase/v3/tracing"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -1087,7 +1088,7 @@ func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) {
|
|||
req, ok := qreq.(*pilosa.QueryRequest)
|
||||
|
||||
if DoPerQueryProfiling {
|
||||
backend := pilosa.CurrentBackend()
|
||||
backend := storage.DefaultBackend
|
||||
reqHash := hash(req.Query)
|
||||
|
||||
qlen := len(req.Query)
|
||||
|
|
|
|||
4
index.go
4
index.go
|
|
@ -93,10 +93,6 @@ func (i *Index) NewTx(txo Txo) Tx {
|
|||
return i.holder.txf.NewTx(txo)
|
||||
}
|
||||
|
||||
func (i *Index) NeedsSnapshot() bool {
|
||||
return i.holder.txf.NeedsSnapshot()
|
||||
}
|
||||
|
||||
// CreatedAt is an timestamp for a specific version of an index.
|
||||
func (i *Index) CreatedAt() int64 {
|
||||
i.mu.RLock()
|
||||
|
|
|
|||
68
mmap_test.go
68
mmap_test.go
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright 2021 Molecula Corp. All rights reserved.
|
||||
package pilosa
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/molecula/featurebase/v3/logger"
|
||||
)
|
||||
|
||||
type cv struct {
|
||||
cols []uint64
|
||||
vals []int64
|
||||
}
|
||||
|
||||
func forceSnapshotsCheckMapping(t *testing.T) {
|
||||
depth := uint64(6)
|
||||
f, idx, tx := mustOpenBSIFragment(t, "i", "f", viewStandard, 0)
|
||||
tx.Rollback()
|
||||
f.Logger = logger.NewLogfLogger(t)
|
||||
defer f.Clean(t)
|
||||
|
||||
tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard})
|
||||
defer tx.Rollback()
|
||||
|
||||
for i := 0; i < f.MaxOpN; i++ {
|
||||
_, _ = f.setBit(tx, 0, uint64(32*i))
|
||||
}
|
||||
// force snapshot so we get a mmapped row...
|
||||
err := f.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("initial snapshot error: %v", err)
|
||||
}
|
||||
|
||||
values := make([]cv, 1024)
|
||||
for i := range values {
|
||||
cols := make([]uint64, 128)
|
||||
vals := make([]int64, 128)
|
||||
for j := range cols {
|
||||
// pick values in the first 16 cols of each of the 16
|
||||
// shards in a default shardwidth, so each set will
|
||||
// probably change some values from the previous one.
|
||||
cols[j] = uint64(((rand.Int63n(16) & int64(i>>2)) << 16) + rand.Int63n(16))
|
||||
vals[j] = int64(rand.Int63n(1 << depth))
|
||||
}
|
||||
values[i] = cv{cols, vals}
|
||||
}
|
||||
|
||||
// modify the original bitmap, until it causes a snapshot, which
|
||||
// then invalidates the other map...
|
||||
for i := 0; i < 32; i++ {
|
||||
cv := values[i%len(values)]
|
||||
// periodically force gc, so if we have a small pool of maps
|
||||
// we'll go in and out of mapping mode
|
||||
if i%5 == 0 {
|
||||
runtime.GC()
|
||||
}
|
||||
err := f.importValue(tx, cv.cols, cv.vals, depth, (i%3 == 1))
|
||||
if err != nil {
|
||||
t.Fatalf("importValue[%d]: %v", i, err)
|
||||
}
|
||||
err = f.Snapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot[%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
19
pilosa.go
19
pilosa.go
|
|
@ -2,13 +2,11 @@
|
|||
package pilosa
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/molecula/featurebase/v3/disco"
|
||||
pnet "github.com/molecula/featurebase/v3/net"
|
||||
"github.com/molecula/featurebase/v3/storage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -157,20 +155,3 @@ func AddressWithDefaults(addr string) (*pnet.URI, error) {
|
|||
}
|
||||
return pnet.NewURIFromAddress(addr)
|
||||
}
|
||||
|
||||
// CurrentBackend is one step in an attempt to centralize (and either minimize
|
||||
// or completely remove), the calls to environment variables throughout the
|
||||
// tests. Ideally we could get rid of this and rely completely on the
|
||||
// configuration parameters.
|
||||
func CurrentBackend() string {
|
||||
return os.Getenv("PILOSA_STORAGE_BACKEND")
|
||||
}
|
||||
|
||||
// CurrentBackendOrDefault tries the environment variable first, but falls back
|
||||
// to the default backend if the environment variable is empty.
|
||||
func CurrentBackendOrDefault() string {
|
||||
if backend := os.Getenv("PILOSA_STORAGE_BACKEND"); backend != "" {
|
||||
return backend
|
||||
}
|
||||
return storage.DefaultBackend
|
||||
}
|
||||
|
|
|
|||
10
pprof.go
10
pprof.go
|
|
@ -19,10 +19,7 @@ import (
|
|||
// commented out—in holder.go.
|
||||
func CPUProfileForDur(dur time.Duration, outpath string) {
|
||||
// per-query pprof output:
|
||||
backend := CurrentBackend()
|
||||
if backend == "" {
|
||||
backend = storage.DefaultBackend
|
||||
}
|
||||
backend := storage.DefaultBackend
|
||||
path := outpath + "." + backend
|
||||
f, err := os.Create(path)
|
||||
vprint.PanicOn(err)
|
||||
|
|
@ -45,10 +42,7 @@ func CPUProfileForDur(dur time.Duration, outpath string) {
|
|||
// commented out—in holder.go.
|
||||
func MemProfileForDur(dur time.Duration, outpath string) {
|
||||
// per-query pprof output:
|
||||
backend := CurrentBackend()
|
||||
if backend == "" {
|
||||
backend = storage.DefaultBackend
|
||||
}
|
||||
backend := storage.DefaultBackend
|
||||
path := outpath + "." + backend
|
||||
f, err := os.Create(path)
|
||||
vprint.PanicOn(err)
|
||||
|
|
|
|||
22
server.go
22
server.go
|
|
@ -64,11 +64,10 @@ type Server struct { // nolint: maligned
|
|||
schemator disco.Schemator
|
||||
|
||||
// External
|
||||
systemInfo SystemInfo
|
||||
gcNotifier GCNotifier
|
||||
logger logger.Logger
|
||||
queryLogger logger.Logger
|
||||
snapshotQueue SnapshotQueue
|
||||
systemInfo SystemInfo
|
||||
gcNotifier GCNotifier
|
||||
logger logger.Logger
|
||||
queryLogger logger.Logger
|
||||
|
||||
nodeID string
|
||||
uri pnet.URI
|
||||
|
|
@ -544,13 +543,6 @@ func (s *Server) UpAndDown() error {
|
|||
func (s *Server) Open() error {
|
||||
s.logger.Infof("open server. PID %v", os.Getpid())
|
||||
|
||||
if s.holder.NeedsSnapshot() {
|
||||
// Start background monitoring.
|
||||
s.snapshotQueue = newSnapshotQueue(10, 2, s.logger)
|
||||
} else {
|
||||
s.snapshotQueue = defaultSnapshotQueue //TODO (twg) rethink this
|
||||
}
|
||||
|
||||
// Log startup
|
||||
err := s.holder.logStartup()
|
||||
if err != nil {
|
||||
|
|
@ -612,7 +604,6 @@ func (s *Server) Open() error {
|
|||
return errors.Wrap(err, "opening Holder")
|
||||
}
|
||||
// bring up the background tasks for the holder.
|
||||
s.holder.SnapshotQueue = s.snapshotQueue
|
||||
s.holder.Activate()
|
||||
// if we joined existing cluster then broadcast "resize on add" message
|
||||
if initState == disco.InitialClusterStateExisting {
|
||||
|
|
@ -743,11 +734,6 @@ func (s *Server) Close() error {
|
|||
if s.holder != nil {
|
||||
errh = s.holder.Close()
|
||||
}
|
||||
if s.snapshotQueue != nil {
|
||||
s.holder.SnapshotQueue = nil
|
||||
s.snapshotQueue.Stop()
|
||||
s.snapshotQueue = nil
|
||||
}
|
||||
|
||||
// prefer to return holder error over cluster
|
||||
// error. This order is somewhat arbitrary. It would be better if we had
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
package pilosa
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -10,23 +9,6 @@ import (
|
|||
"github.com/molecula/featurebase/v3/testhook"
|
||||
)
|
||||
|
||||
// Ensure the file handle count is working
|
||||
func TestCountOpenFiles(t *testing.T) {
|
||||
roaringOnlyTest(t)
|
||||
|
||||
// Windows is not supported yet
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping unsupported countOpenFiles test on Windows.")
|
||||
}
|
||||
count, err := countOpenFiles()
|
||||
if err != nil {
|
||||
t.Errorf("countOpenFiles failed: %s", err)
|
||||
}
|
||||
if count == 0 {
|
||||
t.Error("countOpenFiles returned invalid value 0.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorAntiEntropyZero(t *testing.T) {
|
||||
|
||||
td, err := testhook.TempDirInDir(t, *TempDir, "")
|
||||
|
|
|
|||
495
snapshotqueue.go
495
snapshotqueue.go
|
|
@ -1,495 +0,0 @@
|
|||
// Copyright 2021 Molecula Corp. All rights reserved.
|
||||
package pilosa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/bits"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/molecula/featurebase/v3/logger"
|
||||
"github.com/molecula/featurebase/v3/testhook"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// snapshotQueue is a thing which can handle enqueuing snapshots. A snapshot
|
||||
// queue distinguishes between high-priority requests, which get satisfied
|
||||
// by the next available worker, and regular requests, which get enqueued
|
||||
// if there's space in the queue, and otherwise dropped. There's also a
|
||||
// separate background task to scan a holder for fragments which may need
|
||||
// snapshots, but which is processed only when the queue is empty, and only
|
||||
// slowly. "Await" awaits an existing snapshot if one is already enqueued.
|
||||
// "Immediate" tries to do one right away. (If one's already enqueued, this
|
||||
// can leave it in the queue, which will ignore anything that shows up with
|
||||
// the request flag cleared.)
|
||||
//
|
||||
// Await, Enqueue, and Immediate should be called only with the fragment lock
|
||||
// held.
|
||||
//
|
||||
// If you create a queue, it should get stopped at some point. The
|
||||
// atomicSnapshotQueue implementation used as defaultSnapshotQueue has
|
||||
// a Start function which will tell you whether it actually started a
|
||||
// queue. This logic exists because in a normal server case, you probably
|
||||
// want the queue to be shut down as part of server shutdown, but if you're
|
||||
// running cluster tests, you probably want to start and shop the queue as
|
||||
// part of the test, not stop it when any server terminates.
|
||||
//
|
||||
// It's less likely to be desireable to start/stop individual queues,
|
||||
// because fragments use the defaultSnapshotQueue anyway. This design
|
||||
// needs revisiting.
|
||||
type SnapshotQueue interface {
|
||||
Immediate(*fragment) error
|
||||
Enqueue(*fragment)
|
||||
Await(*fragment) error
|
||||
ScanHolder(*Holder, chan struct{})
|
||||
Stop()
|
||||
}
|
||||
|
||||
// queuelessSnapshotQueue isn't a snapshot queue, but it satisfies the
|
||||
// interface.
|
||||
type queuelessSnapshotQueue struct{}
|
||||
|
||||
func (q *queuelessSnapshotQueue) Enqueue(f *fragment) {
|
||||
// We don't actually try to enqueue the snapshot; it breaks things
|
||||
// if a snapshot gets caused during a transaction.
|
||||
}
|
||||
|
||||
func (q *queuelessSnapshotQueue) Await(f *fragment) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *queuelessSnapshotQueue) Immediate(f *fragment) error {
|
||||
return f.snapshot()
|
||||
}
|
||||
|
||||
func (q *queuelessSnapshotQueue) ScanHolder(h *Holder, done chan struct{}) {
|
||||
}
|
||||
|
||||
func (q *queuelessSnapshotQueue) Stop() {
|
||||
}
|
||||
|
||||
var defaultSnapshotQueue = &queuelessSnapshotQueue{}
|
||||
|
||||
// newSnapshotQueue makes a new snapshot queue, of depth N, with
|
||||
// w worker threads.
|
||||
func newSnapshotQueue(n int, w int, l logger.Logger) SnapshotQueue {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sq := &prioritySnapshotQueue{
|
||||
normal: make(chan snapshotRequest, n),
|
||||
urgent: make(chan snapshotRequest),
|
||||
background: make(chan snapshotRequest),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
maxOpN: 10000,
|
||||
logger: l,
|
||||
}
|
||||
if sq.logger == nil {
|
||||
sq.logger = logger.NewStandardLogger(os.Stderr)
|
||||
}
|
||||
_ = testhook.Opened(NewAuditor(), sq, nil)
|
||||
sq.spawnWorkers(w)
|
||||
return sq
|
||||
}
|
||||
|
||||
type snapshotRequest struct {
|
||||
frag *fragment
|
||||
when time.Time
|
||||
}
|
||||
|
||||
// prioritySnapshotQueue gives preference to "immediate" requests, and
|
||||
// dispreference to "background" requests from ScanHolder. It timestamps
|
||||
// requests, so it can discard a request if the most recent snapshot is
|
||||
// newer than the request. The snapshotPending flag in the fragment is
|
||||
// used to track that a given fragment thinks it has been successfully
|
||||
// enqueued. Background requests are not considered enqueued, since
|
||||
// they'll never get processed if there's anything else. In normal workloads,
|
||||
// immediate/urgent snapshots should be rare, but we'll happily drop
|
||||
// most requests on the floor; the scanner should pick them up once things
|
||||
// are quiet.
|
||||
type prioritySnapshotQueue struct {
|
||||
logger logger.Logger
|
||||
urgent chan snapshotRequest
|
||||
normal chan snapshotRequest
|
||||
background chan snapshotRequest
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
scanWG, workerWG sync.WaitGroup
|
||||
maxOpN int
|
||||
observedOpN [16]uint32
|
||||
stats struct {
|
||||
enqueued uint32
|
||||
skipped uint32
|
||||
}
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func (sq *prioritySnapshotQueue) spawnWorkers(w int) {
|
||||
sq.mu.Lock()
|
||||
defer sq.mu.Unlock()
|
||||
if sq.ctx.Err() != nil {
|
||||
sq.logger.Infof("prioritySnapshotQueue worker: already done")
|
||||
return
|
||||
}
|
||||
sq.workerWG.Add(w)
|
||||
for i := 0; i < w; i++ {
|
||||
go sq.worker(sq.ctx, sq.urgent, sq.normal, sq.background)
|
||||
}
|
||||
}
|
||||
|
||||
func (sq *prioritySnapshotQueue) worker(ctx context.Context, urgent, normal, background chan snapshotRequest) {
|
||||
defer sq.workerWG.Done()
|
||||
done := ctx.Done()
|
||||
ok := true
|
||||
var req snapshotRequest
|
||||
for ok {
|
||||
req.frag = nil
|
||||
select {
|
||||
case _, ok = <-done:
|
||||
case req, ok = <-urgent:
|
||||
default:
|
||||
select {
|
||||
case _, ok = <-done:
|
||||
case req, ok = <-urgent:
|
||||
case req, ok = <-normal:
|
||||
default:
|
||||
select {
|
||||
case _, ok = <-done:
|
||||
case req, ok = <-urgent:
|
||||
case req, ok = <-normal:
|
||||
case req, ok = <-background:
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.frag != nil {
|
||||
sq.process(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process actually runs a fragment. it will do this if either the fragment
|
||||
// has a pending snapshot, or the force flag is set.
|
||||
func (sq *prioritySnapshotQueue) process(req snapshotRequest) {
|
||||
f := req.frag
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.snapshotStamp.Before(req.when) {
|
||||
f.snapshotErr = f.snapshot()
|
||||
if f.snapshotErr != nil {
|
||||
fmt.Printf("ERROR: snapshot error: %v\n", f.snapshotErr)
|
||||
sq.logger.Errorf("snapshot error: %v", f.snapshotErr)
|
||||
}
|
||||
f.snapshotPending = false
|
||||
f.snapshotCond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop shuts down the snapshot queue. It first marks it as done, causing
|
||||
// the background scanner(s), if any, to shut down, then waits for them, then
|
||||
// closes and nils the queues. The background scanner has to get stopped
|
||||
// because otherwise it might try to write to those closed queues.
|
||||
func (sq *prioritySnapshotQueue) Stop() {
|
||||
sq.mu.Lock()
|
||||
defer sq.mu.Unlock()
|
||||
if sq.stopped {
|
||||
return
|
||||
}
|
||||
sq.stopped = true
|
||||
sq.cancel()
|
||||
// scanners need to be done before we close the other channels.
|
||||
sq.scanWG.Wait()
|
||||
close(sq.normal)
|
||||
sq.normal = nil
|
||||
close(sq.urgent)
|
||||
sq.urgent = nil
|
||||
close(sq.background)
|
||||
sq.background = nil
|
||||
_ = testhook.Closed(NewAuditor(), sq, nil)
|
||||
enqueued := atomic.LoadUint32(&sq.stats.enqueued)
|
||||
skipped := atomic.LoadUint32(&sq.stats.skipped)
|
||||
if skipped > 0 || enqueued > 1 {
|
||||
sq.logger.Infof("snapshot queue: enqueued %d, skipped %d\n", sq.stats.enqueued, sq.stats.skipped)
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue tries to add a fragment to the queue, if the fragment is not already
|
||||
// enqueued. You should hold a lock on the fragment when calling this.
|
||||
func (sq *prioritySnapshotQueue) Enqueue(f *fragment) {
|
||||
if f.snapshotPending {
|
||||
return
|
||||
}
|
||||
sq.observeOpN(uint32(f.opN))
|
||||
sq.mu.RLock()
|
||||
defer sq.mu.RUnlock()
|
||||
if sq.normal == nil {
|
||||
sq.logger.Infof("requested snapshot after snapshot queue was closed")
|
||||
return
|
||||
}
|
||||
// we have to set this before enqueing, because it's
|
||||
// otherwise possible that we're at the head of the queue,
|
||||
// and the recipient gets the fragment before we execute the
|
||||
// line after the send.
|
||||
f.snapshotPending = true
|
||||
// try to enqueue snapshot
|
||||
select {
|
||||
case sq.normal <- snapshotRequest{frag: f, when: time.Now()}:
|
||||
atomic.AddUint32(&sq.stats.enqueued, 1)
|
||||
return
|
||||
default:
|
||||
atomic.AddUint32(&sq.stats.skipped, 1)
|
||||
f.snapshotPending = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Await returns when f is not pending a snapshot. Call with the fragment lock
|
||||
// held. Await waits on a condition variable inside f, associated with the
|
||||
// fragment's lock, so this does not conflict with the lock being used for
|
||||
// snapshots.
|
||||
//
|
||||
// Note that workers don't stop just because the queue's been stopped; only
|
||||
// the background scanner is stopped. So an Await shouldn't block forever
|
||||
// even if the queue gets shut down. If you're reading this, possibly that
|
||||
// analysis is incorrect.
|
||||
func (sq *prioritySnapshotQueue) Await(f *fragment) (err error) {
|
||||
for f.snapshotPending {
|
||||
f.snapshotCond.Wait()
|
||||
}
|
||||
err, f.snapshotErr = f.snapshotErr, nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Immediate forces an immediate snapshot of the given fragment. Call with
|
||||
// the fragment locked. If the queue is already closing, the fragment does
|
||||
// not get snapshotted.
|
||||
func (sq *prioritySnapshotQueue) Immediate(f *fragment) error {
|
||||
sq.mu.RLock()
|
||||
// no deferred unlock, because we want to unlock this before calling Await.
|
||||
// Not because that needs this lock, but because once we're that far, we
|
||||
// *don't* need this lock anymore so someone else should have it.
|
||||
if sq.urgent == nil {
|
||||
sq.mu.RUnlock()
|
||||
sq.logger.Errorf("requested immediate snapshot after snapshot queue was closed")
|
||||
return errors.New("requested immediate snapshot after snapshot queue was closed")
|
||||
}
|
||||
f.snapshotPending = true
|
||||
sq.observeOpN(uint32(f.opN))
|
||||
req := snapshotRequest{frag: f, when: time.Now()}
|
||||
// if the fragment was already in the work queue, it's *possible*
|
||||
// that the only available worker just picked it off the queue, and
|
||||
// is now waiting on getting the fragment's lock, so it can run
|
||||
// a snapshot. So we let go of the lock on the fragment, send the
|
||||
// request, then request the fragment lock again, because Await will
|
||||
// be sleeping on the condition variable associated with the lock,
|
||||
// which means it needs to hold the lock so it can let it go during
|
||||
// the wait... No, really, this made sense.
|
||||
f.mu.Unlock()
|
||||
sq.urgent <- req
|
||||
sq.mu.RUnlock()
|
||||
f.mu.Lock()
|
||||
return sq.Await(f)
|
||||
}
|
||||
|
||||
// ScanHolder spawns a goroutine which iterates through the holder's
|
||||
// indexes/fields/views/fragments, looking for fragments which have OpN
|
||||
// high enough to justify a snapshot but don't seem to have one pending.
|
||||
// It then dumps these in the low priority background queue.
|
||||
func (sq *prioritySnapshotQueue) ScanHolder(h *Holder, done chan struct{}) {
|
||||
sq.mu.Lock()
|
||||
sq.scanWG.Add(1)
|
||||
go sq.scanHolderWorker(h, sq.background, done)
|
||||
sq.mu.Unlock()
|
||||
}
|
||||
|
||||
// observeOpN reports that a given value of opN was "observed", meaning,
|
||||
// we encountered a fragment which had that value. This happens for every
|
||||
// enqueue/immediate, including enqueue attempts which fail to actually
|
||||
// enter the queue, and it also happens for fragments noticed by the background
|
||||
// scan but which don't have high enough opN to trigger a snapshot.
|
||||
func (sq *prioritySnapshotQueue) observeOpN(n uint32) {
|
||||
// aka "log2(n) + 1", or 0 for n==0
|
||||
pow2 := 32 - bits.LeadingZeros32(n)
|
||||
// 15 == 16384. Our usual fragment maxOpN is 10k, so most fragments
|
||||
// should end up in the 8k-16k bucket, rather than the 16k+ bucket,
|
||||
// unless we've got a lot of ingests with large batches going on,
|
||||
// in which case the 16k bucket will win.
|
||||
if pow2 > 15 {
|
||||
pow2 = 15
|
||||
}
|
||||
// store in inverse order so the lowest slot in the array is the
|
||||
// highest cardinality
|
||||
atomic.AddUint32(&sq.observedOpN[15-pow2], 1)
|
||||
}
|
||||
|
||||
// computeMaxOpN tries to pick a reasonable new maxOpN for the background
|
||||
// scan to use. On a quiet system, we want to gradually lower opN, picking
|
||||
// the fragments with the highest opN values first, because those offer the
|
||||
// largest benefit. So, whenever we check a fragment in the background, if we
|
||||
// *don't* snapshot it, we'll "observe" its OpN value, and then we pick a
|
||||
// value which picks up at least 1/4 of them.
|
||||
//
|
||||
// If there's ingest activity, the Immediate and Enqueue operations will
|
||||
// "observe" the OpN of fragments submitted to them. This can drive OpN back
|
||||
// up, if those fragments frequently have very high opN values, which reflects
|
||||
// the fact that we have enough of that activity that we don't need the
|
||||
// background scanner adding more.
|
||||
//
|
||||
// If we have enough ingest activity that the background scanner never actually
|
||||
// gets to submit work, we'll rarely get here, because the background scanner
|
||||
// will block until there's no snapshots pending for the normal workload.
|
||||
// When we do, we'll probably pick a MaxOpN which is dominated by the ingest
|
||||
// workload's opN values. So for instance, if everything coming in from the
|
||||
// ingest workload has 10k or more items, because that's the default fragment
|
||||
// maxOpN, that will probably set the background snapshot queue value to 8k.
|
||||
func (sq *prioritySnapshotQueue) computeMaxOpN() {
|
||||
sq.logger.Debugf("observedOpN by power of 2: %d\n", sq.observedOpN[:])
|
||||
total := uint32(0)
|
||||
for i := range sq.observedOpN {
|
||||
total += atomic.LoadUint32(&sq.observedOpN[i])
|
||||
}
|
||||
target := (total / 4) + 1
|
||||
subTotal := uint32(0)
|
||||
for i := range sq.observedOpN {
|
||||
v := atomic.LoadUint32(&sq.observedOpN[i])
|
||||
subTotal += v
|
||||
if subTotal >= target {
|
||||
prevMaxOpN := sq.maxOpN
|
||||
sq.maxOpN = (1 << (15 - uint(i))) / 2
|
||||
if sq.maxOpN > 0 {
|
||||
sq.maxOpN--
|
||||
}
|
||||
if prevMaxOpN != sq.maxOpN {
|
||||
sq.logger.Infof("background scan: %d/%d fragments considered have opN %d or higher\n",
|
||||
subTotal, total, sq.maxOpN)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// It's conceptually possible that we'll miss a couple of observations
|
||||
// here but that's not really important. This is all pretty approximate.
|
||||
for i := range sq.observedOpN {
|
||||
atomic.StoreUint32(&sq.observedOpN[i], 0)
|
||||
}
|
||||
}
|
||||
|
||||
// prioritySnapshotQueueScanner is the data type that implements HolderOperator
|
||||
// and represents a single scan of a holder, with a given maxOpN.
|
||||
type prioritySnapshotQueueScanner struct {
|
||||
HolderFilterAll
|
||||
HolderProcessNone
|
||||
sq *prioritySnapshotQueue
|
||||
holder *Holder
|
||||
queue chan snapshotRequest
|
||||
ctx context.Context
|
||||
maxOpN int
|
||||
seen, hits, counter int
|
||||
}
|
||||
|
||||
func (s *prioritySnapshotQueueScanner) ProcessFragment(f *fragment) error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
s.seen++
|
||||
// we can't defer this reasonably, because otherwise we'll keep
|
||||
// the fragment locked forever if we end up trying to send it
|
||||
// to the queue, but the workers are busy on other fragments.
|
||||
f.mu.Lock()
|
||||
open := f.open
|
||||
snapshotPending, opN := f.snapshotPending, f.opN
|
||||
f.mu.Unlock()
|
||||
|
||||
// a pending snapshot is one that is either in the normal or
|
||||
// immediate queue, or is trying to get into the normal queue
|
||||
// and about to fail, but either way, it already got observed
|
||||
// there, so we don't need to observe it here. A closed fragment
|
||||
// doesn't matter to us -- it should be a transient state that
|
||||
// happens during a shutdown, or shouldn't happen, but we don't
|
||||
// care about it.
|
||||
if snapshotPending || !open {
|
||||
return nil
|
||||
}
|
||||
if opN <= s.maxOpN {
|
||||
// observe the value but don't do a snapshot
|
||||
s.sq.observeOpN(uint32(opN))
|
||||
s.counter++
|
||||
if s.counter == 1000 {
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-s.ctx.Done():
|
||||
return io.EOF
|
||||
}
|
||||
s.counter = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// we don't observe values when we decide to trigger a snapshot,
|
||||
// because those values will be changing anyway. we could also
|
||||
// observe them as zero, but that's also sort of wrong.
|
||||
s.hits++
|
||||
select {
|
||||
case s.queue <- snapshotRequest{frag: f, when: time.Now()}:
|
||||
s.sq.logger.Debugf("found fragment needing snapshot: %s\n", f.path())
|
||||
case <-s.ctx.Done():
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func contextMergedWithStructChan(ctx context.Context, ch chan struct{}) (context.Context, context.CancelFunc) {
|
||||
canCancel, cancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
case <-ch:
|
||||
cancel()
|
||||
case <-canCancel.Done():
|
||||
// don't need to cancel, but do need to exit this
|
||||
// function
|
||||
}
|
||||
}()
|
||||
return canCancel, cancel
|
||||
}
|
||||
|
||||
// scanHolderWorker is a background task that scans a holder looking for
|
||||
// fragments which need snapshots taken. It's the cleanup task for snapshots
|
||||
// that would have been requested by Enqueue, but the queue was full.
|
||||
func (sq *prioritySnapshotQueue) scanHolderWorker(h *Holder, background chan snapshotRequest, done chan struct{}) {
|
||||
defer sq.scanWG.Done()
|
||||
ctx, cancel := contextMergedWithStructChan(sq.ctx, done)
|
||||
defer cancel()
|
||||
scanner := &prioritySnapshotQueueScanner{
|
||||
sq: sq,
|
||||
holder: h,
|
||||
queue: background,
|
||||
ctx: sq.ctx,
|
||||
maxOpN: sq.maxOpN,
|
||||
}
|
||||
for {
|
||||
err := h.Process(ctx, scanner)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if scanner.hits > 0 {
|
||||
sq.logger.Infof("background scan: %d/%d fragments needed snapshots\n", scanner.hits, scanner.seen)
|
||||
scanner.hits = 0
|
||||
} else {
|
||||
sq.logger.Debugf("background scan: no fragments needed snapshots, waiting\n")
|
||||
// No reason to be active if we're not finding anything.
|
||||
select {
|
||||
case <-time.After(60 * time.Second):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
scanner.seen = 0
|
||||
sq.computeMaxOpN()
|
||||
scanner.maxOpN = sq.maxOpN
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/molecula/featurebase/v3/debugstats"
|
||||
"github.com/molecula/featurebase/v3/roaring"
|
||||
txkey "github.com/molecula/featurebase/v3/short_txkey"
|
||||
"github.com/molecula/featurebase/v3/storage"
|
||||
"github.com/molecula/featurebase/v3/vprint"
|
||||
)
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ func (w *callStats) reset() {
|
|||
}
|
||||
|
||||
func (c *callStats) report() (r string) {
|
||||
backend := CurrentBackend()
|
||||
backend := storage.DefaultBackend
|
||||
r = fmt.Sprintf("callStats: (%v)\n", backend)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ package storage
|
|||
|
||||
// public strings that pilosa/server/config.go can reference
|
||||
const (
|
||||
RoaringBackend string = "roaring"
|
||||
RBFBackend string = "rbf"
|
||||
BoltBackend string = "bolt"
|
||||
RBFBackend string = "rbf"
|
||||
)
|
||||
|
||||
// DefaultBackend is set here. pilosa/server/config.go references it
|
||||
|
|
|
|||
|
|
@ -593,7 +593,7 @@ func prependTestServerOpts(opts []server.CommandOption) []server.CommandOption {
|
|||
pilosa.OptServerOpenTranslateStore(pilosa.OpenInMemTranslateStore),
|
||||
pilosa.OptServerNodeDownRetries(5, 100*time.Millisecond),
|
||||
pilosa.OptServerStorageConfig(&storage.Config{
|
||||
Backend: pilosa.CurrentBackendOrDefault(),
|
||||
Backend: storage.DefaultBackend,
|
||||
FsyncEnabled: false,
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
## tournament.sh runs a sequence of duels between greens and blues.
|
||||
## Each test run changes the PILOSA_STORAGE_BACKEND and runs either
|
||||
## one or two backends through the rigors of make testv-race.
|
||||
## logs are saved to the tourna.log.${i} files.
|
||||
|
||||
for i in rbf roaring bolt rbf_roaring roaring_rbf roaring_bolt; do
|
||||
echo "$(date) starting ${i}, output to tourna.log.${i}"
|
||||
echo "***=== ${i} ====================*** $(date)" &> tourna.log.${i}
|
||||
PILOSA_STORAGE_BACKEND=${i} make testv-race 2>&1 > tourna.log.${i}
|
||||
done
|
||||
|
||||
10
tx_test.go
10
tx_test.go
|
|
@ -4,13 +4,11 @@ package pilosa_test
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
pilosa "github.com/molecula/featurebase/v3"
|
||||
"github.com/molecula/featurebase/v3/http"
|
||||
"github.com/molecula/featurebase/v3/server"
|
||||
"github.com/molecula/featurebase/v3/storage"
|
||||
"github.com/molecula/featurebase/v3/test"
|
||||
. "github.com/molecula/featurebase/v3/vprint" // nolint:staticcheck
|
||||
)
|
||||
|
|
@ -47,15 +45,7 @@ func queryBalances(m0api *pilosa.API, acctOwnerID uint64, fldAcct0, fldAcct1, in
|
|||
return
|
||||
}
|
||||
|
||||
func skipForRoaring(t *testing.T) {
|
||||
src := pilosa.CurrentBackend()
|
||||
if (storage.DefaultBackend == pilosa.RoaringTxn) || strings.Contains(src, "roaring") {
|
||||
t.Skip("skip if roaring pseudo-txn involved -- won't show transactional rollback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPI_ImportAtomicRecord(t *testing.T) {
|
||||
skipForRoaring(t)
|
||||
c := test.MustRunCluster(t, 1,
|
||||
[]server.CommandOption{
|
||||
server.OptCommandServerOptions(
|
||||
|
|
|
|||
24
txfactory.go
24
txfactory.go
|
|
@ -17,8 +17,7 @@ import (
|
|||
|
||||
// public strings that pilosa/server/config.go can reference
|
||||
const (
|
||||
RoaringTxn string = "roaring"
|
||||
RBFTxn string = "rbf"
|
||||
RBFTxn string = "rbf"
|
||||
)
|
||||
|
||||
// DetectMemAccessPastTx true helps us catch places in api and executor
|
||||
|
|
@ -377,9 +376,8 @@ type TxFactory struct {
|
|||
type txtype int
|
||||
|
||||
const (
|
||||
noneTxn txtype = 0
|
||||
roaringTxn txtype = 1 // these don't really have any transactions
|
||||
rbfTxn txtype = 2
|
||||
noneTxn txtype = 0
|
||||
rbfTxn txtype = 2
|
||||
)
|
||||
|
||||
// DirectoryName just returns a string version of the transaction type. We
|
||||
|
|
@ -388,8 +386,6 @@ const (
|
|||
// replaced/removed) during that refactor.
|
||||
func (ty txtype) DirectoryName() string {
|
||||
switch ty {
|
||||
case roaringTxn:
|
||||
return "roaring"
|
||||
case rbfTxn:
|
||||
return "rbf"
|
||||
}
|
||||
|
|
@ -397,18 +393,12 @@ func (ty txtype) DirectoryName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (txf *TxFactory) NeedsSnapshot() (b bool) {
|
||||
return txf.typ == roaringTxn
|
||||
}
|
||||
|
||||
func MustBackendToTxtype(backend string) (typ txtype) {
|
||||
if strings.Contains(backend, "_") {
|
||||
panic("blue-green comparisons removed")
|
||||
}
|
||||
|
||||
switch backend {
|
||||
case RoaringTxn: // "roaring"
|
||||
return roaringTxn
|
||||
case RBFTxn: // "rbf"
|
||||
return rbfTxn
|
||||
}
|
||||
|
|
@ -839,8 +829,6 @@ func (ty txtype) String() string {
|
|||
switch ty {
|
||||
case noneTxn:
|
||||
return "noneTxn"
|
||||
case roaringTxn:
|
||||
return "roaring"
|
||||
case rbfTxn:
|
||||
return "rbf"
|
||||
}
|
||||
|
|
@ -946,16 +934,10 @@ func anyGlobalDBWrappersStillOpen() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (f *TxFactory) hasRoaring() bool {
|
||||
return f.typ == roaringTxn
|
||||
}
|
||||
|
||||
func (f *TxFactory) hasRBF() bool {
|
||||
return f.typ == rbfTxn
|
||||
}
|
||||
|
||||
var _ = (&TxFactory{}).hasRoaring // happy linter
|
||||
|
||||
func (f *TxFactory) GetDBShardPath(index string, shard uint64, idx *Index, ty txtype, write bool) (shardPath string, err error) {
|
||||
dbs, err := f.dbPerShard.GetDBShard(index, shard, idx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
func Test_TxFactory_verifyStringConstantsMatch(t *testing.T) {
|
||||
// txtype.String() method MUST return strings that match
|
||||
// our const definitions at the top of txfactory.go.
|
||||
check := []txtype{roaringTxn, rbfTxn}
|
||||
expect := []string{RoaringTxn, RBFTxn}
|
||||
check := []txtype{rbfTxn}
|
||||
expect := []string{RBFTxn}
|
||||
for i, chk := range check {
|
||||
obs := chk.String()
|
||||
if obs != expect[i] {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue