From 547ee14f5b202646f831a2f970eee68599d85eb5 Mon Sep 17 00:00:00 2001 From: Todd Gruben Date: Wed, 5 Aug 2020 16:31:17 -0500 Subject: [PATCH] all test green on rbf. WOOT. - rbf had races around the new rootRecords cache in tx - rbf tx needed a write lock on the db now that rootRecords are written - added a global registry for rbfDB to correctly dedup instances - implement DeleteFragment, DeleteIndex for rbf - use badger style keys for rbf to allow content checksumming to be list containers in the same order - lots of other integration of rbf into pilosa layer. --- Makefile | 14 +- api.go | 17 + api_test.go | 5 +- badger.go | 317 ++-- badger_test.go | 420 ++--- blake3.go | 8 + blake3_test.go | 24 + bluegreentx.go | 28 +- catcher.go | 4 - cmd/demo-lmdb/lmdb.go | 267 +++ cmd/demo-lmdb/vprint.go | 179 ++ cmd/loader/loader.go | 150 -- cmd/slurp/slurp.go | 205 +++ cmd/{loader => slurp}/vprint.go | 0 executor.go | 14 +- executor_test.go | 1 - extensions/distinct.go | 21 - field.go | 25 +- field_internal_test.go | 55 + fragment.go | 1 + fragment_internal_test.go | 11 +- gid.go | 160 ++ go.mod | 9 +- go.sum | 27 +- holder.go | 10 +- holder_test.go | 1 + http/client.go | 63 + http/handler.go | 106 +- index.go | 31 +- license.exceptions | 4 +- lmdb/lmdb.go | 1800 ++++++++++++++++++++ lmdb/lmdb_test.go | 1315 ++++++++++++++ lmdb/txpool.go | 524 ++++++ mmap_test.go | 3 +- pprof.go | 47 + rbf.go | 380 +++++ rbf/cursor.go | 17 +- rbf/cursorx.go | 33 +- rbf/db.go | 55 +- rbf/rbf.go | 39 +- rbf/tx.go | 208 +-- rbf/tx_test.go | 10 +- rbf/vprint.go | 39 + roaring/container_stash.go | 64 +- roaring/containers_test.go | 6 +- roaring/roaring.go | 184 +- roaring/roaring_helpers_test.go | 72 +- roaring/roaring_internal_test.go | 18 +- roaring/unmarshal_binary.go | 18 +- rrtx.go | 695 ++++++++ server.go | 2 - server/handler_test.go | 85 +- server_internal_test.go | 2 + translator_test.go | 1 - tx.go | 830 --------- txfactory.go | 176 +- txpath/txpath.go | 157 ++ txpath/txpath_test.go | 89 + extensions/dummy.go => txpath/txprefix.go~ | 7 +- txpath/txprefix_test.go~ | 44 + 60 files changed, 7151 insertions(+), 1946 deletions(-) create mode 100644 cmd/demo-lmdb/lmdb.go create mode 100644 cmd/demo-lmdb/vprint.go delete mode 100644 cmd/loader/loader.go create mode 100644 cmd/slurp/slurp.go rename cmd/{loader => slurp}/vprint.go (100%) delete mode 100644 extensions/distinct.go create mode 100644 gid.go create mode 100644 lmdb/lmdb.go create mode 100644 lmdb/lmdb_test.go create mode 100644 lmdb/txpool.go create mode 100644 pprof.go create mode 100644 rbf.go create mode 100644 rrtx.go create mode 100644 txpath/txpath.go create mode 100644 txpath/txpath_test.go rename extensions/dummy.go => txpath/txprefix.go~ (81%) create mode 100644 txpath/txprefix_test.go~ diff --git a/Makefile b/Makefile index e38ed3860..ca1963417 100644 --- a/Makefile +++ b/Makefile @@ -172,6 +172,18 @@ topt-rbf: @echo " log.topt.rbf green: \c"; cat log.topt.rbf | grep PASS |wc -l @echo " log.topt.rbf red: \c"; cat log.topt.rbf | grep '\-\-\- FAIL' |wc -l +topt-rbf-race: + mv log.topt.rbf-race log.topt.rbf-race.prev || true + PILOSA_TXSRC=rbf go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.rbf-race + @echo " log.topt.rbf-race green: \c"; cat log.topt.rbf-race | grep PASS |wc -l + @echo " log.topt.rbf-race red: \c"; cat log.topt.rbf-race | grep '\-\-\- FAIL' |wc -l + +topt-lmdb: + mv log.topt.lmdb log.topt.lmdb.prev || true + PILOSA_TXSRC=lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb + @echo " log.topt.lmdb green: \c"; cat log.topt.lmdb | grep PASS |wc -l + @echo " log.topt.lmdb red: \c"; cat log.topt.lmdb | grep '\-\-\- FAIL' |wc -l + topt-race: mv log.topt.race log.topt.race.prev || true go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.race @@ -221,7 +233,7 @@ bg-rbf: # Run golangci-lint golangci-lint: require-golangci-lint - golangci-lint run --skip-files '.*\.peg\.go' + golangci-lint run --timeout 3m --skip-files '.*\.peg\.go' # Alias linter: golangci-lint diff --git a/api.go b/api.go index 8510c9016..287dbc66c 100644 --- a/api.go +++ b/api.go @@ -1769,6 +1769,23 @@ func (api *API) ActiveQueries(ctx context.Context) ([]ActiveQueryStatus, error) return api.tracker.ActiveQueries(), nil } +// TranslateIndexDB is an internal function to load the index keys database +func (api *API) TranslateIndexDB(ctx context.Context, indexName string, partitionID int, rd io.Reader) error { + idx := api.holder.Index(indexName) + store := idx.TranslateStore(partitionID) + _, err := store.ReadFrom(rd) + return err +} + +// TranslateFieldDB is an internal function to load the field keys database +func (api *API) TranslateFieldDB(ctx context.Context, indexName, fieldName string, rd io.Reader) error { + idx := api.holder.Index(indexName) + field := idx.Field(fieldName) + store := field.TranslateStore() + _, err := store.ReadFrom(rd) + return err +} + type serverInfo struct { ShardWidth uint64 `json:"shardWidth"` Memory uint64 `json:"memory"` diff --git a/api_test.go b/api_test.go index 3942a7337..f592afb6e 100644 --- a/api_test.go +++ b/api_test.go @@ -134,8 +134,9 @@ func TestAPI_ImportColumnAttrs(t *testing.T) { if err != nil { t.Fatal(err) } - if len(res.ColumnAttrSets) != 100 { - t.Fatal("incorrect number of column attrs set") + m := len(res.ColumnAttrSets) + if m != 100 { + t.Fatalf("incorrect number of column attrs set; m = %v", m) } for _, v := range res.ColumnAttrSets { diff --git a/badger.go b/badger.go index ec4710f08..1b5e35f24 100644 --- a/badger.go +++ b/badger.go @@ -23,7 +23,6 @@ import ( "os" "runtime" "sort" - "strconv" "strings" "sync" "time" @@ -32,6 +31,7 @@ import ( badger "github.com/dgraph-io/badger/v2" badgeroptions "github.com/dgraph-io/badger/v2/options" "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/txpath" "github.com/pkg/errors" ) @@ -113,12 +113,19 @@ import ( var badgerDefaultLogger *BadgerLog var badgerTestLogger *BadgerLog +const BadgerLogToStderr = false + func init() { // badger test output clutters up the screen, dump to /dev/null for now. // TODO(jea): figure out where badger logging should go. null, err := os.Open(os.DevNull) panicOn(err) - badgerTestLogger = &BadgerLog{Logger: log.New(null, "badger ", log.LstdFlags)} + var out io.Writer = null + if BadgerLogToStderr { + // view badger logs + out = os.Stderr + } + badgerTestLogger = &BadgerLog{Logger: log.New(out, "badger ", log.LstdFlags)} badgerDefaultLogger = badgerTestLogger // BadgerDB recommends a minimum of 128 GOMAXPROCS to make use of the IOPs @@ -247,17 +254,6 @@ func DumpAllBadger() { } } -// newBadgerDBWrapper creates a new empty database, blowing away -// any prior path + "-badgerdb" directory. -func (r *badgerRegistrar) newBadgerDBWrapper(path string) (*BadgerDBWrapper, error) { - bpath := badgerPath(path) - err := os.RemoveAll(bpath) - if err != nil { - return nil, err - } - return r.openBadgerDBWrapper(bpath) -} - // badgerPath is a helper for determining the full directory // in which the badger database will be stored. func badgerPath(path string) string { @@ -297,6 +293,7 @@ func (r *badgerRegistrar) openBadgerDBWrapper(bpath string) (*BadgerDBWrapper, e opt.Compression = badgeroptions.None // turn off compression. opt.ZSTDCompressionLevel = 0 // really, just in case. opt.SyncWrites = true // default is true, safe. + //opt.KeepL0InMemory = true // speedup? // MaxCacheSize docs: // @@ -307,9 +304,15 @@ func (r *badgerRegistrar) openBadgerDBWrapper(bpath string) (*BadgerDBWrapper, e // encryption both are disabled, adding a cache will lead to // unnecessary overhead which will affect the read performance. // Setting size to zero disables the cache altogether. + //opt.MaxCacheSize = 1 << 30 // slows down 135 sec vs 113 sec on our benchmark opt.MaxCacheSize = 0 opt.LoadBloomsOnOpen = false // should speed up start-up time. + //opt.KeepBlocksInCache = true // default false + //opt.KeepBlockIndicesInCache = true // default false + + opt.BlockSize = 8 * 1024 // default 4 * 1024 + // to get memory only do: //opt := badger.DefaultOptions("").WithLogger(badgerDefaultLogger).WithInMemory(true) @@ -342,7 +345,7 @@ func (w *BadgerDBWrapper) DeleteIndex(indexName string) error { if strings.Contains(indexName, "'") { return fmt.Errorf("error: bad indexName `%v` in BadgerDBWrapper.DeleteIndex() call: indexName cannot contain apostrophes/single quotes.", indexName) } - prefix := badgerIndexOnlyPrefix(indexName) + prefix := txpath.IndexOnlyPrefix(indexName) return w.DeletePrefix(prefix) } @@ -475,27 +478,19 @@ func (w *BadgerDBWrapper) UnprotectedListOpenItAsString() (r string) { // but when set is highly useful for debugging. It has no impact // on transaction behavior. // -func (w *BadgerDBWrapper) NewBadgerTx(write bool, initialIndexName string) (tx *BadgerTx) { - w.muDb.Lock() - defer w.muDb.Unlock() +func (w *BadgerDBWrapper) NewBadgerTx(write bool, initialIndexName string, frag *fragment) (tx *BadgerTx) { tx = &BadgerTx{ + frag: frag, write: write, tx: w.db.NewTransaction(write), Db: w, - initloc: stack(), doAllocZero: w.doAllocZero, initialIndexName: initialIndexName, DeleteEmptyContainer: w.DeleteEmptyContainer, + //initloc: "", // stack(), } - if w.openTx == nil { - w.openTx = make(map[*BadgerTx]bool) - } - - w.muOpenTxIt.Lock() - w.openTx[tx] = write - w.muOpenTxIt.Unlock() return } @@ -531,9 +526,11 @@ type BadgerTx struct { Db *BadgerDBWrapper tx *badger.Txn + frag *fragment opcount int - initloc string // stack trace of where we were initially created. + // keep linter happy, comment out until needed again for debugging. + //initloc string // stack trace of where we were initially created. doAllocZero bool @@ -554,7 +551,8 @@ func (tx *BadgerTx) Type() string { } func (tx *BadgerTx) UseRowCache() bool { - return false + //the row cache speeds up queries. + return true } // overWriteOurAllocs provides detection of memory @@ -592,12 +590,6 @@ func (tx *BadgerTx) overWriteOurAllocs() { //} } -// WholeDatabaseBlake3Hash returns the root-hash from the Merkle tree -// built by hashing all bits stored in the database backing this transaction. -func (tx *BadgerTx) WholeDatabaseBlake3Hash(index, field, view string, shard uint64) (hash string, err error) { - return -} - // Pointer gives us a memory address for the underlying transaction for debugging. // It is public because we use it in roaring to report invalid container memory access // outside of a transaction. @@ -666,122 +658,6 @@ func (tx *BadgerTx) RoaringBitmap(index, field, view string, shard uint64) (*roa return tx.OffsetRange(index, field, view, shard, 0, 0, LeftShifted16MaxContainerKey) } -// badgerKey produces the bytes that we use as a key to query badger. -// The roaringContainerKey argument is a container key into a roaring Container. -// Output examples: -// -// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@00000000000000000000" // smallest container-key -// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@18446744073709551615" // largest container-key (math.MaxUint64) -// -// NB must be kept in sync with badgerPrefix() and badgerKeyExtractContainerKey(). -// -func badgerKey(index, field, view string, shard uint64, roaringContainerKey uint64) []byte { - // The %020d which adds zero padding up to 20 runes is required to - // allow the textual sort to accurately - // reflect a numeric sort order. This is because, as a string, - // math.MaxUint64 is 20 bytes long. - // Example of such a badgerKey with a container-key that is math.MaxUint64: - // ...........................................12345678901234567890 - // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 - - prefix := badgerPrefix(index, field, view, shard) - ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) - bkey := append(prefix, ckey...) - MustValidateKey(bkey) - return bkey -} - -var ckeyPartExpected = []byte(";ckey@") - -// MustValidatekey will panic on a bad badgerKey with an informative message. -func MustValidateKey(bkey []byte) { - n := len(bkey) - if n < 56 { - panic(fmt.Sprintf("bkey too short min size is 56 but we see %v in '%v'", n, string(bkey))) - } - beforeCkey := bkey[n-26 : n-20] - if !bytes.Equal(beforeCkey, ckeyPartExpected) { - panic(fmt.Sprintf(`bkey did not have expected ";ckey@" at 26 bytes from the end of the bkey '%v'; instead had '%v'`, string(bkey), string(beforeCkey))) - } -} - -func shardFromBadgerKey(bkey []byte) (shard uint64) { - MustValidateKey(bkey) - - n := len(bkey) - // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 -> idx:'i';fld:'f';vw:'standard';shd:'1 - by := bkey[:n-27] - beg := bytes.LastIndex(by, []byte("'")) - if beg == -1 { - panic(fmt.Sprintf("bad bkey='%v' did not have single quote to being shard decoding", string(bkey))) - } - parseMe := string(by[beg+1:]) - shard, err := strconv.ParseUint(parseMe, 10, 64) - if err != nil { - panic(fmt.Sprintf("could not parse parseMe '%v' in strconv.ParseUint(), error: '%v'", parseMe, err)) - } - return shard -} - -// badgerKeyAndPrefix returns the equivalent of badgerKey() and badgerPrefix() calls. -func badgerKeyAndPrefix(index, field, view string, shard uint64, roaringContainerKey uint64) (key, prefix []byte) { - prefix = badgerPrefix(index, field, view, shard) - ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) - bkey := append(prefix, ckey...) - MustValidateKey(bkey) - return bkey, prefix -} - -var _ = badgerKeyAndPrefix // keep linter happy - -// badgerKeyExtractContainerKey extracts the containerKey from bkey. -func badgerKeyExtractContainerKey(bkey []byte) (containerKey uint64) { - MustValidateKey(bkey) - // The zero padding means that the container-key is always the last 20 bytes of the bkey. - // - // Be sure to catch the problematic case of a user passing in only a prefix. A prefix - // ends in 'key@' rather than a full key that has 'key@00000000000000000001' (for example) - // at the end. The ParseUint call below will fail in that case. - n := len(bkey) - if n < 20 { - panic(fmt.Sprintf("badgerKeyExtractContainerKey() error: bad bkey '%v', too short!", string(bkey))) - } - last := bkey[n-20:] // badgerKey() and badgerPrefix() always return more than 20 rune []byte. - var err error - containerKey, err = strconv.ParseUint(string(last), 10, 64) // has to be the container key - if err != nil { - panic(fmt.Sprintf("badgerKeyExtractContainerKey() error: bad bkey '%v', could not convert last 20 bytes ('%v') to a unit64: '%v'", string(bkey), string(last), err)) - } - return -} - -func badgerAllShardPrefix(index, field, view string) []byte { - return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:", index, field, view)) -} - -// badgerPrefix returns everything from badgerKey up to and -// including the '@' fune in a badger key. The prefix excludes the roaring container key itself. -// NB must be kept in sync with badgerKey() and badgerKeyExtractContainerKey(). -func badgerPrefix(index, field, view string, shard uint64) []byte { - return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:'%020v';ckey@", index, field, view, shard)) -} - -// badgerIndexOnlyPrefix returns a prefix suitable for DeleteIndex and a key-scan to -// remove all storage associated with one index. -// -// The full name of the index must be provided, no partial index names will work. -// -// The provided key is terminated by `';` and so DeleteIndex("i") will not delete the index "i2". -// -func badgerIndexOnlyPrefix(indexName string) []byte { - return []byte(fmt.Sprintf("idx:'%v';", indexName)) -} - -// same for deleting a whole field. -func badgerFieldPrefix(index, field string) []byte { - return []byte(fmt.Sprintf("idx:'%v';fld:'%v';", index, field)) -} - // Container returns the requested roaring.Container, selected by fragment and ckey func (tx *BadgerTx) Container(index, field, view string, shard uint64, ckey uint64) (c *roaring.Container, err error) { @@ -790,7 +666,7 @@ func (tx *BadgerTx) Container(index, field, view string, shard uint64, ckey uint // you must use copy() to copy it to another byte slice. // BUT here we are already inside the Txn. - bkey := badgerKey(index, field, view, shard, ckey) + bkey := txpath.Key(index, field, view, shard, ckey) tx.mu.Lock() var item *badger.Item item, err = tx.tx.Get(bkey) @@ -815,22 +691,22 @@ func (tx *BadgerTx) Container(index, field, view string, shard uint64, ckey uint // PutContainer stores rc under the specified fragment and container ckey. func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey uint64, rc *roaring.Container) error { - bkey := badgerKey(index, field, view, shard, ckey) + bkey := txpath.Key(index, field, view, shard, ckey) var by []byte ct := roaring.ContainerType(rc) switch ct { - case containerArray: + case roaring.ContainerArray: by = fromArray16(roaring.AsArray(rc)) - case containerBitmap: + case roaring.ContainerBitmap: by = fromArray64(roaring.AsBitmap(rc)) - case containerRun: + case roaring.ContainerRun: by = fromInterval16(roaring.AsRuns(rc)) - case containerNil: - panic("wat? nil container is unexpected, no?!?") + case roaring.ContainerNil: + panic("wat? nil roaring.Container is unexpected, no?!?") default: - panic(fmt.Sprintf("unknown container type: %v", ct)) + panic(fmt.Sprintf("unknown roaring.Container type: %v", ct)) } entry := badger.NewEntry(bkey, by).WithMeta(ct) tx.mu.Lock() @@ -857,7 +733,7 @@ func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey u // RemoveContainer deletes the container specified by the shard and container key ckey func (tx *BadgerTx) RemoveContainer(index, field, view string, shard uint64, ckey uint64) error { - bkey := badgerKey(index, field, view, shard, ckey) + bkey := txpath.Key(index, field, view, shard, ckey) tx.mu.Lock() err := tx.tx.Delete(bkey) tx.mu.Unlock() @@ -949,7 +825,7 @@ func (tx *BadgerTx) Remove(index, field, view string, shard uint64, a ...uint64) func (tx *BadgerTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) { lo, hi := lowbits(key), highbits(key) - bkey := badgerKey(index, field, view, shard, hi) + bkey := txpath.Key(index, field, view, shard, hi) tx.mu.Lock() item, err := tx.tx.Get(bkey) tx.mu.Unlock() @@ -970,7 +846,7 @@ func (tx *BadgerTx) Contains(index, field, view string, shard uint64, key uint64 func (tx *BadgerTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - prefix := badgerAllShardPrefix(index, field, view) + prefix := txpath.AllShardPrefix(index, field, view) bi := NewBadgerIterator(tx, prefix) defer bi.Close() @@ -983,7 +859,7 @@ func (tx *BadgerTx) SliceOfShards(index, field, view, optionalViewPath string) ( for bi.Next() { item := bi.it.Item() key := item.Key() - shard := shardFromBadgerKey(key) + shard := txpath.ShardFromKey(key) if firstDone { if shard != lastShard { sliceOfShards = append(sliceOfShards, shard) @@ -1009,10 +885,10 @@ func (tx *BadgerTx) SliceOfShards(index, field, view, optionalViewPath string) ( func (tx *BadgerTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) { // needle example: "idx:'i';fld:'f';vw:'v';shd:'00000000000000000000';key@00000000000000000000" - needle := badgerKey(index, field, view, shard, firstRoaringContainerKey) + needle := txpath.Key(index, field, view, shard, firstRoaringContainerKey) // prefix example: "idx:'i';fld:'f';vw:'v';shard:'00000000000000000000';key@" - prefix := badgerPrefix(index, field, view, shard) + prefix := txpath.Prefix(index, field, view, shard) bi := NewBadgerIterator(tx, prefix) bi.Seek(needle) @@ -1045,12 +921,9 @@ type BadgerIterator struct { } // NewBadgerIterator creates an iterator on tx that will -// only return badgerKeys that start with prefix. +// only return txpath.Keys that start with prefix. func NewBadgerIterator(tx *BadgerTx, prefix []byte) (bi *BadgerIterator) { - tx.Db.muOpenTxIt.Lock() - defer tx.Db.muOpenTxIt.Unlock() - opts := badger.DefaultIteratorOptions opts.PrefetchValues = false // else by default, pre-fetches the 1st 100 values, which would be slow. opts.Reverse = false @@ -1064,10 +937,13 @@ func NewBadgerIterator(tx *BadgerTx, prefix []byte) (bi *BadgerIterator) { it: it, prefix: prefix, } + tx.Db.muOpenTxIt.Lock() if tx.Db.openIt == nil { tx.Db.openIt = make(map[*BadgerIterator]bool) } tx.Db.openIt[bi] = false // true for reverse, false for forward iteration. + tx.Db.muOpenTxIt.Unlock() + bi.it.Seek(prefix) return } @@ -1150,7 +1026,7 @@ func (bi *BadgerIterator) Value() (containerKey uint64, c *roaring.Container) { panic("item was nil") } key := item.Key() - containerKey = badgerKeyExtractContainerKey(key) + containerKey = txpath.KeyExtractContainerKey(key) err := item.Value(func(v []byte) error { c = bi.tx.toContainer(item.UserMeta(), v) @@ -1256,8 +1132,8 @@ func (tx *BadgerTx) Count(index, field, view string, shard uint64) (uint64, erro // Returns zero if the bitmap is empty. Odd, but this is what roaring.Max does. func (tx *BadgerTx) Max(index, field, view string, shard uint64) (uint64, error) { - prefix := badgerPrefix(index, field, view, shard) - seekto := badgerPrefix(index, field, view, shard+1) + prefix := txpath.Prefix(index, field, view, shard) + seekto := txpath.Prefix(index, field, view, shard+1) it := NewBadgerReverseIterator(tx, prefix, seekto) // this iterator is still open, when we commit/discard tx. defer it.Close() @@ -1318,6 +1194,24 @@ func (tx *BadgerTx) UnionInPlace(index, field, view string, shard uint64, others // roaring.countRange counts the number of bits set between [start, end). func (tx *BadgerTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + if tx.frag == nil { + return tx.countRangeNoFrag(index, field, view, shard, start, end) + } + + // For speed, exploit the fact that on startup the rowCache will + // have already loaded fragments. + rowID := start / ShardWidth + row, err := tx.frag.unprotectedRow(tx, rowID) + if err != nil { + return 0, err + } + return row.Count(), nil +} + +// CountRange returns the count of hot bits in the start, end range on the fragment. +// roaring.countRange counts the number of bits set between [start, end). +func (tx *BadgerTx) countRangeNoFrag(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + if start >= end { return 0, nil } @@ -1398,27 +1292,24 @@ func (tx *BadgerTx) OffsetRange(index, field, view string, shard, offset, start, off := highbits(offset) hi0, hi1 := highbits(start), highbits(endx) - // TODO(jea): question: do we have to account for ShardWidth here? what if the move goes - // beyond a shard? + needle := txpath.Key(index, field, view, shard, hi0) + prefix := txpath.Prefix(index, field, view, shard) - needle := badgerKey(index, field, view, shard, hi0) - prefix := badgerPrefix(index, field, view, shard) - - n2, pre2 := badgerKeyAndPrefix(index, field, view, shard, hi0) + n2, pre2 := txpath.KeyAndPrefix(index, field, view, shard, hi0) if string(n2) != string(needle) { - panic(fmt.Sprintf("problem! n2(%v) != needle(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(n2), string(needle))) + panic(fmt.Sprintf("problem! n2(%v) != needle(%v), txpath.KeyAndPrefix not consitent with txpath.Key()", string(n2), string(needle))) } if string(pre2) != string(prefix) { - panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(pre2), string(prefix))) + panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), txpath.KeyAndPrefix not consitent with txpath.Key()", string(pre2), string(prefix))) } - it := NewBadgerIterator(tx, prefix) // see OffsetRange() panic 'Only one iterator can be active at one time, for a RW txn + it := NewBadgerIterator(tx, prefix) defer it.Close() it.Seek(needle) for ; it.it.ValidForPrefix(prefix); it.Next() { item := it.it.Item() bkey := item.Key() - k := badgerKeyExtractContainerKey(bkey) + k := txpath.KeyExtractContainerKey(bkey) // >= hi1 is correct b/c endx cannot have any lowbits set. if uint64(k) >= hi1 { @@ -1426,7 +1317,6 @@ func (tx *BadgerTx) OffsetRange(index, field, view string, shard, offset, start, } destCkey := off + (k - hi0) err := item.Value(func(v []byte) error { - c := tx.toContainer(item.UserMeta(), v) other.Containers.Put(destCkey, c.Freeze()) @@ -1538,7 +1428,7 @@ func (tx *BadgerTx) ImportRoaringBits(index, field, view string, shard uint64, i newC := roaring.Union(oldC, synthC) // UnionInPlace was giving us crashes on overly large containers. - if roaring.ContainerType(newC) == containerBitmap { + if roaring.ContainerType(newC) == roaring.ContainerBitmap { newC.Repair() // update the bit-count so .n is valid. b/c UnionInPlace doesn't update it. } if newC.N() != existN { @@ -1574,14 +1464,6 @@ func toInterval16(a []byte) []roaring.Interval16 { return (*[2048]roaring.Interval16)(unsafe.Pointer(&a[0]))[: len(a)/4 : len(a)/4] } -// should really be exported from the pilosa/roaring package so we don't get out of sync... -const ( - containerNil byte = iota // no container - containerArray // slice of bit position values - containerBitmap // slice of 1024 uint64s - containerRun // container of run-encoded bits -) - func (tx *BadgerTx) toContainer(typ byte, v []byte) (r *roaring.Container) { if len(v) == 0 { @@ -1589,7 +1471,8 @@ func (tx *BadgerTx) toContainer(typ byte, v []byte) (r *roaring.Container) { } var w []byte - if tx.doAllocZero { + useRowCache := tx.UseRowCache() + if tx.doAllocZero || useRowCache { // Do electric fence-inspired bad-memory read detection. // // The v []byte lives in BadgerDB's memory-mapped vlog-file, @@ -1610,26 +1493,37 @@ func (tx *BadgerTx) toContainer(typ byte, v []byte) (r *roaring.Container) { w = make([]byte, len(v)) copy(w, v) - // register w so we can catch out-of-tx memory access - tx.acMu.Lock() - defer tx.acMu.Unlock() - tx.ourAllocs = append(tx.ourAllocs, w) + if !useRowCache { + // register w so we can catch out-of-tx memory access + tx.acMu.Lock() + defer tx.acMu.Unlock() + tx.ourAllocs = append(tx.ourAllocs, w) + } } else { w = v } switch typ { - case containerArray: + case roaring.ContainerArray: c := roaring.NewContainerArray(toArray16(w)) - tx.ourContainers = append(tx.ourContainers, c) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } return c - case containerBitmap: + case roaring.ContainerBitmap: c := roaring.NewContainerBitmap(-1, toArray64(w)) - tx.ourContainers = append(tx.ourContainers, c) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } return c - case containerRun: + case roaring.ContainerRun: c := roaring.NewContainerRun(toInterval16(w)) - tx.ourContainers = append(tx.ourContainers, c) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } return c default: panic(fmt.Sprintf("unknown container: %v", typ)) @@ -1670,7 +1564,7 @@ func fromInterval16(a []roaring.Interval16) []byte { // keys available in badger. func (w *BadgerDBWrapper) StringifiedBadgerKeys(optionalUseThisTx Tx) (r string) { if optionalUseThisTx == nil { - tx := w.NewBadgerTx(!writable, "") + tx := w.NewBadgerTx(!writable, "", nil) defer tx.Rollback() r = stringifiedBadgerKeysTx(tx) return @@ -1685,7 +1579,7 @@ func (w *BadgerDBWrapper) StringifiedBadgerKeys(optionalUseThisTx Tx) (r string) } // countBitsSet returns the number of bits set (or "hot") in -// the roaring container value found by the badgerKey() +// the roaring container value found by the txpath.Key() // formatted bkey. func (tx *BadgerTx) countBitsSet(bkey []byte) (n int) { @@ -1723,7 +1617,7 @@ func (tx *BadgerTx) Dump() { func stringifiedBadgerKeysTx(tx *BadgerTx) (r string) { r = "allkeys:[\n" - it := tx.tx.NewIterator(badger.DefaultIteratorOptions) + it := tx.tx.NewIterator(badger.DefaultIteratorOptions) // PrefetchValues true okay here. defer it.Close() any := false for it.Rewind(); it.Valid(); it.Next() { @@ -1731,7 +1625,7 @@ func stringifiedBadgerKeysTx(tx *BadgerTx) (r string) { item := it.Item() bkey := item.Key() key := string(bkey) - ckey := badgerKeyExtractContainerKey(bkey) + ckey := txpath.KeyExtractContainerKey(bkey) hash := "" srbm := "" err := item.Value(func(val []byte) error { @@ -1794,9 +1688,9 @@ func zeroKeyContainerAsString(ct *roaring.Container) (r string) { } var containerTypeNames = map[byte]string{ - containerArray: "array", - containerBitmap: "bitmap", - containerRun: "run", + roaring.ContainerArray: "array", + roaring.ContainerBitmap: "bitmap", + roaring.ContainerRun: "run", } func bitmapAsString(rbm *roaring.Bitmap) (r string) { @@ -1889,12 +1783,12 @@ func (w *BadgerDBWrapper) DeleteField(index, field, fieldPath string) error { if err != nil { return errors.Wrap(err, "removing directory") } - prefix := badgerFieldPrefix(index, field) + prefix := txpath.FieldPrefix(index, field) return w.DeletePrefix(prefix) } func (w *BadgerDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { - prefix := badgerPrefix(index, field, view, shard) + prefix := txpath.Prefix(index, field, view, shard) return w.DeletePrefix(prefix) } @@ -1922,7 +1816,8 @@ func (w *BadgerDBWrapper) DeletePrefix(prefix []byte) error { o.PrefetchValues = false // key-only iteration, no values. // note: panic: Unclosed iterator at time of Txn.Discard ? panic on segfault here? - // This means we messed up and Closed() the Database already; too early. + // This means we messed up and Closed() the Database already; too early. For + // example in TxFactor.CloseIndex() in txfactory.go:331. it := txn.NewIterator(o) defer it.Close() diff --git a/badger_test.go b/badger_test.go index 821356867..f48a61103 100644 --- a/badger_test.go +++ b/badger_test.go @@ -32,7 +32,6 @@ import ( "fmt" "math" "os" - "strconv" "testing" "github.com/dgraph-io/badger/v2" @@ -46,7 +45,7 @@ var _ = &roaring.Bitmap{} func badgerDBMustHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() exists, err := tx.Contains(index, field, view, shard, bitvalue) panicOn(err) @@ -59,7 +58,7 @@ func badgerDBMustHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view string func badgerDBMustNotHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() exists, err := tx.Contains(index, field, view, shard, bitvalue) panicOn(err) @@ -70,7 +69,7 @@ func badgerDBMustNotHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view str } func badgerDBMustSetBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) // add a bit changed, err := tx.Add(index, field, view, shard, doBatched, putme) @@ -88,14 +87,14 @@ func badgerDBMustSetBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, } func badgerDBMustDeleteBitvalueContainer(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) hi := highbits(putme) panicOn(tx.RemoveContainer(index, field, view, shard, hi)) panicOn(tx.Commit()) } func badgerDBMustDeleteBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) _, err := tx.Remove(index, field, view, shard, putme) panicOn(err) panicOn(tx.Commit()) @@ -105,7 +104,7 @@ func mustOpenEmptyBadgerWrapper(path string) (w *BadgerDBWrapper, cleaner func() var err error fn := badgerPath(path) panicOn(os.RemoveAll(fn)) - w, err = globalBadgerReg.newBadgerDBWrapper(path) + w, err = globalBadgerReg.openBadgerDBWrapper(path) panicOn(err) // verify it is empty @@ -120,13 +119,107 @@ func mustOpenEmptyBadgerWrapper(path string) (w *BadgerDBWrapper, cleaner func() } } -// +// end of helper utilities +////////////////////////// + +////////////////////////// +// begin Tx method tests + +func TestBadger_DeleteFragment(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteFragment") + defer clean() + defer dbwrap.Close() + index, field, view, shard0 := "i", "f", "v", uint64(0) + tx := dbwrap.NewBadgerTx(writable, index, nil) + + shard1 := uint64(1) + + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + shards := []uint64{shard0, shard1} + for _, s := range shards { + for _, v := range bits { + changed, err := tx.Add(index, field, view, s, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + } + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + } + err := tx.Commit() + panicOn(err) + + // end of setup + + survivor := shard0 + victim := shard1 + err = dbwrap.DeleteFragment(index, field, view, victim, nil) + panicOn(err) + + tx = dbwrap.NewBadgerTx(!writable, index, nil) + defer tx.Rollback() + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if s == survivor { + if !exists { + panic(fmt.Sprintf("ARG survivor died : bit %v", v)) + } + } else if s == victim { // victim, should have been deleted + if exists { + panic(fmt.Sprintf("ARG victim lived : bit %v", v)) + } + } + } + } +} + +func TestBadger_Max_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Max_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewBadgerTx(!writable, index, nil) + defer tx.Rollback() + + max, err := tx.Max(index, field, view, shard) + panicOn(err) + expected := putmeValues[len(putmeValues)-1] + if max != expected { + panic(fmt.Sprintf("expected Max() of %v but got max=%v", expected, max)) + } +} + +// and the rest + func TestBadger_SetBitmap(t *testing.T) { dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SetBitmap") defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) bitvalue := uint64(0) changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) if changed <= 0 { @@ -147,7 +240,7 @@ func TestBadger_SetBitmap(t *testing.T) { // commited, so should be visible outside the txn // - tx2 := dbwrap.NewBadgerTx(!writable, index) + tx2 := dbwrap.NewBadgerTx(!writable, index, nil) exists, err = tx2.Contains(index, field, view, shard, bitvalue) panicOn(err) if !exists { @@ -163,11 +256,11 @@ func TestBadger_SetBitmap(t *testing.T) { } func TestBadger_OffsetRange(t *testing.T) { - dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SetBitmap") + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_OffsetRange") defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) bitvalue := uint64(1 << 20) changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) @@ -201,7 +294,7 @@ func TestBadger_OffsetRange(t *testing.T) { start := uint64(0 << 16) endx := bitvalue + 1<<16 - tx2 := dbwrap.NewBadgerTx(!writable, index) + tx2 := dbwrap.NewBadgerTx(!writable, index, nil) rbm2, err := tx2.OffsetRange(index, field, view, shard, offset, start, endx) panicOn(err) tx2.Rollback() @@ -215,7 +308,7 @@ func TestBadger_OffsetRange(t *testing.T) { // now offset by 2M offset = uint64(2 << 20) - tx3 := dbwrap.NewBadgerTx(!writable, index) + tx3 := dbwrap.NewBadgerTx(!writable, index, nil) rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) panicOn(err) tx3.Rollback() @@ -243,7 +336,7 @@ func TestBadger_Count_on_many_containers(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() n, err := tx.Count(index, field, view, shard) @@ -259,7 +352,7 @@ func TestBadger_Count_dense_containers(t *testing.T) { defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) expected := 0 // can't do more than about 100k writes per badger txn by default, so @@ -288,7 +381,7 @@ func TestBadger_ContainerIterator_on_empty(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() bitvalue := uint64(0) citer, found, err := tx.ContainerIterator(index, field, view, shard, bitvalue) @@ -306,7 +399,7 @@ func TestBadger_ContainerIterator_on_one_bit(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() bitvalue := uint64(42) @@ -358,55 +451,12 @@ func TestBadger_ContainerIterator_on_one_bit(t *testing.T) { } } -func TestBadger_badgerKey_badgerPrefix(t *testing.T) { - - // badgerPrefix() must agree with badgerKey(), but not have the key at the end. - // This is important for iteration over containers. - - index, field, view, shard := "i", "f", "v", uint64(0) - - // needle examples with the container-key extremes: - // "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" // smallest - // "index:'i';field:'f';view:'v';shard:'0';key@18446744073709551615" // largest - needle := badgerKey(index, field, view, shard, 0) - - // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" - prefix := badgerPrefix(index, field, view, shard) - - if !bytes.HasPrefix(needle, prefix) { - panic(fmt.Sprintf("badgerPrefix() output '%v'was not a prefix of badgerKey() '%v'", string(needle), string(prefix))) - } - if len(prefix)+20 != len(needle) { - panic(fmt.Sprintf("badgerPrefix() output '%v'was 20 characters shorter than badgerKey() '%v'", string(needle), string(prefix))) - } - - // validate assumption that badgerKeyExtractContainerKey() makes about strconv.ParseUint() error reporting; - // for distinguishing prefixes from full keys. Even if the shard number is so large that the prefix - // starts with a legitimate decimal number. - shouldNotParse := "12345123451234';key@" - containerKey, err := strconv.ParseUint(shouldNotParse, 10, 64) - if err == nil { - panic(fmt.Sprintf("strconv.ParseUint should have returned an error parsing this string '%v'; instead we got '%v'", shouldNotParse, containerKey)) - } - - // verify panic on submitting a prefix - func() { - defer func() { - r := recover() - if r == nil { - panic(fmt.Sprintf("should have seen panic on call to badgerKeyExtractContainerKey(prefix='%v')", prefix)) - } - }() - badgerKeyExtractContainerKey(prefix) // should panic. - }() -} - func TestBadger_ContainerIterator_on_one_bit_fail_to_find(t *testing.T) { dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() putme := uint64(1<<16) + 3 // in the key:1 container @@ -461,7 +511,7 @@ func TestBadger_ContainerIterator_empty_iteration_loop(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() putme := uint64(1<<16) + 3 // in the key:1 container @@ -511,7 +561,7 @@ func TestBadger_ForEach_on_one_bit(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() bitvalue := uint64(42) @@ -570,7 +620,7 @@ func TestBadger_RemoveContainer_one_bit_test(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // delete, but rollback instead of commit - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) hi := highbits(putme) panicOn(tx.RemoveContainer(index, field, view, shard, hi)) tx.Rollback() @@ -579,7 +629,7 @@ func TestBadger_RemoveContainer_one_bit_test(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // c) within one Tx, after delete it should be gone as viewed within the txn. - tx = dbwrap.NewBadgerTx(writable, index) + tx = dbwrap.NewBadgerTx(writable, index, nil) hi = highbits(putme) exists, err := tx.Contains(index, field, view, shard, putme) @@ -631,7 +681,7 @@ func TestBadger_Remove_one_bit_test(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // delete, but rollback instead of commit - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) hi, lo := highbits(putme), lowbits(putme) _, _ = hi, lo _, err := tx.Remove(index, field, view, shard, hi) @@ -642,7 +692,7 @@ func TestBadger_Remove_one_bit_test(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // c) within one Tx, after delete it should be gone as viewed within the txn. - tx = dbwrap.NewBadgerTx(writable, index) + tx = dbwrap.NewBadgerTx(writable, index, nil) exists, err := tx.Contains(index, field, view, shard, putme) panicOn(err) @@ -717,31 +767,6 @@ func TestBadger_reverse_badger_iterator(t *testing.T) { panicOn(err) } -func TestBadger_Max_on_many_containers(t *testing.T) { - dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Max_on_many_containers") - defer clean() - defer dbwrap.Close() - index, field, view, shard := "i", "f", "v", uint64(0) - - putmeValues := []uint64{0, 2 << 16, 4 << 16} - - for _, putme := range putmeValues { - badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) - badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) - badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) - } - - tx := dbwrap.NewBadgerTx(!writable, index) - defer tx.Rollback() - - max, err := tx.Max(index, field, view, shard) - panicOn(err) - expected := putmeValues[len(putmeValues)-1] - if max != expected { - panic(fmt.Sprintf("expected Max() of %v but got max=%v", expected, max)) - } -} - func TestBadger_Min_on_many_containers(t *testing.T) { dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Min_on_many_containers") defer clean() @@ -749,7 +774,7 @@ func TestBadger_Min_on_many_containers(t *testing.T) { index, field, view, shard := "i", "f", "v", uint64(0) // verify no containers flag works - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) min, containersExist, err := tx.Min(index, field, view, shard) _ = min panicOn(err) @@ -766,7 +791,7 @@ func TestBadger_Min_on_many_containers(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx = dbwrap.NewBadgerTx(!writable, index) + tx = dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() min, containersExist, err = tx.Min(index, field, view, shard) @@ -787,7 +812,7 @@ func TestBadger_CountRange_on_many_containers(t *testing.T) { index, field, view, shard := "i", "f", "v", uint64(0) // verify no containers flag works - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) n, err := tx.CountRange(index, field, view, shard, 0, math.MaxUint64) panicOn(err) if n != 0 { @@ -803,7 +828,7 @@ func TestBadger_CountRange_on_many_containers(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx = dbwrap.NewBadgerTx(!writable, index) + tx = dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() n, err = tx.CountRange(index, field, view, shard, 0, math.MaxUint64) @@ -831,7 +856,7 @@ func TestBadger_CountRange_middle_container(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() // pick out just the middle container with the 1 bit set on it. @@ -856,7 +881,7 @@ func TestBadger_CountRange_many_middle_container(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() // get them all @@ -887,7 +912,7 @@ func TestBadger_UnionInPlace(t *testing.T) { badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx2 := dbwrap.NewBadgerTx(!writable, index) + tx2 := dbwrap.NewBadgerTx(!writable, index, nil) n, err := tx2.Count(index, field, view, shard) panicOn(err) if n != 2 { @@ -902,7 +927,7 @@ func TestBadger_UnionInPlace(t *testing.T) { } mustAddR(others3.Add(4 << 16)) // outside the 2<<16 container - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() err = tx.UnionInPlace(index, field, view, shard, others, others2, others3) panicOn(err) @@ -927,7 +952,7 @@ func TestBadger_RoaringBitmap(t *testing.T) { putme := expected badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() rbm, err := tx.RoaringBitmap(index, field, view, shard) @@ -959,7 +984,7 @@ func TestBadger_reverse_badger_iterator_and_prefix_valid(t *testing.T) { }) panicOn(err) - tx := dbwrap.NewBadgerTx(!writable, "no-index-avail") + tx := dbwrap.NewBadgerTx(!writable, "no-index-avail", nil) prefix := []byte("b:") it := NewBadgerIterator(tx, prefix) @@ -1020,7 +1045,7 @@ func TestBadger_just_reverse_badger_iterator_and_prefix_valid(t *testing.T) { }) panicOn(err) - tx := dbwrap.NewBadgerTx(!writable, "no-index-avail") + tx := dbwrap.NewBadgerTx(!writable, "no-index-avail", nil) seekto := []byte("c:") prefix := []byte("b:") @@ -1050,7 +1075,7 @@ func TestBadger_ImportRoaringBits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() tx.DeleteEmptyContainer = true // traditional badger Tx behavior, but not Roaring. @@ -1133,7 +1158,7 @@ func TestBadger_ImportRoaringBits_set_nonoverlapping_bits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() // get some roaring bits, get an itr RoaringIterator from them @@ -1183,7 +1208,7 @@ func TestBadger_ImportRoaringBits_clear_nonoverlapping_bits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) defer tx.Rollback() // get some roaring bits, get an itr RoaringIterator from them @@ -1257,7 +1282,7 @@ func TestBadger_DeleteIndex(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) bitvalue := uint64(777) bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} for _, v := range bits { @@ -1294,7 +1319,7 @@ func TestBadger_DeleteIndex(t *testing.T) { err = dbwrap.DeleteIndex(index) panicOn(err) - tx = dbwrap.NewBadgerTx(!writable, index2) + tx = dbwrap.NewBadgerTx(!writable, index2, nil) defer tx.Rollback() exists, err = tx.Contains(index2, field, view, shard, bitvalue) panicOn(err) @@ -1319,7 +1344,7 @@ func TestBadger_DeleteIndex_over100k(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) + tx := dbwrap.NewBadgerTx(writable, index, nil) bitvalue := uint64(777) limit := uint64(100002) // default batch size in DeleteIndex is 100k keys per delete transaction. //limit := uint64(101) @@ -1332,7 +1357,7 @@ func TestBadger_DeleteIndex_over100k(t *testing.T) { panicOn(err) if v%100000 == 0 { panicOn(tx.Commit()) - tx = dbwrap.NewBadgerTx(writable, index) + tx = dbwrap.NewBadgerTx(writable, index, nil) } } @@ -1349,7 +1374,7 @@ func TestBadger_DeleteIndex_over100k(t *testing.T) { err = dbwrap.DeleteIndex(index) panicOn(err) - tx = dbwrap.NewBadgerTx(!writable, index2) + tx = dbwrap.NewBadgerTx(!writable, index2, nil) defer tx.Rollback() exists, err := tx.Contains(index2, field, view, shard, bitvalue) panicOn(err) @@ -1450,92 +1475,6 @@ func mustRemove(changeCount int, err error) { panicOn(err) } -func TestBadger_DeleteFragment(t *testing.T) { - - // setup - dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteFragment") - defer clean() - defer dbwrap.Close() - index, field, view, shard0 := "i", "f", "v", uint64(0) - tx := dbwrap.NewBadgerTx(writable, index) - - shard1 := uint64(1) - - bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} - shards := []uint64{shard0, shard1} - for _, s := range shards { - for _, v := range bits { - changed, err := tx.Add(index, field, view, s, doBatched, v) - if changed <= 0 { - panic("should have changed") - } - panicOn(err) - } - } - - for _, s := range shards { - for _, v := range bits { - exists, err := tx.Contains(index, field, view, s, v) - panicOn(err) - if !exists { - panic("ARG bitvalue was NOT SET!!!") - } - } - } - err := tx.Commit() - panicOn(err) - - // end of setup - - survivor := shard0 - victim := shard1 - err = dbwrap.DeleteFragment(index, field, view, victim, nil) - panicOn(err) - - tx = dbwrap.NewBadgerTx(!writable, index) - defer tx.Rollback() - - for _, s := range shards { - for _, v := range bits { - exists, err := tx.Contains(index, field, view, s, v) - panicOn(err) - if s == survivor { - if !exists { - panic(fmt.Sprintf("ARG survivor died : bit %v", v)) - } - } else if s == victim { // victim, should have been deleted - if exists { - panic(fmt.Sprintf("ARG victim lived : bit %v", v)) - } - } - } - } -} - -func TestBadger_shardFromBadgerKey(t *testing.T) { - if shardFromBadgerKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615")) != 1 { - panic("problem") - } - if shardFromBadgerKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'0';ckey@18446744073709551615")) != 0 { - panic("problem") - } - if shardFromBadgerKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'18446744073709551615';ckey@18446744073709551615")) != 18446744073709551615 { - panic("problem") - } - - func() { - defer func() { - r := recover() - if r == nil { - panic("should have panic-ed") - } - }() - // called for the panic of a short ckey, only 19 bytes instead of 20 - shardFromBadgerKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'18446744073709551615';ckey@1844674407370955161")) - }() - -} - func TestBadger_SliceOfShards(t *testing.T) { dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SliceOfShards") @@ -1547,7 +1486,7 @@ func TestBadger_SliceOfShards(t *testing.T) { for _, shard := range shards { badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewBadgerTx(!writable, index) + tx := dbwrap.NewBadgerTx(!writable, index, nil) defer tx.Rollback() slc, err := tx.SliceOfShards(index, field, view, "") @@ -1559,6 +1498,89 @@ func TestBadger_SliceOfShards(t *testing.T) { } } +// Benchmark performance of setValue for BSI ranges. +func BenchmarkBadger_Write(b *testing.B) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("BenchmarkBadger_Write") + //defer clean() + _ = clean + defer dbwrap.Close() + + putmeValues := []uint64{3, 2 << 16} + index, field, view, shard := "i", "f", "v", uint64(0) + + for _, putme := range putmeValues { + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + } + /* + + dbwrap, clean := mustOpenEmptyBadgerWrapper("BenchmarkBadger_Write") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewBadgerTx(writable, index, nil) + + bitvalue := uint64(1 << 20) + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + bitvalue2 := uint64(1<<20 + 1) + changed, err = tx.Add(index, field, view, shard, doBatched, bitvalue2) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + exists, err = tx.Contains(index, field, view, shard, bitvalue2) + panicOn(err) + if !exists { + panic("ARG bitvalue2 was NOT SET!!!") + } + + err = tx.Commit() + panicOn(err) + + offset := uint64(0 << 20) + start := uint64(0 << 16) + endx := bitvalue + 1<<16 + + tx2 := dbwrap.NewBadgerTx(!writable, index, nil) + rbm2, err := tx2.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx2.Rollback() + + // should see our 1M value + s2 := bitmapAsString(rbm2) + expect2 := "c(1048576, 1048577)" + if s2 != expect2 { + panic(fmt.Sprintf("s2='%v', but expected '%v'", s2, expect2)) + } + + // now offset by 2M + offset = uint64(2 << 20) + tx3 := dbwrap.NewBadgerTx(!writable, index, nil) + rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx3.Rollback() + + //expect to see 3M == 3145728 + s3 := bitmapAsString(rbm3) + expect3 := "c(3145728, 3145729)" + + if s3 != expect3 { + panic(fmt.Sprintf("s3='%v', but expected '%v'", s3, expect3)) + } + */ +} + func reportTestBadgersNeedingClose() { globalBadgerReg.mu.Lock() defer globalBadgerReg.mu.Unlock() diff --git a/blake3.go b/blake3.go index 22e3da86b..8daac81a6 100644 --- a/blake3.go +++ b/blake3.go @@ -21,6 +21,7 @@ import ( cryptorand "crypto/rand" "github.com/zeebo/blake3" + "golang.org/x/mod/sumdb/dirhash" ) // Blake3Hasher is a thread/goroutine safe way to @@ -93,3 +94,10 @@ func cryptoRandInt64() int64 { r := int64(binary.LittleEndian.Uint64(b)) return r } + +func HashOfDir(path string) string { + prefix := "" + h, err := dirhash.HashDir(path, prefix, dirhash.Hash1) + panicOn(err) + return h +} diff --git a/blake3_test.go b/blake3_test.go index f2c85cb93..86bbbdc75 100644 --- a/blake3_test.go +++ b/blake3_test.go @@ -16,6 +16,8 @@ package pilosa import ( "fmt" + "io/ioutil" + "os" "testing" "encoding/hex" @@ -45,3 +47,25 @@ func TestCryptoRandInt64(t *testing.T) { panic("cryptoRandInt64() gave 0, very high odds it has broken") } } + +func TestHashOfDir(t *testing.T) { + dir, err := ioutil.TempDir(".", "TestHashOfDir-dir") + panicOn(err) + b := dir + sep + "A" + sep + "B" + c := dir + sep + "A" + sep + "C" + panicOn(os.MkdirAll(b, 0755)) + panicOn(os.MkdirAll(c, 0755)) + bmessage := []byte("hello B\n") + panicOn(ioutil.WriteFile(b+sep+"b_content", bmessage, 0644)) + cmessage := []byte("hello C\n") + panicOn(ioutil.WriteFile(c+sep+"c_content", cmessage, 0644)) + defer os.RemoveAll(dir) + hsh := HashOfDir(dir) + + c2message := []byte("hello C2\n") + panicOn(ioutil.WriteFile(c+sep+"c_content", c2message, 0644)) + hsh2 := HashOfDir(dir) + if hsh2 == hsh { + panic("HashOfDir did not detect 1 byte change") + } +} diff --git a/bluegreentx.go b/bluegreentx.go index 3084a2390..1f1d9fc4e 100644 --- a/bluegreentx.go +++ b/bluegreentx.go @@ -85,7 +85,8 @@ func (c *blueGreenTx) Readonly() bool { func (c *blueGreenTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { c.checker.see(index, field, view, shard) - // TODO(jea): does this need to be different, to handle c.a iteration at the same time? + // can't really do simultaneous iteration on A and B, so punt and + // just give back B. return c.b.NewTxIterator(index, field, view, shard) } @@ -141,17 +142,20 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { if bKey != aKey { AlwaysPrintf("problem in caller %v", Caller(2)) c.Dump() - panic(fmt.Sprintf("compareTxState[%v]: A(%v) found key %v, B(%v) found %v, at %v", here, c.as, aKey, c.bs, bKey, stack())) // crashing here on TestBSIGroup_importValue + panic(fmt.Sprintf("compareTxState[%v]: A(%v) found key %v, B(%v) found %v, at %v", here, c.as, aKey, c.bs, bKey, stack())) } if err := aValue.BitwiseCompare(bValue); err != nil { c.Dump() + vv("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at stack=%v", here, aKey, err, c.as, c.bs, stack()) panic(fmt.Sprintf("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at stack=%v", here, aKey, err, c.as, c.bs, stack())) } } // end checking everything in A, but does B have more? if bIter.Next() { - bKey, _ := bIter.Value() + AlwaysPrintf("bIter has more than it should. problem in caller %v", Caller(2)) c.Dump() + bKey, _ := bIter.Value() + vv("compareTxState[%v]: B(%v) found key %v, A(%v) didn't, at %v", here, c.bs, bKey, c.as, stack()) panic(fmt.Sprintf("compareTxState[%v]: B(%v) found key %v, A(%v) didn't, at %v", here, c.bs, bKey, c.as, stack())) } } @@ -177,7 +181,7 @@ func (c *blueGreenTx) Rollback() { c.mu.Lock() defer c.mu.Unlock() if c.rollbackOrCommitDone { - return + return // avoid using discarded tx for Dump, which will panic. } c.rollbackOrCommitDone = true @@ -188,13 +192,16 @@ func (c *blueGreenTx) Rollback() { panic(r) } }() + fmt.Printf("blueGreenTx.Rollback() about to call (%v) a.Rollback()\n", c.as) c.a.Rollback() + fmt.Printf("blueGreenTx.Rollback() about to call (%v) b.Rollback()\n", c.bs) c.b.Rollback() } func (c *blueGreenTx) Commit() error { c.mu.Lock() defer c.mu.Unlock() + fmt.Printf("blueGreenTx.Commit() called.\n") if c.rollbackOrCommitDone { return nil } @@ -332,7 +339,9 @@ func (c *blueGreenTx) RemoveContainer(index, field, view string, shard uint64, k } func (c *blueGreenTx) UseRowCache() bool { - return c.b.UseRowCache() + // avoid cross-talk between our two implementations + // by never allowing either to use the row cache. + return false } func (c *blueGreenTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { @@ -612,7 +621,10 @@ func (c *blueGreenTx) OffsetRange(index, field, view string, shard, offset, star b, errB := c.b.OffsetRange(index, field, view, shard, offset, start, end) err = roaringBitmapDiff(a, b) - panicOn(err) + if err != nil { + c.Dump() + panicOn(err) + } compareErrors(errA, errB) return b, errB } @@ -731,7 +743,7 @@ func (m *MultiReaderB) Read(p []byte) (nB int, errB error) { } cmp := bytes.Compare(p[:nB], p2[:nB]) if cmp != 0 { - panic(fmt.Sprintf("MultiReaderB reads p and p2 (cmp= %v) differed.", cmp)) // \np ='%v'; \np2 ='%v'", cmp, string(p[:nB]), string(p2[:nA]))) + panic(fmt.Sprintf("MultiReaderB reads p and p2 (cmp= %v) differed.", cmp)) } } return @@ -754,7 +766,7 @@ type blueGreenChecker struct { // see would mark a thing as seen. func (b *blueGreenChecker) see(index, field, view string, shard uint64) { // keep this next Printf. Useful to see the sequence of Tx operations. - //fmt.Printf("blueGreenTx.%v\n", Caller(1)) + fmt.Printf("blueGreenTx.%v on index='%v'\n", Caller(1), index) b.mu.Lock() defer b.mu.Unlock() diff --git a/catcher.go b/catcher.go index 2394eb331..ff98bccb8 100644 --- a/catcher.go +++ b/catcher.go @@ -48,10 +48,6 @@ func (c *catcherTx) NewTxIterator(index, field, view string, shard uint64) *roar return c.b.NewTxIterator(index, field, view, shard) } -func (c *catcherTx) WholeDatabaseBlake3Hash(index, field, view string, shard uint64) (hash string, err error) { - return c.b.WholeDatabaseBlake3Hash(index, field, view, shard) -} - func (c *catcherTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { defer func() { if r := recover(); r != nil { diff --git a/cmd/demo-lmdb/lmdb.go b/cmd/demo-lmdb/lmdb.go new file mode 100644 index 000000000..bf91af113 --- /dev/null +++ b/cmd/demo-lmdb/lmdb.go @@ -0,0 +1,267 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !386 + +package main + +import ( + "bytes" + "fmt" + "log" + "os" + + "github.com/glycerine/lmdb-go/lmdb" +) + +// note: use runtime.LockOSThread on any write goroutine; must create and write the txn from +// the same goroutine. + +// This example demonstrates a complete workflow for a simple application +// working with LMDB. First, an Env is configured and mapped to memory. Once +// mapped, database handles are opened and normal database operations may +// begin. +func main() { + // Create an environment and make sure it is eventually closed. + env, err := lmdb.NewEnv() + panicOn(err) + defer env.Close() + + // Configure and open the environment. Most configuration must be done + // before opening the environment. The go documentation for each method + // should indicate if it must be called before calling env.Open() + err = env.SetMaxDBs(1) + panicOn(err) + err = env.SetMapSize(1 << 30) + panicOn(err) + path := "./db-lmdb" + panicOn(os.MkdirAll(path, 0755)) + err = env.Open(path, 0, 0644) // lmdb.Create ? + panicOn(err) + + // In any real application it is important to check for readers that were + // never closed by their owning process, and for which the owning process + // has exited. See the documentation on transactions for more information. + staleReaders, err := env.ReaderCheck() + panicOn(err) + if staleReaders > 0 { + log.Printf("cleared %d reader slots from dead processes", staleReaders) + } + + // Open a database handle that will be used for the entire lifetime of this + // application. Because the database may not have existed before, and the + // database may need to be created, we need to get the database handle in + // an update transacation. + var dbi lmdb.DBI + _ = dbi + err = env.Update(func(txn *lmdb.Txn) (err error) { + dbi, err = txn.CreateDBI("example") + return err + }) + panicOn(err) + + // The database referenced by our DBI handle is now ready for the + // application to use. Here the application just opens a readonly + // transaction and reads the data stored in the "hello" key and prints its + // value to the application's standard output. + err = env.View(func(txn *lmdb.Txn) (err error) { + v, err := txn.Get(dbi, []byte("hello")) + if err != nil { + return err + } + fmt.Println(string(v)) + return nil + }) + _ = err + //panicOn(err) // mdb_get: MDB_NOTFOUND: No matching key/data pair found + + err = env.Update(func(txn *lmdb.Txn) (err error) { + panicOn(txn.Put(dbi, []byte("099"), []byte("A"), 0)) + panicOn(txn.Put(dbi, []byte("101"), []byte("B"), 0)) + panicOn(txn.Put(dbi, []byte("199"), []byte("C"), 0)) + panicOn(txn.Put(dbi, []byte("200"), []byte("D"), 0)) + panicOn(txn.Put(dbi, []byte("300"), []byte("E"), 0)) + panicOn(txn.Put(dbi, []byte("399"), []byte("F"), 0)) + return nil + }) + _ = err + + // find max in [000,100) and get 099 + // find max in [100,200) and get 199 + // find max in [300,400) and get 399 + // find max in [400,500) and get nothing back + err = env.View(func(txn *lmdb.Txn) (err error) { + v, err := txn.Get(dbi, []byte("hello")) + if err != nil { + return err + } + fmt.Printf("key 'hello' retreived value: '%v'\n", string(v)) + return nil + }) + _ = err + + err = env.View(func(txn *lmdb.Txn) (err error) { + cur, err := txn.OpenCursor(dbi) + panicOn(err) + defer cur.Close() + + var cur2 *lmdb.Cursor + var err2 error + var k, k2, v, v2 []byte + i := 0 + for { + if i == 0 { + // lmdb.SetRange : The first key no less than the specified key. + k, v, err = cur.Get([]byte("200"), nil, lmdb.SetRange) + + // cur2 should start at 'a' + cur2, err2 = txn.OpenCursor(dbi) + panicOn(err2) + defer cur2.Close() + k2, v2, err2 = cur2.Get(nil, nil, lmdb.Next) + _ = err2 + } else { + k, v, err = cur.Get(nil, nil, lmdb.Next) + k2, v2, err2 = cur2.Get(nil, nil, lmdb.Next) + _ = err2 + } + if lmdb.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + fmt.Printf("i=%v, %s %s\n", i, k, v) + fmt.Printf("i=%v, k2:%s v2:%s\n", i, k2, v2) + i++ + } + // return nil // unreachable + }) + _ = err + + // panicOn(txn.Put(dbi, []byte("099"), []byte("A"), 0)) + // panicOn(txn.Put(dbi, []byte("101"), []byte("B"), 0)) + // panicOn(txn.Put(dbi, []byte("199"), []byte("C"), 0)) + // panicOn(txn.Put(dbi, []byte("200"), []byte("D"), 0)) + // panicOn(txn.Put(dbi, []byte("300"), []byte("E"), 0)) + // panicOn(txn.Put(dbi, []byte("399"), []byte("F"), 0)) + // + // find max in [300,400) and get 399 + // find max in [000,100) and get 099 + // find max in [100,200) and get 199 + // find max in [400,500) and get nothing back + // find max in [201,300) and get nothing back + + err = env.View(func(txn *lmdb.Txn) (err error) { + cur, err := txn.OpenCursor(dbi) + panicOn(err) + defer cur.Close() + + var k, v []byte + + // find max in [300,400) and get 399 + + // lmdb.SetRange : The first key no less than the specified key. + k, v, err = cur.Get([]byte("400"), nil, lmdb.SetRange) + if lmdb.IsNotFound(err) { + fmt.Printf("400 not found, as expected\n") // happens on starting empty db + } else { + fmt.Printf("Get 400 => %v: %v\n", string(k), string(v)) + } + + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + fmt.Printf("Get 400 then Get Prev => not found\n") + } else { + fmt.Printf("Get 400 then Get Prev => %v: %v\n", string(k), string(v)) // 399: F, so wraps backwards from beginning. + } + panicOn(err) + + // now try for 199 in [100,200) + + k, v, err = cur.Get([]byte("200"), nil, lmdb.SetRange) + if lmdb.IsNotFound(err) { + panic("200 not found, not expected") + } else { + fmt.Printf("Get 200 => %v: %v\n", string(k), string(v)) // Get 200 => 200: D + } + + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + fmt.Printf("Get 200 then Get Prev => not found\n") + } else { + fmt.Printf("Get 200 then Get Prev => %v: %v\n", string(k), string(v)) // Get 200 then Get Prev => 199: C + } + panicOn(err) + + k, v, err = cur.Get([]byte("500"), nil, lmdb.SetRange) + if lmdb.IsNotFound(err) { + fmt.Printf("500 not found, as expected\n") // 500 not found, as expected + } else { + panic(fmt.Sprintf("Get 500 => %v: %v\n", string(k), string(v))) + } + + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + fmt.Printf("Get 500 then Get Prev => not found\n") + } else { + fmt.Printf("Get 500 then Get Prev => %v: %v\n", string(k), string(v)) // Get 500 then Get Prev => 399: F + } + panicOn(err) + + k, v, err = cur.Get([]byte("100"), nil, lmdb.SetRange) + if lmdb.IsNotFound(err) { + panic("100 not found, not expected") + } else { + fmt.Printf("Get 100 => %v: %v\n", string(k), string(v)) // Get 100 => 101: B + } + + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + fmt.Printf("Get 100 then Get Prev => not found\n") + } else { + fmt.Printf("Get 100 then Get Prev => %v: %v\n", string(k), string(v)) // Get 100 then Get Prev => 099: A + } + panicOn(err) + + // find max in [201,300) and get nothing back + + k, v, err = cur.Get([]byte("300"), nil, lmdb.SetRange) + if lmdb.IsNotFound(err) { + panic("300 not found, not expected") + } else { + fmt.Printf("Get 300 => %v: %v\n", string(k), string(v)) // Get 300 => 300: E + } + + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + fmt.Printf("Get 300 then Get Prev => not found\n") + } else { + fmt.Printf("Get 300 then Get Prev => %v: %v\n", string(k), string(v)) // Get 300 then Get Prev => 200: D + } + cmp := bytes.Compare(k, []byte("201")) + if cmp >= 0 { + fmt.Printf("key k = '%v' was >= 201", string(k)) + } else { + fmt.Printf("key k = '%v' was < 201", string(k)) // key k = '200' was < 201 + } + panicOn(err) + + return nil + }) + panicOn(err) + + vv("done") +} diff --git a/cmd/demo-lmdb/vprint.go b/cmd/demo-lmdb/vprint.go new file mode 100644 index 000000000..d66762ef2 --- /dev/null +++ b/cmd/demo-lmdb/vprint.go @@ -0,0 +1,179 @@ +// home: https://github.com/glyerine/vprint +// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved. +// License: MIT +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// +build !386 + +package main + +import ( + "fmt" + "io" + "os" + "path" + "runtime" + "runtime/debug" + "sync" + "time" +) + +const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00" +const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00" + +// for tons of debug output +var VerboseVerbose bool = false + +// convience functions for . import +var pp = PP +var vv = VV + +var panicOn = PanicOn + +func init() { + // keeper linter happy + _ = pp + _ = vv +} + +func PanicOn(err error) { + if err != nil { + panic(err) + } +} + +func PP(format string, a ...interface{}) { + if VerboseVerbose { + TSPrintf(format, a...) + } +} + +func VV(format string, a ...interface{}) { + TSPrintf(format, a...) +} + +func AlwaysPrintf(format string, a ...interface{}) { + TSPrintf(format, a...) +} + +var tsPrintfMut sync.Mutex + +// time-stamped printf +func TSPrintf(format string, a ...interface{}) { + tsPrintfMut.Lock() + Printf("\n%s %s ", FileLine(3), ts()) + Printf(format+"\n", a...) + tsPrintfMut.Unlock() +} + +// get timestamp for logging purposes +func ts() string { + return time.Now().Format(RFC3339UsecTz0) +} + +// so we can multi write easily, use our own printf +var OurStdout io.Writer = os.Stdout + +// Printf formats according to a format specifier and writes to standard output. +// It returns the number of bytes written and any write error encountered. +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(OurStdout, format, a...) +} + +func FileLine(depth int) string { + _, fileName, fileLine, ok := runtime.Caller(depth) + var s string + if ok { + s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine) + } else { + s = "" + } + return s +} + +func stack() string { + return string(debug.Stack()) +} + +func FileExists(name string) bool { + fi, err := os.Stat(name) + if err != nil { + return false + } + if fi.IsDir() { + return false + } + return true +} + +func DirExists(name string) bool { + fi, err := os.Stat(name) + if err != nil { + return false + } + if fi.IsDir() { + return true + } + return false +} + +func FileSize(name string) (int64, error) { + fi, err := os.Stat(name) + if err != nil { + return -1, err + } + return fi.Size(), nil +} + +// Caller returns the name of the calling function. +func Caller(upStack int) string { + // elide ourself and runtime.Callers + target := upStack + 2 + + pc := make([]uintptr, target+2) + n := runtime.Callers(0, pc) + + f := runtime.Frame{Function: "unknown"} + if n > 0 { + frames := runtime.CallersFrames(pc[:n]) + for i := 0; i <= target; i++ { + contender, more := frames.Next() + if i == target { + f = contender + } + if !more { + break + } + } + } + return f.Function +} + +// happy linter: +var _ = DirExists +var _ = FileExists +var _ = Caller +var _ = stack +var _ = RFC3339MsecTz0 +var _ = RFC3339UsecTz0 +var _ = AlwaysPrintf +var _ = FileSize diff --git a/cmd/loader/loader.go b/cmd/loader/loader.go deleted file mode 100644 index 2a6a00ff2..000000000 --- a/cmd/loader/loader.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2020 Pilosa Corp. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "archive/tar" - "compress/gzip" - "context" - "time" - //"fmt" - "fmt" - "github.com/pilosa/pilosa/v2" - "github.com/pilosa/pilosa/v2/http" - "io" - "io/ioutil" - gohttp "net/http" - //"log" - "os" - //"path/filepath" - //"sort" - "strconv" - "strings" -) - -func UploadTar(srcFile string, client *http.InternalClient) error { - t0 := time.Now() - - f, err := os.Open(srcFile) - if err != nil { - return (err) - } - defer f.Close() - var tarReader *tar.Reader - if strings.HasSuffix(srcFile, "gz") { - gzf, err := gzip.NewReader(f) - if err != nil { - return err - } - tarReader = tar.NewReader(gzf) - } else { - tarReader = tar.NewReader(f) - } - viewData := make(map[string][]byte) - //given ordered by index/field/view - //trait_store/product_count__commercial_cd_or_share_certificate/views/bsig_product_count__commercial_cd_or_share_certificate/fragments/255 - lastIndex := "" - lastField := "" - lastShard := uint64(0) - //vv("top of tar loop") - n := 0 - for { - header, err := tarReader.Next() - if err == io.EOF { - if header != nil { - panic("header should not be nil on err io.EOF") - } - //submit any stuff we have left - if len(viewData) > 0 { - request := &pilosa.ImportRoaringRequest{ - Views: viewData, - } - // Submit(lastIndex, lastField, lastShard, request) - //vv("about to submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard) - uri := GetImportRoaringURI(lastIndex, lastShard) - err := client.ImportRoaring(context.Background(), uri, lastIndex, lastField, lastShard, false, request) - panicOn(err) - //vv("done with submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard) - } - return nil - } - //vv("got header '%v'", header.Name) - n++ - if n%500 == 0 { - vv("n = %v, progress, elapsed '%v'", n, time.Since(t0)) - } - parts := strings.Split(header.Name, "/") - index := parts[0] - field := parts[1] - view := parts[3] - shard, err := strconv.ParseUint(parts[5], 10, 64) - if err != nil { - return err - } - // TODO: shards can be loaded in parallel, so maybe farm out to a worker set of goro. - if index != lastIndex || field != lastField || shard != lastShard { - if len(viewData) > 0 { - request := &pilosa.ImportRoaringRequest{ - Views: viewData, - } - //vv("about to submit lastIndex='%v' lastShard='%v'", lastIndex, lastShard) - uri := GetImportRoaringURI(lastIndex, lastShard) - panicOn(client.ImportRoaring(context.Background(), uri, lastIndex, lastField, lastShard, false, request)) - viewData = make(map[string][]byte) - //vv("done with submit lastIndex='%v' lastShard='%v'; took='%v'", lastIndex, lastShard, time.Since(t0)) - - } - } - roaringData, err := ioutil.ReadAll(tarReader) - if err != nil { - return err - } - if _, already := viewData[view]; already { - panic(fmt.Sprintf("view '%v' already present!", view)) - } - viewData[view] = roaringData - lastIndex = index - lastField = field - - //lastShard = shard - //vv("bottom of loop") - } -} - -func main() { - - host := "127.0.0.1:10101" - h := &gohttp.Client{} - c, err := http.NewInternalClient(host, h) - panicOn(err) - - tarSrcPath := "q2.tar.gz" - t0 := time.Now() - panicOn(UploadTar(tarSrcPath, c)) - vv("total elapsed '%v'", time.Since(t0)) -} - -var globURI *pilosa.URI - -func init() { - var err error - globURI, err = pilosa.NewURIFromHostPort("127.0.0.1", 10101) - panicOn(err) -} - -// get correct node to go to. -func GetImportRoaringURI(index string, shard uint64) *pilosa.URI { - return globURI -} diff --git a/cmd/slurp/slurp.go b/cmd/slurp/slurp.go new file mode 100644 index 000000000..3c97da63f --- /dev/null +++ b/cmd/slurp/slurp.go @@ -0,0 +1,205 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "time" + + //"fmt" + "fmt" + "io" + "io/ioutil" + gohttp "net/http" + + "github.com/pilosa/pilosa/v2" + "github.com/pilosa/pilosa/v2/http" + + //"log" + "os" + //"path/filepath" + //"sort" + "strconv" + "strings" +) + +// slurp: slurp is a load-tester for importing bulk data. +// It allows us to measure write performance. + +type stateMachine struct { + viewData map[string][]byte + lastIndex string + lastField string + lastShard uint64 + state string + client *http.InternalClient + start time.Time +} + +func (r *stateMachine) NewHeader(h *tar.Header, tr *tar.Reader) error { + parts := strings.Split(h.Name, "/") + switch parts[0] { + case "roaring": + index := parts[1] + field := parts[2] + view := parts[4] + shard, err := strconv.ParseUint(parts[6], 10, 64) + panicOn(err) + if index != r.lastIndex || field != r.lastField || shard != r.lastShard { + err := r.Upload() + if err != nil { + return err + } + } + roaringData, err := ioutil.ReadAll(tr) + if err != nil { + return err + } + if _, already := r.viewData[view]; already { + panic(fmt.Sprintf("view '%v' already present!", view)) + } + r.viewData[view] = roaringData + r.lastIndex = index + r.lastField = field + r.lastShard = shard + case "bolt": + if r.state == "roaring" { + err := r.Upload() + if err != nil { + return err + } + vv("Finished import %v", time.Since(r.start)) + } + // + uri := GetImportRoaringURI(r.lastIndex, r.lastShard) + + switch v := parts[len(parts)-1]; v { + case "keys": + index := parts[1] + fieldName := parts[2] + if fieldName == "_keys" { + //skip index keys are not not real fields so will have no need for field keys + return nil + + } + + byteData, err := ioutil.ReadAll(tr) + panicOn(err) + br := bytes.NewReader(byteData) + err = r.client.ImportFieldKeys(context.Background(), uri, index, fieldName, false, br) + if err != nil { + return err + } + default: + pilosa.VV("%v", h.Name) + index := parts[1] + partition, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return err + } + byteData, err := ioutil.ReadAll(tr) + panicOn(err) + + br := bytes.NewReader(byteData) + err = r.client.ImportIndexKeys(context.Background(), uri, index, int(partition), false, br) + if err != nil { + return err + } + } + } + r.state = parts[0] + return nil +} +func (r *stateMachine) Upload() error { + if len(r.viewData) > 0 { + request := &pilosa.ImportRoaringRequest{ + Views: r.viewData, + } + uri := GetImportRoaringURI(r.lastIndex, r.lastShard) + err := r.client.ImportRoaring(context.Background(), uri, r.lastIndex, r.lastField, r.lastShard, false, request) + if err != nil { + return err + } + r.viewData = make(map[string][]byte) + } + return nil +} + +func UploadTar(srcFile string, client *http.InternalClient) error { + + f, err := os.Open(srcFile) + if err != nil { + return (err) + } + defer f.Close() + var tarReader *tar.Reader + if strings.HasSuffix(srcFile, "gz") { + gzf, err := gzip.NewReader(f) + if err != nil { + return err + } + tarReader = tar.NewReader(gzf) + } else { + tarReader = tar.NewReader(f) + } + runner := &stateMachine{ + viewData: make(map[string][]byte), + start: time.Now(), + } + runner.client = client + for { + header, err := tarReader.Next() + if err == io.EOF { + _ = runner.Upload() + break + } + if err != nil { + panicOn(err) + } + err = runner.NewHeader(header, tarReader) + panicOn(err) + } + return nil +} + +func main() { + + host := "127.0.0.1:10101" + h := &gohttp.Client{} + c, err := http.NewInternalClient(host, h) + panicOn(err) + + tarSrcPath := os.Args[1] //"q2.tar.gz" + t0 := time.Now() + println("uploading", tarSrcPath) + panicOn(UploadTar(tarSrcPath, c)) + vv("total elapsed '%v'", time.Since(t0)) +} + +var globURI *pilosa.URI + +func init() { + var err error + globURI, err = pilosa.NewURIFromHostPort("127.0.0.1", 10101) + panicOn(err) +} + +// get correct node to go to. +func GetImportRoaringURI(index string, shard uint64) *pilosa.URI { + return globURI +} diff --git a/cmd/loader/vprint.go b/cmd/slurp/vprint.go similarity index 100% rename from cmd/loader/vprint.go rename to cmd/slurp/vprint.go diff --git a/executor.go b/executor.go index c1e0becd8..67932cf8d 100644 --- a/executor.go +++ b/executor.go @@ -243,9 +243,11 @@ func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shar // Must copy out of Tx data before Commiting, because it will become invalid afterwards. respSafeNoTxData := e.safeCopy(resp) - // Commit transaction. - if err := tx.Commit(); err != nil { - return respSafeNoTxData, err + // Commit transaction if writing; else let the defer Rollback have it. + if needWriteTxn { + if err := tx.Commit(); err != nil { + return respSafeNoTxData, err + } } return respSafeNoTxData, nil } @@ -503,7 +505,8 @@ func (e *executor) execute(ctx context.Context, tx Tx, index string, q *pql.Quer // still need to handle them. Since everything else was // already precomputed by handlePreCallChildren, though, // we don't need this logic in executeCall. - if newIndex := call.CallIndex(); newIndex != "" && newIndex != index { + newIndex := call.CallIndex() + if newIndex != "" && newIndex != index { v, err = e.executeCall(ctx, tx, newIndex, call, nil, opt) } else { v, err = e.executeCall(ctx, tx, index, call, shards, opt) @@ -815,7 +818,7 @@ func (e *executor) executeCall(ctx context.Context, tx Tx, index string, c *pql. return e.executeFieldValueCall(ctx, tx, index, c, shards, opt) case "Precomputed": return e.executePrecomputedCall(ctx, tx, index, c, shards, opt) - default: + default: // e.g. "Row", "Union", "Intersect" or anything that returns a bitmap. statFn() return e.executeBitmapCall(ctx, tx, index, c, shards, opt) } @@ -1374,6 +1377,7 @@ func (e *executor) executePrecomputedCall(ctx context.Context, tx Tx, index stri // executeBitmapCall executes a call that returns a bitmap. func (e *executor) executeBitmapCall(ctx context.Context, tx Tx, index string, c *pql.Call, shards []uint64, opt *execOptions) (*Row, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeBitmapCall") span.LogKV("pqlCallName", c.Name) defer span.Finish() diff --git a/executor_test.go b/executor_test.go index a811dc594..3bd9c1139 100644 --- a/executor_test.go +++ b/executor_test.go @@ -5209,7 +5209,6 @@ func TestExecutor_ForeignIndex(t *testing.T) { } join := c.Query(t, "parent", `Intersect(Row(general=3), Distinct(Row(color="blue"), index="child", field="parent_id"))`).Results[0].(*pilosa.Row) - if !reflect.DeepEqual(join.Keys, []string{"one"}) { t.Fatalf("unexpected keys: %v", join.Keys) } diff --git a/extensions/distinct.go b/extensions/distinct.go deleted file mode 100644 index 42f11c90f..000000000 --- a/extensions/distinct.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 Pilosa Corp. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// +build plugindistinct - -package extensions - -import ( - _ "github.com/molecula/extensions/distinct" -) diff --git a/field.go b/field.go index e878d8ab5..2153ae991 100644 --- a/field.go +++ b/field.go @@ -716,6 +716,7 @@ var fieldQueue = make(chan struct{}, 16) // openViews opens and initializes the views inside the field. func (f *Field) openViews() error { + file, err := os.Open(filepath.Join(f.path, "views")) if os.IsNotExist(err) { return nil @@ -754,17 +755,19 @@ fileLoop: return fmt.Errorf("opening view: view=%s, err=%s", view.name, err) } - // Automatically upgrade BSI v1 fragments if they exist & reopen view. - if bsig := f.bsiGroup(f.name); bsig != nil { - if ok, err := upgradeViewBSIv2(view, bsig.BitDepth); err != nil { - return errors.Wrap(err, "upgrade view bsi v2") - } else if ok { - if err := view.close(); err != nil { - return errors.Wrap(err, "closing upgraded view") - } - view = f.newView(f.viewPath(name), name) - if err := view.open(); err != nil { - return fmt.Errorf("re-opening view: view=%s, err=%s", view.name, err) + if f.idx.Txf.TxType() == roaringFragmentFilesTxn { + // Automatically upgrade BSI v1 fragments if they exist & reopen view. + if bsig := f.bsiGroup(f.name); bsig != nil { + if ok, err := upgradeViewBSIv2(view, bsig.BitDepth); err != nil { + return errors.Wrap(err, "upgrade view bsi v2") + } else if ok { + if err := view.close(); err != nil { + return errors.Wrap(err, "closing upgraded view") + } + view = f.newView(f.viewPath(name), name) + if err := view.open(); err != nil { + return fmt.Errorf("re-opening view: view=%s, err=%s", view.name, err) + } } } } diff --git a/field_internal_test.go b/field_internal_test.go index 0472e7dcc..e963841e4 100644 --- a/field_internal_test.go +++ b/field_internal_test.go @@ -877,3 +877,58 @@ func TestDecimalField_MinMaxForShard(t *testing.T) { }) } } + +func TestBSIGroup_TxReopenDB(t *testing.T) { + f := OpenField(t, OptFieldTypeInt(-100, 200)) + defer f.Close() + + options := &ImportOptions{} + for i, tt := range []struct { + columnIDs []uint64 + values []int64 + checkVal int64 + expCols []uint64 + }{ + { + []uint64{100}, + []int64{1}, + 1, + []uint64{100}, + }, + { + []uint64{100}, + []int64{8}, + 8, + []uint64{100}, + }, + { + []uint64{100}, + []int64{1}, + 1, + []uint64{100}, + }, + } { + tx := f.idx.Txf.NewTx(Txo{Write: writable, Index: f.idx, Field: f.Field}) + // can't do this, we are in a loop, not a function: + // defer tx.Rollback() + + if err := f.importValue(tx, tt.columnIDs, tt.values, options); err != nil { + t.Fatalf("test %d, importing values: %s", i, err.Error()) + } + + panicOn(tx.Commit()) + + tx = f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field}) + // no, same reason as above: defer tx.Rollback() + + if row, err := f.Range(tx, f.name, pql.EQ, tt.checkVal); err != nil { + t.Fatalf("test %d, getting range: %s", i, err.Error()) + } else if !reflect.DeepEqual(row.Columns(), tt.expCols) { + t.Fatalf("test %d, expected columns: %v, but got: %v", i, tt.expCols, row.Columns()) + } + tx.Rollback() + } // loop + + // the test: can we re-open a BSI fragment under badger/rbf. + _ = f.Reopen() +} diff --git a/fragment.go b/fragment.go index 94ee2f6d9..7e340944f 100644 --- a/fragment.go +++ b/fragment.go @@ -574,6 +574,7 @@ func (f *fragment) mustRow(tx Tx, rowID uint64) *Row { // unprotectedRow returns a row from the row cache if available or from storage // (updating the cache). func (f *fragment) unprotectedRow(tx Tx, rowID uint64) (*Row, error) { + useRowCache := tx.UseRowCache() if useRowCache { r, ok := f.rowCache.Fetch(rowID) diff --git a/fragment_internal_test.go b/fragment_internal_test.go index 1d4d62fba..a92f4c745 100644 --- a/fragment_internal_test.go +++ b/fragment_internal_test.go @@ -1786,7 +1786,7 @@ func TestFragment_LRUCache_Persistence(t *testing.T) { // Ensure a fragment's cache can be persisted between restarts. func TestFragment_RankCache_Persistence(t *testing.T) { - skipForRBF(t) + roaringOnlyTest(t) index := mustOpenIndex(IndexOptions{}) defer index.Close() @@ -5329,6 +5329,9 @@ func TestImportValueConcurrent(t *testing.T) { "blueGreenTx because the lack of transactional consistency " + "from Roaring-per-file will create false comparison " + "failures.")) + case lmdbTxn: + t.Skip(fmt.Sprintf("skipping TestImportValueConcurrent under " + + "lmdb since only a single writer is allowed at once.")) } // Since eg.Go gets called multiple times below, each @@ -5646,9 +5649,3 @@ func TestFragment_Bug_Q2DoubleDelete(t *testing.T) { t.Fatalf("expected nothing got %v", res) } } - -func skipForRBF(tb testing.TB) { - if os.Getenv("PILOSA_TXSRC") == "rbf" { - tb.Skip("skip for RBF") - } -} diff --git a/gid.go b/gid.go new file mode 100644 index 000000000..165cef3e4 --- /dev/null +++ b/gid.go @@ -0,0 +1,160 @@ +// Copyright (c) 2014 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package pilosa + +import ( + "bytes" + "errors" + "fmt" + "runtime" + "strconv" + "sync" +) + +// Sourced https://github.com/bradfitz/http2/blob/dc0c5c000ec33e263612939744d51a3b68b9cece/gotrack.go +var goroutineSpace = []byte("goroutine ") +var littleBuf = sync.Pool{ + New: func() interface{} { + buf := make([]byte, 64) + return &buf + }, +} + +var _ = curGID // happy linter + +func curGID() uint64 { + bp := littleBuf.Get().(*[]byte) + defer littleBuf.Put(bp) + b := *bp + b = b[:runtime.Stack(b, false)] + // Parse the 4707 out of "goroutine 4707 [" + b = bytes.TrimPrefix(b, goroutineSpace) + i := bytes.IndexByte(b, ' ') + if i < 0 { + panic(fmt.Sprintf("No space found in %q", b)) + } + b = b[:i] + n, err := parseUintBytes(b, 10, 64) + if err != nil { + panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err)) + } + return n +} + +// parseUintBytes is like strconv.ParseUint, but using a []byte. +func parseUintBytes(s []byte, base int, bitSize int) (n uint64, err error) { + var cutoff, maxVal uint64 + + if bitSize == 0 { + bitSize = int(strconv.IntSize) + } + + s0 := s + switch { + case len(s) < 1: + err = strconv.ErrSyntax + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + + case 2 <= base && base <= 36: + // valid base; nothing to do + + case base == 0: + // Look for octal, hex prefix. + switch { + case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): + base = 16 + s = s[2:] + if len(s) < 1 { + err = strconv.ErrSyntax + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + } + case s[0] == '0': + base = 8 + default: + base = 10 + } + + default: + err = errors.New("invalid base " + strconv.Itoa(base)) + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + } + + n = 0 + cutoff = cutoff64(base) + maxVal = 1<= base { + n = 0 + err = strconv.ErrSyntax + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + } + + if n >= cutoff { + // n*base overflows + n = 1<<64 - 1 + err = strconv.ErrRange + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + } + n *= uint64(base) + + n1 := n + uint64(v) + if n1 < n || n1 > maxVal { + // n+v overflows + n = 1<<64 - 1 + err = strconv.ErrRange + return n, &strconv.NumError{Func: "ParseUint", Num: string(s0), Err: err} + } + n = n1 + } + + return n, nil +} + +// Return the first number n such that n*base >= 1<<64. +func cutoff64(base int) uint64 { + if base < 2 { + return 0 + } + return (1<<64-1)/uint64(base) + 1 +} diff --git a/go.mod b/go.mod index 769fd50b9..38d010727 100644 --- a/go.mod +++ b/go.mod @@ -12,15 +12,18 @@ require ( github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 + github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect + github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311 + github.com/glycerine/lmdb-go v1.9.11 github.com/go-ole/go-ole v1.2.4 // indirect github.com/gogo/protobuf v1.2.0 github.com/golang/protobuf v1.3.3 github.com/google/go-cmp v0.2.0 + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/gorilla/handlers v1.3.0 github.com/gorilla/mux v1.7.0 github.com/hashicorp/memberlist v0.1.3 - github.com/molecula/ext v0.0.0-20200103203257-8a458a73e8c2 // indirect - github.com/molecula/extensions v0.0.0-20191218165536-562244600fd4 + github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/opentracing/opentracing-go v1.1.0 github.com/pelletier/go-toml v1.2.0 github.com/pkg/errors v0.8.1 @@ -38,7 +41,7 @@ require ( github.com/uber/jaeger-lib v2.2.0+incompatible // indirect github.com/zeebo/blake3 v0.0.4 go.uber.org/atomic v1.4.0 // indirect - golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 // indirect + golang.org/x/mod v0.3.0 golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/text v0.3.2 // indirect google.golang.org/grpc v1.28.0 diff --git a/go.sum b/go.sum index 5d09bde2d..726ab0077 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,12 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311 h1:AAXH0ZvYIHHqU06ASy0H2tYAkAGrQlZvEy2QZrrtt4E= +github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311/go.mod h1:B72P/ZM99sNiCmaQJflpmMAF5LsDzStpLdWzn0+Vr2Y= +github.com/glycerine/lmdb-go v1.9.11 h1:Jutsg5jgYxZIHf5DqV4Bu+JVYs3Ieax7DASNigr6TUg= +github.com/glycerine/lmdb-go v1.9.11/go.mod h1:iztA3wBlR0RO8jTYTqGTGoySIEa6vFAXWEByWusDfOY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -77,6 +83,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCy github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v1.3.0 h1:tsg9qP3mjt1h4Roxp+M1paRjrVBfPSOpBuVclh6YluI= github.com/gorilla/handlers v1.3.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= @@ -99,6 +107,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -116,14 +126,6 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b h1:cZADDaNYM7xn/nklO3g198JerGQjadFuA0ofxBJgK0Y= -github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b/go.mod h1:uXd1BiH7xLmgkhVmspdJLENv6uGWrTL/MQX2TN7Yz9s= -github.com/molecula/ext v0.0.0-20191202195653-240f38a75171 h1:4VK7u/RM+54Yaz8aRB9vIaDSnbKi3M0NQYg5tsZvOT4= -github.com/molecula/ext v0.0.0-20191202195653-240f38a75171/go.mod h1:r6EIj0GH8dx5xxFLW6Voi1/mX3wXOUkJu6AoEE/xvGQ= -github.com/molecula/ext v0.0.0-20200103203257-8a458a73e8c2 h1:XOImsA5XhGklFj8Y0TxSm1qWZzEwYxom2JOXiu9GMq0= -github.com/molecula/ext v0.0.0-20200103203257-8a458a73e8c2/go.mod h1:r6EIj0GH8dx5xxFLW6Voi1/mX3wXOUkJu6AoEE/xvGQ= -github.com/molecula/extensions v0.0.0-20191218165536-562244600fd4 h1:mDB/dicofRVFuRYcCVPk+JBiVKXlfbzMahuqHvrYqu4= -github.com/molecula/extensions v0.0.0-20191218165536-562244600fd4/go.mod h1:QQgN5OFjuBAi4Q2UYVMzfvi4k9yvg/qqC+MNFB4I9JI= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= @@ -210,12 +212,14 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= @@ -256,6 +260,9 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= diff --git a/holder.go b/holder.go index e74672d31..79b07fd2f 100644 --- a/holder.go +++ b/holder.go @@ -511,8 +511,10 @@ func (h *Holder) Open() error { if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") { continue } - // Skip badgerdb files too. - if strings.HasSuffix(fi.Name(), "badgerdb") { + // Skip embedded db files too. + if strings.HasSuffix(fi.Name(), "-badgerdb") || + strings.HasSuffix(fi.Name(), "-lmdb") || + strings.HasSuffix(fi.Name(), "-rbfdb") { continue } @@ -611,6 +613,9 @@ func (h *Holder) Close() error { if err := index.Close(); err != nil { return errors.Wrap(err, "closing index") } + if err := index.Txf.CloseDB(); err != nil { + return errors.Wrap(err, "index.Txf.CloseDB()") + } } // Reset opened in case Holder needs to be reopened. @@ -1498,7 +1503,6 @@ func (s *holderSyncer) initializeIndexTranslateReplication() error { if !index.Keys() { continue } - for partitionID := 0; partitionID < s.Cluster.partitionN; partitionID++ { partitionNodes := s.Cluster.partitionNodes(partitionID) isPrimary := partitionNodes[0].ID == node.ID // remote is primary? diff --git a/holder_test.go b/holder_test.go index 079029484..b9bd4ae0a 100644 --- a/holder_test.go +++ b/holder_test.go @@ -403,6 +403,7 @@ func TestHolder_HasData(t *testing.T) { // Ensure holder can delete an index and its underlying files. func TestHolder_DeleteIndex(t *testing.T) { + hldr := test.MustOpenHolder() defer hldr.Close() diff --git a/http/client.go b/http/client.go index d8d9541e2..44c89db4d 100644 --- a/http/client.go +++ b/http/client.go @@ -1685,3 +1685,66 @@ func (c *InternalClient) RetrieveTranslatePartitionFromURI(ctx context.Context, return resp.Body, nil } +func (c *InternalClient) ImportIndexKeys(ctx context.Context, uri *pilosa.URI, index string, partitionID int, remote bool, rddbdata io.Reader) error { + span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportIndexKeys") + defer span.Finish() + + if index == "" { + return pilosa.ErrIndexRequired + } + + if uri == nil { + uri = c.defaultURI + } + + vals := url.Values{} + vals.Set("remote", strconv.FormatBool(remote)) + url := fmt.Sprintf("%s/internal/translate/index/%s/%d", uri, index, partitionID) + + // Generate HTTP request. + httpReq, err := http.NewRequest("POST", url, rddbdata) + if err != nil { + return errors.Wrap(err, "creating request") + } + httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version) + + // Execute request against the host. + resp, err := c.executeRequest(httpReq.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} + +func (c *InternalClient) ImportFieldKeys(ctx context.Context, uri *pilosa.URI, index, field string, remote bool, rddbdata io.Reader) error { + span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.ImportFieldKeys") + defer span.Finish() + + if index == "" { + return pilosa.ErrIndexRequired + } + + if uri == nil { + uri = c.defaultURI + } + + vals := url.Values{} + vals.Set("remote", strconv.FormatBool(remote)) + url := fmt.Sprintf("%s/internal/translate/field/%s/%s", uri, index, field) + + // Generate HTTP request. + httpReq, err := http.NewRequest("POST", url, rddbdata) + if err != nil { + return errors.Wrap(err, "creating request") + } + httpReq.Header.Set("User-Agent", "pilosa/"+pilosa.Version) + + // Execute request against the host. + resp, err := c.executeRequest(httpReq.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} diff --git a/http/handler.go b/http/handler.go index 06a62ebd7..03a039766 100644 --- a/http/handler.go +++ b/http/handler.go @@ -28,8 +28,10 @@ import ( "net/http" _ "net/http/pprof" // Imported for its side-effect of registering pprof endpoints with the server. "net/url" + "os" "reflect" "runtime/debug" + "runtime/pprof" "strconv" "strings" "sync" @@ -44,6 +46,7 @@ import ( "github.com/pilosa/pilosa/v2/tracing" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/zeebo/blake3" ) // Handler represents an HTTP handler. @@ -385,6 +388,9 @@ func newRouter(handler *Handler) *mux.Router { router.HandleFunc("/internal/nodes", handler.handleGetNodes).Methods("GET").Name("GetNodes") router.HandleFunc("/internal/shards/max", handler.handleGetShardsMax).Methods("GET").Name("GetShardsMax") // TODO: deprecate, but it's being used by the client + router.HandleFunc("/internal/translate/index/{index}/{partition}", handler.handlePostTranslateIndexDB).Methods("POST").Name("PostTranslateIndexDB") + router.HandleFunc("/internal/translate/field/{index}/{field}", handler.handlePostTranslateFieldDB).Methods("POST").Name("PostTranslateFieldDB") + router.Use(handler.queryArgValidator) router.Use(handler.addQueryContext) router.Use(handler.extractTracing) @@ -634,14 +640,55 @@ type getStatusResponse struct { LocalID string `json:"localID"` } +func hash(s string) string { + + hasher := blake3.New() + _, _ = hasher.Write([]byte(s)) + var buf [16]byte + _, _ = hasher.Digest().Read(buf[0:]) + + return fmt.Sprintf("%x", buf) +} + +var DoPerQueryProfiling = false + // handlePostQuery handles /query requests. func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) { - // Read previouly parsed request from context qreq := r.Context().Value(contextKeyQueryRequest) qerr := r.Context().Value(contextKeyQueryError) req, ok := qreq.(*pilosa.QueryRequest) - err, _ := qerr.(error) + + if DoPerQueryProfiling { + + txsrc := os.Getenv("PILOSA_TXSRC") + reqHash := hash(req.Query) + + qlen := len(req.Query) + if qlen > 100 { + qlen = 100 + } + name := "_query." + reqHash + "." + txsrc + "." + time.Now().Format("20060102150405") + "." + req.Query[:qlen] + f, err := os.Create(name) + if err != nil { + panic(err) + } + defer f.Close() + + _ = pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + + } // end DoPerQueryProfiling + /* + er = trace.Start(f) + if er != nil { + panic(er) + } + defer trace.Stop() + */ + + var err error + err, _ = qerr.(error) if err != nil || !ok { w.WriteHeader(http.StatusBadRequest) @@ -2203,3 +2250,58 @@ func readBody(r *http.Request) ([]byte, error) { return buf.Bytes(), nil } + +func (h *Handler) handlePostTranslateFieldDB(w http.ResponseWriter, r *http.Request) { + indexName, ok := mux.Vars(r)["index"] + if !ok { + http.Error(w, "index name is required", http.StatusBadRequest) + return + } + + fieldName, ok := mux.Vars(r)["field"] + if !ok { + http.Error(w, "field name is required", http.StatusBadRequest) + return + } + bd, err := readBody(r) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return + } + br := bytes.NewReader(bd) + + err = h.api.TranslateFieldDB(r.Context(), indexName, fieldName, br) + resp := successResponse{h: h, Name: fieldName} + resp.check(err) + resp.write(w, err) +} + +func (h *Handler) handlePostTranslateIndexDB(w http.ResponseWriter, r *http.Request) { + indexName, ok := mux.Vars(r)["index"] + if !ok { + http.Error(w, "index name is required", http.StatusBadRequest) + return + } + + partitionArg, ok := mux.Vars(r)["partition"] + if !ok { + http.Error(w, "partition is required", http.StatusBadRequest) + return + } + partition, err := strconv.ParseUint(partitionArg, 10, 64) + if err != nil { + http.Error(w, "bad partition", http.StatusBadRequest) + return + } + + bd, err := readBody(r) + if err != nil { + http.Error(w, "failed to read body", http.StatusBadRequest) + return + } + br := bytes.NewReader(bd) + err = h.api.TranslateIndexDB(r.Context(), indexName, int(partition), br) + resp := successResponse{h: h, Name: indexName} + resp.check(err) + resp.write(w, err) +} diff --git a/index.go b/index.go index d1a7770df..4a483472f 100644 --- a/index.go +++ b/index.go @@ -33,6 +33,16 @@ import ( "golang.org/x/sync/errgroup" ) +// debug: TODO(jea): remove this init() that does cpu profiling. +func init() { + go func() { + // give time for env var TXSRC to be set. + //time.Sleep(5 * time.Second) + //CPUProfileForDur(5*time.Minute, "cpu.pprof") + //CPUProfileForDur(15*time.Second, "cpu.pprof") + }() +} + // Index represents a container for fields. type Index struct { mu sync.RWMutex @@ -72,21 +82,9 @@ type Index struct { Txf *TxFactory } -// OpenIndex opens or starts a new Index on path. Path -// can be empty. -func OpenIndex(holder *Holder, path, name string) (*Index, error) { - openExisting := true - return openOrCreateNewIndex(holder, path, name, openExisting) -} - -// NewIndex returns a new instance of Index at path. It will erase anything -// old already in path. +// NewIndex returns an existing (but possibly empty) instance of +// Index at path. It will not erase any prior content. func NewIndex(holder *Holder, path, name string) (*Index, error) { - openExisting := false - return openOrCreateNewIndex(holder, path, name, openExisting) -} - -func openOrCreateNewIndex(holder *Holder, path, name string, openExisting bool) (*Index, error) { // Emulate what the spf13/cobra does, letting env vars override // the defaults, because we may be under a simple "go test" run where @@ -117,7 +115,7 @@ func openOrCreateNewIndex(holder *Holder, path, name string, openExisting bool) return nil, errors.Wrap(err, "validating name") } - txf, err := NewTxFactory(txsrc, holder.Path, name, openExisting) + txf, err := NewTxFactory(txsrc, holder.Path, name) if err != nil { return nil, errors.Wrap(err, "creating newTxFactory") } @@ -252,6 +250,9 @@ func (i *Index) open(withTimestamp, haveHolderLock bool) (err error) { mu.Lock() defer mu.Unlock() + + i.mu.Lock() + defer i.mu.Unlock() i.translateStores[partitionID] = store return nil }) diff --git a/license.exceptions b/license.exceptions index ace441ae6..d1be57dc1 100644 --- a/license.exceptions +++ b/license.exceptions @@ -11,4 +11,6 @@ ./logger/filewriter_test.go ./vprint.go ./rbf/vprint.go -./cmd/loader/vprint.go +./cmd/slurp/vprint.go +./cmd/demo-lmdb/vprint.go +./gid.go diff --git a/lmdb/lmdb.go b/lmdb/lmdb.go new file mode 100644 index 000000000..631545235 --- /dev/null +++ b/lmdb/lmdb.go @@ -0,0 +1,1800 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build skip_building_lmdb_for_now + +package pilosa + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/glycerine/idem" + "github.com/glycerine/lmdb-go/lmdb" + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pkg/errors" +) + +// lmdbWorker represents a goroutine that has been welded to a +// C thread by runtime.LockOSThread(); so it can do work for lmdb. +// We create a single worker to write and a number of workers to read. +// Each LMDBWrapper maintains a worker pool for access to its database. +type lmdbWorker struct { + write bool + env *lmdb.Env + dbi lmdb.DBI + + // coordinate shutdown + halt *idem.Halter + + // run these jobs + jobCh chan *lmdbJob +} + +// lmdbJob communicates jobs to lmdbWorkers. +type lmdbJob struct { + write bool + fn func(job *lmdbJob) + err error + done chan struct{} +} + +func newLMDBJob(write bool, f func(job *lmdbJob)) *lmdbJob { + return &lmdbJob{ + write: write, + fn: f, + done: make(chan struct{}), + } +} + +func (w *LMDBWrapper) newLMDBWorker(write bool, env *lmdb.Env, dbi lmdb.DBI) (wrk *lmdbWorker) { + w.newLMDBWorkerMu.Lock() + defer w.newLMDBWorkerMu.Unlock() + if write { + if w.lmdbWriterCount > 0 { + panic("can only have one writing lmdb worker") + } + w.lmdbWriterCount++ + } + + wrk = &lmdbWorker{ + write: write, + env: env, + dbi: dbi, + halt: idem.NewHalter(), + jobCh: make(chan *lmdbJob, 100), + } + return +} + +// StartWriter makes it apparent in the stack trace +// which goroutine is writing. +func (w *lmdbWorker) StartWriter() { + if !w.write { + panic("worker is not marked as writer") + } + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer w.halt.Done.Close() + + //vv("lmdbWorker.Start(), on gid = '%v'", curGID()) + + for { + select { + case <-w.halt.ReqStop.Chan: + return + case job := <-w.jobCh: + job.fn(job) + close(job.done) + } + } + }() +} + +// StartReader makes it apparent in the stack trace +// which goroutine(s) are reading. +func (w *lmdbWorker) StartReader() { + if w.write { + panic("worker is not marked as reader") + } + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer w.halt.Done.Close() + + //vv("lmdbWorker.Start(), on gid = '%v'", curGID()) + + for { + select { + case <-w.halt.ReqStop.Chan: + return + case job := <-w.jobCh: + job.fn(job) + close(job.done) + } + } + }() +} + +func (w *lmdbWorker) Stop() { + w.halt.ReqStop.Close() + <-w.halt.Done.Chan +} + +// lmdbRegistrar facilitates shutdown +// of all the lmdb databases started under +// tests. Its needed because most tests don't cleanup +// the *Index(es) they create. But we still +// want to shutdown lmdbDB goroutines +// after tests run. +// +// It also allows opening the same path twice to +// result in sharing the same open database handle, and +// thus the same transactional guarantees. +// +type lmdbRegistrar struct { + mu sync.Mutex + mp map[*LMDBWrapper]bool + + path2db map[string]*LMDBWrapper +} + +var globalLMDBReg *lmdbRegistrar = newLMDBTestRegistrar() + +func newLMDBTestRegistrar() *lmdbRegistrar { + + return &lmdbRegistrar{ + mp: make(map[*LMDBWrapper]bool), + path2db: make(map[string]*LMDBWrapper), + } +} + +// register each lmdb created under tests, so we +// can clean them up. This is called by openLMDBWrapper() while +// holding the r.mu.Lock, since it needs to atomically +// check the registry and make a new instance only +// if one does not exist for its path, and otherwise +// return the existing instance. +func (r *lmdbRegistrar) unprotectedRegister(w *LMDBWrapper) { + r.mp[w] = true + r.path2db[w.path] = w +} + +// unregister removes w from r +func (r *lmdbRegistrar) unregister(w *LMDBWrapper) { + r.mu.Lock() + delete(r.mp, w) + delete(r.path2db, w.path) + r.mu.Unlock() +} + +func DumpAllLMDB() { + globalLMDBReg.mu.Lock() + defer globalLMDBReg.mu.Unlock() + for w := range globalLMDBReg.mp { + _ = w + AlwaysPrintf("this lmdb path='%v' has: \n%v\n", w.path, w.StringifiedLMDBKeys(nil)) + } +} + +// newLMDBWrapper creates a new empty database, blowing away +// any prior path + "-lmdb" directory. +func (r *lmdbRegistrar) newLMDBWrapper(path string) (*LMDBWrapper, error) { + bpath := lmdbPath(path) + err := os.RemoveAll(bpath) + if err != nil { + return nil, err + } + return r.openLMDBWrapper(bpath) +} + +// lmdbPath is a helper for determining the full directory +// in which the lmdb database will be stored. +func lmdbPath(path string) string { + if !strings.HasSuffix(path, "-lmdb") { + return path + "-lmdb" + } + return path +} + +// openLMDBDB opens the database in the bpath directoy +// without deleting any prior content. Any LMDBDB +// database directory will have the "-lmdb" suffix. +// +// openLMDBDB will check the registry and make a new instance only +// if one does not exist for its bpath. Otherwise it returns +// the existing instance. This insures only one lmdbDB +// per bpath in this pilosa node. +func (r *lmdbRegistrar) openLMDBWrapper(path0 string) (*LMDBWrapper, error) { + path := lmdbPath(path0) + + r.mu.Lock() + defer r.mu.Unlock() + w, ok := r.path2db[path] + if ok { + // creates the effect of having only one lmdb open per pilosa node. + return w, nil + } + // otherwise, make a new lmdb and store it in globalLMDBReg + + runtime.LockOSThread() + //vv("NewEnv for lmdb, gid = '%v'", curGID()) + + env, err := lmdb.NewEnv() + panicOn(err) + + err = env.SetMaxDBs(1) + panicOn(err) + err = env.SetMapSize(1 << 38) // 256 GB + panicOn(err) + + const MaxReaders = 254 // default is 126 + err = env.SetMaxReaders(MaxReaders) + panicOn(err) + + panicOn(os.MkdirAll(path, 0755)) + + flags := uint(lmdb.NoReadahead) // | uint(lmdb.NoLock) <<< yikes no + + // unsafe, but get upper bound on performance. TODO: remove these. + // WriteMap = C.MDB_WRITEMAP // Use a writable memory map. + // NoMetaSync = C.MDB_NOMETASYNC // Don't fsync metapage after commit. + // NoSync = C.MDB_NOSYNC // Don't fsync after commit. + flags = flags | lmdb.WriteMap | lmdb.NoMetaSync | lmdb.NoSync + + err = env.Open(path, flags, 0644) + if err != nil { + AlwaysPrintf("error env.Open(path='%v'): '%v'; on gid = '%v'", path, err, curGID()) + } + panicOn(err) + + // In any real application it is important to check for readers that were + // never closed by their owning process, and for which the owning process + // has exited. See the documentation on transactions for more information. + staleReaders, err := env.ReaderCheck() + panicOn(err) + if staleReaders > 0 { + log.Printf("cleared %d reader slots from dead processes", staleReaders) + } + + // Open a database handle that will be used for the entire lifetime of this + // application. Because the database may not have existed before, and the + // database may need to be created, we need to get the database handle in + // an update transacation. + var dbi lmdb.DBI + name := filepath.Base(path) + err = env.Update(func(txn *lmdb.Txn) (err error) { + dbi, err = txn.CreateDBI(name) + return err + }) + panicOn(err) + + //vv("made new dbi=%v on gid = '%v'", dbi, gid) + + w = &LMDBWrapper{ + name: name, + env: env, + reg: r, + path: path, + dbi: dbi, + halt: idem.NewHalter(), + jobQ: make(chan *lmdbJob), + hasher: NewBlake3Hasher(), + } + r.unprotectedRegister(w) + + w.startStack = stack() + + writer := w.newLMDBWorker(true, env, dbi) + writer.StartWriter() + w.writer = writer + + reader := w.newLMDBWorker(false, env, dbi) + reader.StartReader() + w.readers = []*lmdbWorker{reader} + + w.startFunnel() + return w, nil +} + +func (w *LMDBWrapper) startFunnel() { + go func() { + defer w.halt.Done.Close() + + // use rw to enforce the lmdb.NoLock semanitcs + // of all readers finished before writer allowed to start. + var rw sync.RWMutex + for { + select { + case <-w.halt.ReqStop.Chan: + return + case job := <-w.jobQ: + if job.write { + rw.Lock() + select { + case w.writer.jobCh <- job: + case <-w.halt.ReqStop.Chan: + rw.Unlock() + return + } + select { + case <-job.done: + rw.Unlock() + case <-w.halt.ReqStop.Chan: + rw.Unlock() + return + } + } else { + // TODO: keep a readyReader queue and send jobs to more than one ready readers. + // For now we just have one reader. + rw.RLock() + select { + case w.readers[0].jobCh <- job: + case <-w.halt.ReqStop.Chan: + rw.RUnlock() + return + } + select { + case <-job.done: + rw.RUnlock() + case <-w.halt.ReqStop.Chan: + rw.RUnlock() + return + } + } + } + } + }() +} + +var ErrShutdown = fmt.Errorf("shutting down") + +func (w *LMDBWrapper) submit(job *lmdbJob) error { + select { + case <-w.halt.ReqStop.Chan: + return ErrShutdown + case w.jobQ <- job: + select { + case <-w.halt.ReqStop.Chan: + return ErrShutdown + case <-job.done: + } + } + return job.err +} + +// DeleteIndex deletes all the containers associated with +// the named index from the lmdb database. +func (w *LMDBWrapper) DeleteIndex(indexName string) error { + + // We use the apostrophie rune `'` to locate the end of the + // index name in the key prefix, so we cannot allow indexNames + // themselves to contain apostrophies. + if strings.Contains(indexName, "'") { + return fmt.Errorf("error: bad indexName `%v` in LMDBWrapper.DeleteIndex() call: indexName cannot contain apostrophes/single quotes.", indexName) + } + prefix := badgerIndexOnlyPrefix(indexName) + return w.DeletePrefix(prefix) +} + +// statically confirm that LMDBTx satisfies the Tx interface. +var _ Tx = (*LMDBTx)(nil) + +// LMDBWrapper provides the NewLMDBTx() method. +// Execute lmdbJob's via LMDBWrapper.submit(); these must +// be done by the lmdb goroutine worker pool. +type LMDBWrapper struct { + halt *idem.Halter + jobQ chan *lmdbJob + + newLMDBWorkerMu sync.Mutex + lmdbWriterCount int + + env *lmdb.Env + + muDb sync.Mutex + + path string + name string + dbi lmdb.DBI + + // track our registrar for Close / goro leak reporting purposes. + reg *lmdbRegistrar + + // openTx and openIt are LMDBWrapper scoped tables of all open + // transactions and iterators. These are primarily for debugging purposes. + // openTx and openIt should only be read/written after locking the muOpenTxIt mutex. + + // the bool value is the writable attribute of the key *LMDBTx + openTx map[*LMDBTx]bool + + // the bool value is whether the iterator is reversed + openIt map[*LMDBIterator]bool + + // protect openTx and openIt + muOpenTxIt sync.Mutex + + // make LMDBWrapper.Close() idempotent, avoiding panic on double Close() + closed bool + + // GcEveryDur controls how often the background goroutine + // runs garbage collection on the on-disk values-log. + // It defaults to running a GC every 1 minute if left as 0. + GcEveryDur time.Duration + + hasher *Blake3Hasher + + // doAllocZero sets the corresponding flag on all new LMDBTx. + // When doAllocZero is true, we zero out any data from lmdb + // after transcation commit and rollback. This simulates + // what would happen if we were to use the mmap-ed data + // from lmdb directly. Currently we copy by default for + // safety because otherwise TestAPI_ImportColumnAttrs sees + // corrupted data. + doAllocZero bool + + // stack() from our creation point, to track tests + // that haven't closed us. + startStack string + + DeleteEmptyContainer bool + + writer *lmdbWorker + readers []*lmdbWorker + + nextTxSn int64 +} + +// unprotectedListOpenTxAsString is a debugging helper. +// It is not thread safe, but is only used for debugging. Called internally while +// holding locks. +func (w *LMDBWrapper) unprotectedListOpenTxAsString() (r string) { + + r = "openTx list = [" + for txn, write := range w.openTx { + r += fmt.Sprintf("txn p=%p(write:%v), ", txn, write) + } + return r + "]" +} + +var _ = (*LMDBWrapper)(nil).unprotectedListOpenTxAsString // linter happy + +// UnprotectedListOpenItAsString is exported because it is +// used for debugging in some of the pilosa_test tests. +// It is not thread safe, but only used for debugging. Called internally +// while holding locks and externally while not. +func (w *LMDBWrapper) UnprotectedListOpenItAsString() (r string) { + r = "openIt list = [" + for it, reverse := range w.openIt { + r += fmt.Sprintf("it p=%p(reverse:%v), ", it, reverse) + } + return r + "]" +} + +// NewLMDBTx produces LMDB based ACID transactions. If +// the transaction will modify data, then the write flag must be true. +// Read-only queries should set write to false, to allow more concurrency. +// Methods on a LMDBTx are thread-safe, and can be called from +// different goroutines. +// +// initialIndexName is optional. It is set by the TxFactory from the Txo +// options provided at the Tx creation point. It allows us to recognize +// and isolate cross-index queries more quickly. It can always be empty "" +// but when set is highly useful for debugging. It has no impact +// on transaction behavior. +// +func (w *LMDBWrapper) NewLMDBTx(write bool, initialIndexName string) (tx *LMDBTx) { + //w.muDb.Lock() // deadlocked here + //defer w.muDb.Unlock() + + rwflag := uint(0) // writable txn denotated by lack of the lmdb.Readonly flag. + if !write { + rwflag = lmdb.Readonly + } + + sn := atomic.AddInt64(&w.nextTxSn, 1) + //vv("about to create txn sn=%v, write='%v'; on '%v'/%v, stack=\n'%v'", sn, write, initialIndexName, w.path, stack()) + lmdbTxn, err := w.env.BeginTxn(nil, rwflag) + panicOn(err) + //vv("back from creating txn sn=%v, write='%v'; on '%v'/%v", sn, write, initialIndexName, w.path) + + tx = &LMDBTx{ + sn: sn, + write: write, + tx: lmdbTxn, + dbi: w.dbi, + Db: w, + //initloc: stack(), + doAllocZero: w.doAllocZero, + initialIndexName: initialIndexName, + DeleteEmptyContainer: w.DeleteEmptyContainer, + } + return +} + +// Close shuts down the LMDB database. +func (w *LMDBWrapper) Close() (err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + if !w.closed { + w.reg.unregister(w) + w.halt.ReqStop.Close() + w.closed = true + w.writer.Stop() + for _, reader := range w.readers { + reader.Stop() + } + } + w.env.CloseDBI(w.dbi) + return nil +} + +// LMDBTx wraps a lmdb.Txn and provides the Tx interface +// method implementations. +// The methods on LMDBTx are thread-safe, and can be called +// from different goroutines. +type LMDBTx struct { + + // mu serializes lmdb operations on this single txn instance. + // + // reference: https://godoc.org/github.com/dgraph-io/lmdb + // "Running [two separate -jea] transactions concurrently is OK. However, a + // transaction itself isn't thread safe, and should only + // be run serially. It doesn't matter if a transaction is + // created by one goroutine and passed down to other, as + // long as the Txn APIs are called serially." + mu sync.Mutex + sn int64 // serial number + + write bool + dbi lmdb.DBI + Db *LMDBWrapper + tx *lmdb.Txn + + opcount int + + //initloc string // stack trace of where we were initially created. + + doAllocZero bool + + // for tracking txn boundary issues, track all the memory + // that we deploy for roaring containers, and zero it on + // transaction commit/rollback. + acMu sync.Mutex // protect ourAllocs and ourContainers + ourAllocs [][]byte + ourContainers []*roaring.Container + + initialIndexName string + + DeleteEmptyContainer bool + + unlocked bool // runtime.UnlockOSThread has been done. +} + +func (tx *LMDBTx) Type() string { + return LmdbTxn +} + +func (tx *LMDBTx) UseRowCache() bool { + return true +} + +// overWriteOurAllocs provides detection of memory +// access outside the transactional context, similar to the +// old school electric fence techniques but without setting +// memory mappings to read-only... instead we just zero +// out the memory allocated to roaring containers by a +// transaction after the commit or rollback. This, +// hopefully, will cause some downstream confusion and +// test failures, which we can use to locate who has been +// holding on to memory they should have copied prior +// to transaction commit. +func (tx *LMDBTx) overWriteOurAllocs() { + + tx.acMu.Lock() + defer tx.acMu.Unlock() + for _, s := range tx.ourAllocs { + + // The Go compiler recognizes the following pattern and inserts + // an efficient memclr instruction. + // See https://github.com/golang/go/issues/5373 + // and https://codereview.appspot.com/137880043 + for i := range s { + s[i] = 0 + // or + // Seebs suggested we might see even more crashes :) + // but since it will be slow (no memclr), we'll leave the default 0 for now. + //s[i] = -2 + } + } + // keep this around if we need to activate out-of-mmap memory access again. + //for _, v := range tx.ourContainers { + //v.Invalid = true + //v.Tx = tx + //} +} + +// Pointer gives us a memory address for the underlying transaction for debugging. +// It is public because we use it in roaring to report invalid container memory access +// outside of a transaction. +func (tx *LMDBTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +// Rollback rolls back the transaction. +func (tx *LMDBTx) Rollback() { + tx.mu.Lock() // hung here? on defer on panic? + defer tx.mu.Unlock() + + //pp("LMDBTx.Rollback p=%p, its: '%v' initloc: '%v',\n rollbackloc:'%v'", tx, tx.Db.UnprotectedListOpenItAsString(), tx.initloc, stack()) + tx.tx.Abort() // must hold tx.mu mutex lock + + tx.Db.muOpenTxIt.Lock() + delete(tx.Db.openTx, tx) + tx.Db.muOpenTxIt.Unlock() + + if tx.doAllocZero { + // and clear our allocs, to find code using them outside of a txn. + tx.overWriteOurAllocs() + } + if !tx.unlocked { + //runtime.UnlockOSThread() + tx.unlocked = true + } + //vv("done rolling back LMDBTx sn=%v", tx.sn) +} + +// Commit commits the transaction to permanent storage. +// Commits can handle up to 100k updates to fragments +// at once, but not more. This is a LMDBDB imposed limit. +func (tx *LMDBTx) Commit() error { + tx.mu.Lock() + defer tx.mu.Unlock() + + tx.Db.muOpenTxIt.Lock() + delete(tx.Db.openTx, tx) + tx.Db.muOpenTxIt.Unlock() + + //pp("LMDBTx.Commit (write:%v) p=%p, stackID=%x openit: '%v' initloc: '%v', commitloc:\n%v", tx.write, tx, stackID, tx.Db.UnprotectedListOpenItAsString(), tx.initloc, stack()) + + err := tx.tx.Commit() // must hold tx.mu mutex lock + panicOn(err) + + if tx.doAllocZero { + tx.overWriteOurAllocs() + } + if !tx.unlocked { + //runtime.UnlockOSThread() + tx.unlocked = true + } + //vv("done committing LMDBTx sn=%v", tx.sn) + return err +} + +// Readonly returns true iff the LMDBTx is read-only. +func (tx *LMDBTx) Readonly() bool { + return !tx.write +} + +// RoaringBitmap returns the roaring.Bitmap for all bits in the fragment. +func (tx *LMDBTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + + return tx.OffsetRange(index, field, view, shard, 0, 0, LeftShifted16MaxContainerKey) +} + +// Container returns the requested roaring.Container, selected by fragment and ckey +func (tx *LMDBTx) Container(index, field, view string, shard uint64, ckey uint64) (c *roaring.Container, err error) { + + // values returned from Get() are only valid while the transaction + // is open. If you need to use a value outside of the transaction then + // you must use copy() to copy it to another byte slice. + // BUT here we are already inside the Txn. + + bkey := badgerKey(index, field, view, shard, ckey) + tx.mu.Lock() + //var item *lmdb.Item + //item, err = tx.tx.Get(bkey) + + v, err := tx.tx.Get(tx.dbi, bkey) + tx.mu.Unlock() + + if lmdb.IsNotFound(err) { + // Seems crazy, but we, for now at least, + // match what RoaringTx does by returning nil, nil. + return nil, nil + } else { + if err != nil { + vv("unexpected error on Container for bkey = '%v'; err='%v' ignoring for now TODO fix me", string(bkey), err) + //panicOn(err) // mdb_get: invalid argument + return nil, nil + } + } + n := len(v) + if n > 0 { + c = tx.toContainer(v[n-1], v[0:(n-1)]) + } + return +} + +// PutContainer stores rc under the specified fragment and container ckey. +func (tx *LMDBTx) PutContainer(index, field, view string, shard uint64, ckey uint64, rc *roaring.Container) error { + + bkey := badgerKey(index, field, view, shard, ckey) + var by []byte + + ct := roaring.ContainerType(rc) + + switch ct { + case containerArray: + by = fromArray16(roaring.AsArray(rc)) + case containerBitmap: + by = fromArray64(roaring.AsBitmap(rc)) + case containerRun: + by = fromInterval16(roaring.AsRuns(rc)) + case containerNil: + panic("wat? nil container is unexpected, no?!?") + default: + panic(fmt.Sprintf("unknown container type: %v", ct)) + } + tx.mu.Lock() + err := tx.tx.Put(tx.dbi, bkey, append(by, ct), 0) // TODO: this might make a copy; can meta byte be stored elsewhere? + tx.mu.Unlock() + //panicOn(err) // mdb_put: invalid argument + + // TODO(jea): need to handle? + // lmdb.TxnFull + // lmdb.CursorFull + // lmdb.PageFull + return err +} + +// RemoveContainer deletes the container specified by the shard and container key ckey +func (tx *LMDBTx) RemoveContainer(index, field, view string, shard uint64, ckey uint64) error { + bkey := badgerKey(index, field, view, shard, ckey) + tx.mu.Lock() + err := tx.tx.Del(tx.dbi, bkey, nil) + tx.mu.Unlock() + if lmdb.IsNotFound(err) { + return nil + } + return err +} + +// Add sets all the a bits hot in the specified fragment. +func (tx *LMDBTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + + // pure hack to match RoaringTx + defer func() { + if !batched { + if changeCount > 0 { + changeCount = 1 + } + } + }() + + // TODO: optimization: group 'a' elements into their containers, + // and then do all the Adds on that + // container at once, so we don't retrieve a container per bit. + // (maybe, for example, using ImportRoaringBits with clear=false). + + for _, v := range a { + hi, lo := highbits(v), lowbits(v) + + var rct *roaring.Container + rct, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + if err != nil { + return 0, err + } + chng := false + // TODO optimization: set all the bits in the current container at once. group by container first. + rc1, chng := rct.Add(lo) + panicOn(err) + if chng { + changeCount++ + } + if err != nil { + return changeCount, err + } + err = tx.PutContainer(index, field, view, shard, hi, rc1) + //panicOn(err) + } + return +} + +// Remove clears all the specified a bits in the chosen fragment. +func (tx *LMDBTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + + // TODO: optimization: group 'a' elements into their containers, + // and then do all the Removes on that + // container at once, so we don't retrieve a container per bit. + // (maybe, for example, using ImportRoaringBits with clear=true). + for _, v := range a { + hi, lo := highbits(v), lowbits(v) + + var rct *roaring.Container + rct, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + if err != nil { + return 0, err + } + chng := false + rc1, chng := rct.Remove(lo) + panicOn(err) + if chng { + changeCount++ + } + if err != nil { + return changeCount, err + } + if rc1.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, hi) + if err != nil { + //vv("err = '%v'", err) + return + } + } else { + err = tx.PutContainer(index, field, view, shard, hi, rc1) + panicOn(err) + } + } + return +} + +// Contains returns exists true iff the bit chosen by key is +// hot (set to 1) in specified fragment. +func (tx *LMDBTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) { + + lo, hi := lowbits(key), highbits(key) + bkey := badgerKey(index, field, view, shard, hi) + tx.mu.Lock() + var v []byte + v, err = tx.tx.Get(tx.dbi, bkey) + tx.mu.Unlock() + if lmdb.IsNotFound(err) { + //vv("Contains did not find bkey '%v'", string(bkey)) + return false, nil + } + if err != nil { + return false, err + } + n := len(v) + if n > 0 { + c := tx.toContainer(v[n-1], v[0:(n-1)]) + exists = c.Contains(lo) + } + return exists, err +} + +func (tx *LMDBTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + + prefix := badgerAllShardPrefix(index, field, view) + + bi := NewLMDBIterator(tx, prefix) + defer bi.Close() + // bi.Seek(prefix) + // if !bi.cur.Valid() { + // return + //} + + lastShard := uint64(0) + firstDone := false + for bi.Next() { + shard := shardFromBadgerKey(bi.lastKey) + if firstDone { + if shard != lastShard { + sliceOfShards = append(sliceOfShards, shard) + } + lastShard = shard + } else { + // first time + lastShard = shard + firstDone = true + sliceOfShards = append(sliceOfShards, shard) + } + + } + return +} + +// key is the container key for the first roaring Container +// roaring docs: Iterator returns a ContainterIterator which *after* a call to Next(), a call to Value() will +// return the first container at or after key. found will be true if a +// container is found at key. +// +// LMDBTx notes: We auto-stop at the end of this shard, not going beyond. +func (tx *LMDBTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) { + + // needle example: "idx:'i';fld:'f';vw:'v';shd:'00000000000000000000';key@00000000000000000000" + needle := badgerKey(index, field, view, shard, firstRoaringContainerKey) + + // prefix example: "idx:'i';fld:'f';vw:'v';shard:'00000000000000000000';key@" + prefix := badgerPrefix(index, field, view, shard) + + bi := NewLMDBIterator(tx, prefix) + ok := bi.Seek(needle) + if !ok { + ////vv("ContainerIterator not ok on seek to needle '%v'; bi='%#v'", string(needle), bi) + return bi, false, nil + } + //vv("ContainerIterator IS ok on seek to needle '%v'", string(needle)) + + // if !bi.ValidForPrefix(prefix) { + // return bi, false, nil + //} + + // have to compare b/c lmdb might give us valid iterator + // that is past our needle if needle isn't present. + return bi, bytes.Equal(bi.lastKey, needle), nil +} + +// LMDBIterator is the iterator returned from a LMDBTx.ContainerIterator() call. +// It implements the roaring.ContainerIterator interface. +type LMDBIterator struct { + tx *LMDBTx + cur *lmdb.Cursor + dbi lmdb.DBI + + prefix []byte + seekto []byte + + // seen counts how many Next() calls we have seen. + // It is used to match roaring.ContainerIterator semantics. + // Also useful for testing. + seen int + + lastKey []byte + lastVal []byte // *roaring.Container + lastOK bool + lastConsumed bool +} + +// NewLMDBIterator creates an iterator on tx that will +// only return badgerKeys that start with prefix. +func NewLMDBIterator(tx *LMDBTx, prefix []byte) (bi *LMDBIterator) { + cur, err := tx.tx.OpenCursor(tx.dbi) + panicOn(err) + + bi = &LMDBIterator{ + dbi: tx.dbi, + tx: tx, + cur: cur, + prefix: prefix, + } + return +} + +// Close tells the database and transaction that the user is done +// with the iterator. +// From the lmdb docs: It is important to call this when you're done with iteration. +// else you will get an error on tx.Discard()/Commit(). +func (bi *LMDBIterator) Close() { + bi.cur.Close() +} + +// Valid returns false if there are no more values in the iterator's range. +func (bi *LMDBIterator) Valid() bool { + return bi.lastOK +} + +// Seek allows the iterator to start at needle instead of the global begining. +func (bi *LMDBIterator) Seek(needle []byte) (ok bool) { + //vv("Seek needle '%v'", string(needle)) + + bi.seen++ // if ommited, red TestLMDB_ContainerIterator_empty_iteration_loop() in lmdb_test.go. + + getflag := uint(lmdb.SetRange) + k, v, err := bi.cur.Get(needle, nil, getflag) + + if lmdb.IsNotFound(err) { + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + if len(bi.prefix) > 0 { + ok = bytes.HasPrefix(k, bi.prefix) + if !ok { + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + } + if len(k) == 0 { + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + + bi.lastKey = k + bi.lastVal = v + if len(v) == 0 { + // actually under !tx.DeleteEmptyContainer, we can have empty containers. + + //vv("Seek got len v == 0, k='%v', needle='%v'; here is Dump:", string(k), string(needle)) + //bi.tx.Dump() + //vv("done with dump.") + + // lmdb.go:1008 2020-08-09T16:39:21.228243-05:00 done with dump. + // panic: len v should not be zero here; for found needle='idx:'valck';fld:'f';vw:'bsig_f';shd:'00000000000000000041';ckey@00000000000000000016' / k='idx:'valck';fld:'f';vw:'bsig_f';shd:'00000000000000000041';ckey@00000000000000000032' + // we see in the Dump shard 41, ckey 32; but not shard 41 ckey 16 + + // repull with the key k we *did* get back + var k2, v2 []byte + k2, v2, err = bi.cur.Get(k, nil, getflag) + if err != nil { + panic(fmt.Sprintf("repull should not error, since we got key k='%v' already!", string(k))) + } + if string(k2) != string(k) { + panic(fmt.Sprintf("repull should gives same k2 back, since we got key k='%v' already! k2='%v'", string(k), string(k2))) + } + if len(v2) > 0 { + vv("good, v2 had data on repull.") + bi.lastVal = v2 + } else { + //panic(fmt.Sprintf("len v should not be zero here; for found needle='%v' / k='%v'", string(needle), string(k))) + + // just bail + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + } + bi.lastOK = true + bi.lastConsumed = false + + return true +} + +func (bi *LMDBIterator) ValidForPrefix(prefix []byte) bool { + if !bi.lastOK { + return false + } + if len(bi.prefix) == 0 { + return true + } + return bytes.HasPrefix(bi.lastKey, bi.prefix) +} + +func (bi *LMDBIterator) String() (r string) { + return fmt.Sprintf("LMDBIterator{prefix: '%v', seekto: '%v', seen:%v, lastKey:'%v', lastOK:%v, lastConsumed:%v}", string(bi.prefix), string(bi.seekto), bi.seen, string(bi.lastKey), bi.lastOK, bi.lastConsumed) +} + +// Next advances the iterator. +func (bi *LMDBIterator) Next() (ok bool) { + //vv("top of LMDBIterator.Next(); bi = '%v'", bi) + // defer func() { + // vv("LMDBIterator.Next() returning ok='%v'; bi = '%v'", ok, bi) + // }() + if bi.lastOK && !bi.lastConsumed { + //vv("have seek value that has not been consumed, consume it now.") + bi.seen++ + bi.lastConsumed = true + if len(bi.lastVal) == 0 { + panic("bi.lastVal should not have len 0 if lastOK true") + } + return true + } + + getflag := uint(lmdb.Next) + prefix := bi.prefix + + if bi.seen == 0 { + if len(bi.prefix) > 0 { + getflag = lmdb.SetRange + } + } else { + prefix = nil + } + + bi.seen++ +skipEmpty: + k, v, err := bi.cur.Get(prefix, nil, getflag) + ////vv("Next Get() returned err='%v', k='%v'; stack=\n%v\n", err, string(k), stack()) + //vv("Next Get(getflag='%v' (reference lmdb.Next='%v' and lmdb.SetRange='%v'); prefix='%v') returned err='%v', k='%v'", getflag, uint(lmdb.Next), uint(lmdb.SetRange), string(prefix), err, string(k)) + if lmdb.IsNotFound(err) { + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + if len(bi.prefix) > 0 { + ok = bytes.HasPrefix(k, bi.prefix) + if !ok { + bi.lastKey = nil + bi.lastVal = nil + bi.lastOK = false + bi.lastConsumed = false + return false + } + } + bi.lastKey = k + bi.lastVal = v + if len(v) == 0 { + // actually under !tx.DeleteEmptyContainer, we can have empty containers! + goto skipEmpty + + //vv("v should not have len 0, in Next. k='%v'; here is Dump:", string(k)) + //bi.tx.Dump() + //vv("done with Dump") + //panic("v should not have len 0") + } + bi.lastOK = true + bi.lastConsumed = true + + return true +} + +// Value retrieves what is pointed at currently by the iterator. +func (bi *LMDBIterator) Value() (containerKey uint64, c *roaring.Container) { + if !bi.lastOK { + panic("bi.cur not valid") + } + containerKey = badgerKeyExtractContainerKey(bi.lastKey) + + v := bi.lastVal + n := len(v) + if n > 0 { + c = bi.tx.toContainer(v[n-1], v[0:(n-1)]) + } else { + panic("v should not be empty!") + } + return +} + +// lmdbFinder implements roaring.IteratorFinder. +// It is used by LMDBTx.ForEach() +type lmdbFinder struct { + tx *LMDBTx + index string + field string + view string + shard uint64 + needClose []Closer +} + +// FindIterator lets lmdbFinder implement the roaring.FindIterator interface. +func (bf *lmdbFinder) FindIterator(seek uint64) (roaring.ContainerIterator, bool) { + a, found, err := bf.tx.ContainerIterator(bf.index, bf.field, bf.view, bf.shard, seek) + panicOn(err) + bf.needClose = append(bf.needClose, a) + return a, found +} + +// Close closes all bf.needClose listed Closers. +func (bf *lmdbFinder) Close() { + for _, i := range bf.needClose { + i.Close() + } +} + +// NewTxIterator returns a *roaring.Iterator that MUST have Close() called on it BEFORE +// the transaction Commits or Rollsback. +func (tx *LMDBTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + bf := &lmdbFinder{tx: tx, index: index, field: field, view: view, shard: shard, needClose: make([]Closer, 0)} + itr := roaring.NewIterator(bf) + return itr +} + +// ForEach applies fn to each bitmap in the fragment. +func (tx *LMDBTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + itr := tx.NewTxIterator(index, field, view, shard) + defer itr.Close() + + // Seek can create many container iterators, thus bf.Close() needClose list. + itr.Seek(0) + // v is the bit we are operating on. + for v, eof := itr.Next(); !eof; v, eof = itr.Next() { + if err := fn(v); err != nil { + return err + } + } + return nil +} + +// ForEachRange applies fn on the selected range of bits on the chosen fragment. +func (tx *LMDBTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + + itr := tx.NewTxIterator(index, field, view, shard) + defer itr.Close() + + itr.Seek(start) + + // v is the bit we are operating on. + for v, eof := itr.Next(); !eof && v < end; v, eof = itr.Next() { + if err := fn(v); err != nil { + return err + } + } + return nil +} + +// Count operates on the full bitmap level, so it sums over all the containers +// in the bitmap. +func (tx *LMDBTx) Count(index, field, view string, shard uint64) (uint64, error) { + + a, found, err := tx.ContainerIterator(index, field, view, shard, 0) + panicOn(err) + defer a.Close() + if !found { + //vv("not found") + return 0, nil + } + result := int32(0) + //vv("a = '%v'", a.(*LMDBIterator).String()) + for a.Next() { + ckey, cont := a.Value() + //vv("on a.Next() loop... a.Value() got ckey '%v'", string(ckey)) + _ = ckey + result += cont.N() + } + //vv("a.Next() returned false") + return uint64(result), nil +} + +// Max is the maximum bit-value in your bitmap. +// Returns zero if the bitmap is empty. Odd, but this is what roaring.Max does. +func (tx *LMDBTx) Max(index, field, view string, shard uint64) (uint64, error) { + + prefix := badgerPrefix(index, field, view, shard) + seekto := badgerPrefix(index, field, view, shard+1) + + cur, err := tx.tx.OpenCursor(tx.dbi) + panicOn(err) + defer cur.Close() + + k, v, err := cur.Get(seekto, nil, lmdb.SetRange) + _, _ = k, v + if lmdb.IsNotFound(err) { + // we have nothing >= seekto, but we might have stuff before it, and we'll wrap backwards. + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + // empty database + return 0, nil + } + } else { + // we found something >= seekto, so backup by 1. + k, v, err = cur.Get(nil, nil, lmdb.Prev) + if lmdb.IsNotFound(err) { + // nothing before seekto + return 0, nil + } + } + + // have something, are we in [prefix, seekto) ? + cmp := bytes.Compare(k, prefix) + if cmp >= 0 { + // good, got max in k, v + } else { + return 0, nil // nothing in [prefix, seekto). + } + + hb := badgerKeyExtractContainerKey(k) + n := len(v) + if n == 0 { + return 0, nil + } + rc := tx.toContainer(v[n-1], v[0:(n-1)]) + + lb := rc.Max() + return hb<<16 | uint64(lb), nil +} + +// Min returns the smallest bit set in the fragment. If no bit is hot, +// the second return argument is false. +func (tx *LMDBTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + + // Seek can create many container iterators, thus the bf.Close() needClose list. + bf := &lmdbFinder{tx: tx, index: index, field: field, view: view, shard: shard, needClose: make([]Closer, 0)} + defer bf.Close() + itr := roaring.NewIterator(bf) + + itr.Seek(0) + + // v is the bit we are operating on. + v, eof := itr.Next() + if eof { + return 0, false, nil + } + return v, true, nil +} + +// UnionInPlace unions all the others Bitmaps into a new Bitmap, and then writes it to the +// specified fragment. +func (tx *LMDBTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + + rbm.UnionInPlace(others...) + // iterate over the containers that changed within rbm, and write them back to disk. + + it, found := rbm.Containers.Iterator(0) + _ = found // don't care about the value of found, because first containerKey might be > 0 + + for it.Next() { + containerKey, rc := it.Value() + + // TODO: only write the changed ones back, as optimization? + // Compare to ImportRoaringBits. + err := tx.PutContainer(index, field, view, shard, containerKey, rc) + panicOn(err) + } + return nil +} + +// CountRange returns the count of hot bits in the start, end range on the fragment. +// roaring.countRange counts the number of bits set between [start, end). +func (tx *LMDBTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + if start >= end { + return 0, nil + } + + skey := highbits(start) + ekey := highbits(end) + + citer, found, err := tx.ContainerIterator(index, field, view, shard, skey) + _ = found + panicOn(err) + + defer citer.Close() + + // If range is entirely in one container then just count that range. + if skey == ekey { + citer.Next() + _, c := citer.Value() + return uint64(c.CountRange(int32(lowbits(start)), int32(lowbits(end)))), nil + } + + for citer.Next() { + k, c := citer.Value() + if k < skey { + citer.Close() + panic(fmt.Sprintf("should be impossible for k(%v) to be less than skey(%v). tx p=%p", k, skey, tx)) + } + + // k > ekey handles the case when start > end and where start and end + // are in different containers. Same container case is already handled above. + if k > ekey { + break + } + if k == skey { + n += uint64(c.CountRange(int32(lowbits(start)), roaring.MaxContainerVal+1)) + continue + } + if k < ekey { + n += uint64(c.N()) + continue + } + if k == ekey { + n += uint64(c.CountRange(0, int32(lowbits(end)))) + break + } + } + + return n, nil +} + +// OffsetRange creates a new roaring.Bitmap to return in other. For all the +// hot bits in [start, endx) of the chosen fragment, it stores +// them into other but with offset added to their bit position. +// The primary client is doing this, using ShardWidth, already; see +// fragment.rowFromStorage() in fragment.go. For example: +// +// data, err := tx.OffsetRange(f.index, f.field, f.view, f.shard, +// f.shard*ShardWidth, rowID*ShardWidth, (rowID+1)*ShardWidth) +// ^ offset ^ start ^ endx +// +// The start and endx arguments are container keys that have been shifted left by 16 bits; +// their highbits() will be taken to determine the actual container keys. This +// is done to conform to the roaring.OffsetRange() argument convention. +// +func (tx *LMDBTx) OffsetRange(index, field, view string, shard, offset, start, endx uint64) (other *roaring.Bitmap, err error) { + + // roaring does these three checks in its OffsetRange + if lowbits(offset) != 0 { + panic("offset must not contain low bits") + } + if lowbits(start) != 0 { + panic("range start must not contain low bits") + } + if lowbits(endx) != 0 { + panic("range end must not contain low bits") + } + + other = roaring.NewSliceBitmap() + off := highbits(offset) + hi0, hi1 := highbits(start), highbits(endx) + + needle := badgerKey(index, field, view, shard, hi0) + prefix := badgerPrefix(index, field, view, shard) + + n2, pre2 := badgerKeyAndPrefix(index, field, view, shard, hi0) + if string(n2) != string(needle) { + panic(fmt.Sprintf("problem! n2(%v) != needle(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(n2), string(needle))) + } + if string(pre2) != string(prefix) { + panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(pre2), string(prefix))) + } + + it := NewLMDBIterator(tx, prefix) // see OffsetRange() panic 'Only one iterator can be active at one time, for a RW txn + defer it.Close() + it.Seek(needle) + for ; it.ValidForPrefix(prefix); it.Next() { + //vv("through the look, it.lastKey '%v' must have been valid for prefix '%v'", string(it.lastKey), string(prefix)) + bkey := it.lastKey + k := badgerKeyExtractContainerKey(bkey) + + // >= hi1 is correct b/c endx cannot have any lowbits set. + if uint64(k) >= hi1 { + break + } + destCkey := off + (k - hi0) + + v := it.lastVal + n := len(v) + if n == 0 { + //vv("why is it.lastVal == v == nil for it.lastKey '%v' must have been valid for prefix '%v'", string(it.lastKey), string(prefix)) + continue + } + c := tx.toContainer(v[n-1], v[0:(n-1)]) + other.Containers.Put(destCkey, c.Freeze()) + } + return other, nil +} + +// IncrementOpN increments the tx opcount by changedN +func (tx *LMDBTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + tx.opcount += changedN +} + +// ImportRoaringBits handles deletes by setting clear=true. +// rowSet[rowID] returns the number of bit changed on that rowID. +func (tx *LMDBTx) ImportRoaringBits(index, field, view string, shard uint64, itr roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { + n := itr.Len() + if n == 0 { + return + } + rowSet = make(map[uint64]int) + + var currRow uint64 + + var oldC *roaring.Container + for itrKey, synthC := itr.NextContainer(); synthC != nil; itrKey, synthC = itr.NextContainer() { + if rowSize != 0 { + currRow = itrKey / rowSize + } + nsynth := int(synthC.N()) + if nsynth == 0 { + continue + } + // INVAR: nsynth > 0 + + oldC, err = tx.Container(index, field, view, shard, itrKey) + panicOn(err) + if err != nil { + return + } + + if oldC == nil || oldC.N() == 0 { + // no container at the itrKey in lmdb (or all zero container). + if clear { + // changed of 0 and empty rowSet is perfect, no need to change the defaults. + continue + } else { + + changed += nsynth + rowSet[currRow] += nsynth + + err = tx.PutContainer(index, field, view, shard, itrKey, synthC) + if err != nil { + return + } + continue + } + } + + if clear { + existN := oldC.N() // number of bits set in the old container + newC := oldC.Difference(synthC) + + // update rowSet and changes + if newC.N() == existN { + // INVAR: do changed need adjusting? nope. same bit count, + // so no change could have happened. + continue + } else { + changes := int(existN - newC.N()) + changed += changes + rowSet[currRow] -= changes + + if tx.DeleteEmptyContainer && newC.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, itrKey) + if err != nil { + return + } + continue + } + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + return + } + continue + } + } else { + // setting bits + + existN := oldC.N() + if existN == roaring.MaxContainerVal+1 { + // completely full container already, set will do nothing. so changed of 0 default is perfect. + continue + } + if existN == 0 { + // can nsynth be zero? No, because of the continue/invariant above where nsynth > 0 + changed += nsynth + rowSet[currRow] += nsynth + err = tx.PutContainer(index, field, view, shard, itrKey, synthC) + if err != nil { + return + } + continue + } + + newC := roaring.Union(oldC, synthC) // UnionInPlace was giving us crashes on overly large containers. + + if roaring.ContainerType(newC) == containerBitmap { + newC.Repair() // update the bit-count so .n is valid. b/c UnionInPlace doesn't update it. + } + if newC.N() != existN { + changes := int(newC.N() - existN) + changed += changes + rowSet[currRow] += changes + + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + panicOn(err) + return + } + continue + } + } + } + return +} + +func (tx *LMDBTx) toContainer(typ byte, v []byte) (r *roaring.Container) { + + if len(v) == 0 { + return nil + } + + var w []byte + if tx.doAllocZero { + // Do electric fence-inspired bad-memory read detection. + // + // The v []byte lives in LMDBDB's memory-mapped vlog-file, + // and LMDB will recycle it after tx ends with rollback or commit. + // + // Problem is, at least some operations were not respecting transaction boundaries. + // This technique helped us find them. The rowCache was an example. + // + // See the global const DetectMemAccessPastTx + // at the top of txfactory.go to activate/deactivate this. + // + // Seebs suggested this nice variation: we could use individual mmaps for these + // copies, which would be unusable in production, but workable for testing, and then unmap them, + // which would get us probable segfaults on future accesses to them. + // + // The go runtime also has an -efence flag which may be similarly useful if really pressed. + // + w = make([]byte, len(v)) + copy(w, v) + + // register w so we can catch out-of-tx memory access + tx.acMu.Lock() + defer tx.acMu.Unlock() + tx.ourAllocs = append(tx.ourAllocs, w) + } else { + w = v + } + + switch typ { + case containerArray: + c := roaring.NewContainerArray(toArray16(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + case containerBitmap: + c := roaring.NewContainerBitmap(-1, toArray64(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + case containerRun: + c := roaring.NewContainerRun(toInterval16(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + default: + panic(fmt.Sprintf("unknown container: %v", typ)) + } +} + +// StringifiedLMDBKeys returns a string with all the container +// keys available in lmdb. +func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx) (r string) { + if optionalUseThisTx == nil { + tx := w.NewLMDBTx(!writable, "") + defer tx.Rollback() + r = stringifiedLMDBKeysTx(tx) + return + } + + btx, ok := optionalUseThisTx.(*LMDBTx) + if !ok { + return fmt.Sprintf("", optionalUseThisTx) + } + r = stringifiedLMDBKeysTx(btx) + return +} + +// countBitsSet returns the number of bits set (or "hot") in +// the roaring container value found by the badgerKey() +// formatted bkey. +func (tx *LMDBTx) countBitsSet(bkey []byte) (n int) { + + v, err := tx.tx.Get(tx.dbi, bkey) + if lmdb.IsNotFound(err) { + // some queries bkey may not be present! don't panic. + //panic(fmt.Sprintf("lmdb did not have value for bkey = '%v'", string(bkey))) + return 0 + } + panicOn(err) + + n = len(v) + if n > 0 { + rc := tx.toContainer(v[n-1], v[0:(n-1)]) + n = int(rc.N()) + } + return +} + +func (tx *LMDBTx) Dump() { + fmt.Printf("%v\n", stringifiedLMDBKeysTx(tx)) +} + +// stringifiedLMDBKeysTx reports all the lmdb keys and a +// corresponding blake3 hash viewable by txn within the entire +// lmdb database. +// It also reports how many bits are hot in the roaring container +// (how many bits are set, or 1 rather than 0). +// +// By convention, we must return the empty string if there +// are no keys present. The tests use this to confirm +// an empty database. +func stringifiedLMDBKeysTx(tx *LMDBTx) (r string) { + + r = "allkeys:[\n" + it := NewLMDBIterator(tx, nil) + defer it.Close() + any := false + for it.Next() { + any = true + + bkey := it.lastKey + key := string(bkey) + ckey := badgerKeyExtractContainerKey(bkey) + hash := "" + srbm := "" + v := it.lastVal + n := len(v) + if n == 0 { + panic("should not have empty v here") + } + hash = blake3sum16(v[0:(n - 1)]) + ct := tx.toContainer(v[n-1], v[0:(n-1)]) + cts := roaring.NewSliceContainers() + cts.Put(ckey, ct) + rbm := &roaring.Bitmap{Containers: cts} + srbm = bitmapAsString(rbm) + + r += fmt.Sprintf("%v -> %v (%v hot)\n", key, hash, tx.countBitsSet(bkey)) + r += " ......." + srbm + "\n" + } + r += "]\n all-in-blake3:" + blake3sum16([]byte(r)) + + if !any { + return "" + } + return "lmdb-" + r +} + +func (w *LMDBWrapper) DeleteField(index, field, fieldPath string) error { + + // under blue-green roaring_lmdb, the directory will not be found, b/c roaring will have + // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: + // "If the path does not exist, RemoveAll returns nil (no error)" + err := os.RemoveAll(fieldPath) + if err != nil { + return errors.Wrap(err, "removing directory") + } + prefix := badgerFieldPrefix(index, field) + return w.DeletePrefix(prefix) +} + +func (w *LMDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { + prefix := badgerPrefix(index, field, view, shard) + return w.DeletePrefix(prefix) +} + +func (w *LMDBWrapper) DeletePrefix(prefix []byte) error { + + tx := w.NewLMDBTx(writable, w.name) + + // NewLMDBTx will grab these, so don't lock until after it. + w.muDb.Lock() + defer w.muDb.Unlock() + + bi := NewLMDBIterator(tx, prefix) + + for bi.Next() { + err := bi.cur.Del(0) + panicOn(err) + } + bi.Close() + + err := tx.Commit() + panicOn(err) + + return nil +} + +func (tx *LMDBTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader RoaringBitmap") + } + var buf bytes.Buffer + sz, err = rbm.WriteTo(&buf) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader rbm.WriteTo(buf)") + } + return ioutil.NopCloser(&buf), sz, err +} diff --git a/lmdb/lmdb_test.go b/lmdb/lmdb_test.go new file mode 100644 index 000000000..4d19be47a --- /dev/null +++ b/lmdb/lmdb_test.go @@ -0,0 +1,1315 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build skip_building_lmdb_for_now + +package pilosa + +import ( + "bytes" + "fmt" + "math" + "os" + "strconv" + "testing" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// helpers, each runs their own new txn, and commits if a change/delete +// was made. The txn is rolled back if it is just viewing the data. + +func LMDBMustHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue '%v' was NOT SET!!!", bitvalue)) + } + + tx.Rollback() +} + +func LMDBMustNotHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG bitvalue '%v' WAS SET but should not have been.!!!", bitvalue)) + } + tx.Rollback() +} + +func LMDBMustSetBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewLMDBTx(writable, index) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed != 1 { + panic("should have 1 bit changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + panicOn(tx.Commit()) +} + +func LMDBMustDeleteBitvalueContainer(dbwrap *LMDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewLMDBTx(writable, index) + hi := highbits(putme) + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + panicOn(tx.Commit()) +} + +func LMDBMustDeleteBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewLMDBTx(writable, index) + _, err := tx.Remove(index, field, view, shard, putme) + panicOn(err) + panicOn(tx.Commit()) +} + +func mustOpenEmptyLMDBWrapper(path string) (w *LMDBWrapper, cleaner func()) { + var err error + fn := lmdbPath(path) + panicOn(os.RemoveAll(fn)) + w, err = globalLMDBReg.newLMDBWrapper(fn) + panicOn(err) + + // verify it is empty + allkeys := w.StringifiedLMDBKeys(nil) + if allkeys != "" { + panic(fmt.Sprintf("freshly created database was not empty! had keys:'%v'", allkeys)) + } + + return w, func() { + w.Close() // stop any started background GC goroutine. + os.RemoveAll(fn) + } +} + +// end of helper utilities +////////////////////////// + +////////////////////////// +// begin Tx method tests + +func TestLMDB_DeleteFragment(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLmdb_DeleteFragment") + defer clean() + defer dbwrap.Close() + index, field, view, shard0 := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + + shard1 := uint64(1) + + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + shards := []uint64{shard0, shard1} + for _, s := range shards { + for _, v := range bits { + changed, err := tx.Add(index, field, view, s, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + } + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + } + err := tx.Commit() + panicOn(err) + + // end of setup + + survivor := shard0 + victim := shard1 + err = dbwrap.DeleteFragment(index, field, view, victim, nil) + panicOn(err) + + tx = dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if s == survivor { + if !exists { + panic(fmt.Sprintf("ARG survivor died : bit %v", v)) + } + } else if s == victim { // victim, should have been deleted + if exists { + panic(fmt.Sprintf("ARG victim lived : bit %v", v)) + } + } + } + } +} + +func TestLMDB_Max_on_many_containers(t *testing.T) { + path := "TestLMDB_Max_on_many_containers" + dbwrap, clean := mustOpenEmptyLMDBWrapper(path) + + defer clean() + defer dbwrap.Close() + index, field, view := "i", "f", "v" + + // 099 + // 101 + // 199 + // 300 + // 399 + // + // find max in [300,400) and get 399 + // find max in [000,100) and get 099 + // find max in [100,200) and get 199 + // find max in [400,500) and get nothing back + // find max in [200,300) and get nothing back + + shards := []int{99, 101, 199, 300, 399} + + for _, sh := range shards { + shard := uint64(sh) + for _, pm := range shards { + putme := uint64(pm) + if putme > shard { + continue + } + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + } + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + for _, shard := range shards { + max, err := tx.Max(index, field, view, uint64(shard)) + panicOn(err) + //vv("highbits of max = %v from shard = %v", max, shard) + if max != uint64(shard) { + panic(fmt.Sprintf("expected max (%v) to be == shard = %v", max, shard)) + } + } + + // check for not found + max, err := tx.Max(index, field, view, uint64(200)) + panicOn(err) + if max != 0 { + panic("expected not found to give 0 max back with nil err") + } + max, err = tx.Max(index, field, view, uint64(400)) + panicOn(err) + if max != 0 { + panic("expected not found to give 0 max back with nil err") + } + +} + +// and the rest + +func TestLMDB_SetBitmap(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_SetBitmap") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + bitvalue := uint64(0) + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + err = tx.Commit() + panicOn(err) + + // + // commited, so should be visible outside the txn + // + + tx2 := dbwrap.NewLMDBTx(!writable, index) + exists, err = tx2.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!! on tx2") + } + + n, err := tx2.Count(index, field, view, shard) + panicOn(err) + if n != 1 { + panic(fmt.Sprintf("should have Count 1; instead n = %v", n)) + } + tx2.Rollback() +} + +func TestLMDB_OffsetRange(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_OffsetRange") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + + bitvalue := uint64(1 << 20) + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + bitvalue2 := uint64(1<<20 + 1) + changed, err = tx.Add(index, field, view, shard, doBatched, bitvalue2) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + exists, err = tx.Contains(index, field, view, shard, bitvalue2) + panicOn(err) + if !exists { + panic("ARG bitvalue2 was NOT SET!!!") + } + + err = tx.Commit() + panicOn(err) + + offset := uint64(0 << 20) + start := uint64(0 << 16) + endx := bitvalue + 1<<16 + + tx2 := dbwrap.NewLMDBTx(!writable, index) + rbm2, err := tx2.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx2.Rollback() + + // should see our 1M value + s2 := bitmapAsString(rbm2) + expect2 := "c(1048576, 1048577)" + if s2 != expect2 { + panic(fmt.Sprintf("s2='%v', but expected '%v'", s2, expect2)) + } + + // now offset by 2M + offset = uint64(2 << 20) + tx3 := dbwrap.NewLMDBTx(!writable, index) + rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx3.Rollback() + + //expect to see 3M == 3145728 + s3 := bitmapAsString(rbm3) + expect3 := "c(3145728, 3145729)" + + if s3 != expect3 { + panic(fmt.Sprintf("s3='%v', but expected '%v'", s3, expect3)) + } +} + +func TestLMDB_Count_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_Count_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if int(n) != len(putmeValues) { + panic(fmt.Sprintf("expected Count of %v but got n=%v", len(putmeValues), n)) + } +} + +func TestLMDB_Count_dense_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_Count_dense_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + tx := dbwrap.NewLMDBTx(writable, index) + + expected := 0 + for i := uint64(0); i < (1<<16)+2; i += 2 { + changed, err := tx.Add(index, field, view, shard, doBatched, i) + panicOn(err) + if changed <= 0 { + panic("wat? should have changed") + } + expected++ + } + defer tx.Rollback() + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if int(n) != expected { + panic(fmt.Sprintf("expected Count of %v but got n=%v", expected, n)) + } +} + +func TestLMDB_ContainerIterator_on_empty(t *testing.T) { + // iterate on empty container, should not find anything. + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ContainerIterator") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + bitvalue := uint64(0) + citer, found, err := tx.ContainerIterator(index, field, view, shard, bitvalue) + panicOn(err) + defer citer.Close() + if found { + panic("should not have found anything") + } + panicOn(err) +} + +func TestLMDB_ContainerIterator_on_one_bit(t *testing.T) { + // set one bit, iterate. + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(bitvalue)) + if !found { + panic("ContainerIterator did not find the 42 bit") + } + panicOn(err) + defer citer.Close() + + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 0 { + panic("42 should have had key 0") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if !container.Contains(lowbits(bitvalue)) { + panic("container did not have our bitvalue!") + } + loopCount++ + if loopCount > 0 { // happier linter + break + } + } + if loopCount != 1 { + panic("ContainerIterator did not return a citer that scanned our set bit") + } +} + +func TestLMDB_badgerKey_badgerPrefix(t *testing.T) { + + // badgerPrefix() must agree with badgerKey(), but not have the key at the end. + // This is important for iteration over containers. + + index, field, view, shard := "i", "f", "v", uint64(0) + + // needle examples with the container-key extremes: + // "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" // smallest + // "index:'i';field:'f';view:'v';shard:'0';key@18446744073709551615" // largest + needle := badgerKey(index, field, view, shard, 0) + + // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" + prefix := badgerPrefix(index, field, view, shard) + + if !bytes.HasPrefix(needle, prefix) { + panic(fmt.Sprintf("badgerPrefix() output '%v'was not a prefix of badgerKey() '%v'", string(needle), string(prefix))) + } + if len(prefix)+20 != len(needle) { + panic(fmt.Sprintf("badgerPrefix() output '%v'was 20 characters shorter than badgerKey() '%v'", string(needle), string(prefix))) + } + + // validate assumption that badgerKeyExtractContainerKey() makes about strconv.ParseUint() error reporting; + // for distinguishing prefixes from full keys. Even if the shard number is so large that the prefix + // starts with a legitimate decimal number. + shouldNotParse := "12345123451234';key@" + containerKey, err := strconv.ParseUint(shouldNotParse, 10, 64) + if err == nil { + panic(fmt.Sprintf("strconv.ParseUint should have returned an error parsing this string '%v'; instead we got '%v'", shouldNotParse, containerKey)) + } + + // verify panic on submitting a prefix + func() { + defer func() { + r := recover() + if r == nil { + panic(fmt.Sprintf("should have seen panic on call to badgerKeyExtractContainerKey(prefix='%v')", prefix)) + } + }() + badgerKeyExtractContainerKey(prefix) // should panic. + }() +} + +func TestLMDB_ContainerIterator_on_one_bit_fail_to_find(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := putme + 1 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + if !found { + panic("ContainerIterator did not find the searchme") + } + defer citer.Close() + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 1 { + panic("Containeriterator searching for highbits(searchme) should not have had a bit") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if container.Contains(lowbits(searchme)) { + panic("container should have putme but not our searchme!") + } + loopCount++ + // only want first pass. keep linter happy by avoiding raw break + if loopCount > 0 { + break + } + } + panicOn(err) +} + +func TestLMDB_ContainerIterator_empty_iteration_loop(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ContainerIterator_empty_iteration_loop") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := uint64(1 << 17) // in the next container, key:2 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + panicOn(err) + if found { + panic("ContainerIterator found the searchme, when it should not have") + } + defer citer.Close() + if citer.Next() { + panic("expected no looping, 0 iterations, b/c started searchme past our data in putme") + } + + // expect to see a blow up from the citer.Value() call, verify that we do. + func() { + defer func() { + r := recover() + if r == nil { + panic("expected a panic from citer.Value() in this case") + } + }() + citer.Value() // should panic + }() + +} + +func TestLMDB_ForEach_on_one_bit(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + count := 0 + err = tx.ForEach(index, field, view, shard, func(v uint64) error { + if v != bitvalue { + panic(fmt.Sprintf("bitvalue corrupt got %v want %v", v, bitvalue)) + } + count += 1 + return nil + }) + panicOn(err) + if count != 1 { + panic(fmt.Sprintf("Expected single iteration got %v ", count)) + } +} + +func TestLMDB_RemoveContainer_one_bit_test(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_RemoveContainer_one_bit_test") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 13, 77, 1511} + + for _, putme := range putmeValues { + + // a) delete of whole container in a seperate txn. Commit should establish the deletion. + + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // b) deletion + rollback on the txn should restore the deleted bit + + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // delete, but rollback instead of commit + tx := dbwrap.NewLMDBTx(writable, index) + hi := highbits(putme) + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + tx.Rollback() + + // verify that the rollback undid the deletion. + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // c) within one Tx, after delete it should be gone as viewed within the txn. + tx = dbwrap.NewLMDBTx(writable, index) + hi = highbits(putme) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG putme '%v' was NOT SET!!!", putme)) + } + + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + + exists, err = tx.Contains(index, field, view, shard, putme) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG putme '%v' was SET even after RemoveContiner in this txn.", putme)) + } + + tx.Rollback() + + // verify that the rollback undid the deletion. + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + // leave with clean slate + LMDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + } +} + +func TestLMDB_Remove_one_bit_test(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_Remove_one_bit_test") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 13, 77, 1511} + + for _, putme := range putmeValues { + + // a) delete of whole container in a seperate txn. Commit should establish the deletion. + + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustDeleteBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // b) deletion + rollback on the txn should restore the deleted bit + + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // delete, but rollback instead of commit + tx := dbwrap.NewLMDBTx(writable, index) + hi, lo := highbits(putme), lowbits(putme) + _, _ = hi, lo + _, err := tx.Remove(index, field, view, shard, hi) + panicOn(err) + tx.Rollback() + + // verify that the rollback undid the deletion. + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // c) within one Tx, after delete it should be gone as viewed within the txn. + tx = dbwrap.NewLMDBTx(writable, index) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG putme '%v' was NOT SET!!!", putme)) + } + + mustRemove(tx.Remove(index, field, view, shard, putme)) + + exists, err = tx.Contains(index, field, view, shard, putme) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG putme '%v' was SET even after Remove in this txn.", putme)) + } + + tx.Rollback() + + // verify that the rollback undid the deletion. + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + // leave with clean slate + LMDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + } +} + +func TestLMDB_Min_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_Min_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + // verify no containers flag works + tx := dbwrap.NewLMDBTx(!writable, index) + min, containersExist, err := tx.Min(index, field, view, shard) + _ = min + panicOn(err) + if containersExist { + panic("no containers should exist") + } + tx.Rollback() + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx = dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + min, containersExist, err = tx.Min(index, field, view, shard) + panicOn(err) + if !containersExist { + panic("containers should exist") + } + expected := putmeValues[0] + if min != expected { + panic(fmt.Sprintf("expected Min() of %v but got min=%v", expected, min)) + } +} + +func TestLMDB_CountRange_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_CountRange_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + // verify no containers flag works + tx := dbwrap.NewLMDBTx(!writable, index) + n, err := tx.CountRange(index, field, view, shard, 0, math.MaxUint64) + panicOn(err) + if n != 0 { + panic("no containers should exist") + } + tx.Rollback() + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx = dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + n, err = tx.CountRange(index, field, view, shard, 0, math.MaxUint64) + panicOn(err) + if n == 0 { + panic("containers should exist") + } + expected := uint64(len(putmeValues)) + if n != expected { + panic(fmt.Sprintf("expected CountRange() of %v but got n=%v", expected, n)) + } +} + +func TestLMDB_CountRange_middle_container(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_CountRange_middle_container") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + // pick out just the middle container with the 1 bit set on it. + n, err := tx.CountRange(index, field, view, shard, 4, (2<<16)+1) + panicOn(err) + if n != 1 { + panic("middle 1 bit container should exist") + } +} + +func TestLMDB_CountRange_many_middle_container(t *testing.T) { + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_CountRange_many_middle_container") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + // get them all + n, err := tx.CountRange(index, field, view, shard, 0, (4<<16)+1) + panicOn(err) + if n != 3 { + panic("count should have been all 3 bits") + } +} + +func TestLMDB_UnionInPlace(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_UnionInPlace") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16} + + others := roaring.NewBitmap() + others2 := roaring.NewBitmap() + others3 := roaring.NewBitmap() + // populate others with putmeValues +1 into others + + for _, putme := range putmeValues { + LMDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx2 := dbwrap.NewLMDBTx(!writable, index) + n, err := tx2.Count(index, field, view, shard) + panicOn(err) + if n != 2 { + panic("should have 2 bits set") + } + tx2.Rollback() + + for _, putme := range putmeValues { + mustAddR(others.Add(putme)) // should not change count, b/c putme already in the rbm + mustAddR(others.Add(putme + 1)) + mustAddR(others2.Add(putme + 2)) + } + mustAddR(others3.Add(4 << 16)) // outside the 2<<16 container + + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + err = tx.UnionInPlace(index, field, view, shard, others, others2, others3) + panicOn(err) + + // end game, check we got the union. + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + n = rbm.Count() + if n != 7 { + panic("should have a total 3 + 3 +1 = 7 bits set on the containers") + } +} + +func TestLMDB_RoaringBitmap(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_RoaringBitmap") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + expected := uint64(3) + putme := expected + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + + slc := rbm.Slice() + if slc[0] != uint64(expected) { + panic(fmt.Sprintf("should have gotten %v back", expected)) + } +} + +// no reverse iterator on LMDB; we did a special case for Max +// rather than a general purpose reverse iterator which we +// aren't using for anything else. +//func TestLMDB_reverse_badger_iterator_and_prefix_valid(t *testing.T) +//func TestLMDB_just_reverse_badger_iterator_and_prefix_valid(t *testing.T) + +func TestLMDB_ImportRoaringBits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ImportRoaringBits") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + tx.DeleteEmptyContainer = true // traditional badger Tx behavior, but not Roaring. + + //bitvalue := uint64(42) + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the union in place with the same set gives no change. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != 0 { + panic(fmt.Sprintf("should have not changed any bits on the second import, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the clear path + clear = true + + for _, v := range bits { + // clear 1 bit at a time + data := getTestBitmapAsRawRoaring(v) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != 1 { + panic(fmt.Sprintf("should have changed 1 bit: '%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + } + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 0 { + panic(fmt.Sprintf("n = %v not zero so the clearbits didn't happen!", n)) + } + allkeys := stringifiedLMDBKeysTx(tx) + + // should have no keys + if allkeys != "" { + panic("badger should have no keys now") + } +} + +func TestLMDB_ImportRoaringBits_set_nonoverlapping_bits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ImportRoaringBits_set_nonoverlapping_bits") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd, overlapping set and set them. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize, nil) + _ = rowSet + if changed != 4 { + panic(fmt.Sprintf("should have changed 2 bits: the 1 and the 3, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) +} + +func TestLMDB_ImportRoaringBits_clear_nonoverlapping_bits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_ImportRoaringBits_clear_nonoverlapping_bits") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + defer tx.Rollback() + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} //, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd overlapping set and clear them. + clear = true + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize, nil) + _ = rowSet + if changed != 2 { + panic(fmt.Sprintf("should have changed 1 bit: the 2, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 2 { // just the 0 and the 1<<16 bits should be left set. + panic(fmt.Sprintf("n = %v not 2 so the clearbits didn't happen!", n)) + } + +} + +func TestLMDB_DeleteIndex(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_DeleteIndex") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + bitvalue := uint64(777) + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + for _, v := range bits { + changed, err := tx.Add(index, field, view, shard, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + + index2 := "i2" // should not be deleted, even though it shares a prefix with 'i' + changed, err := tx.Add(index2, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + exists, err := tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!! on index2") + } + err = tx.Commit() + panicOn(err) + + // end of setup + err = dbwrap.DeleteIndex(index) + panicOn(err) + + tx = dbwrap.NewLMDBTx(!writable, index2) + defer tx.Rollback() + exists, err = tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("after delete of '%v', another index '%v' was gone too?!?", index, index2)) + } + + for _, v := range bits { + exists, err = tx.Contains(index, field, view, shard, v) + panicOn(err) + if exists { + allkeys := stringifiedLMDBKeysTx(tx) + panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) + } + } +} + +func TestLMDB_DeleteIndex_over100k(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_DeleteIndex_over100k") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewLMDBTx(writable, index) + bitvalue := uint64(777) + limit := uint64(100002) // default batch size in DeleteIndex is 100k keys per delete transaction. + //limit := uint64(101) + for v := uint64(1); v < limit; v++ { + // shift by << 16 to get into a different shard + changed, err := tx.Add(index, field, view, shard, doBatched, v<<16) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + if v%100000 == 0 { + panicOn(tx.Commit()) + tx = dbwrap.NewLMDBTx(writable, index) + } + } + + index2 := "i2" // should not be deleted, even though it shares a prefix with 'i' + changed, err := tx.Add(index2, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + err = tx.Commit() + panicOn(err) + + // end of setup + err = dbwrap.DeleteIndex(index) + panicOn(err) + + tx = dbwrap.NewLMDBTx(!writable, index2) + defer tx.Rollback() + exists, err := tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("after delete of '%v', another index '%v' was gone too?!?", index, index2)) + } + + for v := uint64(0); v < limit; v++ { + exists, err = tx.Contains(index, field, view, shard, v<<16) + panicOn(err) + if exists { + allkeys := stringifiedLMDBKeysTx(tx) + panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) + } + } +} + +func TestLMDB_SliceOfShards(t *testing.T) { + + dbwrap, clean := mustOpenEmptyLMDBWrapper("TestLMDB_SliceOfShards") + defer clean() + defer dbwrap.Close() + index, field, view := "i", "f", "v" + shards := []uint64{0, 1, 2, 3, 1000001, 2000001} + putme := uint64(179) + for _, shard := range shards { + LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + } + tx := dbwrap.NewLMDBTx(!writable, index) + defer tx.Rollback() + + slc, err := tx.SliceOfShards(index, field, view, "") + panicOn(err) + for i := range shards { + if shards[i] != slc[i] { + panic(fmt.Sprintf("expected at i=%v that slc[i]=%v = shards[i]=%v", i, slc[i], shards[i])) + } + } +} diff --git a/lmdb/txpool.go b/lmdb/txpool.go new file mode 100644 index 000000000..492e8603c --- /dev/null +++ b/lmdb/txpool.go @@ -0,0 +1,524 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build skip_building_lmdb_for_now + +package pilosa + +import ( + "fmt" + "io" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// poolTx directs all Tx calls to a pre-made +// goroutine pool that are setup to do +// LMDB operations safely. Each of these +// goroutines has had runtime.LockOSThread() +// called, and we serialize write Tx onto +// a single writer goroutine. +type poolTx struct { + w *LMDBWrapper + b *LMDBTx +} + +var _ = (*LMDBWrapper).newPoolTx // happy linter + +func (w *LMDBWrapper) newPoolTx(write bool, initialIndexName string) (ptx *poolTx) { + + var tx *LMDBTx + + job := newLMDBJob(write, func(j *lmdbJob) { + tx = w.NewLMDBTx(write, initialIndexName) + }) + if suberr := w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + <-job.done + + return &poolTx{ + w: w, + b: tx, + } +} + +var _ Tx = (*poolTx)(nil) + +func (c *poolTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + c.b.IncrementOpN(index, field, view, shard, changedN) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done +} + +func (c *poolTx) NewTxIterator(index, field, view string, shard uint64) (rit *roaring.Iterator) { + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + rit = c.b.NewTxIterator(index, field, view, shard) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ImportRoaringBits() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + changed, rowSet, err = c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Dump() { + c.b.Dump() +} + +func (c *poolTx) Readonly() bool { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Readonly() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Readonly() +} + +func (tx *poolTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +func (c *poolTx) Rollback() { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + c.b.Rollback() + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done +} + +func (c *poolTx) Commit() (err error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Commit() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.Commit() + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) RoaringBitmap(index, field, view string, shard uint64) (rbm *roaring.Bitmap, err error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RoaringBitmap() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + rbm, err = c.b.RoaringBitmap(index, field, view, shard) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Container(index, field, view string, shard uint64, key uint64) (ct *roaring.Container, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Container() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + ct, err = c.b.Container(index, field, view, shard, key) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) (err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see PutContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.PutContainer(index, field, view, shard, key, rc) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) RemoveContainer(index, field, view string, shard uint64, key uint64) (err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RemoveContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.RemoveContainer(index, field, view, shard, key) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) UseRowCache() bool { + return c.b.UseRowCache() +} + +func (c *poolTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Add() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + changeCount, err = c.b.Add(index, field, view, shard, batched, a...) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Remove() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + changeCount, err = c.b.Remove(index, field, view, shard, a...) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Contains() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + exists, err = c.b.Contains(index, field, view, shard, key) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ContainerIterator() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + citer, found, err = c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) (err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEach() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.ForEach(index, field, view, shard, fn) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) (err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEachRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.ForEachRange(index, field, view, shard, start, end, fn) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Count(index, field, view string, shard uint64) (n uint64, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Count() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + n, err = c.b.Count(index, field, view, shard) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Max(index, field, view string, shard uint64) (n uint64, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Max() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + n, err = c.b.Max(index, field, view, shard) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Min(index, field, view string, shard uint64) (m uint64, found bool, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Min() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + m, found, err = c.b.Min(index, field, view, shard) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) (err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see UnionInPlace() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + err = c.b.UnionInPlace(index, field, view, shard, others...) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see CountRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + n, err = c.b.CountRange(index, field, view, shard, start, end) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) OffsetRange(index, field, view string, shard, offset, start, end uint64) (other *roaring.Bitmap, err error) { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see OffsetRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + other, err = c.b.OffsetRange(index, field, view, shard, offset, start, end) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RoaringBitmapReader() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + r, sz, err = c.b.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} + +func (c *poolTx) Type() string { + return c.b.Type() +} + +func (c *poolTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see SliceOfShards() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + job := newLMDBJob(c.b.write, func(j *lmdbJob) { + sliceOfShards, err = c.b.SliceOfShards(index, field, view, optionalViewPath) + }) + if suberr := c.w.submit(job); suberr != nil { + AlwaysPrintf("submit job saw err '%v'", suberr) + return + } + + <-job.done + return +} diff --git a/mmap_test.go b/mmap_test.go index ab9643c73..28f00c81a 100644 --- a/mmap_test.go +++ b/mmap_test.go @@ -86,7 +86,8 @@ func forceSnapshotsCheckMapping(t *testing.T) { // in newGeneration in generation.go. So this is probably useless but it's // a failure mode we've been bitten by once... func TestMmapBehavior(t *testing.T) { - skipForRBF(t) + // rbf and lmdb not happy with this test. + roaringOnlyTest(t) var changed bool var original uint64 diff --git a/pprof.go b/pprof.go new file mode 100644 index 000000000..92a844db5 --- /dev/null +++ b/pprof.go @@ -0,0 +1,47 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "os" + "time" + + _ "net/http/pprof" // Imported for its side-effect of registering pprof endpoints with the server. + "runtime/pprof" +) + +func CPUProfileForDur(dur time.Duration, outpath string) { + + // per-query pprof output: + txsrc := os.Getenv("PILOSA_TXSRC") + if txsrc == "" { + txsrc = "roaring" + } + path := outpath + "." + txsrc + f, err := os.Create(path) + panicOn(err) + + if dur == 0 { + dur = time.Hour + } + vv("starting cpu profile for dur '%v', output to '%v'", dur, path) + _ = pprof.StartCPUProfile(f) + go func() { + <-time.After(dur) + pprof.StopCPUProfile() + f.Close() + vv("stopping cpu profile after dur '%v', output: '%v'", dur, path) + }() +} diff --git a/rbf.go b/rbf.go new file mode 100644 index 000000000..db85ef0fd --- /dev/null +++ b/rbf.go @@ -0,0 +1,380 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "sync" + + "github.com/pilosa/pilosa/v2/rbf" + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/txpath" + "github.com/pkg/errors" +) + +// RbfDBWrapper wraps an *rbf.DB +type RbfDBWrapper struct { + Path string + db *rbf.DB + reg *rbfDBRegistrar + muDb sync.Mutex + + // make Close() idempotent, avoiding panic on double Close() + closed bool + + //DeleteEmptyContainer bool // needed for roaring compat? +} + +// rbfDBRegistrar also allows opening the same path twice to +// result in sharing the same open database handle, and +// thus the same transactional guarantees. +// +type rbfDBRegistrar struct { + mu sync.Mutex + mp map[*RbfDBWrapper]bool + + path2db map[string]*RbfDBWrapper +} + +var globalRbfDBReg *rbfDBRegistrar = newRbfDBRegistrar() + +func newRbfDBRegistrar() *rbfDBRegistrar { + return &rbfDBRegistrar{ + mp: make(map[*RbfDBWrapper]bool), + path2db: make(map[string]*RbfDBWrapper), + } +} + +// register each rbf.DB created, so we dedup and can +// can clean them up. This is called by openRbfDB() while +// holding the r.mu.Lock, since it needs to atomically +// check the registry and make a new instance only +// if one does not exist for its path, and otherwise +// return the existing instance. +func (r *rbfDBRegistrar) unprotectedRegister(w *RbfDBWrapper) { + r.mp[w] = true + r.path2db[w.Path] = w +} + +// unregister removes w from r +func (r *rbfDBRegistrar) unregister(w *RbfDBWrapper) { + r.mu.Lock() + delete(r.mp, w) + delete(r.path2db, w.Path) + r.mu.Unlock() +} + +// rbfPath is a helper for determining the full directory +// in which the RBF database will be stored. +func rbfPath(path string) string { + if !strings.HasSuffix(path, "-rbfdb") { + return path + "-rbfdb" + } + return path +} + +// openRbfDB opens the database in the path directoy +// without deleting any prior content. Any +// database directory will have the "-rbfdb" suffix. +// +// openRbfDB will check the registry and make a new instance only +// if one does not exist for its path. Otherwise it returns +// the existing instance. This insures only one RbfDBWrapper +// per bpath in this pilosa node. +func (r *rbfDBRegistrar) openRbfDB(path0 string) (*RbfDBWrapper, error) { + path := rbfPath(path0) + r.mu.Lock() + defer r.mu.Unlock() + w, ok := r.path2db[path] + if ok { + // creates the effect of having only one DB open per pilosa node. + return w, nil + } + db := rbf.NewDB(path) + + w = &RbfDBWrapper{ + reg: r, + Path: path, + db: db, + } + + r.unprotectedRegister(w) + + err := db.Open() + if err != nil { + panic(fmt.Sprintf("cannot open rbfDB at path '%v': '%v'", path, err)) + } + return w, nil +} + +type RBFTx struct { + // initialIndex is only a debugging aid. Transactions + // can cross indexes. It can be left empty without consequence. + initialIndex string + frag *fragment + tx *rbf.Tx +} + +func (tx *RBFTx) DBPath() string { + return tx.tx.DBPath() +} + +func (tx *RBFTx) Type() string { + return RBFTxn +} + +func (tx *RBFTx) Rollback() { + tx.tx.Rollback() +} + +func (tx *RBFTx) Commit() error { + return tx.tx.Commit() +} + +func (tx *RBFTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + return tx.tx.RoaringBitmap(rbfName(index, field, view, shard)) +} + +func (tx *RBFTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { + return tx.tx.Container(rbfName(index, field, view, shard), key) +} + +func (tx *RBFTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { + return tx.tx.PutContainer(rbfName(index, field, view, shard), key, c) +} + +func (tx *RBFTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + return tx.tx.RemoveContainer(rbfName(index, field, view, shard), key) +} + +func (tx *RBFTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + return tx.tx.Add(rbfName(index, field, view, shard), a...) +} + +func (tx *RBFTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + return tx.tx.Remove(rbfName(index, field, view, shard), a...) +} + +func (tx *RBFTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { + return tx.tx.Contains(rbfName(index, field, view, shard), v) +} + +func (tx *RBFTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { + return tx.tx.ContainerIterator(rbfName(index, field, view, shard), key) +} + +func (tx *RBFTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + return tx.tx.ForEach(rbfName(index, field, view, shard), fn) +} + +func (tx *RBFTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + return tx.tx.ForEachRange(rbfName(index, field, view, shard), start, end, fn) +} + +func (tx *RBFTx) Count(index, field, view string, shard uint64) (uint64, error) { + return tx.tx.Count(rbfName(index, field, view, shard)) +} + +func (tx *RBFTx) Max(index, field, view string, shard uint64) (uint64, error) { + return tx.tx.Max(rbfName(index, field, view, shard)) +} + +func (tx *RBFTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + return tx.tx.Min(rbfName(index, field, view, shard)) +} + +func (tx *RBFTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + return tx.tx.UnionInPlace(rbfName(index, field, view, shard), others...) +} + +// CountRange returns the count of hot bits in the start, end range on the fragment. +// roaring.countRange counts the number of bits set between [start, end). +func (tx *RBFTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + if tx.frag == nil { + return tx.tx.CountRange(rbfName(index, field, view, shard), start, end) + } + + // For speed, exploit the fact that on startup the rowCache will + // have already loaded fragments. + rowID := start / ShardWidth + row, err := tx.frag.unprotectedRow(tx, rowID) + if err != nil { + return 0, err + } + return row.Count(), nil +} + +func (tx *RBFTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { + return tx.tx.OffsetRange(rbfName(index, field, view, shard), offset, start, end) +} + +func (tx *RBFTx) IncrementOpN(index, field, view string, shard uint64, changedN int) {} + +func (tx *RBFTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { + return tx.tx.ImportRoaringBits(rbfName(index, field, view, shard), rit, clear, log, rowSize, data) +} + +func (tx *RBFTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader RoaringBitmap") + } + var buf bytes.Buffer + sz, err = rbm.WriteTo(&buf) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader rbm.WriteTo(buf)") + } + return ioutil.NopCloser(&buf), sz, err +} + +func (tx *RBFTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + + prefix := string(txpath.AllShardPrefix(index, field, view)) + + names, err := tx.tx.BitmapNames() + if err != nil { + return nil, err + } + + // Iterate over shard names and collect shards from matching field/view prefix. + for _, name := range names { + if !strings.HasPrefix(name, prefix) { + continue + } + shard := txpath.ShardFromPrefix([]byte(name)) + sliceOfShards = append(sliceOfShards, shard) + } + return sliceOfShards, nil +} + +func (tx *RBFTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + b, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + return b.Iterator() +} + +func (tx *RBFTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +func (tx *RBFTx) Dump() { + tx.tx.Dump() +} + +// Readonly is true if the transaction is not read-and-write, but only doing reads. +func (tx *RBFTx) Readonly() bool { + return !tx.tx.Writable() +} + +func (tx *RBFTx) UseRowCache() bool { + // since RFB returns memory mapped data, we can't use + // the rowCache without first making a copy. + // So we only use the rowCache if the copy is + // enabled. + return rbf.EnableRowCache +} + +// rbfName returns a NULL-separated key used for identifying bitmap maps in RBF. +func rbfName(index, field, view string, shard uint64) string { + //return fmt.Sprintf("%s\x00%s\x00%s\x00%d", index, field, view, shard) + return string(txpath.Prefix(index, field, view, shard)) +} + +// rbfFieldPrefix returns a prefix for field keys in RBF. +func rbfFieldPrefix(index, field string) string { + //return fmt.Sprintf("%s\x00%s\x00", index, field) + return string(txpath.FieldPrefix(index, field)) +} + +func (w *RbfDBWrapper) DeleteField(index, field, fieldPath string) error { + w.muDb.Lock() + defer w.muDb.Unlock() + + if err := os.RemoveAll(fieldPath); err != nil { + return errors.Wrap(err, "removing directory") + } + + tx, err := w.db.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + if err := tx.DeleteBitmapsWithPrefix(rbfFieldPrefix(index, field)); err != nil { + return err + } + return tx.Commit() +} + +func (w *RbfDBWrapper) DeleteIndex(indexName string) error { + + if strings.Contains(indexName, "'") { + return fmt.Errorf("error: bad indexName `%v` in RbfDBWrapper.DeleteIndex() call: indexName cannot contain apostrophes/single quotes.", indexName) + } + prefix := txpath.IndexOnlyPrefix(indexName) + + w.muDb.Lock() + defer w.muDb.Unlock() + + tx, err := w.db.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + if err := tx.DeleteBitmapsWithPrefix(string(prefix)); err != nil { + return err + } + return tx.Commit() +} + +func (w *RbfDBWrapper) Close() error { + w.muDb.Lock() + defer w.muDb.Unlock() + if !w.closed { + w.reg.unregister(w) + w.closed = true + } + return w.db.Close() +} + +func (w *RbfDBWrapper) NewRBFTx(write bool, initialIndex string, frag *fragment) (*RBFTx, error) { + tx, err := w.db.Begin(write) + if err != nil { + return nil, err + } + return &RBFTx{tx: tx, initialIndex: initialIndex, frag: frag}, nil +} + +func (w *RbfDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag *fragment) error { + tx, err := w.db.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + err = tx.DeleteBitmapsWithPrefix(rbfName(index, field, view, shard)) + if err != nil { + return err + } + return tx.Commit() +} diff --git a/rbf/cursor.go b/rbf/cursor.go index 2bc7c9b7d..5045565d2 100644 --- a/rbf/cursor.go +++ b/rbf/cursor.go @@ -371,7 +371,8 @@ func (c *Cursor) putLeafCell(in leafCell) (err error) { // Split into multiple pages if page size is exceeded. groups := [][]leafCell{cells} - if leafCellsPageSize(cells) >= PageSize { + sz := leafCellsPageSize(cells) + if sz >= PageSize { groups = splitLeafCells(cells) } @@ -674,6 +675,9 @@ func splitLeafCells(cells []leafCell) [][]leafCell { var dataSize int for _, cell := range cells { + if cell.Type == ContainerTypeBitmap { + panic("no! all ContainerTypeBitmap should be ContainerTypeBitmapPtr by now") + } // Determine number of cells on current slice & cell size. cellN := len(slices[len(slices)-1]) sz := align8(leafCellHeaderSize + len(cell.Data)) @@ -823,7 +827,7 @@ func (c *Cursor) Seek(key uint64) (exact bool, err error) { switch typ := readFlags(buf); typ { case PageTypeBranch: n := readCellN(buf) - index, ok := search(n, func(i int) int { + index, xact := search(n, func(i int) int { if v := readBranchCellKey(buf, i); key == v { return 0 } else if key < v { @@ -831,8 +835,8 @@ func (c *Cursor) Seek(key uint64) (exact bool, err error) { } return 1 }) - //if not found (ok) the cell - if !ok && index > 0 { + //if not found (xact) the cell + if !xact && index > 0 { index-- } elem.index = index @@ -848,7 +852,7 @@ func (c *Cursor) Seek(key uint64) (exact bool, err error) { case PageTypeLeaf: n := readCellN(buf) - index, ok := search(n, func(i int) int { + index, xact := search(n, func(i int) int { if v := readLeafCellKey(buf, i); key == v { return 0 } else if key < v { @@ -858,7 +862,7 @@ func (c *Cursor) Seek(key uint64) (exact bool, err error) { }) elem.index = index c.leafPage = buf - return ok, nil + return xact, nil default: return false, fmt.Errorf("rbf.Cursor.Seek(): invalid page type: pgno=%d type=%d", elem.pgno, typ) @@ -1132,6 +1136,7 @@ func ConvertToLeafArgs(key uint64, c *roaring.Container) (result leafCell) { roaring.ConvertRunToBitmap(c) result.Type = ContainerTypeBitmap result.Data = fromArray64(roaring.AsBitmap(c)) + return } result.N = len(r) //note RBF N is number of containers result.Type = ContainerTypeRLE diff --git a/rbf/cursorx.go b/rbf/cursorx.go index 54febc5b0..381b4e6ff 100644 --- a/rbf/cursorx.go +++ b/rbf/cursorx.go @@ -24,6 +24,10 @@ import ( "github.com/pkg/errors" ) +// if enableRowCache, then we must not return mmap-ed memory +// directly, but only a copy. +const EnableRowCache = true + //probably should just implement the container interface // but for now i'll do it func (c *Cursor) Rows() ([]uint64, error) { @@ -53,7 +57,7 @@ func (c *Cursor) Rows() ([]uint64, error) { return rows, err } func (tx *Tx) FieldViews() []string { - r, _ := tx.rootRecords() + r, _ := tx.RootRecords() res := make([]string, len(r)) for i := range r { res[i] = r[i].Name @@ -140,16 +144,33 @@ func (c *Cursor) CurrentPageType() int { } func toContainer(l leafCell, tx *Tx) *roaring.Container { + + orig := l.Data + var cpMaybe []byte + if EnableRowCache { + // make a copy, otherwise the rowCache will see corrupted data + // or mmapped data that may disappear. + cpMaybe = make([]byte, len(orig)) + copy(cpMaybe, orig) + } else { + // not a copy + cpMaybe = orig + } switch l.Type { case ContainerTypeArray: - return roaring.NewContainerArray(toArray16(l.Data)) + return roaring.NewContainerArray(toArray16(cpMaybe)) case ContainerTypeBitmapPtr: - _, bm, _ := tx.leafCellBitmap(toPgno(l.Data)) - return roaring.NewContainerBitmap(l.N, bm) + _, bm, _ := tx.leafCellBitmap(toPgno(cpMaybe)) + cloneMaybe := bm + if EnableRowCache { + cloneMaybe = make([]uint64, len(bm)) + copy(cloneMaybe, bm) + } + return roaring.NewContainerBitmap(l.N, cloneMaybe) case ContainerTypeBitmap: - return roaring.NewContainerBitmap(l.N, toArray64(l.Data)) + return roaring.NewContainerBitmap(l.N, toArray64(cpMaybe)) case ContainerTypeRLE: - return roaring.NewContainerRun(toInterval16(l.Data)) + return roaring.NewContainerRun(toInterval16(cpMaybe)) } return nil } diff --git a/rbf/db.go b/rbf/db.go index b6eeda6b5..b406244ec 100644 --- a/rbf/db.go +++ b/rbf/db.go @@ -39,12 +39,13 @@ const ( ) type DB struct { - data []byte // mmap data - file *os.File // file descriptor - segments []*WALSegment // write-ahead log - pageMap *immutable.Map // pgno-to-WALID mapping - txs map[*Tx]struct{} // active transactions - opened bool // true if open + data []byte // mmap data + file *os.File // file descriptor + segments []*WALSegment // write-ahead log + rootRecords []*RootRecord // cached root records + pageMap *immutable.Map // pgno-to-WALID mapping + txs map[*Tx]struct{} // active transactions + opened bool // true if open mu sync.RWMutex // general mutex rwmu sync.Mutex // mutex for restricting single writer @@ -58,12 +59,13 @@ type DB struct { // NewDB returns a new instance of DB. func NewDB(path string) *DB { - return &DB{ + db := &DB{ txs: make(map[*Tx]struct{}), pageMap: immutable.NewMap(&uint32Hasher{}), Path: path, MaxSize: DefaultMaxSize, } + return db } // DataPath returns the path to the data file for the DB. @@ -73,10 +75,12 @@ func (db *DB) DataPath() string { // WALPath returns the path to the WAL directory. func (db *DB) WALPath() string { + return filepath.Join(db.Path, "wal") } func CreateDirIfNotExist(path string) { + dir := filepath.Dir(path) if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.MkdirAll(dir, 0755) @@ -89,6 +93,7 @@ func CreateDirIfNotExist(path string) { // Open opens a database with the file specified in Path. // Creates a new file if one does not already exist. func (db *DB) Open() (err error) { + db.mu.Lock() defer db.mu.Unlock() @@ -137,6 +142,7 @@ func (db *DB) Open() (err error) { } func (db *DB) openWALSegments() error { + fis, err := ioutil.ReadDir(db.WALPath()) if err != nil { return fmt.Errorf("read dir: %w", err) @@ -170,6 +176,7 @@ func (db *DB) openWALSegments() error { // only copy pages that aren't in use by an active transaction. The page map // is rebuilt as well for all WAL pages still in use. func (db *DB) checkpoint() error { + if !db.opened { return nil } @@ -257,8 +264,11 @@ func (db *DB) checkpoint() error { break } + segpath := segment.Path() if err := segment.Close(); err != nil { return err + } else if err := os.Remove(segpath); err != nil { + return err } db.segments, db.segments[0] = db.segments[1:], nil } @@ -269,6 +279,7 @@ func (db *DB) checkpoint() error { } func (db *DB) findNextWALMetaPage(walID int64) (metaWALID int64, metaFlags uint32, err error) { + maxWALID := db.maxWALID() for ; walID <= maxWALID; walID++ { @@ -292,6 +303,7 @@ func (db *DB) findNextWALMetaPage(walID int64) (metaWALID int64, metaFlags uint3 // minActiveWALID returns the lowest WAL ID in use by any active transaction. // Returns 0 if no transactions are active. func (db *DB) minActiveWALID() int64 { + var walID int64 for tx := range db.txs { if walID == 0 || walID > tx.walID { @@ -303,12 +315,14 @@ func (db *DB) minActiveWALID() int64 { // ActiveWALSegment returns the most recent WAL segment. func (db *DB) ActiveWALSegment() *WALSegment { + db.mu.RLock() defer db.mu.RUnlock() return db.activeWALSegment() } func (db *DB) activeWALSegment() *WALSegment { + if len(db.segments) == 0 { return nil } @@ -317,12 +331,14 @@ func (db *DB) activeWALSegment() *WALSegment { // MinWALID returns the lowest WAL ID available in the WAL. func (db *DB) MinWALID() int64 { + db.mu.RLock() defer db.mu.RUnlock() return db.minWALID() } func (db *DB) minWALID() int64 { + if len(db.segments) == 0 { return 0 } @@ -331,12 +347,14 @@ func (db *DB) minWALID() int64 { // MaxWALID returns the highest WAL ID available in the WAL. func (db *DB) MaxWALID() int64 { + db.mu.RLock() defer db.mu.RUnlock() return db.maxWALID() } func (db *DB) maxWALID() int64 { + if len(db.segments) == 0 { return 0 } @@ -346,6 +364,7 @@ func (db *DB) maxWALID() int64 { // WALPageN returns the number of pages across all segments. func (db *DB) WALPageN() int64 { + db.mu.RLock() defer db.mu.RUnlock() @@ -358,6 +377,7 @@ func (db *DB) WALPageN() int64 { // SyncWAL flushes the active segment to disk. func (db *DB) SyncWAL() error { + if s := db.ActiveWALSegment(); s != nil { return s.Sync() } @@ -366,6 +386,8 @@ func (db *DB) SyncWAL() error { // readWALPage reads a single page at the given WAL ID. func (db *DB) readWALPage(walID int64) ([]byte, error) { + // + // TODO(BBJ): Binary search for segment. for _, s := range db.segments { if walID >= s.MinWALID() && walID <= s.MaxWALID() { @@ -376,6 +398,7 @@ func (db *DB) readWALPage(walID int64) ([]byte, error) { } func (db *DB) writeWALPage(page []byte, isMeta bool) (walID int64, err error) { + if err := db.ensureWritableWALSegment(); err != nil { return 0, err } @@ -383,6 +406,7 @@ func (db *DB) writeWALPage(page []byte, isMeta bool) (walID int64, err error) { } func (db *DB) writeBitmapPage(pgno uint32, page []byte) (walID int64, err error) { + if err := db.ensureWritableWALSegment(); err != nil { return 0, err } @@ -401,6 +425,7 @@ func (db *DB) writeBitmapPage(pgno uint32, page []byte) (walID int64, err error) } func (db *DB) ensureWritableWALSegment() error { + if s := db.activeWALSegment(); s != nil && s.Size() < MaxWALSegmentFileSize { return nil } @@ -409,6 +434,7 @@ func (db *DB) ensureWritableWALSegment() error { // addWALSegment appends a new, writable segment and closing an existing segments for write. func (db *DB) addWALSegment() error { + // Close previous last segment for writes. base := int64(1) if s := db.activeWALSegment(); s != nil { @@ -430,6 +456,7 @@ func (db *DB) addWALSegment() error { // Close closes the database. func (db *DB) Close() (err error) { + // TODO(bbj): Add wait group to hang until last Tx is complete. // Wait for writer lock. @@ -466,6 +493,7 @@ func (db *DB) Close() (err error) { // closeWALSegments closes the WAL and all its segments. func (db *DB) closeWALSegments() (err error) { + for _, s := range db.segments { if e := s.Close(); e != nil && err == nil { err = e @@ -476,6 +504,7 @@ func (db *DB) closeWALSegments() (err error) { // Size returns the size of the database & WAL, in bytes. func (db *DB) Size() (int64, error) { + db.mu.RLock() defer db.mu.RUnlock() @@ -488,12 +517,14 @@ func (db *DB) Size() (int64, error) { // WALSize returns the size of all WAL segments, in bytes. func (db *DB) WALSize() int64 { + db.mu.RLock() defer db.mu.RUnlock() return db.walSize() } func (db *DB) walSize() int64 { + var sz int64 for _, s := range db.segments { sz += s.Size() @@ -504,6 +535,7 @@ func (db *DB) walSize() int64 { // WALSegments returns the WAL segments currently on the DB. // This should only be used for debugging & testing purposes. func (db *DB) WALSegments() []*WALSegment { + db.mu.RLock() defer db.mu.RUnlock() return db.segments @@ -511,6 +543,7 @@ func (db *DB) WALSegments() []*WALSegment { // init initializes a new database file. func (db *DB) init() error { + if err := db.initMetaPage(); err != nil { return fmt.Errorf("meta: %w", err) } else if err := db.initRootRecordPage(); err != nil { @@ -523,6 +556,7 @@ func (db *DB) init() error { // initMetaPage initializes the meta page. func (db *DB) initMetaPage() error { + page := make([]byte, PageSize) writeMetaMagic(page) writeMetaPageN(page, 3) @@ -534,6 +568,7 @@ func (db *DB) initMetaPage() error { // initRootRecordPage initializes the initial root record page. func (db *DB) initRootRecordPage() error { + page := make([]byte, PageSize) writePageNo(page, 1) writeFlags(page, PageTypeRootRecord) @@ -543,6 +578,7 @@ func (db *DB) initRootRecordPage() error { // initFreelistPage initializes the initial freelist btree page. func (db *DB) initFreelistPage() error { + page := make([]byte, PageSize) writePageNo(page, 2) writeFlags(page, PageTypeLeaf) @@ -552,6 +588,7 @@ func (db *DB) initFreelistPage() error { // Begin starts a new transaction. func (db *DB) Begin(writable bool) (_ *Tx, err error) { + // TODO(BBJ): Acquire write lock if writable. // Ensure only one writable transaction at a time. @@ -566,7 +603,7 @@ func (db *DB) Begin(writable bool) (_ *Tx, err error) { return nil, ErrClosed } - tx := &Tx{db: db, pageMap: db.pageMap, writable: writable} + tx := &Tx{db: db, rootRecords: db.rootRecords, pageMap: db.pageMap, writable: writable} // Copy meta page into transaction's buffer. // This page is only written at the end of a dirty transaction. @@ -614,6 +651,7 @@ func (db *DB) removeTx(tx *Tx) error { // Check performs an integrity check. func (db *DB) Check() error { + tx, err := db.Begin(false) if err != nil { return err @@ -624,6 +662,7 @@ func (db *DB) Check() error { // writePage writes a page to the data file. func (db *DB) writePage(pgno uint32, page []byte) error { + _, err := db.file.WriteAt(page, int64(pgno)*PageSize) return err } diff --git a/rbf/rbf.go b/rbf/rbf.go index e92fa7f38..728f530dc 100644 --- a/rbf/rbf.go +++ b/rbf/rbf.go @@ -351,7 +351,7 @@ func (c *leafCell) Values(tx *Tx) []uint16 { } // firstValue the first value from the container. -func (c *leafCell) firstValue() uint16 { +func (c *leafCell) firstValue(tx *Tx) uint16 { switch c.Type { case ContainerTypeArray: a := toArray16(c.Data) @@ -360,7 +360,9 @@ func (c *leafCell) firstValue() uint16 { r := toInterval16(c.Data) return r[0].Start case ContainerTypeBitmapPtr: - for i, v := range toArray64(c.Data) { + _, slc, err := tx.leafCellBitmap(toPgno(c.Data)) + panicOn(err) + for i, v := range slc { for j := uint(0); j < 64; j++ { if v&(1<= 0; i-- { + for j := 63; j >= 0; j-- { + if a[i]&(1<= 0; i-- { - for j := 63; j >= 0; j-- { - if a[i]&(1< page size %d", offset, 16+len(cell.Data), PageSize) + assert(offset+16+len(cell.Data) <= PageSize, "leaf cell write extends beyond page: offset %d + len(cell.Data)(%v) + 16 == %v > page size %d", offset, len(cell.Data), offset+16+len(cell.Data), PageSize) copy(page[offset+16:], cell.Data) } diff --git a/rbf/tx.go b/rbf/tx.go index 6083493d7..450ecb815 100644 --- a/rbf/tx.go +++ b/rbf/tx.go @@ -14,12 +14,11 @@ package rbf import ( - "bytes" "fmt" "io" "math" "sort" - "strconv" + //"strconv" "strings" "sync" @@ -29,13 +28,14 @@ import ( // Tx represents a transaction. type Tx struct { - mu sync.RWMutex - db *DB // parent db - meta [PageSize]byte // copy of current meta page - walID int64 // max WAL ID at start of tx - pageMap *immutable.Map // mapping of database pages to WAL IDs - writable bool // if true, tx can write - dirty bool // if true, changes have been made + mu sync.RWMutex + db *DB // parent db + meta [PageSize]byte // copy of current meta page + walID int64 // max WAL ID at start of tx + rootRecords []*RootRecord // read-only cache of root records + pageMap *immutable.Map // mapping of database pages to WAL IDs + writable bool // if true, tx can write + dirty bool // if true, changes have been made // If Rollback() has already completed, don't do it again. // Note db == nil means that commit has already been done. @@ -47,6 +47,10 @@ type Tx struct { DeleteEmptyContainer bool } +func (tx *Tx) DBPath() string { + return tx.db.Path +} + // Writable returns true if the transaction can mutate data. func (tx *Tx) Writable() bool { return tx.writable @@ -69,7 +73,17 @@ func (tx *Tx) Commit() error { } else if err := tx.db.SyncWAL(); err != nil { return err } + + // future plan: after checkpoint is moved to background + // or not every removeTx, then we can move the + // tx.db.rootRecords = tx.rootRecords into removeTx(). + + // avoid race detector firing on a write race here + // vs the read of rootRecords at db.Begin() + tx.db.mu.Lock() + tx.db.rootRecords = tx.rootRecords tx.db.pageMap = tx.pageMap + tx.db.mu.Unlock() } if err := tx.db.checkpoint(); err != nil { @@ -83,6 +97,7 @@ func (tx *Tx) Commit() error { func (tx *Tx) Rollback() { tx.mu.Lock() defer tx.mu.Unlock() + // allow Rollback to be called more than once. if tx.rollbackDone { return @@ -118,7 +133,7 @@ func (tx *Tx) Root(name string) (uint32, error) { } func (tx *Tx) root(name string) (uint32, error) { - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return 0, err } @@ -140,7 +155,7 @@ func (tx *Tx) BitmapNames() ([]string, error) { } // Read list of root records. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return nil, err } @@ -162,8 +177,6 @@ func (tx *Tx) CreateBitmap(name string) error { } func (tx *Tx) createBitmap(name string) error { - //vv("createBitmap(name='%v'", name) - if tx.db == nil { return ErrTxClosed } else if !tx.writable { @@ -173,7 +186,7 @@ func (tx *Tx) createBitmap(name string) error { } // Read list of root records. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return err } @@ -252,7 +265,7 @@ func (tx *Tx) DeleteBitmap(name string) error { } // Read list of root records. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return err } @@ -274,7 +287,7 @@ func (tx *Tx) DeleteBitmap(name string) error { if err := tx.writeRootRecordPages(records); err != nil { return fmt.Errorf("write bitmaps: %w", err) } - + tx.rootRecords = records return nil } @@ -290,7 +303,7 @@ func (tx *Tx) DeleteBitmapsWithPrefix(prefix string) error { } // Read list of root records. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return err } @@ -317,7 +330,7 @@ func (tx *Tx) DeleteBitmapsWithPrefix(prefix string) error { if err := tx.writeRootRecordPages(records); err != nil { return fmt.Errorf("write bitmaps: %w", err) } - + tx.rootRecords = records return nil } @@ -336,7 +349,7 @@ func (tx *Tx) RenameBitmap(oldname, newname string) error { } // Read list of root records. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return err } @@ -356,8 +369,12 @@ func (tx *Tx) RenameBitmap(oldname, newname string) error { return nil } -// rootRecords returns a list of root records. -func (tx *Tx) rootRecords() ([]*RootRecord, error) { +// RootRecords returns a list of root records. +func (tx *Tx) RootRecords() (rr []*RootRecord, err error) { + if tx.rootRecords != nil { + return tx.rootRecords, nil + } + var records []*RootRecord for pgno := readMetaRootRecordPageNo(tx.meta[:]); pgno != 0; { page, err := tx.readPage(pgno) @@ -375,11 +392,15 @@ func (tx *Tx) rootRecords() ([]*RootRecord, error) { // Read next overflow page number. pgno = WalkRootRecordPages(page) } + + // Cache result + tx.rootRecords = records return records, nil } // writeRootRecordPages writes a list of root record pages. func (tx *Tx) writeRootRecordPages(records []*RootRecord) (err error) { + // Release all existing root record pages. for pgno := readMetaRootRecordPageNo(tx.meta[:]); pgno != 0; { page, err := tx.readPage(pgno) @@ -434,13 +455,14 @@ func (tx *Tx) writeRootRecordPages(records []*RootRecord) (err error) { } } + // Update cache records. + tx.rootRecords = records + return nil } // Add sets a given bit on the bitmap. func (tx *Tx) Add(name string, a ...uint64) (changeCount int, err error) { - //vv("rbf Tx.Add(a='%#v')", a) - tx.mu.Lock() defer tx.mu.Unlock() @@ -609,9 +631,10 @@ func (tx *Tx) PutContainer(name string, key uint64, ct *roaring.Container) error tx.mu.Lock() defer tx.mu.Unlock() - if ct.N() == 0 { - return nil + if tx.DeleteEmptyContainer && ct.N() == 0 { + return tx.RemoveContainer(name, key) } + cell := ConvertToLeafArgs(key, ct) if err := tx.createBitmapIfNotExists(name); err != nil { @@ -745,7 +768,7 @@ func (tx *Tx) inusePageSet() (map[uint32]struct{}, error) { } // Traverse every b-tree and mark pages as in-use. - records, err := tx.rootRecords() + records, err := tx.RootRecords() if err != nil { return m, err } @@ -826,7 +849,7 @@ func (tx *Tx) nextFreelistPageNo() (uint32, error) { } cell := c.cell() - v := cell.firstValue() + v := cell.firstValue(tx) pgno := uint32((cell.Key << 16) | uint64(v)) return pgno, nil @@ -870,7 +893,6 @@ func (tx *Tx) deallocateTree(pgno uint32) error { } func (tx *Tx) readPage(pgno uint32) ([]byte, error) { - // fmt.Println("readPage", pgno) // Meta page is always cached on the transaction. if pgno == 0 { return tx.meta[:], nil @@ -964,10 +986,11 @@ func (tx *Tx) ContainerIterator(name string, key uint64) (citer roaring.Containe // INVAR: c is not nil - if _, err := c.Seek(key); err != nil { + exact, err := c.Seek(key) + if err != nil { return nil, false, err } - return &containerIterator{cursor: c}, true, nil + return &containerIterator{cursor: c}, exact, nil } func (tx *Tx) ForEach(name string, fn func(i uint64) error) error { @@ -1084,7 +1107,7 @@ func (tx *Tx) Max(name string) (uint64, error) { } cell := c.cell() - return uint64((cell.Key << 16) | uint64(cell.lastValue())), nil + return uint64((cell.Key << 16) | uint64(cell.lastValue(tx))), nil } func (tx *Tx) Min(name string) (uint64, bool, error) { @@ -1103,11 +1126,28 @@ func (tx *Tx) Min(name string) (uint64, bool, error) { } cell := c.cell() - return uint64((cell.Key << 16) | uint64(cell.firstValue())), true, nil + return uint64((cell.Key << 16) | uint64(cell.firstValue(tx))), true, nil } func (tx *Tx) UnionInPlace(name string, others ...*roaring.Bitmap) error { - panic("TODO") + rbm, err := tx.RoaringBitmap(name) + panicOn(err) + + rbm.UnionInPlace(others...) + // iterate over the containers that changed within rbm, and write them back to disk. + + it, found := rbm.Containers.Iterator(0) + _ = found // don't care about the value of found, because first containerKey might be > 0 + + for it.Next() { + containerKey, rc := it.Value() + + // TODO: only write the changed ones back, as optimization? + // Compare to ImportRoaringBits. + err := tx.PutContainer(name, containerKey, rc) + panicOn(err) + } + return nil } // roaring.countRange counts the number of bits set between [start, end). @@ -1187,8 +1227,10 @@ func (tx *Tx) OffsetRange(name string, offset, start, endx uint64) (*roaring.Bit panic("range endx must not contain low bits") } - tx.mu.RLock() - defer tx.mu.RUnlock() + // need write lock here (not just read lock) b/c caching the tx.rootRecords = records + // is a write the race detector fires on. + tx.mu.Lock() + defer tx.mu.Unlock() c, err := tx.cursor(name) if err != nil { @@ -1261,15 +1303,15 @@ func (si *emptyContainerIterator) Value() (uint64, *roaring.Container) { panic("emptyContainerIterator never has any Values") } -func (tx *Tx) Dump(index string) { - fmt.Println(tx.DumpString(index)) +func (tx *Tx) Dump() { + fmt.Println(tx.DumpString()) } -func (tx *Tx) DumpString(index string) (r string) { +func (tx *Tx) DumpString() (r string) { r = "allkeys:[\n" // grab root records, for a list of bitmaps. - records, err := tx.rootRecords() + records, err := tx.RootRecords() panicOn(err) n := 0 for _, rr := range records { @@ -1292,7 +1334,7 @@ func (tx *Tx) DumpString(index string) (r string) { ckey := cell.Key ct := toContainer(cell, tx) - s := stringOfCkeyCt(ckey, ct, rr.Name, index) + s := stringOfCkeyCt(ckey, ct, rr.Name) r += s n++ } @@ -1307,55 +1349,21 @@ func (tx *Tx) DumpString(index string) (r string) { } func containerToBytes(ct *roaring.Container) []byte { + ty := roaring.ContainerType(ct) switch ty { - case containerNil: + case roaring.ContainerNil: panic("nil container") - case containerArray: + case roaring.ContainerArray: return fromArray16(roaring.AsArray(ct)) - case containerBitmap: + case roaring.ContainerBitmap: return fromArray64(roaring.AsBitmap(ct)) - case containerRun: + case roaring.ContainerRun: return fromInterval16(roaring.AsRuns(ct)) } panic(fmt.Sprintf("unknown container type '%v'", int(ty))) } -func badgerKey(index, field, view string, shard uint64, roaringContainerKey uint64) []byte { - // The %020d which adds zero padding up to 20 runes is required to - // allow the textual sort to accurately - // reflect a numeric sort order. This is because, as a string, - // math.MaxUint64 is 20 bytes long. - // Example of such a badgerKey with a container-key that is math.MaxUint64: - // ...........................................12345678901234567890 - // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 - - prefix := badgerPrefix(index, field, view, shard) - ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) - bkey := append(prefix, ckey...) - MustValidateKey(bkey) - return bkey -} - -// badgerPrefix returns everything from badgerKey up to and -// including the '@' fune in a badger key. The prefix excludes the roaring container key itself. -// NB must be kept in sync with badgerKey() and badgerKeyExtractContainerKey(). -func badgerPrefix(index, field, view string, shard uint64) []byte { - return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:'%020v';ckey@", index, field, view, shard)) -} - -// MustValidatekey will panic on a bad badgerKey with an informative message. -func MustValidateKey(bkey []byte) { - n := len(bkey) - if n < 56 { - panic(fmt.Sprintf("bkey too short min size is 56 but we see %v in '%v'", n, string(bkey))) - } - beforeCkey := bkey[n-26 : n-20] - if !bytes.Equal(beforeCkey, ckeyPartExpected) { - panic(fmt.Sprintf(`bkey did not have expected ";ckey@" at 26 bytes from the end of the bkey '%v'; instead had '%v'`, string(bkey), string(beforeCkey))) - } -} - func bitmapAsString(rbm *roaring.Bitmap) (r string) { r = "c(" slc := rbm.Slice() @@ -1380,30 +1388,7 @@ func bitmapAsString(rbm *roaring.Bitmap) (r string) { return r + ")" } -// should really be exported from the pilosa/roaring package so we don't get out of sync... -const ( - containerNil byte = iota // no container - containerArray // slice of bit position values - containerBitmap // slice of 1024 uint64s - containerRun // container of run-encoded bits -) - -var ckeyPartExpected = []byte(";ckey@") - -func invName(rbfName string) (field, view string, shard uint64) { - s := strings.Split(rbfName, "\x00") - if len(s) != 3 { - panic("should have 3 parts") - } - field = s[0] - view = s[1] - var err error - shard, err = strconv.ParseUint(s[2], 10, 64) - panicOn(err) - return -} - -func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName, index string) (s string) { +func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName string) (s string) { by := containerToBytes(ct) hash := blake3sum16(by) @@ -1413,8 +1398,7 @@ func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName, index string) (s rbm := &roaring.Bitmap{Containers: cts} srbm := bitmapAsString(rbm) - field, view, shard := invName(rrName) - bkey := string(badgerKey(index, field, view, shard, ckey)) + bkey := rrName + fmt.Sprintf("%020d", ckey) s = fmt.Sprintf("%v -> %v (%v hot)\n", bkey, hash, ct.N()) s += " ......." + srbm + "\n" @@ -1422,7 +1406,6 @@ func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName, index string) (s } func (tx *Tx) ImportRoaringBits(name string, itr roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { - // begin write boilerplate if tx.db == nil { err = ErrTxClosed @@ -1484,6 +1467,7 @@ func (tx *Tx) ImportRoaringBits(name string, itr roaring.RoaringIterator, clear } if clear { + existN := oldC.N() // number of bits set in the old container newC := oldC.Difference(synthC) @@ -1496,14 +1480,6 @@ func (tx *Tx) ImportRoaringBits(name string, itr roaring.RoaringIterator, clear changes := int(existN - newC.N()) changed += changes rowSet[currRow] -= changes - - if tx.DeleteEmptyContainer && newC.N() == 0 { - err = tx.RemoveContainer(name, itrKey) - if err != nil { - return - } - continue - } err = tx.PutContainer(name, itrKey, newC) if err != nil { return @@ -1529,9 +1505,9 @@ func (tx *Tx) ImportRoaringBits(name string, itr roaring.RoaringIterator, clear continue } - newC := oldC.UnionInPlace(synthC) + newC := roaring.Union(oldC, synthC) // UnionInPlace was giving us crashes on overly large containers. - if roaring.ContainerType(newC) == containerBitmap { + if roaring.ContainerType(newC) == roaring.ContainerBitmap { newC.Repair() // update the bit-count so .n is valid. b/c UnionInPlace doesn't update it. } if newC.N() != existN { diff --git a/rbf/tx_test.go b/rbf/tx_test.go index e245b73da..19aeb4d53 100644 --- a/rbf/tx_test.go +++ b/rbf/tx_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/pilosa/pilosa/v2/rbf" + "github.com/pilosa/pilosa/v2/txpath" ) func TestTx_CommitRollback(t *testing.T) { @@ -472,7 +473,8 @@ func TestTx_Dump(t *testing.T) { defer tx.Rollback() index, field, view, shard := "i", "f", "v", uint64(15) - nm := rbfName(field, view, shard) + + nm := rbfName(index, field, view, shard) if err := tx.CreateBitmap(nm); err != nil { t.Fatal(err) @@ -481,12 +483,12 @@ func TestTx_Dump(t *testing.T) { } // test that we don't crash, and get *something* back - s := tx.DumpString(index) + s := tx.DumpString() if s == "" { panic("should have had 3 containers!") } } -func rbfName(field, view string, shard uint64) string { - return fmt.Sprintf("%s\x00%s\x00%d", field, view, shard) +func rbfName(index, field, view string, shard uint64) string { + return string(txpath.Prefix(index, field, view, shard)) } diff --git a/rbf/vprint.go b/rbf/vprint.go index dfd630c70..1838348e9 100644 --- a/rbf/vprint.go +++ b/rbf/vprint.go @@ -29,8 +29,10 @@ import ( "io" "os" "path" + "path/filepath" "runtime" "runtime/debug" + "strings" "sync" "time" ) @@ -167,3 +169,40 @@ func Caller(upStack int) string { } var _ = stack // happy linter +var _ = listFilesUnderDir + +// listFilesUnderDir returns the paths of files found under directory root. +// If includeRoot is true, it returns the full path, otherwise paths are relative to root. +// If requriedSuffix is supplied, the returned file paths will end in that, +// and any other files found during the walk of the directory tree will be ignored. +// If ignoreEmpty is true, files of size 0 will be excluded. +func listFilesUnderDir(root string, includeRoot bool, requiredSuffix string, ignoreEmpty bool) (files []string, err error) { + if !DirExists(root) { + return nil, fmt.Errorf("listFilesUnderDir error: root directory '%v' not found", root) + } + n := len(root) + 1 + if includeRoot { + n = 0 + } + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if len(path) < n { + // ignore + } else { + if info == nil { + panic(fmt.Sprintf("info was nil for path = '%v'", path)) + } + if info.IsDir() { + // skip directories. + } else { + if ignoreEmpty && info.Size() == 0 { + return nil + } + if requiredSuffix == "" || strings.HasSuffix(path, requiredSuffix) { + files = append(files, path[n:]) + } + } + } + return nil + }) + return +} diff --git a/roaring/container_stash.go b/roaring/container_stash.go index 095f462e5..de5d17047 100644 --- a/roaring/container_stash.go +++ b/roaring/container_stash.go @@ -76,12 +76,12 @@ func (c *Container) String() string { froze = c.flags.String() } switch c.typeID { - case containerArray: + case ContainerArray: return fmt.Sprintf("<%s%sarray container, N=%d>", froze, space, c.N()) - case containerBitmap: + case ContainerBitmap: return fmt.Sprintf("<%s%sbitmap container, N=%d>", froze, space, c.N()) - case containerRun: + case ContainerRun: return fmt.Sprintf("<%s%srun container, N=%d, len %dx interval>", froze, space, c.N(), len(c.runs())) default: @@ -105,7 +105,7 @@ func NewContainerBitmap(n int, bitmap []uint64) *Container { if bitmap == nil { return NewContainerBitmapN(nil, 0) } - c := &Container{typeID: containerBitmap} + c := &Container{typeID: ContainerBitmap} if len(bitmap) != bitmapN { // adjust to required length c.setBitmapCopy(bitmap) @@ -128,7 +128,7 @@ func NewContainerBitmapN(bitmap []uint64, n int32) *Container { if bitmap == nil { bitmap = make([]uint64, bitmapN) } - c := &Container{typeID: containerBitmap, n: n} + c := &Container{typeID: ContainerBitmap, n: n} if len(bitmap) != bitmapN { // adjust to required length c.setBitmapCopy(bitmap) @@ -141,7 +141,7 @@ func NewContainerBitmapN(bitmap []uint64, n int32) *Container { // NewContainerArray returns an array container using the provided set of // values. It's okay if the slice is nil; that's a length of zero. func NewContainerArray(set []uint16) *Container { - c := &Container{typeID: containerArray} + c := &Container{typeID: ContainerArray} c.setArray(set) return c } @@ -150,7 +150,7 @@ func NewContainerArray(set []uint16) *Container { // values. It's okay if the slice is nil; that's a length of zero. It copies // the provided slice to new storage. func NewContainerArrayCopy(set []uint16) *Container { - c := &Container{typeID: containerArray} + c := &Container{typeID: ContainerArray} c.setArrayMaybeCopy(set, true) return c } @@ -166,7 +166,7 @@ func NewContainerArrayN(set []uint16, n int32) *Container { // NewContainerRun creates a new run container using a provided (possibly nil) // slice of intervals. func NewContainerRun(set []Interval16) *Container { - c := &Container{typeID: containerRun} + c := &Container{typeID: ContainerRun} c.setRuns(set) for _, run := range set { c.n += int32(run.Last-run.Start) + 1 @@ -177,7 +177,7 @@ func NewContainerRun(set []Interval16) *Container { // NewContainerRunCopy creates a new run container using a provided (possibly nil) // slice of intervals. It copies the provided slice to new storage. func NewContainerRunCopy(set []Interval16) *Container { - c := &Container{typeID: containerRun} + c := &Container{typeID: ContainerRun} c.setRunsMaybeCopy(set, true) for _, run := range set { c.n += int32(run.Last-run.Start) + 1 @@ -188,7 +188,7 @@ func NewContainerRunCopy(set []Interval16) *Container { // NewContainerRunN creates a new run array using a provided (possibly nil) // slice of intervals. It overrides n using the provided value. func NewContainerRunN(set []Interval16, n int32) *Container { - c := &Container{typeID: containerRun, n: n} + c := &Container{typeID: ContainerRun, n: n} c.setRuns(set) return c } @@ -232,7 +232,7 @@ func (c *Container) setN(n int32) { func (c *Container) typ() byte { if c == nil { - return containerNil + return ContainerNil } return c.typeID } @@ -299,11 +299,11 @@ func (c *Container) unmapOrClone() *Container { c.flags &^= flagPristine // mapped: we want to unmap the storage. switch c.typeID { - case containerArray: + case ContainerArray: c.setArrayMaybeCopy(c.array(), true) - case containerRun: + case ContainerRun: c.setRunsMaybeCopy(c.runs(), true) - case containerBitmap: + case ContainerBitmap: c.setBitmapCopy(c.bitmap()) default: panic(fmt.Sprintf("can't thaw invalid container, type %d", c.typeID)) @@ -317,7 +317,7 @@ func (c *Container) array() []uint16 { panic("attempt to read a nil container's array") } if roaringParanoia { - if c.typeID != containerArray { + if c.typeID != ContainerArray { panic("attempt to read non-array's array") } } @@ -332,7 +332,7 @@ func (c *Container) setArrayMaybeCopy(array []uint16, doCopy bool) { if c == nil || c.frozen() { panic("setArray on nil or frozen container") } - if c.typeID != containerArray { + if c.typeID != ContainerArray { panic("attempt to write non-array's array") } } @@ -376,7 +376,7 @@ func (c *Container) bitmap() []uint64 { panic("attempt to read nil container's bitmap") } if roaringParanoia { - if c.typeID != containerBitmap { + if c.typeID != ContainerBitmap { panic("attempt to read non-bitmap's bitmap") } } @@ -387,7 +387,7 @@ func (c *Container) bitmap() []uint64 { // is provided. The target should be zeroed, or this becomes an implicit // union. func (c *Container) AsBitmap(target []uint64) (out []uint64) { - if c != nil && c.typeID == containerBitmap { + if c != nil && c.typeID == ContainerBitmap { return c.bitmap() } // Reminder: len(nil) == 0. @@ -403,14 +403,14 @@ func (c *Container) AsBitmap(target []uint64) (out []uint64) { if c == nil { return out } - if c.typeID == containerArray { + if c.typeID == ContainerArray { a := c.array() for _, v := range a { out[v/64] |= 1 << (v % 64) } return out } - if c.typeID == containerRun { + if c.typeID == ContainerRun { runs := c.runs() b := (*[1024]uint64)(unsafe.Pointer(&out[0])) for _, r := range runs { @@ -478,7 +478,7 @@ func (c *Container) setBitmap(bitmap []uint64) { panic("setBitmap on nil or frozen container") } if roaringParanoia { - if c.typeID != containerBitmap { + if c.typeID != ContainerBitmap { panic("attempt to write non-bitmap's bitmap") } } @@ -495,7 +495,7 @@ func (c *Container) runs() []Interval16 { panic("attempt to read nil container's runs") } if roaringParanoia { - if c.typeID != containerRun { + if c.typeID != ContainerRun { panic("attempt to read non-run's runs") } } @@ -514,7 +514,7 @@ func (c *Container) setRunsMaybeCopy(runs []Interval16, doCopy bool) { if c == nil || c.frozen() { panic("setRuns on nil or frozen container") } - if c.typeID != containerRun { + if c.typeID != ContainerRun { panic("attempt to write non-run's runs") } } @@ -548,9 +548,9 @@ func (c *Container) setRunsMaybeCopy(runs []Interval16, doCopy bool) { func (c *Container) UpdateOrMake(typ byte, n int32, mapped bool) *Container { if c == nil { switch typ { - case containerRun: + case ContainerRun: c = NewContainerRunN(nil, n) - case containerBitmap: + case ContainerBitmap: c = NewContainerBitmapN(nil, n) default: c = NewContainerArrayN(nil, n) @@ -567,9 +567,9 @@ func (c *Container) UpdateOrMake(typ byte, n int32, mapped bool) *Container { c.setMapped(mapped) // we don't know that any existing slice is usable, so let's ditch it switch c.typeID { - case containerArray: + case ContainerArray: c.pointer, c.len, c.cap = &c.data[0], 0, stashedArraySize - case containerRun: + case ContainerRun: c.pointer, c.len, c.cap = &c.data[0], 0, stashedRunSize default: c.pointer, c.len, c.cap = nil, 0, 0 @@ -590,9 +590,9 @@ func (c *Container) Update(typ byte, n int32, mapped bool) { c.setMapped(mapped) // we don't know that any existing slice is usable, so let's ditch it switch c.typeID { - case containerArray: + case ContainerArray: c.pointer, c.len, c.cap = nil, 0, 0 - case containerRun: + case ContainerRun: c.pointer, c.len, c.cap = nil, 0, 0 default: c.pointer, c.len, c.cap = nil, 0, 0 @@ -604,7 +604,7 @@ func (c *Container) isArray() bool { if c == nil { panic("calling isArray on nil container") } - return c.typeID == containerArray + return c.typeID == ContainerArray } // isBitmap returns true if the container is a bitmap container. @@ -612,7 +612,7 @@ func (c *Container) isBitmap() bool { if c == nil { panic("calling isBitmap on nil container") } - return c.typeID == containerBitmap + return c.typeID == ContainerBitmap } // isRun returns true if the container is a run-length-encoded container. @@ -620,5 +620,5 @@ func (c *Container) isRun() bool { if c == nil { panic("calling isRun on nil container") } - return c.typeID == containerRun + return c.typeID == ContainerRun } diff --git a/roaring/containers_test.go b/roaring/containers_test.go index 8e66854d0..1c4b8fa12 100644 --- a/roaring/containers_test.go +++ b/roaring/containers_test.go @@ -119,8 +119,10 @@ func TestSliceContainers(t *testing.T) { if c == nil { t.Fatalf("Get(%d) returned nil container", key) } - if c.data[0] != set[0] { - t.Fatalf("Get(%d): expected: %v, got: %v", key, set[0], c.data[0]) + if len(c.data) > 0 { // happy linter + if c.data[0] != set[0] { + t.Fatalf("Get(%d): expected: %v, got: %v", key, set[0], c.data[0]) + } } } }) diff --git a/roaring/roaring.go b/roaring/roaring.go index 9219b01ef..6d2eb408d 100644 --- a/roaring/roaring.go +++ b/roaring/roaring.go @@ -62,17 +62,17 @@ const ( ) const ( - containerNil byte = iota // no container - containerArray // slice of bit position values - containerBitmap // slice of 1024 uint64s - containerRun // container of run-encoded bits + ContainerNil byte = iota // no container + ContainerArray // slice of bit position values + ContainerBitmap // slice of 1024 uint64s + ContainerRun // container of run-encoded bits ) // map used for a more descriptive print var containerTypeNames = map[byte]string{ - containerArray: "array", - containerBitmap: "bitmap", - containerRun: "run", + ContainerArray: "array", + ContainerBitmap: "bitmap", + ContainerRun: "run", } var fullContainer = NewContainerRun([]Interval16{{Start: 0, Last: MaxContainerVal}}).Freeze() @@ -845,33 +845,33 @@ func (c *Container) intersectInPlace(other *Container) *Container { } switch c.typ() { - case containerArray: + case ContainerArray: switch other.typ() { - case containerArray: + case ContainerArray: return intersectArrayArrayInPlace(c, other) - case containerBitmap: + case ContainerBitmap: return intersectArrayBitmapInPlace(c, other) - case containerRun: + case ContainerRun: return intersectArrayRunInPlace(c, other) } - case containerBitmap: + case ContainerBitmap: switch other.typ() { - case containerArray: + case ContainerArray: return intersectBitmapArrayInPlace(c, other) - case containerBitmap: + case ContainerBitmap: return intersectBitmapBitmapInPlace(c, other) - case containerRun: + case ContainerRun: return intersectBitmapRunInPlace(c, other) } - case containerRun: + case ContainerRun: switch other.typ() { - case containerArray: + case ContainerArray: return intersectRunArrayInPlace(c, other) - case containerBitmap: + case ContainerBitmap: return intersectRunBitmapInPlace(c, other) - case containerRun: + case ContainerRun: return intersectRunRunInPlace(c, other) } } @@ -881,17 +881,17 @@ func (c *Container) intersectInPlace(other *Container) *Container { func (c *Container) copyInPlace(other *Container) *Container { switch other.typ() { - case containerArray: - c.setTyp(containerArray) + case ContainerArray: + c.setTyp(ContainerArray) c.setArrayMaybeCopy(other.array(), true) - case containerBitmap: - c.setTyp(containerBitmap) + case ContainerBitmap: + c.setTyp(ContainerBitmap) c.setBitmapCopy(other.bitmap()) c.setN(other.N()) - case containerRun: - c.setTyp(containerRun) + case ContainerRun: + c.setTyp(ContainerRun) c.setRunsMaybeCopy(other.runs(), true) c.setN(other.N()) @@ -1026,7 +1026,7 @@ func intersectBitmapArrayInPlace(a, b *Container) *Container { } } array = array[:n] - a.setTyp(containerArray) + a.setTyp(ContainerArray) a.setArray(array) return a @@ -1154,7 +1154,7 @@ func intersectRunArrayInPlace(a, b *Container) *Container { } array = array[:n] - a.setTyp(containerArray) + a.setTyp(ContainerArray) a.setArray(array) return a @@ -1407,7 +1407,7 @@ func (b *Bitmap) unionInPlace(others ...*Bitmap) { // first other container, but for some cases, that will // result in cloning a non-bitmap, then converting it // to a bitmap, and this will be expensive... - if expectedN >= 512 && iContainer.typ() != containerBitmap { + if expectedN >= 512 && iContainer.typ() != ContainerBitmap { // copying the non-bitmap, then converting it, // is expensive. statsHit("unionInPlace/newBitmap") @@ -1428,12 +1428,12 @@ func (b *Bitmap) unionInPlace(others ...*Bitmap) { // convert it preemptively, because union into a // bitmap is nearly always faster. itersToUnion = bitmapIters[i:] - if expectedN >= 512 && tContainer.typ() != containerBitmap { + if expectedN >= 512 && tContainer.typ() != ContainerBitmap { statsHit("unionInPlace/convertToBitmap") switch tContainer.typ() { - case containerArray: + case ContainerArray: tContainer = tContainer.arrayToBitmap() - case containerRun: + case ContainerRun: tContainer = tContainer.runToBitmap() } } @@ -1967,7 +1967,7 @@ func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length in // a run container keeps its data after an initial 2 byte length header var runCount uint16 - if r.currentType == containerRun { + if r.currentType == ContainerRun { runCount = binary.LittleEndian.Uint16(r.data[r.currentDataOffset : r.currentDataOffset+runCountHeaderSize]) r.currentDataOffset += 2 } @@ -1979,13 +1979,13 @@ func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length in r.currentPointer = (*uint16)(unsafe.Pointer(&r.data[r.currentDataOffset])) var size int switch r.currentType { - case containerArray: + case ContainerArray: r.currentLen = r.currentN size = r.currentLen * 2 - case containerBitmap: + case ContainerBitmap: r.currentLen = 1024 size = 8192 - case containerRun: + case ContainerRun: r.currentLen = int(runCount) size = r.currentLen * 4 } @@ -2035,7 +2035,7 @@ func (r *officialRoaringIterator) Next() (key uint64, cType byte, n int, length } // a run container keeps its data after an initial 2 byte length header var runCount uint16 - if r.currentType == containerRun { + if r.currentType == ContainerRun { if int(r.currentDataOffset)+2 > len(r.data) { r.Done(fmt.Errorf("insufficient data for offsets container %d/%d, expect run length at %d/%d bytes", r.currentIdx, r.keys, r.currentDataOffset, len(r.data))) @@ -2052,13 +2052,13 @@ func (r *officialRoaringIterator) Next() (key uint64, cType byte, n int, length r.currentPointer = (*uint16)(unsafe.Pointer(&r.data[r.currentDataOffset])) var size int switch r.currentType { - case containerArray: + case ContainerArray: r.currentLen = r.currentN size = r.currentLen * 2 - case containerBitmap: + case ContainerBitmap: r.currentLen = 1024 size = 8192 - case containerRun: + case ContainerRun: // official format stores runs as start/len, we want to convert, but since // they might be mmapped, we can't write to that memory newRuns := make([]Interval16, runCount) @@ -2257,7 +2257,7 @@ func (b *Bitmap) ImportRoaringRawIterator(itr RoaringIterator, clear bool, log b return newerC, true } newC = oldC.unionInPlace(&synthC) - if newC.typeID == containerBitmap { + if newC.typeID == ContainerBitmap { newC.Repair() } if newC.N() != existN { @@ -2461,12 +2461,12 @@ func BitmapsToRoaring(bitmaps []*Bitmap) []byte { binary.LittleEndian.PutUint32(offset[0:4], uint32(dataOffset+int(offsetEnd))) nextData := data[dataOffset:] switch c.typeID { // TODO: make this work on big endian machines - case containerArray: + case ContainerArray: dataOffset += 2 * copy((*[1 << 16]uint16)(unsafe.Pointer(&nextData[0]))[:], c.array()) - case containerBitmap: + case ContainerBitmap: copy((*[1024]uint64)(unsafe.Pointer(&nextData[0]))[:], c.bitmap()) dataOffset += 8192 - case containerRun: + case ContainerRun: binary.LittleEndian.PutUint16(nextData[0:2], uint16(c.len)) dataOffset += 2 dataOffset += 4 * copy((*[1 << 15]Interval16)(unsafe.Pointer(&nextData[2]))[:], c.runs()) @@ -2489,11 +2489,11 @@ func (b *Bitmap) roaringSize() (int64, int64) { } count++ switch c.typeID { - case containerArray: + case ContainerArray: size += 2 * int64(c.N()) - case containerBitmap: + case ContainerBitmap: size += 8192 - case containerRun: + case ContainerRun: // 2 bytes for the count of runs, plus 4 bytes per run size += 2 + (4 * int64(c.len)) } @@ -3141,39 +3141,39 @@ func (c *Container) optimize() *Container { var newType byte if runs <= runMaxSize && runs <= c.N()/2 { - newType = containerRun + newType = ContainerRun } else if c.N() < ArrayMaxSize { - newType = containerArray + newType = ContainerArray } else { - newType = containerBitmap + newType = ContainerBitmap } // Then convert accordingly. if c.isArray() { - if newType == containerBitmap { + if newType == ContainerBitmap { statsHit("optimize/arrayToBitmap") c = c.arrayToBitmap() - } else if newType == containerRun { + } else if newType == ContainerRun { statsHit("optimize/arrayToRun") c = c.arrayToRun(runs) } else { statsHit("optimize/arrayUnchanged") } } else if c.isBitmap() { - if newType == containerArray { + if newType == ContainerArray { statsHit("optimize/bitmapToArray") c = c.bitmapToArray() - } else if newType == containerRun { + } else if newType == ContainerRun { statsHit("optimize/bitmapToRun") c = c.bitmapToRun(runs) } else { statsHit("optimize/bitmapUnchanged") } } else if c.isRun() { - if newType == containerBitmap { + if newType == ContainerBitmap { statsHit("optimize/runToBitmap") c = c.runToBitmap() - } else if newType == containerArray { + } else if newType == ContainerArray { statsHit("optimize/runToArray") c = c.runToArray() } else { @@ -3202,36 +3202,36 @@ func (c *Container) unionInPlace(other *Container) *Container { return fullContainer } switch c.typ() { - case containerBitmap: + case ContainerBitmap: switch other.typ() { - case containerBitmap: + case ContainerBitmap: return unionBitmapBitmapInPlace(c, other) - case containerArray: + case ContainerArray: return unionBitmapArrayInPlace(c, other) - case containerRun: + case ContainerRun: return unionBitmapRunInPlace(c, other) } - case containerArray: + case ContainerArray: switch other.typ() { - case containerBitmap: + case ContainerBitmap: c = c.arrayToBitmap() return unionBitmapBitmapInPlace(c, other) - case containerArray: + case ContainerArray: return unionArrayArrayInPlace(c, other) - case containerRun: + case ContainerRun: c = c.arrayToBitmap() return unionBitmapRunInPlace(c, other) } - case containerRun: + case ContainerRun: switch other.typ() { - case containerBitmap: + case ContainerBitmap: c = c.runToBitmap() return unionBitmapBitmapInPlace(c, other) - case containerArray: + case ContainerArray: c = c.runToBitmap() return unionBitmapArrayInPlace(c, other) - case containerRun: + case ContainerRun: return unionRunRunInPlace(c, other) } } @@ -3412,7 +3412,7 @@ func (c *Container) bitmapToArray() *Container { if c.frozen() { return NewContainerArray(nil) } - c.setTyp(containerArray) + c.setTyp(ContainerArray) c.setArray(nil) return c } @@ -3441,7 +3441,7 @@ func (c *Container) bitmapToArray() *Container { if c.frozen() { return NewContainerArray(array) } - c.setTyp(containerArray) + c.setTyp(ContainerArray) c.setMapped(false) c.setArray(array) return c @@ -3462,7 +3462,7 @@ func (c *Container) arrayToBitmap() *Container { if c.frozen() { return NewContainerBitmap(0, nil) } - c.setTyp(containerBitmap) + c.setTyp(ContainerBitmap) c.setBitmap(make([]uint64, bitmapN)) return c } @@ -3474,7 +3474,7 @@ func (c *Container) arrayToBitmap() *Container { if c.frozen() { return NewContainerBitmapN(bitmap, c.N()) } - c.setTyp(containerBitmap) + c.setTyp(ContainerBitmap) c.setMapped(false) c.setBitmap(bitmap) return c @@ -3495,7 +3495,7 @@ func (c *Container) runToBitmap() *Container { if c.frozen() { return NewContainerBitmap(0, nil) } - c.setTyp(containerBitmap) + c.setTyp(ContainerBitmap) c.setBitmap(make([]uint64, bitmapN)) return c } @@ -3533,7 +3533,7 @@ func (c *Container) runToBitmap() *Container { if c.frozen() { return NewContainerBitmapN(bitmap, c.N()) } - c.setTyp(containerBitmap) + c.setTyp(ContainerBitmap) c.setMapped(false) c.setBitmap(bitmap) return c @@ -3554,7 +3554,7 @@ func (c *Container) bitmapToRun(numRuns int32) *Container { if c.frozen() { return NewContainerRun(nil) } - c.setTyp(containerRun) + c.setTyp(ContainerRun) c.setRuns(nil) return c } @@ -3605,7 +3605,7 @@ func (c *Container) bitmapToRun(numRuns int32) *Container { if c.frozen() { return NewContainerRunN(runs, c.N()) } - c.setTyp(containerRun) + c.setTyp(ContainerRun) c.setRuns(runs) c.setMapped(false) return c @@ -3626,7 +3626,7 @@ func (c *Container) arrayToRun(numRuns int32) *Container { if c.frozen() { return NewContainerRun(nil) } - c.setTyp(containerRun) + c.setTyp(ContainerRun) c.setRuns(nil) return c } @@ -3651,7 +3651,7 @@ func (c *Container) arrayToRun(numRuns int32) *Container { if c.frozen() { return NewContainerRunN(runs, c.N()) } - c.setTyp(containerRun) + c.setTyp(ContainerRun) c.setMapped(false) c.setRuns(runs) return c @@ -3672,7 +3672,7 @@ func (c *Container) runToArray() *Container { if c.frozen() { return NewContainerArray(nil) } - c.setTyp(containerArray) + c.setTyp(ContainerArray) c.setArray(nil) return c } @@ -3695,7 +3695,7 @@ func (c *Container) runToArray() *Container { if c.frozen() { return NewContainerArray(array) } - c.setTyp(containerArray) + c.setTyp(ContainerArray) c.setMapped(false) c.setArray(array) return c @@ -3708,15 +3708,15 @@ func (c *Container) Clone() (out *Container) { return nil } switch c.typ() { - case containerArray: + case ContainerArray: statsHit("Container/Clone/Array") out = NewContainerArrayCopy(c.array()) - case containerBitmap: + case ContainerBitmap: statsHit("Container/Clone/Bitmap") other := NewContainerBitmapN(nil, c.N()) copy(other.bitmap(), c.bitmap()) out = other - case containerRun: + case ContainerRun: statsHit("Container/Clone/Run") out = NewContainerRunCopy(c.runs()) default: @@ -4717,15 +4717,15 @@ func (c *Container) BitwiseCompare(c2 *Container) error { return nil } switch typePair(c.typ(), c2.typ()) { - case typePair(containerArray, containerArray): + case typePair(ContainerArray, ContainerArray): return compareArrayArray(c.array(), c2.array()) - case typePair(containerArray, containerBitmap): + case typePair(ContainerArray, ContainerBitmap): return compareArrayBitmap(c.array(), c2.bitmap()) - case typePair(containerBitmap, containerArray): + case typePair(ContainerBitmap, ContainerArray): return compareArrayBitmap(c2.array(), c.bitmap()) - case typePair(containerArray, containerRun): + case typePair(ContainerArray, ContainerRun): return compareArrayRuns(c.array(), c2.runs()) - case typePair(containerRun, containerArray): + case typePair(ContainerRun, ContainerArray): return compareArrayRuns(c2.array(), c.runs()) default: c3 := xor(c, c2) @@ -5432,7 +5432,7 @@ func xorArrayBitmap(a, b *Container) *Container { // It's possible that output was converted from bitmap to array in output.remove() // so we only do this conversion if output is still a bitmap container. - if output.typ() == containerBitmap && output.count() < ArrayMaxSize { + if output.typ() == ContainerBitmap && output.count() < ArrayMaxSize { output = output.bitmapToArray() } @@ -6239,9 +6239,9 @@ func readOfficialHeader(buf []byte) (size uint32, containerTyper func(index uint return size, containerTyper, header, pos, haveRuns, err } cf := func(index uint, card int) (newType byte) { - newType = containerBitmap + newType = ContainerBitmap if card < ArrayMaxSize { - newType = containerArray + newType = ContainerArray } return newType } @@ -6268,7 +6268,7 @@ func readOfficialHeader(buf []byte) (size uint32, containerTyper func(index uint pos += isRunBitmapSize containerTyper = func(index uint, card int) byte { if isRunBitmap[index/8]&(1<<(index%8)) != 0 { - return containerRun + return ContainerRun } return cf(index, card) } @@ -6714,7 +6714,7 @@ func differenceRunBitmapInPlace(c, other *Container) { for i, word := range other.bitmap() { bitmap[i] = ^word } - c.setTyp(containerBitmap) + c.setTyp(ContainerBitmap) c.setMapped(false) c.setBitmap(bitmap) c.setN(c.count()) diff --git a/roaring/roaring_helpers_test.go b/roaring/roaring_helpers_test.go index 6d1b67598..2e9e713f3 100644 --- a/roaring/roaring_helpers_test.go +++ b/roaring/roaring_helpers_test.go @@ -252,12 +252,12 @@ type testOp struct { func doContainer(typ byte, data interface{}) *Container { switch typ { - case containerArray: + case ContainerArray: return NewContainerArray(data.([]uint16)) - case containerBitmap: + case ContainerBitmap: c := NewContainerBitmap(-1, data.([]uint64)) return c - case containerRun: + case ContainerRun: return NewContainerRun(data.([]Interval16)) } return nil @@ -272,45 +272,45 @@ func setupContainerTests() map[byte]map[string]*Container { sampleTestContainers = make(map[byte]map[string]*Container) // array containers - sampleTestContainers[containerArray] = map[string]*Container{ - "empty": doContainer(containerArray, arrayEmpty()), - "full": doContainer(containerArray, arrayFull()), - "firstBitSet": doContainer(containerArray, arrayFirstBitSet()), - "lastBitSet": doContainer(containerArray, arrayLastBitSet()), - "firstBitUnset": doContainer(containerArray, arrayFirstBitUnset()), - "lastBitUnset": doContainer(containerArray, arrayLastBitUnset()), - "innerBitsSet": doContainer(containerArray, arrayInnerBitsSet()), - "outerBitsSet": doContainer(containerArray, arrayOuterBitsSet()), - "oddBitsSet": doContainer(containerArray, arrayOddBitsSet()), - "evenBitsSet": doContainer(containerArray, arrayEvenBitsSet()), + sampleTestContainers[ContainerArray] = map[string]*Container{ + "empty": doContainer(ContainerArray, arrayEmpty()), + "full": doContainer(ContainerArray, arrayFull()), + "firstBitSet": doContainer(ContainerArray, arrayFirstBitSet()), + "lastBitSet": doContainer(ContainerArray, arrayLastBitSet()), + "firstBitUnset": doContainer(ContainerArray, arrayFirstBitUnset()), + "lastBitUnset": doContainer(ContainerArray, arrayLastBitUnset()), + "innerBitsSet": doContainer(ContainerArray, arrayInnerBitsSet()), + "outerBitsSet": doContainer(ContainerArray, arrayOuterBitsSet()), + "oddBitsSet": doContainer(ContainerArray, arrayOddBitsSet()), + "evenBitsSet": doContainer(ContainerArray, arrayEvenBitsSet()), } // bitmap containers - sampleTestContainers[containerBitmap] = map[string]*Container{ - "empty": doContainer(containerBitmap, bitmapEmpty()), - "full": doContainer(containerBitmap, bitmapFull()), - "firstBitSet": doContainer(containerBitmap, bitmapFirstBitSet()), - "lastBitSet": doContainer(containerBitmap, bitmapLastBitSet()), - "firstBitUnset": doContainer(containerBitmap, bitmapFirstBitUnset()), - "lastBitUnset": doContainer(containerBitmap, bitmapLastBitUnset()), - "innerBitsSet": doContainer(containerBitmap, bitmapInnerBitsSet()), - "outerBitsSet": doContainer(containerBitmap, bitmapOuterBitsSet()), - "oddBitsSet": doContainer(containerBitmap, bitmapOddBitsSet()), - "evenBitsSet": doContainer(containerBitmap, bitmapEvenBitsSet()), + sampleTestContainers[ContainerBitmap] = map[string]*Container{ + "empty": doContainer(ContainerBitmap, bitmapEmpty()), + "full": doContainer(ContainerBitmap, bitmapFull()), + "firstBitSet": doContainer(ContainerBitmap, bitmapFirstBitSet()), + "lastBitSet": doContainer(ContainerBitmap, bitmapLastBitSet()), + "firstBitUnset": doContainer(ContainerBitmap, bitmapFirstBitUnset()), + "lastBitUnset": doContainer(ContainerBitmap, bitmapLastBitUnset()), + "innerBitsSet": doContainer(ContainerBitmap, bitmapInnerBitsSet()), + "outerBitsSet": doContainer(ContainerBitmap, bitmapOuterBitsSet()), + "oddBitsSet": doContainer(ContainerBitmap, bitmapOddBitsSet()), + "evenBitsSet": doContainer(ContainerBitmap, bitmapEvenBitsSet()), } // run containers - sampleTestContainers[containerRun] = map[string]*Container{ - "empty": doContainer(containerRun, runEmpty()), - "full": doContainer(containerRun, runFull()), - "firstBitSet": doContainer(containerRun, runFirstBitSet()), - "lastBitSet": doContainer(containerRun, runLastBitSet()), - "firstBitUnset": doContainer(containerRun, runFirstBitUnset()), - "lastBitUnset": doContainer(containerRun, runLastBitUnset()), - "innerBitsSet": doContainer(containerRun, runInnerBitsSet()), - "outerBitsSet": doContainer(containerRun, runOuterBitsSet()), - "oddBitsSet": doContainer(containerRun, runOddBitsSet()), - "evenBitsSet": doContainer(containerRun, runEvenBitsSet()), + sampleTestContainers[ContainerRun] = map[string]*Container{ + "empty": doContainer(ContainerRun, runEmpty()), + "full": doContainer(ContainerRun, runFull()), + "firstBitSet": doContainer(ContainerRun, runFirstBitSet()), + "lastBitSet": doContainer(ContainerRun, runLastBitSet()), + "firstBitUnset": doContainer(ContainerRun, runFirstBitUnset()), + "lastBitUnset": doContainer(ContainerRun, runLastBitUnset()), + "innerBitsSet": doContainer(ContainerRun, runInnerBitsSet()), + "outerBitsSet": doContainer(ContainerRun, runOuterBitsSet()), + "oddBitsSet": doContainer(ContainerRun, runOddBitsSet()), + "evenBitsSet": doContainer(ContainerRun, runEvenBitsSet()), } }) diff --git a/roaring/roaring_internal_test.go b/roaring/roaring_internal_test.go index 2041af9ff..b10964cb5 100644 --- a/roaring/roaring_internal_test.go +++ b/roaring/roaring_internal_test.go @@ -2962,7 +2962,7 @@ func TestContainerCombinations(t *testing.T) { cts := setupContainerTests() - containerTypes := []byte{containerArray, containerBitmap, containerRun} + containerTypes := []byte{ContainerArray, ContainerBitmap, ContainerRun} testOps := []testOp{ // intersect @@ -4323,8 +4323,8 @@ func TestBitmapAny(t *testing.T) { } func TestDifferenceInPlace_N(t *testing.T) { - a := doContainer(containerRun, runFull()) - b := doContainer(containerBitmap, bitmapFull()) + a := doContainer(ContainerRun, runFull()) + b := doContainer(ContainerBitmap, bitmapFull()) r := differenceInPlaceWrapper(a, b) if r.N() != 0 { t.Error("expected difference of containers to have n=0") @@ -4352,8 +4352,8 @@ func BenchmarkUnionRunRunInPlace(bm *testing.B) { for _, br := range runs { bm.Run("RunToBitmapRun-"+ar.name+"_"+br.name, func(bm *testing.B) { for i := 0; i < bm.N; i++ { - arun := doContainer(containerRun, ar.fn()) - brun := doContainer(containerRun, br.fn()) + arun := doContainer(ContainerRun, ar.fn()) + brun := doContainer(ContainerRun, br.fn()) abmp := arun.runToBitmap() unionBitmapRunInPlace(abmp, brun) @@ -4362,8 +4362,8 @@ func BenchmarkUnionRunRunInPlace(bm *testing.B) { bm.Run("RunRun-"+ar.name+"_"+br.name, func(bm *testing.B) { for i := 0; i < bm.N; i++ { - arun := doContainer(containerRun, ar.fn()) - brun := doContainer(containerRun, br.fn()) + arun := doContainer(ContainerRun, ar.fn()) + brun := doContainer(ContainerRun, br.fn()) unionRunRunInPlace(arun, brun) } @@ -4390,8 +4390,8 @@ func TestUnionRunRunInPlaceBitwiseCompare(t *testing.T) { for _, a := range runs { for _, b := range runs { t.Run(a.name+"-"+b.name, func(t *testing.T) { - arun := doContainer(containerRun, a.run) - brun := doContainer(containerRun, b.run) + arun := doContainer(ContainerRun, a.run) + brun := doContainer(ContainerRun, b.run) out1 := unionBitmapRunInPlace(arun.runToBitmap(), brun) out2 := unionRunRunInPlace(arun, brun) diff --git a/roaring/unmarshal_binary.go b/roaring/unmarshal_binary.go index 39a0762f9..87e693187 100644 --- a/roaring/unmarshal_binary.go +++ b/roaring/unmarshal_binary.go @@ -49,11 +49,11 @@ func (b *Bitmap) UnmarshalBinary(data []byte) (err error) { for itrErr == nil { var newC *Container switch itrCType { - case containerArray: + case ContainerArray: newC = NewContainerArray((*[4096]uint16)(unsafe.Pointer(itrPointer))[:itrLen:itrLen]) - case containerRun: + case ContainerRun: newC = NewContainerRunN((*[2048]Interval16)(unsafe.Pointer(itrPointer))[:itrLen:itrLen], int32(itrN)) - case containerBitmap: + case ContainerBitmap: newC = NewContainerBitmapN((*[1024]uint64)(unsafe.Pointer(itrPointer))[:1024:itrLen], int32(itrN)) default: panic("invalid container type") @@ -132,20 +132,20 @@ func InspectBinary(data []byte, mapped bool, info *BitmapInfo) (b *Bitmap, mappe for itrErr == nil { var size int switch itrCType { - case containerArray: + case ContainerArray: size = int(itrN) * 2 - case containerBitmap: + case ContainerBitmap: size = 8192 - case containerRun: + case ContainerRun: size = itrLen*interval16Size + runCountHeaderSize } var newC *Container switch itrCType { - case containerArray: + case ContainerArray: newC = NewContainerArray((*[4096]uint16)(unsafe.Pointer(itrPointer))[:itrLen:itrLen]) - case containerRun: + case ContainerRun: newC = NewContainerRunN((*[2048]Interval16)(unsafe.Pointer(itrPointer))[:itrLen:itrLen], int32(itrN)) - case containerBitmap: + case ContainerBitmap: newC = NewContainerBitmapN((*[1024]uint64)(unsafe.Pointer(itrPointer))[:1024:itrLen], int32(itrN)) default: panic("invalid container type") diff --git a/rrtx.go b/rrtx.go new file mode 100644 index 000000000..c5fcc112b --- /dev/null +++ b/rrtx.go @@ -0,0 +1,695 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "sync" + + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pkg/errors" +) + +// MultiTx implements the transaction interface to combine multiple transactions. +type MultiTx struct { + mu sync.Mutex + writable bool + holder *Holder + index *Index + txs map[multiTxKey]Tx +} + +// NewMultiTx returns a new instance of MultiTx for a Holder. +func NewMultiTx(writable bool, holder *Holder) *MultiTx { + return &MultiTx{ + writable: writable, + holder: holder, + txs: make(map[multiTxKey]Tx), + } +} + +// NewMultiTxWithIndex returns a new instance of MultiTx for a single index. +func NewMultiTxWithIndex(writable bool, index *Index) *MultiTx { + return &MultiTx{ + writable: writable, + index: index, + txs: make(map[multiTxKey]Tx), + } +} + +var _ Tx = (*MultiTx)(nil) + +func (mtx *MultiTx) Type() string { + return RoaringTxn +} + +// debugging, what does this Tx see as its database? +func (mtx *MultiTx) Dump() { + mtx.mu.Lock() + defer mtx.mu.Unlock() + if len(mtx.txs) == 0 { + return + } + for _, tx := range mtx.txs { + tx.Dump() + return + } +} + +func (mtx *MultiTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + tx, err := mtx.txNoShard(index) + panicOn(err) + return tx.SliceOfShards(index, field, view, optionalViewPath) +} + +func (mtx *MultiTx) UseRowCache() bool { + return true +} + +func (mtx *MultiTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + tx, err := mtx.tx(index, shard) + panicOn(err) + return tx.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) +} + +func (mtx *MultiTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + tx, err := mtx.tx(index, shard) + panicOn(err) + return tx.NewTxIterator(index, field, view, shard) +} + +// Readonly is true if the transaction is not read-and-write, but only doing reads. +func (mtx *MultiTx) Readonly() bool { + return !mtx.writable +} + +func (mtx *MultiTx) Pointer() string { + return fmt.Sprintf("%p", mtx) +} + +func (mtx *MultiTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { + tx, err := mtx.tx(index, shard) + panicOn(err) + return tx.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) +} + +func (mtx *MultiTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + tx, err := mtx.tx(index, shard) + panicOn(err) + tx.IncrementOpN(index, field, view, shard, changedN) +} + +// Rollback rolls back all underlying transactions. +func (mtx *MultiTx) Rollback() { + for _, tx := range mtx.txs { + tx.Rollback() + } +} + +// Commit commits all underlying transactions. +func (mtx *MultiTx) Commit() (err error) { + for _, tx := range mtx.txs { + if e := tx.Commit(); e != nil && err == nil { + err = e + } + } + return err +} + +func (mtx *MultiTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return nil, err + } + return tx.RoaringBitmap(index, field, view, shard) +} + +func (mtx *MultiTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return nil, err + } + return tx.Container(index, field, view, shard, key) +} + +func (mtx *MultiTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { + tx, err := mtx.tx(index, shard) + if err != nil { + return err + } + return tx.PutContainer(index, field, view, shard, key, c) +} + +func (mtx *MultiTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + tx, err := mtx.tx(index, shard) + if err != nil { + return err + } + return tx.RemoveContainer(index, field, view, shard, key) +} + +func (mtx *MultiTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, err + } + return tx.Add(index, field, view, shard, batched, a...) +} + +func (mtx *MultiTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, err + } + return tx.Remove(index, field, view, shard, a...) +} + +func (mtx *MultiTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return false, err + } + return tx.Contains(index, field, view, shard, v) +} + +func (mtx *MultiTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return nil, false, err + } + return tx.ContainerIterator(index, field, view, shard, key) +} + +func (mtx *MultiTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + tx, err := mtx.tx(index, shard) + if err != nil { + return err + } + return tx.ForEach(index, field, view, shard, fn) +} + +func (mtx *MultiTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + tx, err := mtx.tx(index, shard) + if err != nil { + return err + } + return tx.ForEachRange(index, field, view, shard, start, end, fn) +} + +func (mtx *MultiTx) Count(index, field, view string, shard uint64) (uint64, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, err + } + return tx.Count(index, field, view, shard) +} + +func (mtx *MultiTx) Max(index, field, view string, shard uint64) (uint64, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, err + } + return tx.Max(index, field, view, shard) +} + +func (mtx *MultiTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, false, err + } + return tx.Min(index, field, view, shard) +} + +func (mtx *MultiTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + tx, err := mtx.tx(index, shard) + if err != nil { + return err + } + return tx.UnionInPlace(index, field, view, shard, others...) +} + +func (mtx *MultiTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return 0, err + } + return tx.CountRange(index, field, view, shard, start, end) +} + +func (mtx *MultiTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { + tx, err := mtx.tx(index, shard) + if err != nil { + return nil, err + } + return tx.OffsetRange(index, field, view, shard, offset, start, end) +} + +// tx returns a transaction by index/shard. Reuses transaction if already open. +// Otherwise begins a new transaction. +func (mtx *MultiTx) tx(index string, shard uint64) (_ Tx, err error) { + mtx.mu.Lock() + defer mtx.mu.Unlock() + + mkey := multiTxKey{index: index, shard: shard, write: mtx.writable} + + // Lookup transaction from cache. + tx := mtx.txs[mkey] + if tx != nil { + return tx, nil + } + + // If transaction doesn't exist, lookup the index. + idx := mtx.index + if mtx.holder != nil { + if idx = mtx.holder.Index(index); idx == nil { + return nil, ErrIndexNotFound + } + } + + // Begin tranaction & cache it. + if tx, err = idx.BeginTx(mtx.writable, shard); err != nil { + return nil, err + } + mtx.txs[mkey] = tx + + return tx, nil +} + +// version of the above for SliceOfShards(), where we don't have a shard. +func (mtx *MultiTx) txNoShard(index string) (_ Tx, err error) { + mtx.mu.Lock() + defer mtx.mu.Unlock() + + // Lookup transaction from cache. + for _, tx := range mtx.txs { + if tx.(*RoaringTx).Index.name == index { + return tx, nil + } + } + panic(fmt.Sprintf("no prior RoaringTx available, looking up index='%v'", index)) +} + +type multiTxKey struct { + index string + shard uint64 + write bool +} + +// RoaringTx represents a fake transaction object for Roaring storage. +type RoaringTx struct { + write bool + Index *Index + Field *Field + fragment *fragment +} + +func (tx *RoaringTx) Type() string { + return RoaringTxn +} + +func (tx *RoaringTx) Dump() { + fmt.Printf("%v\n", tx.Index.StringifiedRoaringKeys()) +} + +func (tx *RoaringTx) UseRowCache() bool { + return true +} + +func (tx *RoaringTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + + // SliceOfShards is based on view.openFragments() + + file, err := os.Open(filepath.Join(optionalViewPath, "fragments")) + if os.IsNotExist(err) { + return + } else if err != nil { + return nil, errors.Wrap(err, "opening fragments directory") + } + defer file.Close() + + fis, err := file.Readdir(0) + if err != nil { + return nil, errors.Wrap(err, "reading fragments directory") + } + + for _, fi := range fis { + if fi.IsDir() { + continue + } + // Parse filename into integer. + shard, err := strconv.ParseUint(filepath.Base(fi.Name()), 10, 64) + if err != nil { + //AlwaysPrintf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", index, field, view, fi.Name()) + //v.holder.Logger.Debugf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", v.index, v.field, v.name, fi.Name()) + continue + } + sliceOfShards = append(sliceOfShards, shard) + } + return +} + +func (tx *RoaringTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +// NewTxIterator returns a *roaring.Iterator that MUST have Close() called on it BEFORE +// the transaction Commits or Rollsback. +func (tx *RoaringTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + b, err := tx.bitmap(index, field, view, shard) + panicOn(err) + return b.Iterator() +} + +// ImportRoaringBits return values changed and rowSet will be inaccurate if +// the data []byte is supplied. This mimics the traditional roaring-per-file +// and should be faster. +func (tx *RoaringTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { + f, err := tx.getFragment(index, field, view, shard) + if err != nil { + return 0, nil, err + } + if len(data) > 0 { + // changed and rowSet are ignored anyway when len(data) > 0; + // when we are called from fragment.fillFragmentFromArchive() + // which is the only place the data []byte is supplied. + // blueGreenTx also turns off the checks in this case. + return 0, nil, f.readStorageFromArchive(bytes.NewBuffer(data)) + } + + changed, rowSet, err = f.storage.ImportRoaringRawIterator(rit, clear, true, rowSize) + return +} + +func (tx *RoaringTx) Readonly() bool { + return !tx.write +} + +func (tx *RoaringTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + frag, err := tx.getFragment(index, field, view, shard) + panicOn(err) + frag.incrementOpN(changedN) +} + +// Rollback is a no-op as Roaring does not support transactions. +func (tx *RoaringTx) Rollback() {} + +// Commit is a no-op as Roaring does not support transactions. +func (tx *RoaringTx) Commit() error { + return nil +} + +func (tx *RoaringTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + return tx.bitmap(index, field, view, shard) +} + +func (tx *RoaringTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return nil, err + } + return b.Containers.Get(key), nil +} + +func (tx *RoaringTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return err + } + b.Containers.Put(key, c) + return nil +} + +func (tx *RoaringTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return err + } + b.Containers.Remove(key) + return nil +} + +func (tx *RoaringTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, err + } + if !batched { + changed, err := b.Add(a...) + if changed { + return 1, err + } + return 0, err + } + + // Note: do not replace b.AddN() with b.DirectAddN(). + // DirectAddN() does not do op-log operations inside roaring + // This creates a problem because RoaringTx needs the op-log + // to know when to flush the fragment to disk. + count, err := b.AddN(a...) // AddN does oplog batches. needed to keep op-log up to date. + + return count, err +} + +func (tx *RoaringTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, err + } + changed, err := b.Remove(a...) // green TestFragment_Bug_Q2DoubleDelete + panicOn(err) + if changed { + return 1, err + } else { + return 0, err + } + + // Note: don't replace b.Remove(a...) with b.RemoveN(a...) or + // with b.DirectRemoveN(a...). If you do, you'll see + // TestFragment_Bug_Q2DoubleDelete go red. +} + +func (tx *RoaringTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return false, err + } + return b.Contains(v), nil +} + +func (tx *RoaringTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return nil, false, err + } + citer, found = b.Containers.Iterator(key) + return citer, found, nil +} + +func (tx *RoaringTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return err + } + return b.ForEach(fn) +} + +func (tx *RoaringTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return err + } + return b.ForEachRange(start, end, fn) +} + +func (tx *RoaringTx) Count(index, field, view string, shard uint64) (uint64, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, err + } + return b.Count(), nil +} + +func (tx *RoaringTx) Max(index, field, view string, shard uint64) (uint64, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, err + } + return b.Max(), nil +} + +func (tx *RoaringTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, false, err + } + v, ok := b.Min() + return v, ok, nil +} + +func (tx *RoaringTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return err + } + b.UnionInPlace(others...) + return nil +} + +func (tx *RoaringTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return 0, err + } + return b.CountRange(start, end), nil +} + +func (tx *RoaringTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { + b, err := tx.bitmap(index, field, view, shard) + if err != nil { + return nil, err + } + return b.OffsetRange(offset, start, end), nil +} + +// getFragment is used by IncrementOpN() and by bitmap() +func (tx *RoaringTx) getFragment(index, field, view string, shard uint64) (*fragment, error) { + + // If a fragment is attached, always use it. Since it was set at Tx creation, + // it is highly likely to be correct. + if tx.fragment != nil { + // but still a basic sanity check. + if tx.fragment.index != index || + tx.fragment.field != field || + tx.fragment.view != view || + tx.fragment.shard != shard { + panic(fmt.Sprintf("different fragment cached vs requested. tx.fragment='%#v', index='%v', field='%v'; view='%v'; shard='%v'", tx.fragment, index, field, view, shard)) + } + return tx.fragment, nil + } + + // If a field is attached, start from there. + // Otherwise look up the field from the index. + f := tx.Field + + if f == nil { + // we cannot assume that the tx.Index that we "started" on is the same + // as the index we are being queried; it might be foreign: TestExecutor_ForeignIndex + // So go through the holder + idx := tx.Index.holder.Index(index) + if idx == nil { + // only thing we can try is the cached index, and hope we aren't being asked for a foreign index. + f = tx.Index.Field(field) + if f == nil { + return nil, ErrFieldNotFound + } + } else { + if f = idx.Field(field); f == nil { + return nil, ErrFieldNotFound + } + } + } + // INVAR: f is not nil. + + v := f.view(view) + if v == nil { + return nil, errors.Errorf("view not found: %q", view) + } + + frag := v.Fragment(shard) + + if frag == nil { + return nil, fmt.Errorf("fragment not found: %q / %q / %d", field, view, shard) + } + + // Note: we cannot cache frag into tx.fragment. + // Empirically, it breaks 245 top-level pilosa tests. + // tx.fragment = frag // breaks the world. + + return frag, nil +} + +func (tx *RoaringTx) bitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + frag, err := tx.getFragment(index, field, view, shard) + if err != nil { + return nil, err + } + return frag.storage, nil +} + +type RoaringStore struct{} + +func NewRoaringStore() *RoaringStore { + return &RoaringStore{} +} + +func (db *RoaringStore) Close() error { + return nil +} + +func (db *RoaringStore) DeleteField(index, field, fieldPath string) error { + + // under blue-green badger_roaring, the directory will not be found, b/c badger will have + // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: + // "If the path does not exist, RemoveAll returns nil (no error)" + err := os.RemoveAll(fieldPath) + if err != nil { + return errors.Wrap(err, "removing directory") + } + return nil +} + +// frag should be passed by any RoaringTx user, but for RBF/Badger it can be nil. +func (db *RoaringStore) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { + + fragment, ok := frag.(*fragment) + if !ok { + return fmt.Errorf("RoaringStore.DeleteFragment must get frag of type *fragment, but got '%T'", frag) + } + + // Close data files before deletion. + if err := fragment.Close(); err != nil { + return errors.Wrap(err, "closing fragment") + } + + // Delete fragment file. + if err := os.Remove(fragment.path); err != nil { + return errors.Wrap(err, "deleting fragment file") + } + + // Delete fragment cache file. + if err := os.Remove(fragment.cachePath()); err != nil { + return errors.Wrap(err, fmt.Sprintf("no cache file to delete for shard %d", fragment.shard)) + } + return nil +} + +func (tx *RoaringTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + file, err := os.Open(fragmentPathForRoaring) // open the fragment file + if err != nil { + return nil, -1, err + } + fi, err := file.Stat() + if err != nil { + return nil, -1, errors.Wrap(err, "statting") + } + sz = fi.Size() + r = file + return +} diff --git a/server.go b/server.go index a865ebb4a..1aaa01f7e 100644 --- a/server.go +++ b/server.go @@ -29,8 +29,6 @@ import ( uuid "github.com/satori/go.uuid" - // extensions pulls in some extensions depending on build tags - _ "github.com/pilosa/pilosa/v2/extensions" "github.com/pilosa/pilosa/v2/logger" "github.com/pilosa/pilosa/v2/roaring" "github.com/pilosa/pilosa/v2/stats" diff --git a/server/handler_test.go b/server/handler_test.go index d4648c3c3..31954eff8 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -179,6 +179,17 @@ func TestHandler_Endpoints(t *testing.T) { t.Fatal(err) } defer tx0.Rollback() + if f, err := i0.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeDefault()); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(tx0, 0, 0, nil); err != nil { + t.Fatal(err) + } + if _, err := i0.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil { + t.Fatal(err) + } + if err := tx0.Commit(); err != nil { + t.Fatal(err) + } i1 := hldr.MustCreateIndexIfNotExists("i1", pilosa.IndexOptions{}) tx1, err := holder.BeginTx(true, i1.Index) @@ -186,24 +197,11 @@ func TestHandler_Endpoints(t *testing.T) { t.Fatal(err) } defer tx1.Rollback() - - if f, err := i0.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeDefault()); err != nil { - t.Fatal(err) - } else if _, err := f.SetBit(tx0, 0, 0, nil); err != nil { - t.Fatal(err) - } if f, err := i1.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if _, err := f.SetBit(tx1, 0, 0, nil); err != nil { t.Fatal(err) } - if _, err := i0.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil { - t.Fatal(err) - } - - if err := tx0.Commit(); err != nil { - t.Fatal(err) - } if err := tx1.Commit(); err != nil { t.Fatal(err) } @@ -673,11 +671,13 @@ func TestHandler_Endpoints(t *testing.T) { if field == nil { t.Fatalf("field not found: %s", fieldName) } - if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { - t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) - } - if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { - t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + if field != nil { // happy linter + if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { + t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) + } + if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { + t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + } } }) @@ -702,11 +702,13 @@ func TestHandler_Endpoints(t *testing.T) { if field == nil { t.Fatalf("field not found: %s", fieldName) } - if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { - t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) - } - if !reflect.DeepEqual(pql.NewDecimal(1, -1), field.Options.Max) { - t.Fatalf("field max %d != %d", 10, field.Options.Max) + if field != nil { // happy linter + if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { + t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) + } + if !reflect.DeepEqual(pql.NewDecimal(1, -1), field.Options.Max) { + t.Fatalf("field max %d != %d", 10, field.Options.Max) + } } }) @@ -731,11 +733,13 @@ func TestHandler_Endpoints(t *testing.T) { if field == nil { t.Fatalf("field not found: %s", fieldName) } - if !reflect.DeepEqual(pql.NewDecimal(-1, -1), field.Options.Min) { - t.Fatalf("field min %d != %d", 10, field.Options.Min) - } - if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { - t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + if field != nil { // happy linter + if !reflect.DeepEqual(pql.NewDecimal(-1, -1), field.Options.Min) { + t.Fatalf("field min %d != %d", 10, field.Options.Min) + } + if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { + t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + } } }) @@ -770,11 +774,13 @@ func TestHandler_Endpoints(t *testing.T) { if field == nil { t.Fatalf("field not found: %s", fieldName) } - if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { - t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) - } - if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { - t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + if field != nil { // happy linter + if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 0), field.Options.Min) { + t.Fatalf("field min %d != %d", int64(math.MinInt64), field.Options.Min) + } + if !reflect.DeepEqual(pql.NewDecimal(math.MaxInt64, 0), field.Options.Max) { + t.Fatalf("field max %d != %d", int64(math.MaxInt64), field.Options.Max) + } } }) @@ -800,11 +806,13 @@ func TestHandler_Endpoints(t *testing.T) { if field == nil { t.Fatalf("field not found: %s", fieldName) } - if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 1), field.Options.Min) { - t.Fatalf("field min %d != %d", pql.NewDecimal(math.MinInt64, 1), field.Options.Min) - } - if !reflect.DeepEqual(pql.NewDecimal(105, 1), field.Options.Max) { - t.Fatalf("field max %s != %d", pql.NewDecimal(105, 1), field.Options.Max) + if field != nil { // happy linter + if !reflect.DeepEqual(pql.NewDecimal(math.MinInt64, 1), field.Options.Min) { + t.Fatalf("field min %d != %d", pql.NewDecimal(math.MinInt64, 1), field.Options.Min) + } + if !reflect.DeepEqual(pql.NewDecimal(105, 1), field.Options.Max) { + t.Fatalf("field max %s != %d", pql.NewDecimal(105, 1), field.Options.Max) + } } }) @@ -1054,6 +1062,7 @@ func TestHandler_Endpoints(t *testing.T) { }) t.Run("index handlers", func(t *testing.T) { + // create index w := httptest.NewRecorder() r := test.MustNewHTTPRequest("POST", "/index/idx1", strings.NewReader("")) diff --git a/server_internal_test.go b/server_internal_test.go index a8af8186c..3a302685a 100644 --- a/server_internal_test.go +++ b/server_internal_test.go @@ -23,6 +23,8 @@ import ( // 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.") diff --git a/translator_test.go b/translator_test.go index 9bb381533..dab23bbd2 100644 --- a/translator_test.go +++ b/translator_test.go @@ -289,7 +289,6 @@ func TestTranslation_Reset(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := node0.API.TranslateKeys(ctx, bytes.NewReader(reqBody)); err != nil { t.Fatal(err) } diff --git a/tx.go b/tx.go index 29f2da821..a5c154654 100644 --- a/tx.go +++ b/tx.go @@ -15,18 +15,9 @@ package pilosa import ( - "bytes" - "fmt" "io" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - "github.com/pilosa/pilosa/v2/rbf" "github.com/pilosa/pilosa/v2/roaring" - "github.com/pkg/errors" ) // batch operations want Tx.Add(batched=doBatch), while bit-at-a-time want Tx.Add(batched=!doBatched) @@ -240,824 +231,3 @@ type RawRoaringData struct { func (rr *RawRoaringData) Iterator() (roaring.RoaringIterator, error) { return roaring.NewRoaringIterator(rr.data) } - -// MultiTx implements the transaction interface to combine multiple transactions. -type MultiTx struct { - mu sync.Mutex - writable bool - holder *Holder - index *Index - txs map[multiTxKey]Tx -} - -// NewMultiTx returns a new instance of MultiTx for a Holder. -func NewMultiTx(writable bool, holder *Holder) *MultiTx { - return &MultiTx{ - writable: writable, - holder: holder, - txs: make(map[multiTxKey]Tx), - } -} - -// NewMultiTxWithIndex returns a new instance of MultiTx for a single index. -func NewMultiTxWithIndex(writable bool, index *Index) *MultiTx { - return &MultiTx{ - writable: writable, - index: index, - txs: make(map[multiTxKey]Tx), - } -} - -var _ Tx = (*MultiTx)(nil) - -func (mtx *MultiTx) Type() string { - return RoaringTxn -} - -// debugging, what does this Tx see as its database? -func (mtx *MultiTx) Dump() { - mtx.mu.Lock() - defer mtx.mu.Unlock() - if len(mtx.txs) == 0 { - return - } - for _, tx := range mtx.txs { - tx.Dump() - return - } -} - -func (mtx *MultiTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - tx, err := mtx.txNoShard(index) - panicOn(err) - return tx.SliceOfShards(index, field, view, optionalViewPath) -} - -func (mtx *MultiTx) UseRowCache() bool { - return true -} - -func (mtx *MultiTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { - tx, err := mtx.tx(index, shard) - panicOn(err) - return tx.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) -} - -func (mtx *MultiTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { - tx, err := mtx.tx(index, shard) - panicOn(err) - return tx.NewTxIterator(index, field, view, shard) -} - -// Readonly is true if the transaction is not read-and-write, but only doing reads. -func (mtx *MultiTx) Readonly() bool { - return !mtx.writable -} - -func (mtx *MultiTx) Pointer() string { - return fmt.Sprintf("%p", mtx) -} - -func (mtx *MultiTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { - tx, err := mtx.tx(index, shard) - panicOn(err) - return tx.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) -} - -func (mtx *MultiTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { - tx, err := mtx.tx(index, shard) - panicOn(err) - tx.IncrementOpN(index, field, view, shard, changedN) -} - -// Rollback rolls back all underlying transactions. -func (mtx *MultiTx) Rollback() { - for _, tx := range mtx.txs { - tx.Rollback() - } -} - -// Commit commits all underlying transactions. -func (mtx *MultiTx) Commit() (err error) { - for _, tx := range mtx.txs { - if e := tx.Commit(); e != nil && err == nil { - err = e - } - } - return err -} - -func (mtx *MultiTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return nil, err - } - return tx.RoaringBitmap(index, field, view, shard) -} - -func (mtx *MultiTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return nil, err - } - return tx.Container(index, field, view, shard, key) -} - -func (mtx *MultiTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { - tx, err := mtx.tx(index, shard) - if err != nil { - return err - } - return tx.PutContainer(index, field, view, shard, key, c) -} - -func (mtx *MultiTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { - tx, err := mtx.tx(index, shard) - if err != nil { - return err - } - return tx.RemoveContainer(index, field, view, shard, key) -} - -func (mtx *MultiTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, err - } - return tx.Add(index, field, view, shard, batched, a...) -} - -func (mtx *MultiTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, err - } - return tx.Remove(index, field, view, shard, a...) -} - -func (mtx *MultiTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return false, err - } - return tx.Contains(index, field, view, shard, v) -} - -func (mtx *MultiTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return nil, false, err - } - return tx.ContainerIterator(index, field, view, shard, key) -} - -func (mtx *MultiTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { - tx, err := mtx.tx(index, shard) - if err != nil { - return err - } - return tx.ForEach(index, field, view, shard, fn) -} - -func (mtx *MultiTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { - tx, err := mtx.tx(index, shard) - if err != nil { - return err - } - return tx.ForEachRange(index, field, view, shard, start, end, fn) -} - -func (mtx *MultiTx) Count(index, field, view string, shard uint64) (uint64, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, err - } - return tx.Count(index, field, view, shard) -} - -func (mtx *MultiTx) Max(index, field, view string, shard uint64) (uint64, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, err - } - return tx.Max(index, field, view, shard) -} - -func (mtx *MultiTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, false, err - } - return tx.Min(index, field, view, shard) -} - -func (mtx *MultiTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { - tx, err := mtx.tx(index, shard) - if err != nil { - return err - } - return tx.UnionInPlace(index, field, view, shard, others...) -} - -func (mtx *MultiTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return 0, err - } - return tx.CountRange(index, field, view, shard, start, end) -} - -func (mtx *MultiTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { - tx, err := mtx.tx(index, shard) - if err != nil { - return nil, err - } - return tx.OffsetRange(index, field, view, shard, offset, start, end) -} - -// tx returns a transaction by index/shard. Reuses transaction if already open. -// Otherwise begins a new transaction. -func (mtx *MultiTx) tx(index string, shard uint64) (_ Tx, err error) { - mtx.mu.Lock() - defer mtx.mu.Unlock() - - mkey := multiTxKey{index: index, shard: shard, write: mtx.writable} - - // Lookup transaction from cache. - tx := mtx.txs[mkey] - if tx != nil { - return tx, nil - } - - // If transaction doesn't exist, lookup the index. - idx := mtx.index - if mtx.holder != nil { - if idx = mtx.holder.Index(index); idx == nil { - return nil, ErrIndexNotFound - } - } - - // Begin tranaction & cache it. - if tx, err = idx.BeginTx(mtx.writable, shard); err != nil { - return nil, err - } - mtx.txs[mkey] = tx - - return tx, nil -} - -// version of the above for SliceOfShards(), where we don't have a shard. -func (mtx *MultiTx) txNoShard(index string) (_ Tx, err error) { - mtx.mu.Lock() - defer mtx.mu.Unlock() - - // Lookup transaction from cache. - for _, tx := range mtx.txs { - if tx.(*RoaringTx).Index.name == index { - return tx, nil - } - } - panic(fmt.Sprintf("no prior RoaringTx available, looking up index='%v'", index)) -} - -type multiTxKey struct { - index string - shard uint64 - write bool -} - -// RoaringTx represents a fake transaction object for Roaring storage. -type RoaringTx struct { - write bool - Index *Index - Field *Field - fragment *fragment -} - -func (tx *RoaringTx) Type() string { - return RoaringTxn -} - -func (tx *RoaringTx) Dump() { - fmt.Printf("%v\n", tx.Index.StringifiedRoaringKeys()) -} - -func (tx *RoaringTx) UseRowCache() bool { - return true -} - -func (tx *RoaringTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - - // SliceOfShards is based on view.openFragments() - - file, err := os.Open(filepath.Join(optionalViewPath, "fragments")) - if os.IsNotExist(err) { - return - } else if err != nil { - return nil, errors.Wrap(err, "opening fragments directory") - } - defer file.Close() - - fis, err := file.Readdir(0) - if err != nil { - return nil, errors.Wrap(err, "reading fragments directory") - } - - for _, fi := range fis { - if fi.IsDir() { - continue - } - // Parse filename into integer. - shard, err := strconv.ParseUint(filepath.Base(fi.Name()), 10, 64) - if err != nil { - //AlwaysPrintf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", index, field, view, fi.Name()) - //v.holder.Logger.Debugf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", v.index, v.field, v.name, fi.Name()) - continue - } - sliceOfShards = append(sliceOfShards, shard) - } - return -} - -func (tx *RoaringTx) Pointer() string { - return fmt.Sprintf("%p", tx) -} - -// NewTxIterator returns a *roaring.Iterator that MUST have Close() called on it BEFORE -// the transaction Commits or Rollsback. -func (tx *RoaringTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { - b, err := tx.bitmap(index, field, view, shard) - panicOn(err) - return b.Iterator() -} - -// ImportRoaringBits return values changed and rowSet will be inaccurate if -// the data []byte is supplied. This mimics the traditional roaring-per-file -// and should be faster. -func (tx *RoaringTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { - f, err := tx.getFragment(index, field, view, shard) - if err != nil { - return 0, nil, err - } - if len(data) > 0 { - // changed and rowSet are ignored anyway when len(data) > 0; - // when we are called from fragment.fillFragmentFromArchive() - // which is the only place the data []byte is supplied. - // blueGreenTx also turns off the checks in this case. - return 0, nil, f.readStorageFromArchive(bytes.NewBuffer(data)) - } - - changed, rowSet, err = f.storage.ImportRoaringRawIterator(rit, clear, true, rowSize) - return -} - -func (tx *RoaringTx) Readonly() bool { - return !tx.write -} - -func (tx *RoaringTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { - frag, err := tx.getFragment(index, field, view, shard) - panicOn(err) - frag.incrementOpN(changedN) -} - -// Rollback is a no-op as Roaring does not support transactions. -func (tx *RoaringTx) Rollback() {} - -// Commit is a no-op as Roaring does not support transactions. -func (tx *RoaringTx) Commit() error { - return nil -} - -func (tx *RoaringTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { - return tx.bitmap(index, field, view, shard) -} - -func (tx *RoaringTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return nil, err - } - return b.Containers.Get(key), nil -} - -func (tx *RoaringTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return err - } - b.Containers.Put(key, c) - return nil -} - -func (tx *RoaringTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return err - } - b.Containers.Remove(key) - return nil -} - -func (tx *RoaringTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, err - } - if !batched { - changed, err := b.Add(a...) - if changed { - return 1, err - } - return 0, err - } - - // Note: do not replace b.AddN() with b.DirectAddN(). - // DirectAddN() does not do op-log operations inside roaring - // This creates a problem because RoaringTx needs the op-log - // to know when to flush the fragment to disk. - count, err := b.AddN(a...) // AddN does oplog batches. needed to keep op-log up to date. - - return count, err -} - -func (tx *RoaringTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, err - } - changed, err := b.Remove(a...) // green TestFragment_Bug_Q2DoubleDelete - panicOn(err) - if changed { - return 1, err - } else { - return 0, err - } - - // Note: don't replace b.Remove(a...) with b.RemoveN(a...) or - // with b.DirectRemoveN(a...). If you do, you'll see - // TestFragment_Bug_Q2DoubleDelete go red. -} - -func (tx *RoaringTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return false, err - } - return b.Contains(v), nil -} - -func (tx *RoaringTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return nil, false, err - } - citer, found = b.Containers.Iterator(key) - return citer, found, nil -} - -func (tx *RoaringTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return err - } - return b.ForEach(fn) -} - -func (tx *RoaringTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return err - } - return b.ForEachRange(start, end, fn) -} - -func (tx *RoaringTx) Count(index, field, view string, shard uint64) (uint64, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, err - } - return b.Count(), nil -} - -func (tx *RoaringTx) Max(index, field, view string, shard uint64) (uint64, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, err - } - return b.Max(), nil -} - -func (tx *RoaringTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, false, err - } - v, ok := b.Min() - return v, ok, nil -} - -func (tx *RoaringTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return err - } - b.UnionInPlace(others...) - return nil -} - -func (tx *RoaringTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return 0, err - } - return b.CountRange(start, end), nil -} - -func (tx *RoaringTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { - b, err := tx.bitmap(index, field, view, shard) - if err != nil { - return nil, err - } - return b.OffsetRange(offset, start, end), nil -} - -// getFragment is used by IncrementOpN() and by bitmap() -func (tx *RoaringTx) getFragment(index, field, view string, shard uint64) (*fragment, error) { - - // If a fragment is attached, always use it. Since it was set at Tx creation, - // it is highly likely to be correct. - if tx.fragment != nil { - // but still a basic sanity check. - if tx.fragment.index != index || - tx.fragment.field != field || - tx.fragment.view != view || - tx.fragment.shard != shard { - panic(fmt.Sprintf("different fragment cached vs requested. tx.fragment='%#v', index='%v', field='%v'; view='%v'; shard='%v'", tx.fragment, index, field, view, shard)) - } - return tx.fragment, nil - } - - // If a field is attached, start from there. - // Otherwise look up the field from the index. - f := tx.Field - - if f == nil { - // we cannot assume that the tx.Index that we "started" on is the same - // as the index we are being queried; it might be foreign: TestExecutor_ForeignIndex - // So go through the holder - idx := tx.Index.holder.Index(index) - if idx == nil { - // only thing we can try is the cached index, and hope we aren't being asked for a foreign index. - f = tx.Index.Field(field) - if f == nil { - return nil, ErrFieldNotFound - } - } else { - if f = idx.Field(field); f == nil { - return nil, ErrFieldNotFound - } - } - } - // INVAR: f is not nil. - - v := f.view(view) - if v == nil { - return nil, errors.Errorf("view not found: %q", view) - } - - frag := v.Fragment(shard) - - if frag == nil { - return nil, fmt.Errorf("fragment not found: %q / %q / %d", field, view, shard) - //panic(fmt.Sprintf("fragment not found: %q / %q / %d", field, view, shard)) - } - - // Note: we cannot cache frag into tx.fragment. - // Empirically, it breaks 245 top-level pilosa tests. - // tx.fragment = frag // breaks the world. - - return frag, nil -} - -func (tx *RoaringTx) bitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { - frag, err := tx.getFragment(index, field, view, shard) - if err != nil { - return nil, err - } - return frag.storage, nil -} - -type RoaringStore struct{} - -func NewRoaringStore() *RoaringStore { - return &RoaringStore{} -} - -func (db *RoaringStore) Close() error { - return nil -} - -func (db *RoaringStore) DeleteField(index, field, fieldPath string) error { - - // under blue-green badger_roaring, the directory will not be found, b/c badger will have - // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: - // "If the path does not exist, RemoveAll returns nil (no error)" - err := os.RemoveAll(fieldPath) - if err != nil { - return errors.Wrap(err, "removing directory") - } - return nil -} - -// frag should be passed by any RoaringTx user, but for RBF/Badger it can be nil. -func (db *RoaringStore) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { - - fragment, ok := frag.(*fragment) - if !ok { - return fmt.Errorf("RoaringStore.DeleteFragment must get frag of type *fragment, but got '%T'", frag) - } - - // Close data files before deletion. - if err := fragment.Close(); err != nil { - return errors.Wrap(err, "closing fragment") - } - - // Delete fragment file. - if err := os.Remove(fragment.path); err != nil { - return errors.Wrap(err, "deleting fragment file") - } - - // Delete fragment cache file. - if err := os.Remove(fragment.cachePath()); err != nil { - return errors.Wrap(err, fmt.Sprintf("no cache file to delete for shard %d", fragment.shard)) - } - return nil -} - -func (tx *RoaringTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { - file, err := os.Open(fragmentPathForRoaring) // open the fragment file - if err != nil { - return nil, -1, err - } - fi, err := file.Stat() - if err != nil { - return nil, -1, errors.Wrap(err, "statting") - } - sz = fi.Size() - r = file - return -} - -type RBFTx struct { - index string - tx *rbf.Tx -} - -func (tx *RBFTx) Type() string { - return RBFTxn -} - -func (tx *RBFTx) Rollback() { - tx.tx.Rollback() -} - -func (tx *RBFTx) Commit() error { - return tx.tx.Commit() -} - -func (tx *RBFTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { - return tx.tx.RoaringBitmap(rbfName(field, view, shard)) -} - -func (tx *RBFTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { - return tx.tx.Container(rbfName(field, view, shard), key) -} - -func (tx *RBFTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { - return tx.tx.PutContainer(rbfName(field, view, shard), key, c) -} - -func (tx *RBFTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { - return tx.tx.RemoveContainer(rbfName(field, view, shard), key) -} - -func (tx *RBFTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { - return tx.tx.Add(rbfName(field, view, shard), a...) -} - -func (tx *RBFTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { - return tx.tx.Remove(rbfName(field, view, shard), a...) -} - -func (tx *RBFTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { - return tx.tx.Contains(rbfName(field, view, shard), v) -} - -func (tx *RBFTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { - return tx.tx.ContainerIterator(rbfName(field, view, shard), key) -} - -func (tx *RBFTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { - return tx.tx.ForEach(rbfName(field, view, shard), fn) -} - -func (tx *RBFTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { - return tx.tx.ForEachRange(rbfName(field, view, shard), start, end, fn) -} - -func (tx *RBFTx) Count(index, field, view string, shard uint64) (uint64, error) { - return tx.tx.Count(rbfName(field, view, shard)) -} - -func (tx *RBFTx) Max(index, field, view string, shard uint64) (uint64, error) { - return tx.tx.Max(rbfName(field, view, shard)) -} - -func (tx *RBFTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { - return tx.tx.Min(rbfName(field, view, shard)) -} - -func (tx *RBFTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { - return tx.tx.UnionInPlace(rbfName(field, view, shard), others...) -} - -func (tx *RBFTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { - return tx.tx.CountRange(rbfName(field, view, shard), start, end) -} - -func (tx *RBFTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { - return tx.tx.OffsetRange(rbfName(field, view, shard), offset, start, end) -} - -func (tx *RBFTx) IncrementOpN(index, field, view string, shard uint64, changedN int) {} - -func (tx *RBFTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) { - return tx.tx.ImportRoaringBits(rbfName(field, view, shard), rit, clear, log, rowSize, data) -} - -func (tx *RBFTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { - panic("TODO: Implement RBFTx.RoaringBitmapReader()") -} - -func (tx *RBFTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - prefix := rbfFieldViewPrefix(field, view) - - names, err := tx.tx.BitmapNames() - if err != nil { - return nil, err - } - - // Iterate over shard names and collect shards from matching field/view prefix. - for _, name := range names { - if !strings.HasPrefix(name, prefix) { - continue - } - - s := strings.TrimPrefix(name, prefix) - shard, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return nil, errors.Wrap(err, "parse shard id from rbf key") - } - sliceOfShards = append(sliceOfShards, shard) - } - return sliceOfShards, nil -} - -func (tx *RBFTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { - b, err := tx.RoaringBitmap(index, field, view, shard) - panicOn(err) - return b.Iterator() -} - -func (tx *RBFTx) Pointer() string { - return fmt.Sprintf("%p", tx) -} - -func (tx *RBFTx) Dump() { - tx.tx.Dump(tx.index) -} - -// Readonly is true if the transaction is not read-and-write, but only doing reads. -func (tx *RBFTx) Readonly() bool { - return !tx.tx.Writable() -} - -func (tx *RBFTx) UseRowCache() bool { - return false -} - -// rbfName returns a NULL-separated key used for identifying bitmap maps in RBF. -func rbfName(field, view string, shard uint64) string { - return fmt.Sprintf("%s\x00%s\x00%d", field, view, shard) -} - -// rbfFieldPrefix returns a prefix for field keys in RBF. -func rbfFieldPrefix(field string) string { - return fmt.Sprintf("%s\x00", field) -} - -// rbfFieldViewPrefix returns a NULL-separated prefix for keys in RBF. -func rbfFieldViewPrefix(field, view string) string { - return fmt.Sprintf("%s\x00%s\x00", field, view) -} diff --git a/txfactory.go b/txfactory.go index aa1384972..eb5953d7f 100644 --- a/txfactory.go +++ b/txfactory.go @@ -23,8 +23,8 @@ import ( "syscall" "text/tabwriter" - "github.com/pilosa/pilosa/v2/rbf" "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/txpath" "github.com/pkg/errors" ) @@ -32,6 +32,7 @@ import ( const ( RoaringTxn string = "roaring" BadgerTxn string = "badger" + LmdbTxn string = "lmdb" RBFTxn string = "rbf" // A is listed first, B is second. blueGreenTx returns the B output. BlueGreenBadgerRoaring string = "badger_roaring" @@ -72,10 +73,14 @@ type TxFactory struct { badgerDB *BadgerDBWrapper - rbfDB *rbf.DB + rbfDB *RbfDBWrapper roaringDB *RoaringStore + dbsClosed bool // idemopotent CloseDB() + + //lmDB *LMDBWrapper + // could have more than one *Index, but for now keep it simple, // and allow blueGreenTx to report badger contents via idx idx *Index @@ -100,6 +105,8 @@ const ( blueGreenBadgerRBF txtype = 8 blueGreenRBFBadger txtype = 9 + + lmdbTxn txtype = 10 ) func (txf *TxFactory) NeedsSnapshot() bool { @@ -124,6 +131,8 @@ func (txf *TxFactory) NeedsSnapshot() bool { return false case blueGreenRBFBadger: return false + case lmdbTxn: + return false } panic(fmt.Sprintf("unknown typeOfTx '%v'", txf.typeOfTx)) } @@ -148,16 +157,21 @@ func MustTxsrcToTxtype(txsrc string) txtype { return blueGreenBadgerRBF case BlueGreenRBFBadger: // "rbf_badger" return blueGreenRBFBadger + case LmdbTxn: + return lmdbTxn } panic(fmt.Sprintf("unknown txsrc '%v'", txsrc)) } -// always store files in a subdir of dir. If we are having one +// NewTxFactory always opens an existing database. If you +// want to a fresh database, os.RemoveAll on dir/name ahead of time. +// We always store files in a subdir of dir. If we are having one // database or many can depend on name. -func NewTxFactory(txsrc string, dir, name string, openExisting bool) (f *TxFactory, err error) { +func NewTxFactory(txsrc string, dir, name string) (f *TxFactory, err error) { + //vv("NewTxFactory called for txsrc '%v'; dir='%v'; name='%v'", txsrc, dir, name) ty := MustTxsrcToTxtype(txsrc) - if ty < 1 || ty > 9 { + if ty < 1 || ty > 10 { panic(fmt.Sprintf("invalid txtype '%v'", int(ty))) } @@ -175,17 +189,11 @@ func NewTxFactory(txsrc string, dir, name string, openExisting bool) (f *TxFacto // enables cross-index Tx, which are important and are tested for. path := dir + sep + "honeyBadger" - if openExisting { - f.badgerDB, err = globalBadgerReg.openBadgerDBWrapper(path) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("cannot open badger db. path='%v'", path)) - } - } else { - f.badgerDB, err = globalBadgerReg.newBadgerDBWrapper(path) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("cannot create new badger db. path='%v'", path)) - } + f.badgerDB, err = globalBadgerReg.openBadgerDBWrapper(path) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("cannot open badger db. path='%v'", path)) } + // electric-fence like finding of access to mmapped data beyond // transaction end time. f.badgerDB.doAllocZero = DetectMemAccessPastTx @@ -194,12 +202,25 @@ func NewTxFactory(txsrc string, dir, name string, openExisting bool) (f *TxFacto switch ty { case rbfTxn, blueGreenRBFRoaring, blueGreenRoaringRBF, blueGreenBadgerRBF, blueGreenRBFBadger: - f.rbfDB = rbf.NewDB(filepath.Join(dir, "db.rbf")) - if err := f.rbfDB.Open(); err != nil { - return nil, errors.Wrap(err, "cannot open rbf db") + path := dir + sep + "all-in-one-rbfdb" + f.rbfDB, err = globalRbfDBReg.openRbfDB(path) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("cannot create new rbf db. path='%v'", path)) } } + switch ty { + case lmdbTxn: + panic("lmdb is unfinished and relocated to the ldmb/ subdirectory for the moment.") + /* + path := dir + sep + "all-in-one" + f.lmDB, err = globalLMDBReg.newLMDBWrapper(path) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("cannot create new lmdb db. path='%v'", path)) + } + */ + } + return f, err } @@ -224,7 +245,7 @@ func (f *TxFactory) DeleteIndex(name string) error { case badgerTxn: return f.badgerDB.DeleteIndex(name) case rbfTxn: - panic("todo rbfTxn DeleteIndex(name)") + return f.rbfDB.DeleteIndex(name) case blueGreenBadgerRoaring: return f.badgerDB.DeleteIndex(name) case blueGreenRoaringBadger: @@ -239,22 +260,10 @@ func (f *TxFactory) DeleteFieldFromStore(index, field, fieldPath string) error { return f.roaringDB.DeleteField(index, field, fieldPath) case badgerTxn: return f.badgerDB.DeleteField(index, field, fieldPath) + //case lmdbTxn: + //return f.lmDB.DeleteField(index, field, fieldPath) case rbfTxn: - if err := os.RemoveAll(fieldPath); err != nil { - return errors.Wrap(err, "removing directory") - } - - tx, err := f.rbfDB.Begin(true) - if err != nil { - return err - } - defer tx.Rollback() - - if err := tx.DeleteBitmapsWithPrefix(rbfFieldPrefix(field)); err != nil { - return err - } - return tx.Commit() - + return f.rbfDB.DeleteField(index, field, fieldPath) case blueGreenBadgerRoaring: _ = f.badgerDB.DeleteField(index, field, fieldPath) return f.roaringDB.DeleteField(index, field, fieldPath) @@ -272,16 +281,9 @@ func (f *TxFactory) DeleteFragmentFromStore(index, field, view string, shard uin case badgerTxn: return f.badgerDB.DeleteFragment(index, field, view, shard, frag) case rbfTxn: - tx, err := f.rbfDB.Begin(true) - if err != nil { - return err - } - defer tx.Rollback() - - if err := tx.DeleteBitmapsWithPrefix(rbfFieldViewPrefix(field, view)); err != nil { - return err - } - return tx.Commit() + return f.rbfDB.DeleteFragment(index, field, view, shard, frag) + // case lmdbTxn: + // return f.lmDB.DeleteFragment(index, field, view, shard, frag) case blueGreenBadgerRoaring: _ = f.badgerDB.DeleteFragment(index, field, view, shard, frag) return f.roaringDB.DeleteFragment(index, field, view, shard, frag) @@ -294,32 +296,38 @@ func (f *TxFactory) DeleteFragmentFromStore(index, field, view string, shard uin } func (f *TxFactory) CloseIndex(idx *Index) error { + // under roaring and all the new databases, this is a no-op. + return nil +} + +func (f *TxFactory) CloseDB() error { + if f.dbsClosed { + return nil + } + f.dbsClosed = true switch f.typeOfTx { case roaringFragmentFilesTxn: return nil case badgerTxn: - // note cannot actually close Badger here. - // causes problems b/c tries holder.DeleteIndex tries to delete the index after db is closed. - //return f.badgerDB.Close() - return nil + return f.badgerDB.Close() case rbfTxn: return f.rbfDB.Close() case blueGreenBadgerRoaring: - return nil + return f.badgerDB.Close() case blueGreenRoaringBadger: - return nil - + return f.badgerDB.Close() case blueGreenRBFRoaring: - _ = f.rbfDB.Close() - return nil + return f.rbfDB.Close() case blueGreenRoaringRBF: return f.rbfDB.Close() case blueGreenBadgerRBF: + _ = f.badgerDB.Close() return f.rbfDB.Close() case blueGreenRBFBadger: _ = f.rbfDB.Close() + return f.badgerDB.Close() + case lmdbTxn: return nil - } panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) } @@ -334,52 +342,56 @@ func (f *TxFactory) NewTx(o Txo) Tx { case roaringFragmentFilesTxn: return &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} case badgerTxn: - btx := f.badgerDB.NewBadgerTx(o.Write, indexName) + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) return btx case rbfTxn: - tx, err := f.rbfDB.Begin(o.Write) + tx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) + panicOn(err) if err != nil { - panic(err) // TODO: Add error return on NewTx() + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) } - return &RBFTx{tx: tx, index: indexName} + return tx + case lmdbTxn: + //return f.lmDB.newPoolTx(o.Write, indexName) + case blueGreenBadgerRoaring: - btx := f.badgerDB.NewBadgerTx(o.Write, indexName) + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} return newBlueGreenTx(btx, rtx, f.idx) case blueGreenRoaringBadger: - btx := f.badgerDB.NewBadgerTx(o.Write, indexName) + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} return newBlueGreenTx(rtx, btx, f.idx) case blueGreenBadgerRBF: - btx := f.badgerDB.NewBadgerTx(o.Write, indexName) - rbftx, err := f.rbfDB.Begin(o.Write) + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) + rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) if err != nil { - panic(errors.Wrap(err, "rbfDB.Begin transaction errored")) + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) } - return newBlueGreenTx(btx, &RBFTx{tx: rbftx, index: indexName}, f.idx) + return newBlueGreenTx(btx, rbftx, f.idx) case blueGreenRBFBadger: - btx := f.badgerDB.NewBadgerTx(o.Write, indexName) - rbftx, err := f.rbfDB.Begin(o.Write) + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) + rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) if err != nil { - panic(errors.Wrap(err, "rbfDB.Begin transaction errored")) + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) } - return newBlueGreenTx(&RBFTx{tx: rbftx, index: indexName}, btx, f.idx) + return newBlueGreenTx(rbftx, btx, f.idx) case blueGreenRBFRoaring: - rbftx, err := f.rbfDB.Begin(o.Write) + rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) if err != nil { - panic(errors.Wrap(err, "rbfDB.Begin transaction errored")) + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) } rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} - return newBlueGreenTx(&RBFTx{tx: rbftx, index: indexName}, rtx, f.idx) + return newBlueGreenTx(rbftx, rtx, f.idx) case blueGreenRoaringRBF: - rbftx, err := f.rbfDB.Begin(o.Write) + rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) if err != nil { - panic(errors.Wrap(err, "rbfDB.Begin transaction errored")) + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) } rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} - return newBlueGreenTx(rtx, &RBFTx{tx: rbftx, index: indexName}, f.idx) + return newBlueGreenTx(rtx, rbftx, f.idx) } panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) } @@ -406,6 +418,8 @@ func (ty txtype) String() string { return "blueGreenBadgerRBF" case blueGreenRBFBadger: return "blueGreenRBFBadger" + case lmdbTxn: + return "lmdbTxn" } panic(fmt.Sprintf("unhandled ty '%v' in txtype.String()", int(ty))) } @@ -550,7 +564,7 @@ func stringifiedRawRoaringFragment(path string, index, field, view string, shard srbm := bitmapAsString(rbm) panicOn(err) - bkey := string(badgerKey(index, field, view, shard, ckey)) + bkey := string(txpath.Key(index, field, view, shard, ckey)) r += fmt.Sprintf("%v -> %v (%v hot)\n", bkey, hash, ct.N()) r += " ......." + srbm + "\n" @@ -619,16 +633,16 @@ var _ = fileSize // happy linter func containerToBytes(ct *roaring.Container) []byte { ty := roaring.ContainerType(ct) switch ty { - case containerNil: - panic("nil container") - case containerArray: + case roaring.ContainerNil: + panic("nil roaring.Container") + case roaring.ContainerArray: return fromArray16(roaring.AsArray(ct)) - case containerBitmap: + case roaring.ContainerBitmap: return fromArray64(roaring.AsBitmap(ct)) - case containerRun: + case roaring.ContainerRun: return fromInterval16(roaring.AsRuns(ct)) } - panic(fmt.Sprintf("unknown container type '%v'", int(ty))) + panic(fmt.Sprintf("unknown roaring.Container type '%v'", int(ty))) } type pointerContext struct { diff --git a/txpath/txpath.go b/txpath/txpath.go new file mode 100644 index 000000000..a28a2ae89 --- /dev/null +++ b/txpath/txpath.go @@ -0,0 +1,157 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package txpath consolidates in one place the use of keys to index into our +// various storage/txn back-ends. Databases badgerDB and rbfDB both use it, +// so that debug Dumps are comparable. +package txpath + +import ( + "bytes" + "fmt" + "strconv" +) + +// Key produces the bytes that we use as a key to query the storage/tx engine. +// The roaringContainerKey argument is a container key into a roaring Container. +// Output examples: +// +// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@00000000000000000000" // smallest container-key +// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@18446744073709551615" // largest container-key (math.MaxUint64) +// +// NB must be kept in sync with Prefix() and KeyExtractContainerKey(). +// +func Key(index, field, view string, shard uint64, roaringContainerKey uint64) []byte { + // The %020d which adds zero padding up to 20 runes is required to + // allow the textual sort to accurately + // reflect a numeric sort order. This is because, as a string, + // math.MaxUint64 is 20 bytes long. + // Example of such a Key with a container-key that is math.MaxUint64: + // ...........................................12345678901234567890 + // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 + + prefix := Prefix(index, field, view, shard) + ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) + bkey := append(prefix, ckey...) + MustValidateKey(bkey) + return bkey +} + +var ckeyPartExpected = []byte(";ckey@") + +// MustValidatekey will panic on a bad Key with an informative message. +func MustValidateKey(bkey []byte) { + n := len(bkey) + if n < 56 { + panic(fmt.Sprintf("bkey too short min size is 56 but we see %v in '%v'", n, string(bkey))) + } + beforeCkey := bkey[n-26 : n-20] + if !bytes.Equal(beforeCkey, ckeyPartExpected) { + panic(fmt.Sprintf(`bkey did not have expected ";ckey@" at 26 bytes from the end of the bkey '%v'; instead had '%v'`, string(bkey), string(beforeCkey))) + } +} + +func ShardFromKey(bkey []byte) (shard uint64) { + MustValidateKey(bkey) + + n := len(bkey) + // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 -> idx:'i';fld:'f';vw:'standard';shd:'1 + by := bkey[:n-27] + beg := bytes.LastIndex(by, []byte("'")) + if beg == -1 { + panic(fmt.Sprintf("bad bkey='%v' did not have single quote to being shard decoding", string(bkey))) + } + parseMe := string(by[beg+1:]) + shard, err := strconv.ParseUint(parseMe, 10, 64) + if err != nil { + panic(fmt.Sprintf("could not parse parseMe '%v' in strconv.ParseUint(), error: '%v'", parseMe, err)) + } + return shard +} + +func ShardFromPrefix(prefix []byte) (shard uint64) { + + n := len(prefix) + // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@ -> idx:'i';fld:'f';vw:'standard';shd:'1 + by := prefix[:n-7] + beg := bytes.LastIndex(by, []byte("'")) + if beg == -1 { + panic(fmt.Sprintf("bad prefix='%v' did not have single quote to being shard decoding", string(prefix))) + } + parseMe := string(by[beg+1:]) + shard, err := strconv.ParseUint(parseMe, 10, 64) + if err != nil { + panic(fmt.Sprintf("could not parse parseMe '%v' in strconv.ParseUint(), error: '%v'", parseMe, err)) + } + return shard +} + +// KeyAndPrefix returns the equivalent of Key() and Prefix() calls. +func KeyAndPrefix(index, field, view string, shard uint64, roaringContainerKey uint64) (key, prefix []byte) { + prefix = Prefix(index, field, view, shard) + ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) + bkey := append(prefix, ckey...) + MustValidateKey(bkey) + return bkey, prefix +} + +var _ = KeyAndPrefix // keep linter happy + +// KeyExtractContainerKey extracts the containerKey from bkey. +func KeyExtractContainerKey(bkey []byte) (containerKey uint64) { + MustValidateKey(bkey) + // The zero padding means that the container-key is always the last 20 bytes of the bkey. + // + // Be sure to catch the problematic case of a user passing in only a prefix. A prefix + // ends in 'key@' rather than a full key that has 'key@00000000000000000001' (for example) + // at the end. The ParseUint call below will fail in that case. + n := len(bkey) + if n < 20 { + panic(fmt.Sprintf("KeyExtractContainerKey() error: bad bkey '%v', too short!", string(bkey))) + } + last := bkey[n-20:] // Key() and Prefix() always return more than 20 rune []byte. + var err error + containerKey, err = strconv.ParseUint(string(last), 10, 64) // has to be the container key + if err != nil { + panic(fmt.Sprintf("KeyExtractContainerKey() error: bad bkey '%v', could not convert last 20 bytes ('%v') to a unit64: '%v'", string(bkey), string(last), err)) + } + return +} + +func AllShardPrefix(index, field, view string) []byte { + return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:", index, field, view)) +} + +// Prefix returns everything from Key up to and +// including the '@' fune in a Key. The prefix excludes the roaring container key itself. +// NB must be kept in sync with Key() and KeyExtractContainerKey(). +func Prefix(index, field, view string, shard uint64) []byte { + return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:'%020v';ckey@", index, field, view, shard)) +} + +// IndexOnlyPrefix returns a prefix suitable for DeleteIndex and a key-scan to +// remove all storage associated with one index. +// +// The full name of the index must be provided, no partial index names will work. +// +// The provided key is terminated by `';` and so DeleteIndex("i") will not delete the index "i2". +// +func IndexOnlyPrefix(indexName string) []byte { + return []byte(fmt.Sprintf("idx:'%v';", indexName)) +} + +// same for deleting a whole field. +func FieldPrefix(index, field string) []byte { + return []byte(fmt.Sprintf("idx:'%v';fld:'%v';", index, field)) +} diff --git a/txpath/txpath_test.go b/txpath/txpath_test.go new file mode 100644 index 000000000..d30fd7d7f --- /dev/null +++ b/txpath/txpath_test.go @@ -0,0 +1,89 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package txpath + +import ( + "bytes" + "fmt" + "strconv" + "testing" +) + +func Test_KeyPrefix(t *testing.T) { + + // Prefix() must agree with Key(), but not have the key at the end. + // This is important for iteration over containers. + + index, field, view, shard := "i", "f", "v", uint64(0) + + // needle examples with the container-key extremes: + // "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" // smallest + // "index:'i';field:'f';view:'v';shard:'0';key@18446744073709551615" // largest + needle := Key(index, field, view, shard, 0) + + // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" + prefix := Prefix(index, field, view, shard) + + if !bytes.HasPrefix(needle, prefix) { + panic(fmt.Sprintf("Prefix() output '%v'was not a prefix of Key() '%v'", string(needle), string(prefix))) + } + if len(prefix)+20 != len(needle) { + panic(fmt.Sprintf("Prefix() output '%v'was 20 characters shorter than Key() '%v'", string(needle), string(prefix))) + } + + // validate assumption that KeyExtractContainerKey() makes about strconv.ParseUint() error reporting; + // for distinguishing prefixes from full keys. Even if the shard number is so large that the prefix + // starts with a legitimate decimal number. + shouldNotParse := "12345123451234';key@" + containerKey, err := strconv.ParseUint(shouldNotParse, 10, 64) + if err == nil { + panic(fmt.Sprintf("strconv.ParseUint should have returned an error parsing this string '%v'; instead we got '%v'", shouldNotParse, containerKey)) + } + + // verify panic on submitting a prefix + func() { + defer func() { + r := recover() + if r == nil { + panic(fmt.Sprintf("should have seen panic on call to KeyExtractContainerKey(prefix='%v')", prefix)) + } + }() + KeyExtractContainerKey(prefix) // should panic. + }() +} + +func Test_ShardFromKey(t *testing.T) { + if ShardFromKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615")) != 1 { + panic("problem") + } + if ShardFromKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'0';ckey@18446744073709551615")) != 0 { + panic("problem") + } + if ShardFromKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'18446744073709551615';ckey@18446744073709551615")) != 18446744073709551615 { + panic("problem") + } + + func() { + defer func() { + r := recover() + if r == nil { + panic("should have panic-ed") + } + }() + // called for the panic of a short ckey, only 19 bytes instead of 20 + ShardFromKey([]byte("idx:'i';fld:'f';vw:'standard';shd:'18446744073709551615';ckey@1844674407370955161")) + }() + +} diff --git a/extensions/dummy.go b/txpath/txprefix.go~ similarity index 81% rename from extensions/dummy.go rename to txpath/txprefix.go~ index cf418edb5..f998a219c 100644 --- a/extensions/dummy.go +++ b/txpath/txprefix.go~ @@ -1,4 +1,4 @@ -// Copyright 2019 Pilosa Corp. +// Copyright 2020 Pilosa Corp. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This package contains only things which are conditional on build -// tags. - -package extensions +package txprefix diff --git a/txpath/txprefix_test.go~ b/txpath/txprefix_test.go~ new file mode 100644 index 000000000..8bde1e29d --- /dev/null +++ b/txpath/txprefix_test.go~ @@ -0,0 +1,44 @@ +package txprefix + +func TestBadger_KeyPrefix(t *testing.T) { + + // txprefix.Prefix() must agree with txprefix.Key(), but not have the key at the end. + // This is important for iteration over containers. + + index, field, view, shard := "i", "f", "v", uint64(0) + + // needle examples with the container-key extremes: + // "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" // smallest + // "index:'i';field:'f';view:'v';shard:'0';key@18446744073709551615" // largest + needle := txprefix.Key(index, field, view, shard, 0) + + // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" + prefix := txprefix.Prefix(index, field, view, shard) + + if !bytes.HasPrefix(needle, prefix) { + panic(fmt.Sprintf("txprefix.Prefix() output '%v'was not a prefix of txprefix.Key() '%v'", string(needle), string(prefix))) + } + if len(prefix)+20 != len(needle) { + panic(fmt.Sprintf("txprefix.Prefix() output '%v'was 20 characters shorter than txprefix.Key() '%v'", string(needle), string(prefix))) + } + + // validate assumption that txprefix.KeyExtractContainerKey() makes about strconv.ParseUint() error reporting; + // for distinguishing prefixes from full keys. Even if the shard number is so large that the prefix + // starts with a legitimate decimal number. + shouldNotParse := "12345123451234';key@" + containerKey, err := strconv.ParseUint(shouldNotParse, 10, 64) + if err == nil { + panic(fmt.Sprintf("strconv.ParseUint should have returned an error parsing this string '%v'; instead we got '%v'", shouldNotParse, containerKey)) + } + + // verify panic on submitting a prefix + func() { + defer func() { + r := recover() + if r == nil { + panic(fmt.Sprintf("should have seen panic on call to txprefix.KeyExtractContainerKey(prefix='%v')", prefix)) + } + }() + txprefix.KeyExtractContainerKey(prefix) // should panic. + }() +}