remove a bunch of roaring backend stuff

snapshotQueue, op tracking, roaring-only tests
This commit is contained in:
Matthew Jaffee 2022-02-01 14:46:00 -06:00
parent fa4855c887
commit 69c00a92ad
26 changed files with 197 additions and 2069 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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()

View file

@ -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()
}

View file

@ -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) {

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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, "")

View file

@ -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
}
}

View file

@ -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()

View file

@ -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

View file

@ -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,
}),
),

View file

@ -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

View file

@ -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(

View file

@ -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 {

View file

@ -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] {