diff --git a/.gitignore b/.gitignore index 8db43b3a7..8468c5367 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor .protoc-gen-gofast .DS_Store build +*~ diff --git a/Dockerfile b/Dockerfile index 17b9d0080..a30097e1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ARG MAKE_FLAGS COPY . pilosa -RUN cd pilosa && CGO_ENABLED=0 make install FLAGS="-a -mod=vendor ${BUILD_FLAGS}" ${MAKE_FLAGS} +RUN cd pilosa && make install FLAGS="-a -mod=vendor ${BUILD_FLAGS}" ${MAKE_FLAGS} FROM alpine:3.9.4 diff --git a/Dockerfile-clustertests b/Dockerfile-clustertests index 6241ae70d..8a155b9e9 100644 --- a/Dockerfile-clustertests +++ b/Dockerfile-clustertests @@ -8,7 +8,7 @@ LABEL maintainer "dev@pilosa.com" COPY . /go/src/github.com/pilosa/pilosa/ RUN cd /go/src/github.com/pilosa/pilosa \ - && CGO_ENABLED=0 make install FLAGS="-a -mod=vendor" + && make install FLAGS="-a -mod=vendor" # download pumba for fault injection ADD https://github.com/alexei-led/pumba/releases/download/0.6.0/pumba_linux_amd64 /pumba diff --git a/Makefile b/Makefile index ca1963417..88a3f452b 100644 --- a/Makefile +++ b/Makefile @@ -184,6 +184,12 @@ 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-lmdb-race: + mv log.topt.lmdb log.topt.lmdb.prev || true + PILOSA_TXSRC=lmdb go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb-race + @echo " log.topt.lmdb-race green: \c"; cat log.topt.lmdb-race | grep PASS |wc -l + @echo " log.topt.lmdb-race red: \c"; cat log.topt.lmdb-race | 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 @@ -195,41 +201,64 @@ topt-race: bg-rr: # shorthand for bluegreen test with A:badger; B:roaring mv log.bg-rr log.bg-rr.prev || true - set -o pipefail; PILOSA_TXSRC=badger_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rr + PILOSA_TXSRC=badger_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rr @echo " log.bg-rr green: \c"; cat log.bg-rr | grep PASS |wc -l @echo " log.bg-rr red: \c"; cat log.bg-rr | grep '\-\-\- FAIL' |wc -l rr-bg: # bluegreen with A:roaring; B:badger (B's values are returned). mv log.bg.roar_bg log.bg.roar_bg.prev || true - set -o pipefail; PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg - ##PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg + PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg @echo " log.rr-bg green: \c"; cat log.rr-bg | grep PASS |wc -l @echo " log.rr-bg red: \c"; cat log.rr-bg | grep '\-\-\- FAIL' |wc -l rbf-rr: mv log.rbf-rr log.rbf-rr.prev || true - set -o pipefail; PILOSA_TXSRC=rbf_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-rr + PILOSA_TXSRC=rbf_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-rr @echo " log.rbf-rr green: \c"; cat log.rbf-rr | grep PASS |wc -l @echo " log.rbf-rr red: \c"; cat log.rbf-rr | grep '\-\-\- FAIL' |wc -l rr-rbf: mv log.rr-rbf log.rr-rbf.prev || true - set -o pipefail; PILOSA_TXSRC=roaring_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-rbf + PILOSA_TXSRC=roaring_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-rbf @echo " log.rr-rbf green: \c"; cat log.rr-rbf | grep PASS |wc -l @echo " log.rr-rbf red: \c"; cat log.rr-rbf | grep '\-\-\- FAIL' |wc -l rbf-bg: mv log.rbf-bg log.rbf-bg.prev || true - set -o pipefail; PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-bg + PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-bg @echo " log.rbf-bg green: \c"; cat log.rbf-bg | grep PASS |wc -l @echo " log.rbf-bg red: \c"; cat log.rbf-bg | grep '\-\-\- FAIL' |wc -l bg-rbf: mv log.bg-rbf log.bg-rbf.prev || true - set -o pipefail; PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rbf + PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rbf @echo " log.bg-rbf green: \c"; cat log.bg-rbf | grep PASS |wc -l @echo " log.bg-rbf red: \c"; cat log.bg-rbf | grep '\-\-\- FAIL' |wc -l +lm-rr: + mv log.lm-rr log.lm-rr.prev || true + PILOSA_TXSRC=lmdb_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rr + @echo " log.lm-rr green: \c"; cat log.lm-rr | grep PASS |wc -l + @echo " log.lm-rr red: \c"; cat log.lm-rr | grep '\-\-\- FAIL' |wc -l + +rr-lm: + mv log.rr-lm log.rr-lm.prev || true + PILOSA_TXSRC=roaring_lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-lm + @echo " log.rr-lm green: \c"; cat log.rr-lm | grep PASS |wc -l + @echo " log.rr-lm red: \c"; cat log.rr-lm | grep '\-\-\- FAIL' |wc -l + +bg-lm: + mv log.topt.bg-lm log.topt.bg-lm.prev || true + PILOSA_TXSRC=badger_lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.bg-lm + @echo " log.topt.bg-lm green: \c"; cat log.topt.bg-lm | grep PASS |wc -l + @echo " log.topt.bg-lm red: \c"; cat log.topt.bg-lm | grep '\-\-\- FAIL' |wc -l + +lm-bg: + mv log.topt.lm-bg log.topt.lm-bg.prev || true + PILOSA_TXSRC=lmdb_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lm-bg + @echo " log.topt.lm-bg green: \c"; cat log.topt.lm-bg | grep PASS |wc -l + @echo " log.topt.lm-bg red: \c"; cat log.topt.lm-bg | grep '\-\-\- FAIL' |wc -l + # Run golangci-lint golangci-lint: require-golangci-lint diff --git a/badger.go b/badger.go index bc217ac89..3ab0dbb5f 100644 --- a/badger.go +++ b/badger.go @@ -20,6 +20,7 @@ import ( "io" "io/ioutil" "log" + "math" "os" "runtime" "sort" @@ -31,7 +32,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/pilosa/pilosa/v2/txkey" "github.com/pkg/errors" ) @@ -345,7 +346,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 := txpath.IndexOnlyPrefix(indexName) + prefix := txkey.IndexOnlyPrefix(indexName) return w.DeletePrefix(prefix) } @@ -438,6 +439,8 @@ type BadgerDBWrapper struct { startStack string DeleteEmptyContainer bool + + writeBatch *badger.WriteBatch } // unprotectedListOpenTxAsString is a debugging helper. @@ -488,9 +491,7 @@ func (w *BadgerDBWrapper) NewBadgerTx(write bool, initialIndexName string, frag doAllocZero: w.doAllocZero, initialIndexName: initialIndexName, DeleteEmptyContainer: w.DeleteEmptyContainer, - //initloc: "", // stack(), } - return } @@ -529,9 +530,6 @@ type BadgerTx struct { frag *fragment opcount int - // keep linter happy, comment out until needed again for debugging. - //initloc string // stack trace of where we were initially created. - doAllocZero bool // for tracking txn boundary issues, track all the memory @@ -644,6 +642,7 @@ func (tx *BadgerTx) Commit() error { if tx.doAllocZero { tx.overWriteOurAllocs() } + return err } @@ -677,7 +676,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 := txpath.Key(index, field, view, shard, ckey) + bkey := txkey.Key(index, field, view, shard, ckey) tx.mu.Lock() var item *badger.Item item, err = tx.tx.Get(bkey) @@ -699,10 +698,41 @@ func (tx *BadgerTx) Container(index, field, view string, shard uint64, ckey uint return } +func (w *BadgerDBWrapper) NewWriteBatch() { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch != nil { + panic("must FlushWriteBatch() before calling NewWriteBatch()") + } + w.writeBatch = w.db.NewWriteBatch() +} + +// Flush any remaining un-committed writes in progress. +func (w *BadgerDBWrapper) FlushWriteBatch() (err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch == nil { + panic("CommitWriteBatch error: no batch in progress") + } + err = w.writeBatch.Flush() + w.writeBatch = nil + return +} + +// Cancel any remaining un-committed writes in progress. +func (w *BadgerDBWrapper) CancelWriteBatch() { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch == nil { + panic("CancelWriteBatch error: no batch in progress") + } + w.writeBatch.Cancel() +} + // 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 := txpath.Key(index, field, view, shard, ckey) + bkey := txkey.Key(index, field, view, shard, ckey) var by []byte ct := roaring.ContainerType(rc) @@ -719,13 +749,24 @@ func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey u default: panic(fmt.Sprintf("unknown roaring.Container type: %v", ct)) } - tx.writeCount++ - sz := len(by) + len(bkey) + 2 - tx.writeByteCount += sz + + entry := badger.NewEntry(bkey, by).WithMeta(ct) + + tx.Db.muDb.Lock() + if tx.Db.writeBatch != nil { + err := tx.Db.writeBatch.SetEntry(entry) // Will create txns as needed. + tx.Db.muDb.Unlock() + return err + } + tx.Db.muDb.Unlock() tx.mu.Lock() defer tx.mu.Unlock() + tx.writeCount++ + sz := len(by) + len(bkey) + 2 + tx.writeByteCount += sz + // The integration tests do large bit level loads that exceed 10MB. // So we autocommit and start a new Txn if we are about to // write too much into one Txn. @@ -736,15 +777,28 @@ func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey u // However, emprirically we still get ErrTnTooBig when // tx.writeByteCount=5884222; or when tx.writeCount=16197. // So duck under both those thresholds by some margin. - if tx.writeCount > 10000 || tx.writeByteCount > 4000000 { + if tx.writeCount > 100 || tx.writeByteCount > 2000000 { // avoid ErrTxnTooBig by commiting before going over the limits, // because then we get a error: "Transaction Conflict. Please retry." - panicOn(tx.tx.Commit()) + err := tx.tx.Commit() + panicOn(err) + // badger docs: + // `ErrConflict is returned when a transaction conflicts with another transaction. This can + // happen if the read rows had been updated concurrently by another transaction. + // ErrConflict = errors.New("Transaction Conflict. Please retry")` + //if err == badger.ErrConflict { + // problem is, we don't have the previous entry handy now. + //} + //if err != nil { + // ignore for now to get timings. + //panic(fmt.Sprintf("commit failed on bkey '%v': err '%v'", string(bkey), err)) + //} tx.tx = tx.Db.db.NewTransaction(tx.write) + //vv("NewBadgerTx write txn (p=%p) on gid=%v. b/c over thresholds writeCount=%v; writeByteCount=%v", tx.tx, curGID(), tx.writeCount, tx.writeByteCount) + tx.writeCount = 1 tx.writeByteCount = sz } - entry := badger.NewEntry(bkey, by).WithMeta(ct) err := tx.tx.SetEntry(entry) // ErrTxnTooBig is returned if too many writes are fit into a single transaction. @@ -752,15 +806,30 @@ func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey u // writes/deletes in the transaction exceeds a certain limit. In that case, it // is best to commit the transaction and start a new transaction immediately." // - if err == badger.ErrTxnTooBig { - panic(fmt.Sprintf("got error badger.ErrTxnTooBig, but we shoud never get this now; len(by) = %v; len(bkey)=%v; vs limit is 10MB. tx.writeCount=%v; tx.writeByteCount=%v;", len(by), len(bkey), tx.writeCount, tx.writeByteCount)) - } + /* + if err == badger.ErrTxnTooBig { + + err = tx.tx.Commit() + if err != nil { + panic(fmt.Sprintf("commit after TooBig failed on bkey '%v': err '%v'", string(bkey), err)) + } + + tx.tx = tx.Db.db.NewTransaction(tx.write) + //vv("NewBadgerTx write txn (p=%p) on gid=%v. b/c TooBig writeCount=%v; writeByteCount=%v", tx.tx, curGID(), tx.writeCount, tx.writeByteCount) + tx.writeCount = 1 + tx.writeByteCount = sz + + err = tx.tx.SetEntry(entry) + panicOn(err) + //panic(fmt.Sprintf("got error badger.ErrTxnTooBig, but we shoud never get this now; len(by) = %v; len(bkey)=%v; vs limit is 10MB. tx.writeCount=%v; tx.writeByteCount=%v;", len(by), len(bkey), tx.writeCount, tx.writeByteCount)) + } + */ return err } // 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 := txpath.Key(index, field, view, shard, ckey) + bkey := txkey.Key(index, field, view, shard, ckey) tx.mu.Lock() err := tx.tx.Delete(bkey) tx.mu.Unlock() @@ -769,80 +838,86 @@ func (tx *BadgerTx) RemoveContainer(index, field, view string, shard uint64, cke // Add sets all the a bits hot in the specified fragment. func (tx *BadgerTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + return tx.addOrRemove(index, field, view, shard, batched, false, a...) +} +// Remove clears all the specified a bits in the chosen fragment. +func (tx *BadgerTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + const batched = false + const remove = true + return tx.addOrRemove(index, field, view, shard, batched, remove, a...) +} + +func (tx *BadgerTx) addOrRemove(index, field, view string, shard uint64, batched, remove bool, a ...uint64) (changeCount int, err error) { // pure hack to match RoaringTx defer func() { - if !batched { + if !remove && !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) + if len(a) == 0 { + return 0, nil } - return -} -// Remove clears all the specified a bits in the chosen fragment. -func (tx *BadgerTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + // have to sort, b/c input is not always sorted. + sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) - // 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 lastHi uint64 = math.MaxUint64 // highbits is always less than this starter. + var rc *roaring.Container + var hi uint64 + var lo uint16 - var rct *roaring.Container - rct, err = tx.Container(index, field, view, shard, hi) - panicOn(err) - if err != nil { - return 0, err - } + for i, v := range a { + + hi, lo = highbits(v), lowbits(v) + if hi != lastHi { + // either first time through, or changed to a different container. + // do we need put the last updated container now? + if i > 0 { + // not first time through, write what we got. + if remove && (rc == nil || rc.N() == 0) { + err = tx.RemoveContainer(index, field, view, shard, lastHi) + panicOn(err) + } else { + err = tx.PutContainer(index, field, view, shard, lastHi, rc) + panicOn(err) + } + } + // get the next container + rc, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + } // else same container, keep adding bits to rct. chng := false - rc1, chng := rct.Remove(lo) - panicOn(err) + // rc can be nil before, and nil after, in both Remove/Add below. + // The roaring container add() and remove() methods handle this. + if remove { + rc, chng = rc.Remove(lo) + } else { + rc, chng = rc.Add(lo) + } if chng { changeCount++ } - if err != nil { - return changeCount, err - } - if rc1.N() == 0 { + lastHi = hi + } + // write the last updates. + if remove { + if rc == nil || rc.N() == 0 { err = tx.RemoveContainer(index, field, view, shard, hi) - if err != nil { - return - } + panicOn(err) } else { - err = tx.PutContainer(index, field, view, shard, hi, rc1) + err = tx.PutContainer(index, field, view, shard, hi, rc) panicOn(err) } + } else { + if rc == nil || rc.N() == 0 { + panic("there should be no way to have an empty bitmap AFTER an Add() operation") + } + err = tx.PutContainer(index, field, view, shard, hi, rc) + panicOn(err) } return } @@ -852,7 +927,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 := txpath.Key(index, field, view, shard, hi) + bkey := txkey.Key(index, field, view, shard, hi) tx.mu.Lock() item, err := tx.tx.Get(bkey) tx.mu.Unlock() @@ -873,7 +948,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 := txpath.AllShardPrefix(index, field, view) + prefix := txkey.AllShardPrefix(index, field, view) bi := NewBadgerIterator(tx, prefix) defer bi.Close() @@ -886,7 +961,7 @@ func (tx *BadgerTx) SliceOfShards(index, field, view, optionalViewPath string) ( for bi.Next() { item := bi.it.Item() key := item.Key() - shard := txpath.ShardFromKey(key) + shard := txkey.ShardFromKey(key) if firstDone { if shard != lastShard { sliceOfShards = append(sliceOfShards, shard) @@ -912,10 +987,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 := txpath.Key(index, field, view, shard, firstRoaringContainerKey) + needle := txkey.Key(index, field, view, shard, firstRoaringContainerKey) // prefix example: "idx:'i';fld:'f';vw:'v';shard:'00000000000000000000';key@" - prefix := txpath.Prefix(index, field, view, shard) + prefix := txkey.Prefix(index, field, view, shard) bi := NewBadgerIterator(tx, prefix) bi.Seek(needle) @@ -948,7 +1023,7 @@ type BadgerIterator struct { } // NewBadgerIterator creates an iterator on tx that will -// only return txpath.Keys that start with prefix. +// only return txkey.Keys that start with prefix. func NewBadgerIterator(tx *BadgerTx, prefix []byte) (bi *BadgerIterator) { opts := badger.DefaultIteratorOptions @@ -1053,7 +1128,7 @@ func (bi *BadgerIterator) Value() (containerKey uint64, c *roaring.Container) { panic("item was nil") } key := item.Key() - containerKey = txpath.KeyExtractContainerKey(key) + containerKey = txkey.KeyExtractContainerKey(key) err := item.Value(func(v []byte) error { c = bi.tx.toContainer(item.UserMeta(), v) @@ -1159,8 +1234,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 := txpath.Prefix(index, field, view, shard) - seekto := txpath.Prefix(index, field, view, shard+1) + prefix := txkey.Prefix(index, field, view, shard) + seekto := txkey.Prefix(index, field, view, shard+1) it := NewBadgerReverseIterator(tx, prefix, seekto) // this iterator is still open, when we commit/discard tx. defer it.Close() @@ -1319,15 +1394,15 @@ func (tx *BadgerTx) OffsetRange(index, field, view string, shard, offset, start, off := highbits(offset) hi0, hi1 := highbits(start), highbits(endx) - needle := txpath.Key(index, field, view, shard, hi0) - prefix := txpath.Prefix(index, field, view, shard) + needle := txkey.Key(index, field, view, shard, hi0) + prefix := txkey.Prefix(index, field, view, shard) - n2, pre2 := txpath.KeyAndPrefix(index, field, view, shard, hi0) + n2, pre2 := txkey.KeyAndPrefix(index, field, view, shard, hi0) if string(n2) != string(needle) { - panic(fmt.Sprintf("problem! n2(%v) != needle(%v), txpath.KeyAndPrefix not consitent with txpath.Key()", string(n2), string(needle))) + panic(fmt.Sprintf("problem! n2(%v) != needle(%v), txkey.KeyAndPrefix not consitent with txkey.Key()", string(n2), string(needle))) } if string(pre2) != string(prefix) { - panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), txpath.KeyAndPrefix not consitent with txpath.Key()", string(pre2), string(prefix))) + panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), txkey.KeyAndPrefix not consitent with txkey.Key()", string(pre2), string(prefix))) } it := NewBadgerIterator(tx, prefix) @@ -1336,7 +1411,7 @@ func (tx *BadgerTx) OffsetRange(index, field, view string, shard, offset, start, for ; it.it.ValidForPrefix(prefix); it.Next() { item := it.it.Item() bkey := item.Key() - k := txpath.KeyExtractContainerKey(bkey) + k := txkey.KeyExtractContainerKey(bkey) // >= hi1 is correct b/c endx cannot have any lowbits set. if uint64(k) >= hi1 { @@ -1606,7 +1681,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 txpath.Key() +// the roaring container value found by the txkey.Key() // formatted bkey. func (tx *BadgerTx) countBitsSet(bkey []byte) (n int) { @@ -1652,7 +1727,7 @@ func stringifiedBadgerKeysTx(tx *BadgerTx) (r string) { item := it.Item() bkey := item.Key() key := string(bkey) - ckey := txpath.KeyExtractContainerKey(bkey) + ckey := txkey.KeyExtractContainerKey(bkey) hash := "" srbm := "" err := item.Value(func(val []byte) error { @@ -1810,12 +1885,12 @@ func (w *BadgerDBWrapper) DeleteField(index, field, fieldPath string) error { if err != nil { return errors.Wrap(err, "removing directory") } - prefix := txpath.FieldPrefix(index, field) + prefix := txkey.FieldPrefix(index, field) return w.DeletePrefix(prefix) } func (w *BadgerDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { - prefix := txpath.Prefix(index, field, view, shard) + prefix := txkey.Prefix(index, field, view, shard) return w.DeletePrefix(prefix) } diff --git a/badger_test.go b/badger_test.go index 009f38935..6c271bb0d 100644 --- a/badger_test.go +++ b/badger_test.go @@ -1299,6 +1299,38 @@ func TestBadger_AutoCommit(t *testing.T) { panicOn(err) } +func TestBadger_BigWritesAvoidTxnTooLargeWithAutoCommit(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_BigWritesAvoidTxnTooLargeWithAutoCommit") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + tx := dbwrap.NewBadgerTx(writable, index, nil) + + containerKey := uint64(0) + // setup + bits := make([]uint64, 1024) + n := 0 + for i := range bits { + bits[i] = ^uint64(0) + n += 64 + } + rc := roaring.NewContainerBitmap(n, bits) + + // if we go over 100K big writes, we should autocommit + // rather than panic. + for v := 0; v < 133444; v++ { + containerKey++ + err := tx.PutContainer(index, field, view, shard, containerKey, rc) + panicOn(err) + } + + err := tx.Commit() + panicOn(err) +} + func TestBadger_DeleteIndex(t *testing.T) { // setup @@ -1626,3 +1658,86 @@ func TestMain(m *testing.M) { //reportTestBadgersNeedingClose() os.Exit(ret) } + +/* +func TestBadger_ConflictWriteWriteResolution(t *testing.T) { + + // 1) when do we get write-write conflicts (probably different goroutines) but + // can we get them on different keys? + + // 2) does having a lock registry that insures we are only ever writing + // different keys at once avoid write-write conflicts? + + // 3) how should write-write conflicts be resolved? + // presumably just retying the write? + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ConflictWriteWriteResolution") + defer clean() + defer dbwrap.Close() + + concur := 10 + //bkey := []byte("a") + //by := []byte("value-for-a") + + // read-loop: + for i := 0; i < concur*2; i++ { + go func() { + tx := dbwrap.NewBadgerTx(!writable, "", nil) + + for j := 0; true; j++ { + + bkey := []byte(fmt.Sprintf("key-for-a j=%v", j)) + //by := []byte(fmt.Sprintf("value-for-a j=%v", -1)) + + _, err := tx.tx.Get(bkey) + if err != badger.ErrKeyNotFound { + panicOn(err) + } + + if j%10 == 0 { + vv("committing after 10, gid=%v", curGID()) + tx.Rollback() + tx = dbwrap.NewBadgerTx(!writable, "", nil) + } + + } + }() + } + + // write-loop: + for i := 0; i < concur; i++ { + go func() { + tx := dbwrap.NewBadgerTx(writable, "", nil) + + for j := 0; true; j++ { + + bkey := []byte(fmt.Sprintf("key-for-a j=%v", j)) + by := []byte(fmt.Sprintf("value-for-a j=%v", j)) + + entry := badger.NewEntry(bkey, by) + err := tx.tx.SetEntry(entry) + panicOn(err) + + if j%5 == 0 { + panicOn(tx.tx.Delete(bkey)) + } + + _, err = tx.tx.Get(bkey) + if err != badger.ErrKeyNotFound { + panicOn(err) + } + + if j%10 == 0 { + ///vv("committing after 10, gid=%v", curGID()) + err = tx.tx.Commit() + panicOn(err) + tx = dbwrap.NewBadgerTx(writable, "", nil) + } + + } + }() + } + select {} +} +*/ diff --git a/bluegreentx.go b/bluegreentx.go index 7c62a214a..b960220d5 100644 --- a/bluegreentx.go +++ b/bluegreentx.go @@ -51,7 +51,9 @@ type blueGreenTx struct { func newBlueGreenTx(a, b Tx, idx *Index) *blueGreenTx { as := a.Type() bs := b.Type() - return &blueGreenTx{a: a, b: b, idx: idx, as: as, bs: bs} + c := &blueGreenTx{a: a, b: b, idx: idx, as: as, bs: bs} + c.checker.c = c + return c } var _ = newBlueGreenTx // keep linter happy @@ -140,13 +142,14 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { } bKey, bValue := bIter.Value() if bKey != aKey { + //vv("6960 really ought to be present index='%v',field='%v';view='%v';shard='%v'; isIn=%v", index, field, view, shard, c.isIn(index, field, view, shard, 456130566)) 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())) } 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()) + //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())) } } @@ -155,7 +158,7 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { 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()) + //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())) } } @@ -192,19 +195,24 @@ func (c *blueGreenTx) Rollback() { panic(r) } }() - //fmt.Printf("blueGreenTx.Rollback() about to call (%v) a.Rollback()\n", c.as) + //vv("blueGreenTx.Rollback() about to call (%v) a.Rollback()", c.as) c.a.Rollback() - //fmt.Printf("blueGreenTx.Rollback() about to call (%v) b.Rollback()\n", c.bs) + //vv("blueGreenTx.Rollback() about to call (%v) b.Rollback()", c.bs) c.b.Rollback() + //vv("blueGreenTx.Rollback() done. bgtx p=%p", c) } func (c *blueGreenTx) Commit() error { c.mu.Lock() defer c.mu.Unlock() - //fmt.Printf("blueGreenTx.Commit() called.\n") + + // for rbf 6930 debug stuff: + //in := c.isIn("i", "x", "standard", 0, 456130566) + //fmt.Printf("blueGreenTx.Commit() called. bgtx p=%p; in rbf=%v\n", c, in[0]) if c.rollbackOrCommitDone { return nil } + //vv("blueGreenTx.Commit() called. bgtx p=%p", c) c.rollbackOrCommitDone = true c.checkDatabase() defer func() { @@ -217,6 +225,13 @@ func (c *blueGreenTx) Commit() error { _ = errA errB := c.b.Commit() + /* + tx2, err := c.idx.Txf.rbfDB.NewRBFTx(false, "", nil) + panicOn(err) + inRbf, err := tx2.Contains("i", "x", "standard", 0, 456130566) + panicOn(err) + fmt.Printf("AFTER commits happened, blueGreenTx.Commit() called. bgtx p=%p; in rbf=%v\n", c, inRbf) + */ compareErrors(errA, errB) return errB } @@ -281,9 +296,9 @@ func (c *blueGreenTx) ImportRoaringBits(index, field, view string, shard uint64, // these are the first port of call for debugging, so we leave them in. // ================== begin save comments. //c.checkDatabase() - //vv("got past database check at TOP of ImportRoaringBits") + ////vv("got past database check at TOP of ImportRoaringBits") //c.Dump() - //vv("done with top dump; clear=%v", clear) + ////vv("done with top dump; clear=%v", clear) // ================== end save comments. defer func() { if r := recover(); r != nil { @@ -344,9 +359,30 @@ func (c *blueGreenTx) UseRowCache() bool { return false } +var _ = (&blueGreenTx{}).isIn // happy linter + +func (c *blueGreenTx) isIn(index, field, view string, shard uint64, ckey uint64) (r []bool) { + r = make([]bool, 2) + inA, errA := c.a.Contains(index, field, view, shard, ckey) + panicOn(errA) + inB, errB := c.b.Contains(index, field, view, shard, ckey) + panicOn(errB) + r[0] = inA + r[1] = inB + return +} + func (c *blueGreenTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { c.checker.see(index, field, view, shard) + //vv("blueGreenTx) Add(index=%v, field=%v, view=%v, shard=%v", index, field, view, shard) defer func() { + // rbf 6960 debug code: + /* + in := c.isIn("i", "x", "standard", 0, 456130566) + if in[0] || in[1] { + vv("first time 6960 present isIn=%v; bgtx p=%p stack=\n%v", in, c, stack()) + } + */ if r := recover(); r != nil { AlwaysPrintf("see Add() panic '%v' for index='%v', field='%v', view='%v', shard='%v' at '%v'", r, index, field, view, shard, stack()) panic(r) @@ -590,7 +626,6 @@ func (c *blueGreenTx) UnionInPlace(index, field, view string, shard uint64, othe func (c *blueGreenTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { c.checker.see(index, field, view, shard) - //vv("CountRange start=0x%x, endx=0x%x", start, end) defer func() { if r := recover(); r != nil { c.Dump() @@ -626,6 +661,10 @@ func (c *blueGreenTx) OffsetRange(index, field, view string, shard, offset, star panicOn(err) } compareErrors(errA, errB) + + //vv("end of blue-green OffsetRange, dump:") + //c.Dump() + return b, errB } @@ -692,8 +731,9 @@ func (c *blueGreenTx) SliceOfShards(index, field, view, optionalViewPath string) } for _, kb := range slcB { if !ma[kb] { + //vv("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb) c.Dump() - panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! B(%v) had %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb)) + panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb)) } delete(ma, kb) } @@ -758,6 +798,8 @@ func (m *MultiReaderB) Close() error { type blueGreenChecker struct { visited map[string]map[string]map[string]map[uint64]struct{} + c *blueGreenTx + // lock mu when using visited. // otherwise concurrent map writes on TestAPI_Import/RowIDColumnKey mu sync.Mutex @@ -768,6 +810,9 @@ 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 on index='%v'\n", Caller(1), index) + // is ckey 6960 present in i/x/standard/0 ? + ////vv("6960 present index='%v',field='%v';view='%v';shard='%v'; isIn=%v", index, field, view, shard, b.c.isIn(index, field, view, shard, 456130566)) + b.mu.Lock() defer b.mu.Unlock() diff --git a/cmd/demo-lmdb/lmdb.go b/cmd/demo-lmdb/lmdb.go index 1b24e06ff..bf91af113 100644 --- a/cmd/demo-lmdb/lmdb.go +++ b/cmd/demo-lmdb/lmdb.go @@ -13,7 +13,6 @@ // limitations under the License. // +build !386 -// +build skip_building_lmdb_for_now package main diff --git a/cmd/demo-lmdb/vprint.go b/cmd/demo-lmdb/vprint.go index 1e498e47c..d66762ef2 100644 --- a/cmd/demo-lmdb/vprint.go +++ b/cmd/demo-lmdb/vprint.go @@ -23,7 +23,6 @@ // SOFTWARE. // +build !386 -// +build skip_building_lmdb_for_now package main diff --git a/cmd/lmdb-keydump/keydump.go b/cmd/lmdb-keydump/keydump.go new file mode 100644 index 000000000..2b5a20c13 --- /dev/null +++ b/cmd/lmdb-keydump/keydump.go @@ -0,0 +1,186 @@ +// home https://github.com/glycerine/lmdb-go +// Copyright (c) 2020, the lmdb-go authors +// Copyright (c) 2015, Bryan Matsuo +// 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 the author 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 HOLDER 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. + +// +build amd64 + +package main + +import ( + "fmt" + "os" + "runtime" + "sort" + + "github.com/glycerine/lmdb-go/lmdb" + "github.com/pilosa/pilosa/v2" + "github.com/pilosa/pilosa/v2/txkey" +) + +// keydump simply prints all the keys in the database path specified +// as the first argument on the command line. + +func main() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "must supply path to database as only arg\n") + os.Exit(1) + } + + path := os.Args[1] + if !FileExists(path) { + fmt.Fprintf(os.Stderr, "path '%v' does not exist.\n", path) + os.Exit(1) + } + + maxr := 1 + env, err := lmdb.NewEnvMaxReaders(maxr) + panicOn(err) + defer env.Close() + + panicOn(env.SetMapSize(256 << 30)) + + err = env.SetMaxDBs(10) + panicOn(err) + + //var myflags uint = NoReadahead | NoSubdir + var myflags uint = lmdb.NoSubdir + err = env.Open(path, myflags, 0664) + 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 { + vv("cleared %d reader slots from dead processes", staleReaders) + } + + dbnames := []string{} + + shardSize := make(map[string]int) + + var dbiRoot lmdb.DBI + var dbi lmdb.DBI + err = env.SphynxReader(func(txn *lmdb.Txn, readslot int) (err error) { + //txn.RawRead = true + + dbiRoot, err = txn.OpenRoot(0) + panicOn(err) + + cur, err := txn.OpenCursor(dbiRoot) + panicOn(err) + defer cur.Close() + + for i := 0; true; i++ { + var k, v []byte + var err error + if i == 0 { + // must give it at least a zero byte here to start. + k, v, err = cur.Get([]byte{0}, nil, lmdb.SetRange) + panicOn(err) + } else { + k, v, err = cur.Get([]byte(nil), nil, lmdb.Next) + if lmdb.IsNotFound(err) { + break + } else { + panicOn(err) + } + } + dbnames = append(dbnames, string(k)) + _ = v + } + cur.Close() + return + }) + panicOn(err) + + for _, dbn := range dbnames { + fmt.Printf(` +========================= +database '%v': +========================= + +`, dbn) + err = env.SphynxReader(func(txn *lmdb.Txn, readslot int) (err error) { + //txn.RawRead = true + + dbi, err = txn.OpenDBI(dbn, 0) + panicOn(err) + + cur, err := txn.OpenCursor(dbi) + panicOn(err) + defer cur.Close() + + for i := 0; true; i++ { + var k, v []byte + var err error + if i == 0 { + // must give it at least a zero byte here to start. + k, v, err = cur.Get([]byte{0}, nil, lmdb.SetRange) + panicOn(err) + } else { + k, v, err = cur.Get([]byte(nil), nil, lmdb.Next) + if lmdb.IsNotFound(err) { + break + } else { + panicOn(err) + } + } + + vs := "" + if len(v) < 100 { + vs = fmt.Sprintf("%x", v) + " " + } + fmt.Printf("%04v %v len value; key: '%v' len %v -> %v\n", i, len(v), string(k), len(k), vs) + + pre := txkey.PrefixFromKey(k) + shardSize[string(pre)] += len(v) + } + return + }) + panicOn(err) + } // for dbnames + + fmt.Printf("=================== done.\n") + var lines []*pilosa.LineSorter + for k, v := range shardSize { + lines = append(lines, &pilosa.LineSorter{Line: k, Tot: float64(v)}) + } + sort.Sort(pilosa.SortByTot(lines)) + fd, err := os.Create("shardsize") + panicOn(err) + defer fd.Close() + for _, ln := range lines { + fmt.Fprintf(fd, "%v %v\n", int(ln.Tot), ln.Line) + } +} diff --git a/cmd/lmdb-keydump/vprint.go b/cmd/lmdb-keydump/vprint.go new file mode 100644 index 000000000..e64a964f3 --- /dev/null +++ b/cmd/lmdb-keydump/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 amd64 + +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/dbshard.go b/dbshard.go new file mode 100644 index 000000000..6190442ba --- /dev/null +++ b/dbshard.go @@ -0,0 +1,191 @@ +// 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 ( + "fmt" + "sync" +) + +// types to support a database file per shard + +type DBnode struct { + Holder map[string]*DBholder +} +type DBholder struct { + Index map[string]*DBindex +} +type DBindex struct { + Field map[string]*DBfield +} +type DBfield struct { + View map[string]*DBview +} +type DBview struct { + Shard map[uint64]*DBshard +} + +type DBWrapper interface { + DeleteDBPath(path string) error + Close() error +} + +type DBRegistry interface { + OpenDBWrapper(path string) (DBWrapper, error) +} + +type DBshard struct { + Path string + ID DBID + Open bool + + // With RWMutex, the + // writer who calls Lock() automatically gets priority over + // any reader who arrives later, even if the lock is held + // by a reader to start with. + RWMut sync.RWMutex + + W DBWrapper + ParentDBview *DBview +} + +type DBID struct { + Node string + Holder string + Index string + Field string + View string + Shard uint64 +} + +func (id *DBID) Path() (s string) { + s = id.Node + sep + id.Holder + sep + id.Index + + sep + id.Field + sep + id.View + sep + fmt.Sprintf("%04v", id.Shard) + return +} + +type DBPerShard struct { + Mu sync.Mutex + + Dir string + Node map[string]*DBnode + + // just flat, not buried within the Node heirarchy. + // Easily see how many we have. + Flatmap map[*DBshard]struct{} +} + +func NewDBPerShard(dir string) (d *DBPerShard) { + d = &DBPerShard{ + Dir: dir, + Node: make(map[string]*DBnode), + Flatmap: make(map[*DBshard]struct{}), + } + return +} + +func (per *DBPerShard) GetDBshard(registry DBRegistry, id DBID) (dbs *DBshard, err error) { + per.Mu.Lock() + defer per.Mu.Unlock() + + dbn, ok := per.Node[id.Node] + if !ok { + dbn = &DBnode{ + Holder: make(map[string]*DBholder), + } + per.Node[id.Node] = dbn + } + dbh, ok := dbn.Holder[id.Holder] + if !ok { + dbh = &DBholder{ + Index: make(map[string]*DBindex), + } + dbn.Holder[id.Holder] = dbh + } + dbi, ok := dbh.Index[id.Index] + if !ok { + dbi = &DBindex{ + Field: make(map[string]*DBfield), + } + dbh.Index[id.Index] = dbi + } + dbf, ok := dbi.Field[id.Field] + if !ok { + dbf = &DBfield{ + View: make(map[string]*DBview), + } + dbi.Field[id.Field] = dbf + } + dbv, ok := dbf.View[id.View] + if !ok { + dbv = &DBview{ + Shard: make(map[uint64]*DBshard), + } + dbf.View[id.View] = dbv + } + dbs, ok = dbv.Shard[id.Shard] + if !ok { + dbs = &DBshard{ + ParentDBview: dbv, + ID: id, + Path: per.Dir + sep + id.Path(), + } + dbv.Shard[id.Shard] = dbs + } + if !dbs.Open { + dbs.W, err = registry.OpenDBWrapper(dbs.Path) + if dbs.W != nil { + per.Flatmap[dbs] = struct{}{} + } + } + return +} + +func (per *DBPerShard) Del(dbs *DBshard) (err error) { + per.Mu.Lock() + defer per.Mu.Unlock() + + err = dbs.W.Close() + if err != nil { + return + } + panicOn(dbs.W.DeleteDBPath(dbs.Path)) + delete(per.Flatmap, dbs) + + // delete from the heirarchy + delete(dbs.ParentDBview.Shard, dbs.ID.Shard) + return +} + +func (per *DBPerShard) Close() (err error) { + per.Mu.Lock() + defer per.Mu.Unlock() + + for _, dbn := range per.Node { + for _, dbh := range dbn.Holder { + for _, dbi := range dbh.Index { + for _, dbf := range dbi.Field { + for _, dbv := range dbf.View { + for _, dbs := range dbv.Shard { + err = dbs.W.Close() + panicOn(err) + } + } + } + } + } + } + return +} diff --git a/field.go b/field.go index 2153ae991..c557f0e81 100644 --- a/field.go +++ b/field.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "log" "math" + "math/bits" "os" "path/filepath" "sort" @@ -755,7 +756,7 @@ fileLoop: return fmt.Errorf("opening view: view=%s, err=%s", view.name, err) } - if f.idx.Txf.TxType() == roaringFragmentFilesTxn { + if f.idx.Txf.TxType() == RoaringTxn { // 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 { @@ -2151,17 +2152,9 @@ func isValidCacheType(v string) bool { } } -// TODO(jea): why isn't this bits.Len64(x) using import "math/bits" -// That would be much (80x or more) faster and correct if the high bit is set. -// // bitDepth returns the number of bits required to store a value. func bitDepth(v uint64) uint { - for i := uint(0); i < 63; i++ { - if v < (1 << i) { - return i - } - } - return 63 + return uint(bits.Len64(v)) } // bitDepthInt64 returns the required bit depth for abs(v). diff --git a/fragment.go b/fragment.go index 7e340944f..523aac338 100644 --- a/fragment.go +++ b/fragment.go @@ -2306,6 +2306,12 @@ func (f *fragment) importPositions(tx Tx, set, clear []uint64, rowSet map[uint64 start := rowID * ShardWidth end := (rowID + 1) * ShardWidth + // avoid a 2nd r/w iterator if possible, + // aiming to having fewer write/read Tx conflicts. + badgerTx, isBadger := tx.(*BadgerTx) + if isBadger { + badgerTx.frag = f + } n, err := tx.CountRange(f.index, f.field, f.view, f.shard, start, end) if err != nil { return errors.Wrap(err, "CountRange") diff --git a/fragment_internal_test.go b/fragment_internal_test.go index a92f4c745..a41cee1a9 100644 --- a/fragment_internal_test.go +++ b/fragment_internal_test.go @@ -5179,7 +5179,7 @@ func TestImportClearRestart(t *testing.T) { if err != nil { t.Fatalf("initial small import: %v", err) } - if idx.Txf.TxType() == roaringFragmentFilesTxn { + if idx.Txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f.opN != expOpN { t.Errorf("unexpected opN - %d is not %d", f.opN, expOpN) } @@ -5199,7 +5199,7 @@ func TestImportClearRestart(t *testing.T) { t.Fatalf("reopening fragment: %v", err) } - if idx.Txf.TxType() == roaringFragmentFilesTxn { + if idx.Txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f.opN != expOpN { t.Errorf("unexpected opN after close/open %d is not %d", f.opN, expOpN) } @@ -5237,7 +5237,7 @@ func TestImportClearRestart(t *testing.T) { t.Fatalf("opening new fragment: %v", err) } - if idx.Txf.TxType() == roaringFragmentFilesTxn { + if idx.Txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f2.opN != expOpN { t.Errorf("unexpected opN after close/open %d is not %d", f2.opN, expOpN) } @@ -5323,15 +5323,18 @@ func check(t *testing.T, tx Tx, f *fragment, exp map[uint64]map[uint64]struct{}) func TestImportValueConcurrent(t *testing.T) { f, idx := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) - switch idx.Txf.TxType() { - case blueGreenBadgerRoaring, blueGreenRoaringBadger: - t.Skip(fmt.Sprintf("skipping TestImportValueConcurrent under " + - "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.")) + types := idx.Txf.TxTypes() + for _, ty := range types { + switch ty { + case roaringTxn: + t.Skip(fmt.Sprintf("skipping TestImportValueConcurrent under " + + "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 diff --git a/go.mod b/go.mod index 38d010727..70221964e 100644 --- a/go.mod +++ b/go.mod @@ -12,18 +12,14 @@ 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/glycerine/lmdb-go v1.9.26 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/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 diff --git a/go.sum b/go.sum index 726ab0077..f97f5d051 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy 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/glycerine/lmdb-go v1.9.26 h1:4aIiCQhg5fLChuZuATDHD4Lr6y9CdEHLtvROkzCZKIg= +github.com/glycerine/lmdb-go v1.9.26/go.mod h1:DrPeeTGooMg6B7cjNSP14perptTJzzdBy5YoosthrRs= 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= diff --git a/holder.go b/holder.go index 79b07fd2f..31cd327a6 100644 --- a/holder.go +++ b/holder.go @@ -603,6 +603,11 @@ func (h *Holder) processForeignIndexFields() error { // Close closes all open fragments. func (h *Holder) Close() error { + + if globalUseStatTx { + fmt.Printf("%v\n", globalCallStats.report()) + } + h.Stats.Close() // Notify goroutines of closing and wait for completion. @@ -1472,14 +1477,15 @@ func (s *holderSyncer) setTranslateReadOnlyFlags() { // Obtain a read lock on index to prevent Index.Close() from // destroying the Index.translateStores map before this is // done using it. - index.mu.RLock() + // + // Update: there was another path down to Index.Close(), so + // we shrink to lock to be inside index.TranslateStore() now. for partitionID := 0; partitionID < s.Cluster.partitionN; partitionID++ { ownsPartition := s.Cluster.unprotectedOwnsPartition(s.Node.ID, partitionID) if ts := index.TranslateStore(partitionID); ts != nil { ts.SetReadOnly(!ownsPartition) } } - index.mu.RUnlock() for _, field := range index.Fields() { field.TranslateStore().SetReadOnly(!isCoordinator) diff --git a/index.go b/index.go index 766c8c791..8b60f2481 100644 --- a/index.go +++ b/index.go @@ -33,16 +33,6 @@ 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(time.Minute, "cpu.pprof") - //CPUProfileForDur(15*time.Second, "cpu.pprof") - }() -} - // Index represents a container for fields. type Index struct { mu sync.RWMutex @@ -178,6 +168,8 @@ func (i *Index) TranslateStorePath(partitionID int) string { // TranslateStore returns the translation store for a given partition. func (i *Index) TranslateStore(partitionID int) TranslateStore { + i.mu.RLock() // avoid race with Index.Close() doing i.translateStores = make(map[int]TranslateStore) + defer i.mu.RUnlock() return i.translateStores[partitionID] } @@ -723,3 +715,39 @@ func (idx *Index) Dump(label string) { fmt.Printf("\n%v Index.Dump('%v') for index '%v':\n", fileline, label, idx.name) tx.Dump() } + +func (idx *Index) SliceOfShards(field, view, viewPath string) (sliceOfShards []uint64, err error) { + + // SliceOfShards is based on view.openFragments() + // If we go to a database per shard then index will need this, or + // something like it, to read database files/directories + // and figure out what all the shards are so that a view + // can open its fragments. + + file, err := os.Open(filepath.Join(viewPath, "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 { + idx.holder.Logger.Debugf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", idx.name, field, view, fi.Name()) + continue + } + sliceOfShards = append(sliceOfShards, shard) + } + return +} diff --git a/license.exceptions b/license.exceptions index d1be57dc1..9f6914859 100644 --- a/license.exceptions +++ b/license.exceptions @@ -14,3 +14,5 @@ ./cmd/slurp/vprint.go ./cmd/demo-lmdb/vprint.go ./gid.go +./cmd/lmdb-keydump/vprint.go +./cmd/lmdb-keydump/keydump.go diff --git a/lmdb/lmdb.go b/lmdb.go similarity index 70% rename from lmdb/lmdb.go rename to lmdb.go index 631545235..9e0d30ed9 100644 --- a/lmdb/lmdb.go +++ b/lmdb.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build skip_building_lmdb_for_now +// +build amd64 package pilosa @@ -28,121 +28,13 @@ import ( "strings" "sync" "sync/atomic" - "time" - "github.com/glycerine/idem" "github.com/glycerine/lmdb-go/lmdb" "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/txkey" "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 @@ -199,17 +91,6 @@ func DumpAllLMDB() { } } -// 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 { @@ -240,29 +121,36 @@ func (r *lmdbRegistrar) openLMDBWrapper(path0 string) (*LMDBWrapper, error) { // otherwise, make a new lmdb and store it in globalLMDBReg runtime.LockOSThread() - //vv("NewEnv for lmdb, gid = '%v'", curGID()) - env, err := lmdb.NewEnv() + const MaxReaders = 256 // default is 126 + env, err := lmdb.NewEnvMaxReaders(MaxReaders) panicOn(err) err = env.SetMaxDBs(1) panicOn(err) - err = env.SetMapSize(1 << 38) // 256 GB + err = env.SetMapSize(256 << 30) // 256GB panicOn(err) - const MaxReaders = 254 // default is 126 - err = env.SetMaxReaders(MaxReaders) - panicOn(err) + panicOn(os.MkdirAll(filepath.Dir(path), 0755)) - panicOn(os.MkdirAll(path, 0755)) - - flags := uint(lmdb.NoReadahead) // | uint(lmdb.NoLock) <<< yikes no + flags := uint(lmdb.NoReadahead | lmdb.NoSubdir) // 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 + // flags = flags | lmdb.WriteMap | lmdb.NoMetaSync | lmdb.NoSync // about the same speed + // flags = flags | lmdb.NoMetaSync | lmdb.NoSync // slows things down + //flags = flags | lmdb.WriteMap // seems faster than without: + // kRemove N= 710401 avg/op: 7.714µs sd: 27.83µs total: 5.480656859s + // kAdd N= 722835 avg/op: 9.096µs sd: 105.787µs total: 6.575497725s + + flags = flags | + lmdb.WriteMap | // Use a writable memory map. + lmdb.NoMetaSync | // Don't fsync metapage after commit. + lmdb.NoSync | // Don't fsync after commit. + lmdb.MapAsync | // Flush asynchronously when using the WriteMap flag. + lmdb.NoMemInit // Disable LMDB memory initialization err = env.Open(path, flags, 0644) if err != nil { @@ -291,100 +179,21 @@ func (r *lmdbRegistrar) openLMDBWrapper(path0 string) (*LMDBWrapper, error) { }) 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(), + name: name, + env: env, + reg: r, + path: path, + dbi: dbi, } 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 { @@ -395,7 +204,7 @@ func (w *LMDBWrapper) DeleteIndex(indexName string) error { 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) + prefix := txkey.IndexOnlyPrefix(indexName) return w.DeletePrefix(prefix) } @@ -406,12 +215,6 @@ var _ Tx = (*LMDBTx)(nil) // 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 @@ -423,29 +226,9 @@ type LMDBWrapper struct { // 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 @@ -461,36 +244,13 @@ type LMDBWrapper struct { 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 + "]" +func (w *LMDBWrapper) IsClosed() bool { + w.muDb.Lock() + defer w.muDb.Unlock() + return w.closed } // NewLMDBTx produces LMDB based ACID transactions. If @@ -505,8 +265,8 @@ func (w *LMDBWrapper) UnprotectedListOpenItAsString() (r string) { // 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 +func (w *LMDBWrapper) NewLMDBTx(write bool, initialIndexName string, frag *fragment) (tx *LMDBTx) { + //w.muDb.Lock() //defer w.muDb.Unlock() rwflag := uint(0) // writable txn denotated by lack of the lmdb.Readonly flag. @@ -514,11 +274,12 @@ func (w *LMDBWrapper) NewLMDBTx(write bool, initialIndexName string) (tx *LMDBTx rwflag = lmdb.Readonly } + runtime.LockOSThread() + 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) + lmdbTxn.RawRead = true tx = &LMDBTx{ sn: sn, @@ -526,6 +287,7 @@ func (w *LMDBWrapper) NewLMDBTx(write bool, initialIndexName string) (tx *LMDBTx tx: lmdbTxn, dbi: w.dbi, Db: w, + frag: frag, //initloc: stack(), doAllocZero: w.doAllocZero, initialIndexName: initialIndexName, @@ -540,14 +302,11 @@ func (w *LMDBWrapper) Close() (err error) { 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) + w.env.Close() + w.env = nil } - w.env.CloseDBI(w.dbi) return nil } @@ -558,13 +317,6 @@ func (w *LMDBWrapper) Close() (err error) { 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 @@ -572,6 +324,7 @@ type LMDBTx struct { dbi lmdb.DBI Db *LMDBWrapper tx *lmdb.Txn + frag *fragment opcount int @@ -579,13 +332,6 @@ type LMDBTx struct { 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 @@ -598,42 +344,7 @@ func (tx *LMDBTx) Type() string { } 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 - //} + return false } // Pointer gives us a memory address for the underlying transaction for debugging. @@ -645,25 +356,16 @@ func (tx *LMDBTx) Pointer() string { // Rollback rolls back the transaction. func (tx *LMDBTx) Rollback() { - tx.mu.Lock() // hung here? on defer on panic? + tx.mu.Lock() 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() + runtime.UnlockOSThread() tx.unlocked = true } - //vv("done rolling back LMDBTx sn=%v", tx.sn) } // Commit commits the transaction to permanent storage. @@ -673,23 +375,16 @@ 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() + runtime.UnlockOSThread() tx.unlocked = true } - //vv("done committing LMDBTx sn=%v", tx.sn) + //pp("done committing LMDBTx sn=%v", tx.sn) return err } @@ -712,10 +407,8 @@ func (tx *LMDBTx) Container(index, field, view string, shard uint64, ckey uint64 // 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 := txkey.Key(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() @@ -726,8 +419,7 @@ func (tx *LMDBTx) Container(index, field, view string, shard uint64, ckey uint64 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 + panicOn(err) return nil, nil } } @@ -741,19 +433,19 @@ func (tx *LMDBTx) Container(index, field, view string, shard uint64, ckey uint64 // 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) + bkey := txkey.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: + case roaring.ContainerNil: panic("wat? nil container is unexpected, no?!?") default: panic(fmt.Sprintf("unknown container type: %v", ct)) @@ -772,7 +464,7 @@ func (tx *LMDBTx) PutContainer(index, field, view string, shard uint64, ckey uin // 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) + bkey := txkey.Key(index, field, view, shard, ckey) tx.mu.Lock() err := tx.tx.Del(tx.dbi, bkey, nil) tx.mu.Unlock() @@ -852,7 +544,6 @@ func (tx *LMDBTx) Remove(index, field, view string, shard uint64, a ...uint64) ( if rc1.N() == 0 { err = tx.RemoveContainer(index, field, view, shard, hi) if err != nil { - //vv("err = '%v'", err) return } } else { @@ -868,13 +559,12 @@ func (tx *LMDBTx) Remove(index, field, view string, shard uint64, a ...uint64) ( 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) + bkey := txkey.Key(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 { @@ -890,19 +580,15 @@ func (tx *LMDBTx) Contains(index, field, view string, shard uint64, key uint64) func (tx *LMDBTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - prefix := badgerAllShardPrefix(index, field, view) + prefix := txkey.AllShardPrefix(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) + shard := txkey.ShardFromKey(bi.lastKey) if firstDone { if shard != lastShard { sliceOfShards = append(sliceOfShards, shard) @@ -928,22 +614,16 @@ func (tx *LMDBTx) SliceOfShards(index, field, view, optionalViewPath string) (sl 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) + needle := txkey.Key(index, field, view, shard, firstRoaringContainerKey) // prefix example: "idx:'i';fld:'f';vw:'v';shard:'00000000000000000000';key@" - prefix := badgerPrefix(index, field, view, shard) + prefix := txkey.Prefix(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. @@ -988,8 +668,6 @@ func NewLMDBIterator(tx *LMDBTx, prefix []byte) (bi *LMDBIterator) { // 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() } @@ -1001,12 +679,17 @@ func (bi *LMDBIterator) Valid() bool { // 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. + var k, v []byte + var err error getflag := uint(lmdb.SetRange) - k, v, err := bi.cur.Get(needle, nil, getflag) + if len(needle) == 0 { + k, v, err = bi.cur.Get(oneByteSliceOfZero, nil, getflag) + } else { + k, v, err = bi.cur.Get(needle, nil, getflag) + } if lmdb.IsNotFound(err) { bi.lastKey = nil @@ -1035,40 +718,6 @@ func (bi *LMDBIterator) Seek(needle []byte) (ok bool) { 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 @@ -1089,14 +738,11 @@ 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) } +var oneByteSliceOfZero = []byte{0} + // 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 { @@ -1118,9 +764,18 @@ func (bi *LMDBIterator) Next() (ok bool) { 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)) + var k, v []byte + var err error + if getflag == lmdb.SetRange && len(prefix) == 0 { + // don't do nil as key on setrange, will panic + // b/c keys in LMDB must be at least one byte long. + // http://www.lmdb.tech/doc/group__mdb.html#structMDB__val + // "Key sizes must be between 1 and mdb_env_get_maxkeysize() inclusive." + // But if getflag == lmdb.Next, key can be nil. + k, v, err = bi.cur.Get(oneByteSliceOfZero, nil, getflag) + } else { + k, v, err = bi.cur.Get(prefix, nil, getflag) + } if lmdb.IsNotFound(err) { bi.lastKey = nil bi.lastVal = nil @@ -1143,11 +798,6 @@ skipEmpty: 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 @@ -1160,7 +810,7 @@ func (bi *LMDBIterator) Value() (containerKey uint64, c *roaring.Container) { if !bi.lastOK { panic("bi.cur not valid") } - containerKey = badgerKeyExtractContainerKey(bi.lastKey) + containerKey = txkey.KeyExtractContainerKey(bi.lastKey) v := bi.lastVal n := len(v) @@ -1201,6 +851,7 @@ func (bf *lmdbFinder) 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 @@ -1208,6 +859,7 @@ func (tx *LMDBTx) NewTxIterator(index, field, view string, shard uint64) *roarin // 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() @@ -1247,18 +899,14 @@ func (tx *LMDBTx) Count(index, field, view string, shard uint64) (uint64, error) 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 } @@ -1266,15 +914,19 @@ func (tx *LMDBTx) Count(index, field, view string, shard uint64) (uint64, error) // 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) + prefix := txkey.Prefix(index, field, view, shard) + seekto := txkey.Prefix(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 + var k, v []byte + if len(seekto) == 0 { + _, _, err = cur.Get(oneByteSliceOfZero, nil, lmdb.SetRange) + } else { + _, _, err = cur.Get(seekto, nil, lmdb.SetRange) + } 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) @@ -1299,7 +951,7 @@ func (tx *LMDBTx) Max(index, field, view string, shard uint64) (uint64, error) { return 0, nil // nothing in [prefix, seekto). } - hb := badgerKeyExtractContainerKey(k) + hb := txkey.KeyExtractContainerKey(k) n := len(v) if n == 0 { return 0, nil @@ -1437,24 +1089,23 @@ func (tx *LMDBTx) OffsetRange(index, field, view string, shard, offset, start, e off := highbits(offset) hi0, hi1 := highbits(start), highbits(endx) - needle := badgerKey(index, field, view, shard, hi0) - prefix := badgerPrefix(index, field, view, shard) + needle := txkey.Key(index, field, view, shard, hi0) + prefix := txkey.Prefix(index, field, view, shard) - n2, pre2 := badgerKeyAndPrefix(index, field, view, shard, hi0) + n2, pre2 := txkey.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), txkey.KeyAndPrefix not consitent with txkey.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), txkey.KeyAndPrefix not consitent with txkey.Key()", string(pre2), string(prefix))) } - it := NewLMDBIterator(tx, prefix) // see OffsetRange() panic 'Only one iterator can be active at one time, for a RW txn + it := NewLMDBIterator(tx, prefix) 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) + k := txkey.KeyExtractContainerKey(bkey) // >= hi1 is correct b/c endx cannot have any lowbits set. if uint64(k) >= hi1 { @@ -1465,7 +1116,6 @@ func (tx *LMDBTx) OffsetRange(index, field, view string, shard, offset, start, e 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)]) @@ -1482,6 +1132,7 @@ func (tx *LMDBTx) IncrementOpN(index, field, view string, shard uint64, 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 @@ -1573,7 +1224,7 @@ func (tx *LMDBTx) ImportRoaringBits(index, field, view string, shard uint64, itr 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 { @@ -1600,7 +1251,8 @@ func (tx *LMDBTx) 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 LMDBDB's memory-mapped vlog-file, @@ -1620,36 +1272,19 @@ func (tx *LMDBTx) 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) } else { w = v } switch typ { - case containerArray: + case roaring.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: + case roaring.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: + case roaring.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)) @@ -1660,7 +1295,7 @@ func (tx *LMDBTx) toContainer(typ byte, v []byte) (r *roaring.Container) { // keys available in lmdb. func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx) (r string) { if optionalUseThisTx == nil { - tx := w.NewLMDBTx(!writable, "") + tx := w.NewLMDBTx(!writable, "", nil) defer tx.Rollback() r = stringifiedLMDBKeysTx(tx) return @@ -1675,14 +1310,13 @@ func (w *LMDBWrapper) StringifiedLMDBKeys(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 txkey.Key() // 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) @@ -1696,6 +1330,7 @@ func (tx *LMDBTx) countBitsSet(bkey []byte) (n int) { } func (tx *LMDBTx) Dump() { + fmt.Printf("%v\n", stringifiedLMDBKeysTx(tx)) } @@ -1719,7 +1354,7 @@ func stringifiedLMDBKeysTx(tx *LMDBTx) (r string) { bkey := it.lastKey key := string(bkey) - ckey := badgerKeyExtractContainerKey(bkey) + ckey := txkey.KeyExtractContainerKey(bkey) hash := "" srbm := "" v := it.lastVal @@ -1744,28 +1379,39 @@ func stringifiedLMDBKeysTx(tx *LMDBTx) (r string) { } return "lmdb-" + r } +func (w *LMDBWrapper) DeleteDBPath(path string) (err error) { + err = os.RemoveAll(path) + if err != nil { + return errors.Wrap(err, "DeleteDBPath") + } + lockfile := path + "-lock" + if FileExists(lockfile) { + err = os.RemoveAll(lockfile) + } + return +} 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) + err := w.DeleteDBPath(fieldPath) if err != nil { return errors.Wrap(err, "removing directory") } - prefix := badgerFieldPrefix(index, field) + prefix := txkey.FieldPrefix(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) + prefix := txkey.Prefix(index, field, view, shard) return w.DeletePrefix(prefix) } func (w *LMDBWrapper) DeletePrefix(prefix []byte) error { - tx := w.NewLMDBTx(writable, w.name) + tx := w.NewLMDBTx(writable, w.name, nil) // NewLMDBTx will grab these, so don't lock until after it. w.muDb.Lock() diff --git a/lmdb/txpool.go b/lmdb/txpool.go deleted file mode 100644 index 492e8603c..000000000 --- a/lmdb/txpool.go +++ /dev/null @@ -1,524 +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. - -// +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/lmdb_other.go b/lmdb_other.go new file mode 100644 index 000000000..e239c6848 --- /dev/null +++ b/lmdb_other.go @@ -0,0 +1,423 @@ +// 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 !amd64 + +package pilosa + +// this is a stubbed out file to let 386 build. lmdb won't work well +// on 32-bit; not enough memory map address space. + +import ( + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/pilosa/pilosa/v2/roaring" +) + +var _ = time.Now + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// unregister removes w from r +func (r *lmdbRegistrar) unregister(w *LMDBWrapper) { + panic("lmdb only available on 64-bit arch") +} + +func DumpAllLMDB() { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +var ErrShutdown = fmt.Errorf("shutting down") + +// DeleteIndex deletes all the containers associated with +// the named index from the lmdb database. +func (w *LMDBWrapper) DeleteIndex(indexName string) error { + panic("lmdb only available on 64-bit arch") +} + +// 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{} + +func (w *LMDBWrapper) IsClosed() bool { + panic("lmdb only available on 64-bit arch") +} + +// 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, frag *fragment) (tx *LMDBTx) { + panic("lmdb only available on 64-bit arch") +} + +// Close shuts down the LMDB database. +func (w *LMDBWrapper) Close() (err error) { + panic("lmdb only available on 64-bit arch") +} + +// 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{} + +func (tx *LMDBTx) Type() string { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) UseRowCache() bool { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// Rollback rolls back the transaction. +func (tx *LMDBTx) Rollback() { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// Readonly returns true iff the LMDBTx is read-only. +func (tx *LMDBTx) Readonly() bool { + panic("lmdb only available on 64-bit arch") +} + +// RoaringBitmap returns the roaring.Bitmap for all bits in the fragment. +func (tx *LMDBTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") + +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") + +} + +// LMDBIterator is the iterator returned from a LMDBTx.ContainerIterator() call. +// It implements the roaring.ContainerIterator interface. +type LMDBIterator struct{} + +// NewLMDBIterator creates an iterator on tx that will +// only return badgerKeys that start with prefix. +func NewLMDBIterator(tx *LMDBTx, prefix []byte) (bi *LMDBIterator) { + panic("lmdb only available on 64-bit arch") +} + +// Close tells the database and transaction that the user is done +// with the iterator. +func (bi *LMDBIterator) Close() { + panic("lmdb only available on 64-bit arch") +} + +// Valid returns false if there are no more values in the iterator's range. +func (bi *LMDBIterator) Valid() bool { + panic("lmdb only available on 64-bit arch") +} + +// Seek allows the iterator to start at needle instead of the global begining. +func (bi *LMDBIterator) Seek(needle []byte) (ok bool) { + panic("lmdb only available on 64-bit arch") +} + +func (bi *LMDBIterator) ValidForPrefix(prefix []byte) bool { + panic("lmdb only available on 64-bit arch") +} + +func (bi *LMDBIterator) String() (r string) { + panic("lmdb only available on 64-bit arch") +} + +var oneByteSliceOfZero = []byte{0} + +// Next advances the iterator. +func (bi *LMDBIterator) Next() (ok bool) { + panic("lmdb only available on 64-bit arch") +} + +// Value retrieves what is pointed at currently by the iterator. +func (bi *LMDBIterator) Value() (containerKey uint64, c *roaring.Container) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// Close closes all bf.needClose listed Closers. +func (bf *lmdbFinder) Close() { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +// IncrementOpN increments the tx opcount by changedN +func (tx *LMDBTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) toContainer(typ byte, v []byte) (r *roaring.Container) { + panic("lmdb only available on 64-bit arch") +} + +// StringifiedLMDBKeys returns a string with all the container +// keys available in lmdb. +func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx) (r string) { + panic("lmdb only available on 64-bit arch") +} + +// countBitsSet returns the number of bits set (or "hot") in +// the roaring container value found by the txkey.Key() +// formatted bkey. +func (tx *LMDBTx) countBitsSet(bkey []byte) (n int) { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) Dump() { + panic("lmdb only available on 64-bit arch") +} + +// 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) { + panic("lmdb only available on 64-bit arch") +} + +func (w *LMDBWrapper) DeleteField(index, field, fieldPath string) error { + panic("lmdb only available on 64-bit arch") +} + +func (w *LMDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { + panic("lmdb only available on 64-bit arch") +} + +func (w *LMDBWrapper) DeletePrefix(prefix []byte) error { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + panic("lmdb only available on 64-bit arch") +} + +// 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 { + panic("lmdb only available on 64-bit arch") +} diff --git a/lmdb/lmdb_test.go b/lmdb_test.go similarity index 92% rename from lmdb/lmdb_test.go rename to lmdb_test.go index 4d19be47a..89ba52f11 100644 --- a/lmdb/lmdb_test.go +++ b/lmdb_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build skip_building_lmdb_for_now +// +build amd64 package pilosa @@ -25,6 +25,7 @@ import ( "testing" "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/txkey" ) // helpers, each runs their own new txn, and commits if a change/delete @@ -32,7 +33,7 @@ import ( func LMDBMustHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() exists, err := tx.Contains(index, field, view, shard, bitvalue) panicOn(err) @@ -45,7 +46,7 @@ func LMDBMustHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard func LMDBMustNotHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() exists, err := tx.Contains(index, field, view, shard, bitvalue) panicOn(err) @@ -56,7 +57,7 @@ func LMDBMustNotHaveBitvalue(dbwrap *LMDBWrapper, index, field, view string, sha } func LMDBMustSetBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard uint64, putme uint64) { - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) // add a bit changed, err := tx.Add(index, field, view, shard, doBatched, putme) @@ -74,14 +75,14 @@ func LMDBMustSetBitvalue(dbwrap *LMDBWrapper, index, field, view string, shard u } func LMDBMustDeleteBitvalueContainer(dbwrap *LMDBWrapper, index, field, view string, shard uint64, putme uint64) { - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) 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) + tx := dbwrap.NewLMDBTx(writable, index, nil) _, err := tx.Remove(index, field, view, shard, putme) panicOn(err) panicOn(tx.Commit()) @@ -91,7 +92,7 @@ func mustOpenEmptyLMDBWrapper(path string) (w *LMDBWrapper, cleaner func()) { var err error fn := lmdbPath(path) panicOn(os.RemoveAll(fn)) - w, err = globalLMDBReg.newLMDBWrapper(fn) + w, err = globalLMDBReg.openLMDBWrapper(fn) panicOn(err) // verify it is empty @@ -103,6 +104,9 @@ func mustOpenEmptyLMDBWrapper(path string) (w *LMDBWrapper, cleaner func()) { return w, func() { w.Close() // stop any started background GC goroutine. os.RemoveAll(fn) + if FileExists(fn + "-lock") { + os.RemoveAll(fn) + } } } @@ -119,7 +123,7 @@ func TestLMDB_DeleteFragment(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard0 := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) shard1 := uint64(1) @@ -154,7 +158,7 @@ func TestLMDB_DeleteFragment(t *testing.T) { err = dbwrap.DeleteFragment(index, field, view, victim, nil) panicOn(err) - tx = dbwrap.NewLMDBTx(!writable, index) + tx = dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() for _, s := range shards { @@ -209,7 +213,7 @@ func TestLMDB_Max_on_many_containers(t *testing.T) { } } - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() for _, shard := range shards { @@ -242,7 +246,7 @@ func TestLMDB_SetBitmap(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) bitvalue := uint64(0) changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) if changed <= 0 { @@ -263,7 +267,7 @@ func TestLMDB_SetBitmap(t *testing.T) { // commited, so should be visible outside the txn // - tx2 := dbwrap.NewLMDBTx(!writable, index) + tx2 := dbwrap.NewLMDBTx(!writable, index, nil) exists, err = tx2.Contains(index, field, view, shard, bitvalue) panicOn(err) if !exists { @@ -283,7 +287,7 @@ func TestLMDB_OffsetRange(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) bitvalue := uint64(1 << 20) changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) @@ -317,7 +321,7 @@ func TestLMDB_OffsetRange(t *testing.T) { start := uint64(0 << 16) endx := bitvalue + 1<<16 - tx2 := dbwrap.NewLMDBTx(!writable, index) + tx2 := dbwrap.NewLMDBTx(!writable, index, nil) rbm2, err := tx2.OffsetRange(index, field, view, shard, offset, start, endx) panicOn(err) tx2.Rollback() @@ -331,7 +335,7 @@ func TestLMDB_OffsetRange(t *testing.T) { // now offset by 2M offset = uint64(2 << 20) - tx3 := dbwrap.NewLMDBTx(!writable, index) + tx3 := dbwrap.NewLMDBTx(!writable, index, nil) rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) panicOn(err) tx3.Rollback() @@ -359,7 +363,7 @@ func TestLMDB_Count_on_many_containers(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() n, err := tx.Count(index, field, view, shard) @@ -375,7 +379,7 @@ func TestLMDB_Count_dense_containers(t *testing.T) { defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) expected := 0 for i := uint64(0); i < (1<<16)+2; i += 2 { @@ -401,7 +405,7 @@ func TestLMDB_ContainerIterator_on_empty(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() bitvalue := uint64(0) citer, found, err := tx.ContainerIterator(index, field, view, shard, bitvalue) @@ -419,7 +423,7 @@ func TestLMDB_ContainerIterator_on_one_bit(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() bitvalue := uint64(42) @@ -481,19 +485,19 @@ func TestLMDB_badgerKey_badgerPrefix(t *testing.T) { // 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) + needle := txkey.Key(index, field, view, shard, 0) // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" - prefix := badgerPrefix(index, field, view, shard) + prefix := txkey.Prefix(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))) + panic(fmt.Sprintf("txkey.Prefix() output '%v'was not a prefix of txkey.Key() '%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))) + panic(fmt.Sprintf("txkey.Prefix() output '%v'was 20 characters shorter than txkey.Key() '%v'", string(needle), string(prefix))) } - // validate assumption that badgerKeyExtractContainerKey() makes about strconv.ParseUint() error reporting; + // validate assumption that txkey.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@" @@ -507,10 +511,10 @@ func TestLMDB_badgerKey_badgerPrefix(t *testing.T) { defer func() { r := recover() if r == nil { - panic(fmt.Sprintf("should have seen panic on call to badgerKeyExtractContainerKey(prefix='%v')", prefix)) + panic(fmt.Sprintf("should have seen panic on call to txkey.KeyExtractContainerKey(prefix='%v')", prefix)) } }() - badgerKeyExtractContainerKey(prefix) // should panic. + txkey.KeyExtractContainerKey(prefix) // should panic. }() } @@ -519,7 +523,7 @@ func TestLMDB_ContainerIterator_on_one_bit_fail_to_find(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() putme := uint64(1<<16) + 3 // in the key:1 container @@ -574,7 +578,7 @@ func TestLMDB_ContainerIterator_empty_iteration_loop(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() putme := uint64(1<<16) + 3 // in the key:1 container @@ -624,7 +628,7 @@ func TestLMDB_ForEach_on_one_bit(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() bitvalue := uint64(42) @@ -683,7 +687,7 @@ func TestLMDB_RemoveContainer_one_bit_test(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // delete, but rollback instead of commit - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) hi := highbits(putme) panicOn(tx.RemoveContainer(index, field, view, shard, hi)) tx.Rollback() @@ -692,7 +696,7 @@ func TestLMDB_RemoveContainer_one_bit_test(t *testing.T) { 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) + tx = dbwrap.NewLMDBTx(writable, index, nil) hi = highbits(putme) exists, err := tx.Contains(index, field, view, shard, putme) @@ -744,7 +748,7 @@ func TestLMDB_Remove_one_bit_test(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) // delete, but rollback instead of commit - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) hi, lo := highbits(putme), lowbits(putme) _, _ = hi, lo _, err := tx.Remove(index, field, view, shard, hi) @@ -755,7 +759,7 @@ func TestLMDB_Remove_one_bit_test(t *testing.T) { 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) + tx = dbwrap.NewLMDBTx(writable, index, nil) exists, err := tx.Contains(index, field, view, shard, putme) panicOn(err) @@ -787,7 +791,7 @@ func TestLMDB_Min_on_many_containers(t *testing.T) { index, field, view, shard := "i", "f", "v", uint64(0) // verify no containers flag works - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) min, containersExist, err := tx.Min(index, field, view, shard) _ = min panicOn(err) @@ -804,7 +808,7 @@ func TestLMDB_Min_on_many_containers(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx = dbwrap.NewLMDBTx(!writable, index) + tx = dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() min, containersExist, err = tx.Min(index, field, view, shard) @@ -825,7 +829,7 @@ func TestLMDB_CountRange_on_many_containers(t *testing.T) { index, field, view, shard := "i", "f", "v", uint64(0) // verify no containers flag works - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) n, err := tx.CountRange(index, field, view, shard, 0, math.MaxUint64) panicOn(err) if n != 0 { @@ -841,7 +845,7 @@ func TestLMDB_CountRange_on_many_containers(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx = dbwrap.NewLMDBTx(!writable, index) + tx = dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() n, err = tx.CountRange(index, field, view, shard, 0, math.MaxUint64) @@ -869,7 +873,7 @@ func TestLMDB_CountRange_middle_container(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() // pick out just the middle container with the 1 bit set on it. @@ -894,7 +898,7 @@ func TestLMDB_CountRange_many_middle_container(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() // get them all @@ -925,7 +929,7 @@ func TestLMDB_UnionInPlace(t *testing.T) { LMDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) } - tx2 := dbwrap.NewLMDBTx(!writable, index) + tx2 := dbwrap.NewLMDBTx(!writable, index, nil) n, err := tx2.Count(index, field, view, shard) panicOn(err) if n != 2 { @@ -940,7 +944,7 @@ func TestLMDB_UnionInPlace(t *testing.T) { } mustAddR(others3.Add(4 << 16)) // outside the 2<<16 container - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() err = tx.UnionInPlace(index, field, view, shard, others, others2, others3) panicOn(err) @@ -965,7 +969,7 @@ func TestLMDB_RoaringBitmap(t *testing.T) { putme := expected LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() rbm, err := tx.RoaringBitmap(index, field, view, shard) @@ -989,7 +993,7 @@ func TestLMDB_ImportRoaringBits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() tx.DeleteEmptyContainer = true // traditional badger Tx behavior, but not Roaring. @@ -1062,7 +1066,7 @@ func TestLMDB_ImportRoaringBits(t *testing.T) { // should have no keys if allkeys != "" { - panic("badger should have no keys now") + panic("lmdb should have no keys now") } } @@ -1072,7 +1076,7 @@ func TestLMDB_ImportRoaringBits_set_nonoverlapping_bits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() // get some roaring bits, get an itr RoaringIterator from them @@ -1122,7 +1126,7 @@ func TestLMDB_ImportRoaringBits_clear_nonoverlapping_bits(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) defer tx.Rollback() // get some roaring bits, get an itr RoaringIterator from them @@ -1181,7 +1185,7 @@ func TestLMDB_DeleteIndex(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) bitvalue := uint64(777) bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} for _, v := range bits { @@ -1218,7 +1222,7 @@ func TestLMDB_DeleteIndex(t *testing.T) { err = dbwrap.DeleteIndex(index) panicOn(err) - tx = dbwrap.NewLMDBTx(!writable, index2) + tx = dbwrap.NewLMDBTx(!writable, index2, nil) defer tx.Rollback() exists, err = tx.Contains(index2, field, view, shard, bitvalue) panicOn(err) @@ -1243,7 +1247,7 @@ func TestLMDB_DeleteIndex_over100k(t *testing.T) { defer clean() defer dbwrap.Close() index, field, view, shard := "i", "f", "v", uint64(0) - tx := dbwrap.NewLMDBTx(writable, index) + tx := dbwrap.NewLMDBTx(writable, index, nil) bitvalue := uint64(777) limit := uint64(100002) // default batch size in DeleteIndex is 100k keys per delete transaction. //limit := uint64(101) @@ -1256,7 +1260,7 @@ func TestLMDB_DeleteIndex_over100k(t *testing.T) { panicOn(err) if v%100000 == 0 { panicOn(tx.Commit()) - tx = dbwrap.NewLMDBTx(writable, index) + tx = dbwrap.NewLMDBTx(writable, index, nil) } } @@ -1273,7 +1277,7 @@ func TestLMDB_DeleteIndex_over100k(t *testing.T) { err = dbwrap.DeleteIndex(index) panicOn(err) - tx = dbwrap.NewLMDBTx(!writable, index2) + tx = dbwrap.NewLMDBTx(!writable, index2, nil) defer tx.Rollback() exists, err := tx.Contains(index2, field, view, shard, bitvalue) panicOn(err) @@ -1302,7 +1306,7 @@ func TestLMDB_SliceOfShards(t *testing.T) { for _, shard := range shards { LMDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) } - tx := dbwrap.NewLMDBTx(!writable, index) + tx := dbwrap.NewLMDBTx(!writable, index, nil) defer tx.Rollback() slc, err := tx.SliceOfShards(index, field, view, "") diff --git a/mtx.go b/mtx.go new file mode 100644 index 000000000..19ea9fd2b --- /dev/null +++ b/mtx.go @@ -0,0 +1,308 @@ +// 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 ( + "fmt" + "io" + "sync" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// 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) + +type multiTxKey struct { + index string + shard uint64 + write bool +} + +func (mtx *MultiTx) Type() string { + return mtx.index.Txf.TxType() +} + +// 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("txNoShard: no prior tx in MultiTx available, looking up index='%v'", index)) +} diff --git a/rbf.go b/rbf.go index db85ef0fd..31326f8ea 100644 --- a/rbf.go +++ b/rbf.go @@ -25,7 +25,7 @@ import ( "github.com/pilosa/pilosa/v2/rbf" "github.com/pilosa/pilosa/v2/roaring" - "github.com/pilosa/pilosa/v2/txpath" + "github.com/pilosa/pilosa/v2/txkey" "github.com/pkg/errors" ) @@ -248,7 +248,7 @@ func (tx *RBFTx) RoaringBitmapReader(index, field, view string, shard uint64, fr func (tx *RBFTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { - prefix := string(txpath.AllShardPrefix(index, field, view)) + prefix := string(txkey.AllShardPrefix(index, field, view)) names, err := tx.tx.BitmapNames() if err != nil { @@ -260,7 +260,7 @@ func (tx *RBFTx) SliceOfShards(index, field, view, optionalViewPath string) (sli if !strings.HasPrefix(name, prefix) { continue } - shard := txpath.ShardFromPrefix([]byte(name)) + shard := txkey.ShardFromPrefix([]byte(name)) sliceOfShards = append(sliceOfShards, shard) } return sliceOfShards, nil @@ -296,13 +296,13 @@ func (tx *RBFTx) UseRowCache() bool { // 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)) + return string(txkey.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)) + return string(txkey.FieldPrefix(index, field)) } func (w *RbfDBWrapper) DeleteField(index, field, fieldPath string) error { @@ -330,7 +330,7 @@ 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) + prefix := txkey.IndexOnlyPrefix(indexName) w.muDb.Lock() defer w.muDb.Unlock() diff --git a/rbf/tx_test.go b/rbf/tx_test.go index 19aeb4d53..32704988e 100644 --- a/rbf/tx_test.go +++ b/rbf/tx_test.go @@ -21,7 +21,7 @@ import ( "time" "github.com/pilosa/pilosa/v2/rbf" - "github.com/pilosa/pilosa/v2/txpath" + "github.com/pilosa/pilosa/v2/txkey" ) func TestTx_CommitRollback(t *testing.T) { @@ -490,5 +490,5 @@ func TestTx_Dump(t *testing.T) { } func rbfName(index, field, view string, shard uint64) string { - return string(txpath.Prefix(index, field, view, shard)) + return string(txkey.Prefix(index, field, view, shard)) } diff --git a/roaring/container_stash.go b/roaring/container_stash.go index de5d17047..6cdd53287 100644 --- a/roaring/container_stash.go +++ b/roaring/container_stash.go @@ -492,7 +492,7 @@ func (c *Container) setBitmap(bitmap []uint64) { // runs yields the data viewed as a slice of intervals. func (c *Container) runs() []Interval16 { if c == nil { - panic("attempt to read nil container's runs") + return nil } if roaringParanoia { if c.typeID != ContainerRun { diff --git a/roaring/roaring.go b/roaring/roaring.go index a5044f0b9..49e5b08e5 100644 --- a/roaring/roaring.go +++ b/roaring/roaring.go @@ -3301,6 +3301,7 @@ func (c *Container) bitmapRemove(v uint16) (*Container, bool) { if !c.bitmapContains(v) { return c, false } + // removing the last item? we can just return the empty container. if c.N() == 1 { return nil, true @@ -6853,22 +6854,21 @@ func AsRuns(c *Container) []Interval16 { return c.runs() } -func ConvertArrayToBitmap(c *Container) { - c.arrayToBitmap() +func ConvertArrayToBitmap(c *Container) *Container { + return c.arrayToBitmap() } -func ConvertRunToBitmap(c *Container) { - c.runToBitmap() +func ConvertRunToBitmap(c *Container) *Container { + return c.runToBitmap() } -func Optimize(c *Container) { - c.optimize() +func Optimize(c *Container) *Container { + return c.optimize() } func Union(a, b *Container) (c *Container) { c = union(a, b) // c can be have arrays that are too big, and need // to be optimized into raw bitmaps. - c.optimize() - return c + return c.optimize() } func Difference(a, b *Container) *Container { diff --git a/roaring/roaring_test.go b/roaring/roaring_test.go index 8a0d3ead4..217d24bb7 100644 --- a/roaring/roaring_test.go +++ b/roaring/roaring_test.go @@ -2158,3 +2158,105 @@ func BenchmarkIntersectInPlace(b *testing.B) { bm.IntersectInPlace(data.a2, data.b, data.r1, data.r2) } } + +func TestArrayAddRemoveAddRemove(t *testing.T) { + + var ct *roaring.Container + for i := 0; i < 3; i++ { + switch i { + case 0: + ct = roaring.NewContainerArray(nil) + case 1: + ct = roaring.NewContainerBitmap(0, nil) + case 2: + ct = roaring.NewContainerRun(nil) + } + values := []uint16{1, 0, 13, 77, 1511} + var added, removed bool + for _, v := range values { + ct, added = ct.Add(v) + if !added { + t.Fatalf("expected added to be true for: %d", v) + } + if !ct.Contains(v) { + t.Fatalf("expected bitmap to contain: %d", v) + } + ct, removed = ct.Remove(v) + if !removed { + t.Fatalf("expected removed to be true for: %d", v) + } + if n := ct.N(); n != 0 { + t.Fatalf("expected bitmap count to be zero now, got n=%v", n) + } + if ct.Contains(v) { + t.Fatalf("expected bitmap to not contain: %d", v) + } + } + } +} + +func TestBitmapAddRemoveAddRemove(t *testing.T) { + ct := roaring.NewContainerBitmap(0, nil) + putmeValues := []uint16{1, 0, 13, 77, 1511} + + var added, removed bool + for _, v := range putmeValues { + ct, added = ct.Add(v) + if !added { + t.Fatalf("expected added to be true for: %d", v) + } + if !ct.Contains(v) { + t.Fatalf("expected bitmap to contain: %d", v) + } + ct, removed = ct.Remove(v) + if !removed { + t.Fatalf("expected removed to be true for: %d", v) + } + if ct.Contains(v) { + t.Fatalf("expected bitmap to not contain: %d", v) + } + + if n := ct.N(); n != 0 { + t.Fatalf("expected bitmap count to be zero now, got n=%v", n) + } + + if ct != nil { + b := roaring.AsBitmap(ct) + for i, e := range b { + if e != 0 { + t.Fatalf("expected ct AsBitmap() contents to be all zeros, uint64 i=%v as %v", i, e) + } + } + } + } +} + +func TestRunAddRemoveAddRemove(t *testing.T) { + + ct := roaring.NewContainerRun(nil) + putmeValues := []uint16{1, 0, 13, 77, 1511} + + var added, removed bool + for _, v := range putmeValues { + ct, added = ct.Add(v) + if !added { + t.Fatalf("expected added to be true for: %d", v) + } + if !ct.Contains(v) { + t.Fatalf("expected bitmap to contain: %d", v) + } + ct, removed = ct.Remove(v) + if !removed { + t.Fatalf("expected removed to be true for: %d", v) + } + if n := len(roaring.AsRuns(ct)); n != 0 { + t.Fatalf("expected ct AsRuns() len to be zero now, got n=%v", n) + } + if n := ct.N(); n != 0 { + t.Fatalf("expected bitmap count to be zero now, got n=%v", n) + } + if ct.Contains(v) { + t.Fatalf("expected bitmap to not contain: %d", v) + } + } +} diff --git a/rrtx.go b/rrtx.go index c5fcc112b..a9bfe647c 100644 --- a/rrtx.go +++ b/rrtx.go @@ -21,297 +21,11 @@ import ( "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 @@ -356,8 +70,7 @@ func (tx *RoaringTx) SliceOfShards(index, field, view, optionalViewPath string) // 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()) + tx.Index.holder.Logger.Debugf("WARNING: couldn't use non-integer file as shard in index/field/view %s/%s/%s: %s", index, field, view, fi.Name()) continue } sliceOfShards = append(sliceOfShards, shard) diff --git a/server/cluster_test.go b/server/cluster_test.go index 744b63dcb..eb281d08f 100644 --- a/server/cluster_test.go +++ b/server/cluster_test.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "net/http" + "os" "reflect" "strings" "testing" @@ -183,6 +184,19 @@ func TestClusterResize_AddNode(t *testing.T) { } }) t.Run("ContinuousShards", func(t *testing.T) { + // Why are we skipping this test under blue-green with Roaring? + // + // We see red test: during resize during importRoaringBits + // PILOSA_TXSRC=rbf_roaring go test -v -tags=' shardwidth20' "-gcflags=all=-d=checkptr=0" -run TestClusterResize_AddNode/"ContinuousShards" + // green: + // PILOSA_TXSRC=roaring_rbf go test -v -tags=' shardwidth20' "-gcflags=all=-d=checkptr=0" -run TestClusterResize_AddNode/"ContinuousShards" + // + // but rbf_badger and badger_rbf are both green (use the same data values for containers). + // + // Conclude: roaring reads a different size of data []byte in (due to ops log) bits vs others (RBF, badger), so + // we can't do blue-green with roaring on this test. + skipTestUnderBlueGreenWithRoaring(t) + // Configure node0 m0 := test.MustRunCluster(t, 1)[0] defer m0.Close() @@ -725,3 +739,12 @@ func TestClusterMutualTLS(t *testing.T) { t.Fatal(err) } } + +func skipTestUnderBlueGreenWithRoaring(t *testing.T) { + src := os.Getenv("PILOSA_TXSRC") + if strings.Contains(src, "_") { + if strings.Contains(src, "roaring") { + t.Skip("skip for roaring blue-green") + } + } +} diff --git a/stattx.go b/stattx.go new file mode 100644 index 000000000..5aa3389c1 --- /dev/null +++ b/stattx.go @@ -0,0 +1,666 @@ +// 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 ( + "fmt" + "io" + "math" + "os" + "sort" + "sync" + "time" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// statTx is useful to profile on a +// per method basis, and to play with +// read/write locking. +type statTx struct { + b Tx + stats *callStats +} + +// for now, just track call stats globally. But each statTx has +// a pointer to a callStats, so could be made per index or per shard, etc. +var globalCallStats = newCallStats() + +type callStats struct { + // protect elap + mu sync.Mutex + + // track how much time each call took. + elap map[kall]*elapsed +} + +type elapsed struct { + dur []float64 +} + +func newCallStats() *callStats { + w := &callStats{} + w.reset() + return w +} + +func (w *callStats) reset() { + w.mu.Lock() + defer w.mu.Unlock() + + w.elap = make(map[kall]*elapsed) + for i := kall(0); i < kLast; i++ { + w.elap[i] = &elapsed{} + } +} + +type LineSorter struct { + Line string + Tot float64 +} + +type SortByTot []*LineSorter + +func (p SortByTot) Len() int { + return len(p) +} +func (p SortByTot) Less(i, j int) bool { + return p[i].Tot < p[j].Tot +} +func (p SortByTot) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} + +func (c *callStats) report() (r string) { + txsrc := os.Getenv("PILOSA_TXSRC") + r = fmt.Sprintf("callStats: (%v)\n", txsrc) + c.mu.Lock() + defer c.mu.Unlock() + var lines []*LineSorter + for i := kall(0); i < kLast; i++ { + slc := c.elap[i].dur + n := len(slc) + if n == 0 { + continue + } + mean, sd, totaltm := computeMeanSd(slc) + if n == 1 { + sd = 0 + mean = slc[0] + totaltm = slc[0] + } + line := fmt.Sprintf(" %20v N=%8v avg/op: %12v sd: %12v total: %12v\n", i.String(), n, time.Duration(mean), time.Duration(sd), time.Duration(totaltm)) + lines = append(lines, &LineSorter{Line: line, Tot: totaltm}) + } + sort.Sort(SortByTot(lines)) + for i := range lines { + r += lines[i].Line + } + return +} + +var NaN = math.NaN() + +func computeMeanSd(slc []float64) (mean, sd, tot float64) { + if len(slc) < 2 { + return NaN, NaN, NaN + } + for _, v := range slc { + tot += v + } + n := float64(len(slc)) + mean = tot / n + + variance := 0.0 + for _, v := range slc { + tmp := (v - mean) + variance += tmp * tmp + } + variance = variance / n // biased, but we don't care b/c we can have very small n + sd = math.Sqrt(variance) + if sd < 1e-8 { + // sd is super close to zero, NaN out the z-score rather than +/- Inf + sd = NaN + } + return +} + +func (c *callStats) add(k kall, dur time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + e := c.elap[k] + e.dur = append(e.dur, float64(dur)) +} + +func newStatTx(b Tx) *statTx { + w := &statTx{ + b: b, + + // For now, just track call stats globally. + // But this could be made per-Tx by making this be stats: newCallStats(), + // for example. + stats: globalCallStats, + } + return w +} + +type kall int + +// constants for kall argument to callStats.add() +const ( + kIncrementOpN kall = iota + kNewTxIterator + kImportRoaringBits + kRollback + kCommit + kRoaringBitmap + kContainer + kPutContainer + kRemoveContainer + kAdd + kRemove + kContains + kContainerIterator + kForEach + kForEachRange + kCount + kMax + kMin + kUnionInPlace + kCountRange + kOffsetRange + kRoaringBitmapReader + kSliceOfShards + kLast // mark the end, always keep this last. The following aren't tracked atm: + kType + kDump + kReadonly + kPointer + kUseRowCache +) + +func (k kall) String() string { + switch k { + case kIncrementOpN: + return "kIncrementOpN" + case kNewTxIterator: + return "kNewTxIterator" + case kImportRoaringBits: + return "kImportRoaringBits" + case kRollback: + return "kRollback" + case kCommit: + return "kCommit" + case kRoaringBitmap: + return "kRoaringBitmap" + case kContainer: + return "kContainer" + case kPutContainer: + return "kPutContainer" + case kRemoveContainer: + return "kRemoveContainer" + case kAdd: + return "kAdd" + case kRemove: + return "kRemove" + case kContains: + return "kContains" + case kContainerIterator: + return "kContainerIterator" + case kForEach: + return "kForEach" + case kForEachRange: + return "kForEachRange" + case kCount: + return "kCount" + case kMax: + return "kMax" + case kMin: + return "kMin" + case kUnionInPlace: + return "kUnionInPlace" + case kCountRange: + return "kCountRange" + case kOffsetRange: + return "kOffsetRange" + case kRoaringBitmapReader: + return "kRoaringBitmapReader" + case kSliceOfShards: + return "kSliceOfShards" + case kLast: + return "kLast" + case kType: + return "kType" + case kDump: + return "kDump" + case kReadonly: + return "kReadonly" + case kPointer: + return "kPointer" + case kUseRowCache: + return "kUseRowCache" + } + panic(fmt.Sprintf("unknown kall '%v'", int(k))) +} + +var _ = newStatTx // happy linter +var _ = kPointer +var _ = kUseRowCache +var _ = kType +var _ = kDump +var _ = kReadonly + +var _ Tx = (*statTx)(nil) + +//IncrementOpN +func (c *statTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + me := kIncrementOpN + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + c.b.IncrementOpN(index, field, view, shard, changedN) +} + +func (c *statTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + me := kNewTxIterator + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + return c.b.NewTxIterator(index, field, view, shard) +} + +func (c *statTx) 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) { + me := kImportRoaringBits + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ImportRoaringBits() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) +} + +func (c *statTx) Dump() { + c.b.Dump() +} + +func (c *statTx) 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 *statTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +func (c *statTx) Rollback() { + me := kRollback + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + c.b.Rollback() +} + +func (c *statTx) Commit() error { + me := kCommit + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Commit() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Commit() +} + +func (c *statTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + me := kRoaringBitmap + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RoaringBitmap() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.RoaringBitmap(index, field, view, shard) +} + +func (c *statTx) Container(index, field, view string, shard uint64, key uint64) (ct *roaring.Container, err error) { + me := kContainer + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Container() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Container(index, field, view, shard, key) +} + +func (c *statTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) error { + me := kPutContainer + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see PutContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.PutContainer(index, field, view, shard, key, rc) +} + +func (c *statTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + me := kRemoveContainer + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RemoveContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.RemoveContainer(index, field, view, shard, key) +} + +func (c *statTx) UseRowCache() bool { + return c.b.UseRowCache() +} + +func (c *statTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + me := kAdd + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Add() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Add(index, field, view, shard, batched, a...) +} + +func (c *statTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + me := kRemove + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Remove() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Remove(index, field, view, shard, a...) +} + +func (c *statTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) { + me := kContains + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Contains() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Contains(index, field, view, shard, key) +} + +func (c *statTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) { + me := kContainerIterator + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ContainerIterator() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) +} + +func (c *statTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + me := kForEach + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEach() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.ForEach(index, field, view, shard, fn) +} + +func (c *statTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + me := kForEachRange + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEachRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.ForEachRange(index, field, view, shard, start, end, fn) +} + +func (c *statTx) Count(index, field, view string, shard uint64) (uint64, error) { + me := kCount + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Count() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Count(index, field, view, shard) +} + +func (c *statTx) Max(index, field, view string, shard uint64) (uint64, error) { + me := kMax + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Max() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Max(index, field, view, shard) +} + +func (c *statTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + me := kMin + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Min() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Min(index, field, view, shard) +} + +func (c *statTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + me := kUnionInPlace + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see UnionInPlace() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.UnionInPlace(index, field, view, shard, others...) +} + +func (c *statTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + me := kCountRange + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see CountRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.CountRange(index, field, view, shard, start, end) +} + +func (c *statTx) OffsetRange(index, field, view string, shard, offset, start, end uint64) (other *roaring.Bitmap, err error) { + me := kOffsetRange + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see OffsetRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.OffsetRange(index, field, view, shard, offset, start, end) +} + +func (c *statTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + me := kRoaringBitmapReader + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RoaringBitmapReader() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) +} + +func (c *statTx) Type() string { + return c.b.Type() +} + +func (c *statTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + me := kSliceOfShards + + t0 := time.Now() + defer func() { + c.stats.add(me, time.Since(t0)) + }() + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see SliceOfShards() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.SliceOfShards(index, field, view, optionalViewPath) +} diff --git a/txfactory.go b/txfactory.go index 446a0ab9e..0d4dcd7a9 100644 --- a/txfactory.go +++ b/txfactory.go @@ -24,7 +24,7 @@ import ( "text/tabwriter" "github.com/pilosa/pilosa/v2/roaring" - "github.com/pilosa/pilosa/v2/txpath" + "github.com/pilosa/pilosa/v2/txkey" "github.com/pkg/errors" ) @@ -34,15 +34,6 @@ const ( 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" - BlueGreenRoaringBadger string = "roaring_badger" - - BlueGreenRBFRoaring string = "rbf_roaring" - BlueGreenRoaringRBF string = "roaring_rbf" - - BlueGreenBadgerRBF string = "badger_rbf" - BlueGreenRBFBadger string = "rbf_badger" ) // DefaultTxsrc is set here. pilosa/server/config.go references it @@ -69,18 +60,17 @@ var sep = string(os.PathSeparator) // transactions so that RBF, or Badger, or Roaring-fragment-files, or several // of these at once in parallel, is used as the storage and transction layer. type TxFactory struct { - typeOfTx txtype + typeOfTx string - badgerDB *BadgerDBWrapper - - rbfDB *RbfDBWrapper + types []txtype // blue-green split individually here + badgerDB *BadgerDBWrapper + lmDB *LMDBWrapper + 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 @@ -90,77 +80,55 @@ type TxFactory struct { type txtype int const ( - noneTxn txtype = 0 - - roaringFragmentFilesTxn txtype = 1 // these don't really have any transactions - badgerTxn txtype = 2 - rbfTxn txtype = 3 - - // A is listed first, B is second. blueGreenTx returns the B output. - blueGreenBadgerRoaring txtype = 4 - blueGreenRoaringBadger txtype = 5 - - blueGreenRBFRoaring txtype = 6 - blueGreenRoaringRBF txtype = 7 - - blueGreenBadgerRBF txtype = 8 - blueGreenRBFBadger txtype = 9 - - lmdbTxn txtype = 10 + noneTxn txtype = 0 + roaringTxn txtype = 1 // these don't really have any transactions + badgerTxn txtype = 2 + rbfTxn txtype = 3 + lmdbTxn txtype = 4 ) -func (txf *TxFactory) NeedsSnapshot() bool { - switch txf.typeOfTx { - case noneTxn: - panic("noneTxn should not occur") - case roaringFragmentFilesTxn: - return true - case badgerTxn: - return false - case rbfTxn: - return false - case blueGreenBadgerRoaring: - return true - case blueGreenRoaringBadger: - return true - case blueGreenRBFRoaring: - return true - case blueGreenRoaringRBF: - return true - case blueGreenBadgerRBF: - return false - case blueGreenRBFBadger: - return false - case lmdbTxn: - return false +func (txf *TxFactory) NeedsSnapshot() (b bool) { + for _, ty := range txf.types { + switch ty { + case roaringTxn: + b = true + } } - panic(fmt.Sprintf("unknown typeOfTx '%v'", txf.typeOfTx)) + return } -func MustTxsrcToTxtype(txsrc string) txtype { - switch txsrc { - case RoaringTxn: // "roaring" - return roaringFragmentFilesTxn - case BadgerTxn: // "badger" - return badgerTxn - case RBFTxn: // "rbf" - return rbfTxn - case BlueGreenBadgerRoaring: //"badger_roaring" - return blueGreenBadgerRoaring - case BlueGreenRoaringBadger: // "roaring_badger" - return blueGreenRoaringBadger - case BlueGreenRBFRoaring: //"rbf_roaring" - return blueGreenRBFRoaring - case BlueGreenRoaringRBF: //"roaring_rbf" - return blueGreenRoaringRBF - case BlueGreenBadgerRBF: // "badger_rbf" - return blueGreenBadgerRBF - case BlueGreenRBFBadger: // "rbf_badger" - return blueGreenRBFBadger - case LmdbTxn: - return lmdbTxn +func MustTxsrcToTxtype(txsrc string) (types []txtype) { + + var srcs []string + if strings.Contains(txsrc, "_") { + srcs = strings.Split(txsrc, "_") + if len(srcs) != 2 { + panic("only two blue-green comparisons permitted") + } + } else { + srcs = append(srcs, txsrc) } - panic(fmt.Sprintf("unknown txsrc '%v'", txsrc)) + + for i, s := range srcs { + switch s { + case RoaringTxn: // "roaring" + types = append(types, roaringTxn) + case BadgerTxn: // "badger" + types = append(types, badgerTxn) + case RBFTxn: // "rbf" + types = append(types, rbfTxn) + case LmdbTxn: // "lmdb" + types = append(types, lmdbTxn) + default: + panic(fmt.Sprintf("unknown txsrc '%v'", s)) + } + if i == 1 { + if types[1] == types[0] { + panic(fmt.Sprintf("cannot blue-green the same txsrc on both arms: '%v'", s)) + } + } + } + return } // NewTxFactory always opens an existing database. If you @@ -170,55 +138,48 @@ func MustTxsrcToTxtype(txsrc string) txtype { 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 > 10 { - panic(fmt.Sprintf("invalid txtype '%v'", int(ty))) - } + types := MustTxsrcToTxtype(txsrc) f = &TxFactory{ - typeOfTx: ty, + types: types, + typeOfTx: txsrc, roaringDB: NewRoaringStore(), } - switch ty { - case badgerTxn, blueGreenBadgerRoaring, blueGreenRoaringBadger, blueGreenBadgerRBF, blueGreenRBFBadger: + for _, ty := range f.types { + switch ty { + case roaringTxn: + // no-op. these are just files in a directory - // one, big, bad-ass badger for all data: the honeyBadger. - // - // Note that having a single Tx backing store for all indexes - // enables cross-index Tx, which are important and are tested for. - path := dir + sep + "honeyBadger" + case badgerTxn: + // one, big, bad-ass badger for all data: the honeyBadger. + // + // Note that having a single Tx backing store for all indexes + // enables cross-index Tx, which are important and are tested for. + path := dir + sep + "honeyBadger" - f.badgerDB, err = globalBadgerReg.openBadgerDBWrapper(path) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("cannot open 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 - } + // electric-fence like finding of access to mmapped data beyond + // transaction end time. + f.badgerDB.doAllocZero = DetectMemAccessPastTx - switch ty { - case rbfTxn, blueGreenRBFRoaring, blueGreenRoaringRBF, blueGreenBadgerRBF, blueGreenRBFBadger: - - 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.") - /* + case rbfTxn: + 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)) + } + case lmdbTxn: path := dir + sep + "all-in-one" - f.lmDB, err = globalLMDBReg.newLMDBWrapper(path) + f.lmDB, err = globalLMDBReg.openLMDBWrapper(path) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("cannot create new lmdb db. path='%v'", path)) } - */ + } } return f, err @@ -233,227 +194,160 @@ type Txo struct { Shard uint64 } -func (f *TxFactory) TxType() txtype { +func (f *TxFactory) TxType() string { return f.typeOfTx } -func (f *TxFactory) DeleteIndex(name string) error { - switch f.typeOfTx { - case roaringFragmentFilesTxn: - // from holder.go:955, by default is already done there with os.RemoveAll() - return nil - case badgerTxn: - return f.badgerDB.DeleteIndex(name) - case rbfTxn: - return f.rbfDB.DeleteIndex(name) - case blueGreenBadgerRoaring: - return f.badgerDB.DeleteIndex(name) - case blueGreenRoaringBadger: - return f.badgerDB.DeleteIndex(name) - case blueGreenRBFBadger: - _ = f.rbfDB.DeleteIndex(name) - return f.badgerDB.DeleteIndex(name) - case blueGreenBadgerRBF: - _ = f.badgerDB.DeleteIndex(name) - return f.rbfDB.DeleteIndex(name) - case blueGreenRBFRoaring: - // roaring already done - return f.rbfDB.DeleteIndex(name) - case blueGreenRoaringRBF: - // roaring already done - return f.rbfDB.DeleteIndex(name) - } - panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) +func (f *TxFactory) TxTypes() []txtype { + return f.types } -func (f *TxFactory) DeleteFieldFromStore(index, field, fieldPath string) error { - switch f.typeOfTx { - case roaringFragmentFilesTxn: - return f.roaringDB.DeleteField(index, field, fieldPath) - case badgerTxn: - return f.badgerDB.DeleteField(index, field, fieldPath) - case rbfTxn: - return f.rbfDB.DeleteField(index, field, fieldPath) - case blueGreenBadgerRoaring: - _ = f.badgerDB.DeleteField(index, field, fieldPath) - return f.roaringDB.DeleteField(index, field, fieldPath) - case blueGreenRoaringBadger: - _ = f.roaringDB.DeleteField(index, field, fieldPath) - return f.badgerDB.DeleteField(index, field, fieldPath) - case blueGreenRBFBadger: - _ = f.badgerDB.DeleteField(index, field, fieldPath) - return f.rbfDB.DeleteField(index, field, fieldPath) - case blueGreenBadgerRBF: - _ = f.rbfDB.DeleteField(index, field, fieldPath) - return f.badgerDB.DeleteField(index, field, fieldPath) - case blueGreenRBFRoaring: - _ = f.rbfDB.DeleteField(index, field, fieldPath) - return f.roaringDB.DeleteField(index, field, fieldPath) - case blueGreenRoaringRBF: - _ = f.roaringDB.DeleteField(index, field, fieldPath) - return f.rbfDB.DeleteField(index, field, fieldPath) - //case lmdbTxn: - //return f.lmDB.DeleteField(index, field, fieldPath) +func (f *TxFactory) DeleteIndex(name string) (err error) { + for _, ty := range f.types { + switch ty { + case roaringTxn: + // from holder.go:955, by default is already done there with os.RemoveAll() + case badgerTxn: + err = f.badgerDB.DeleteIndex(name) + case rbfTxn: + err = f.rbfDB.DeleteIndex(name) + case lmdbTxn: + err = f.lmDB.DeleteIndex(name) + default: + panic(fmt.Sprintf("unknown txtyp : '%v'", ty)) + } } - panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) + return } -func (f *TxFactory) DeleteFragmentFromStore(index, field, view string, shard uint64, frag *fragment) error { - switch f.typeOfTx { - case roaringFragmentFilesTxn: - return f.roaringDB.DeleteFragment(index, field, view, shard, frag) - case badgerTxn: - return f.badgerDB.DeleteFragment(index, field, view, shard, frag) - case rbfTxn: - return f.rbfDB.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) - case blueGreenRoaringBadger: - _ = f.roaringDB.DeleteFragment(index, field, view, shard, frag) - return f.badgerDB.DeleteFragment(index, field, view, shard, frag) - case blueGreenRBFBadger: - _ = f.rbfDB.DeleteFragment(index, field, view, shard, frag) - return f.badgerDB.DeleteFragment(index, field, view, shard, frag) - case blueGreenBadgerRBF: - _ = f.badgerDB.DeleteFragment(index, field, view, shard, frag) - return f.rbfDB.DeleteFragment(index, field, view, shard, frag) - case blueGreenRBFRoaring: - _ = f.rbfDB.DeleteFragment(index, field, view, shard, frag) - return f.roaringDB.DeleteFragment(index, field, view, shard, frag) - case blueGreenRoaringRBF: - _ = f.roaringDB.DeleteFragment(index, field, view, shard, frag) - return f.rbfDB.DeleteFragment(index, field, view, shard, frag) - // case lmdbTxn: - // return f.lmDB.DeleteFragment(index, field, view, shard, frag) +func (f *TxFactory) DeleteFieldFromStore(index, field, fieldPath string) (err error) { + for _, ty := range f.types { + switch ty { + case roaringTxn: + err = f.roaringDB.DeleteField(index, field, fieldPath) + case badgerTxn: + err = f.badgerDB.DeleteField(index, field, fieldPath) + case rbfTxn: + err = f.rbfDB.DeleteField(index, field, fieldPath) + case lmdbTxn: + err = f.lmDB.DeleteField(index, field, fieldPath) + default: + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", ty)) + } } - panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) + return +} +func (f *TxFactory) DeleteFragmentFromStore( + index, field, view string, shard uint64, frag *fragment, +) (err error) { + + for _, ty := range f.types { + switch ty { + case roaringTxn: + err = f.roaringDB.DeleteFragment(index, field, view, shard, frag) + case badgerTxn: + err = f.badgerDB.DeleteFragment(index, field, view, shard, frag) + case rbfTxn: + err = f.rbfDB.DeleteFragment(index, field, view, shard, frag) + case lmdbTxn: + err = f.lmDB.DeleteFragment(index, field, view, shard, frag) + default: + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", ty)) + } + } + return } func (f *TxFactory) CloseIndex(idx *Index) error { // under roaring and all the new databases, this is a no-op. + //idx.Dump("CloseIndex") + return nil } -func (f *TxFactory) CloseDB() error { +func (f *TxFactory) CloseDB() (err error) { if f.dbsClosed { return nil } f.dbsClosed = true - switch f.typeOfTx { - case roaringFragmentFilesTxn: - return nil - case badgerTxn: - return f.badgerDB.Close() - case rbfTxn: - return f.rbfDB.Close() - case blueGreenBadgerRoaring: - return f.badgerDB.Close() - case blueGreenRoaringBadger: - return f.badgerDB.Close() - case blueGreenRBFRoaring: - 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 + + for _, ty := range f.types { + switch ty { + case roaringTxn: + // no-op + case badgerTxn: + err = f.badgerDB.Close() + case rbfTxn: + err = f.rbfDB.Close() + case lmdbTxn: + err = f.lmDB.Close() + default: + panic(fmt.Sprintf("unknown txtype: '%v'", ty)) + } } - panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) + return } -func (f *TxFactory) NewTx(o Txo) Tx { +var globalUseStatTx = false + +func init() { + v := os.Getenv("PILOSA_CALLSTAT") + if v != "" { + globalUseStatTx = true + } +} + +func (f *TxFactory) NewTx(o Txo) (txn Tx) { + defer func() { + if globalUseStatTx { + txn = newStatTx(txn) + } + }() indexName := "" if o.Index != nil { indexName = o.Index.name } + var txns []Tx - switch f.typeOfTx { - 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, o.Fragment) - return btx - case rbfTxn: - tx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) - panicOn(err) - if err != nil { - panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) + for _, ty := range f.types { + switch ty { + case roaringTxn: + txns = append(txns, &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment}) + case badgerTxn: + //tx := NewMultiTxWithIndex(o.Write, o.Index) + //txns = append(txns, tx + btx := f.badgerDB.NewBadgerTx(o.Write, indexName, o.Fragment) + txns = append(txns, btx) + case rbfTxn: + //tx := NewMultiTxWithIndex(o.Write, o.Index) + tx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) + if err != nil { + panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) + } + txns = append(txns, tx) + case lmdbTxn: + txns = append(txns, f.lmDB.NewLMDBTx(o.Write, indexName, o.Fragment)) + default: + panic(fmt.Sprintf("unknown txtyp: '%v'", ty)) } - return tx - case lmdbTxn: - //return f.lmDB.newPoolTx(o.Write, indexName) - - case blueGreenBadgerRoaring: - 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, 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, o.Fragment) - rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) - if err != nil { - panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) - } - return newBlueGreenTx(btx, rbftx, f.idx) - case blueGreenRBFBadger: - 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.NewRBFTx transaction errored")) - } - return newBlueGreenTx(rbftx, btx, f.idx) - - case blueGreenRBFRoaring: - rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) - if err != nil { - 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, rtx, f.idx) - case blueGreenRoaringRBF: - rbftx, err := f.rbfDB.NewRBFTx(o.Write, indexName, o.Fragment) - if err != nil { - 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, f.idx) } - panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) + if len(txns) > 1 { + return newBlueGreenTx(txns[0], txns[1], f.idx) + } + return txns[0] } func (ty txtype) String() string { switch ty { case noneTxn: return "noneTxn" - case roaringFragmentFilesTxn: - return "roaringFragmentFilesTxn" + case roaringTxn: + return "roaringTxn" case badgerTxn: return "badgerTxn" case rbfTxn: return "rbfTxn" - case blueGreenBadgerRoaring: - return "blueGreenBadgerRoaring" - case blueGreenRoaringBadger: - return "blueGreenRoaringBadger" - case blueGreenRBFRoaring: - return "blueGreenRBFRoaring" - case blueGreenRoaringRBF: - return "blueGreenRoaringRBF" - case blueGreenBadgerRBF: - return "blueGreenBadgerRBF" - case blueGreenRBFBadger: - return "blueGreenRBFBadger" case lmdbTxn: return "lmdbTxn" } @@ -600,7 +494,7 @@ func stringifiedRawRoaringFragment(path string, index, field, view string, shard srbm := bitmapAsString(rbm) panicOn(err) - bkey := string(txpath.Key(index, field, view, shard, ckey)) + bkey := string(txkey.Key(index, field, view, shard, ckey)) r += fmt.Sprintf("%v -> %v (%v hot)\n", bkey, hash, ct.N()) r += " ......." + srbm + "\n" diff --git a/txpath/txpath.go b/txkey/txkey.go similarity index 94% rename from txpath/txpath.go rename to txkey/txkey.go index a28a2ae89..4d070c99c 100644 --- a/txpath/txpath.go +++ b/txkey/txkey.go @@ -12,10 +12,10 @@ // 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 +// Package txkey 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 +package txkey import ( "bytes" @@ -155,3 +155,12 @@ func IndexOnlyPrefix(indexName string) []byte { func FieldPrefix(index, field string) []byte { return []byte(fmt.Sprintf("idx:'%v';fld:'%v';", index, field)) } + +func PrefixFromKey(bkey []byte) (prefix []byte) { + MustValidateKey(bkey) + beg := bytes.LastIndex(bkey, []byte("@")) + if beg == -1 { + panic(fmt.Sprintf("bad bkey='%v' did not have '@' extract prefix", string(bkey))) + } + return bkey[:beg+1] +} diff --git a/txpath/txpath_test.go b/txkey/txkey_test.go similarity index 82% rename from txpath/txpath_test.go rename to txkey/txkey_test.go index d30fd7d7f..af06d261a 100644 --- a/txpath/txpath_test.go +++ b/txkey/txkey_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package txpath +package txkey import ( "bytes" @@ -87,3 +87,22 @@ func Test_ShardFromKey(t *testing.T) { }() } + +func Test_PrefixFromKey(t *testing.T) { + k := []byte("idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615") + x := []byte("idx:'i';fld:'f';vw:'standard';shd:'1';ckey@") + pre := PrefixFromKey(k) + if !bytes.Equal(pre, x) { + nx := len(x) + npre := len(pre) + if nx != npre { + panic(fmt.Sprintf("nx=%v, npre=%v; expected '%v', observed '%v'", nx, npre, string(x), string(pre))) + } + for i := 0; i < nx; i++ { + if x[i] != pre[i] { + panic(fmt.Sprintf("first diff at index %v, expected '%v', observed '%v'", i, string(x[:i]), string(pre[:i]))) + } + } + panic(fmt.Sprintf("expected:\n%v\n, observed:\n%v\n", string(x), string(pre))) + } +} diff --git a/txpath/txprefix.go~ b/txpath/txprefix.go~ deleted file mode 100644 index f998a219c..000000000 --- a/txpath/txprefix.go~ +++ /dev/null @@ -1,15 +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 txprefix diff --git a/txpath/txprefix_test.go~ b/txpath/txprefix_test.go~ deleted file mode 100644 index 8bde1e29d..000000000 --- a/txpath/txprefix_test.go~ +++ /dev/null @@ -1,44 +0,0 @@ -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. - }() -}