From 97b530ca784f1d04c29e46ac71515681af4951ff Mon Sep 17 00:00:00 2001 From: Jason Aten Date: Mon, 6 Jul 2020 23:22:16 -0400 Subject: [PATCH] integration of Tx, RoaringTx and BadgerTx implementations. - all tests green on RoaringTx - RoaringTx on by default - blueGreenTx testing framework available for A-vs-B comparison of Tx implementations - flag -tx added to server command line but not wired to change NewIndex() selection yet. - 918 green tests, 14 tests red on BadgerTx. A full list of the 14 red tests on BadgerTx follows. Note that these red tests represent not defects in BadgerDB or BadgerTx but rather failures of the pre-existing pilosa infrastructure to yet be fully adapted from files to using a transactional storage engine. As such these are tests that RBF should not be expected to pass yet either. Fixing the pilosa infrastructure to allow these tests to go green under Badger is the next and highest priority order of business, but RBF can get much testing benefit from the 918 green tests we do have, and hence we merge as much as we have today. The 14 red tests when NewIndex() is set to use BadgerTx are as follows. Note in particular that pilosa cluster resizing is not working yet under a transactional store. TestCluster_ResizeStates/Multiple_nodes,_with_data TestImportClearRestart/0MaxOpN10000 TestImportClearRestart/1MaxOpN10000 TestImportClearRestart/2MaxOpN10000 TestImportClearRestart/3MaxOpN10000 TestExecutor_Execute_Existence/Row TestExecutor_ForeignIndex TestExecutor_Execute_CountDistinct/Distinct TestExecutor_Execute_CountDistinct/Count(Distinct) TestExecutor_Execute_CountDistinct/GroupBy(Distinct) TestExecutor_BareDistinct TestExecutor_Execute_TopNDistinct/TopN TestHolderSyncer_IntField/BasicSync TestHolderSyncer_IntField/MultiShard --- Makefile | 22 + api.go | 43 +- badger.go | 1701 +++++++++++++++++++++++++++++++++++ badger_test.go | 1442 +++++++++++++++++++++++++++++ blake3.go | 95 ++ blake3_test.go | 47 + bluegreentx.go | 421 +++++++++ cache.go | 16 + catcher.go | 277 ++++++ cluster_internal_test.go | 18 +- ctl/server.go | 3 + executor.go | 143 ++- executor_internal_test.go | 12 +- executor_test.go | 14 +- field.go | 4 +- fragment.go | 179 +++- fragment_internal_test.go | 755 ++++++++++++---- go.mod | 13 +- go.sum | 55 ++ holder.go | 17 +- holder_internal_test.go | 15 +- holder_test.go | 96 +- index.go | 36 +- license.exceptions | 1 + mmap_test.go | 3 +- pql/ast.go | 2 +- pql/decimal.go | 8 + roaring/containers_btree.go | 2 + roaring/containers_slice.go | 7 + roaring/roaring.go | 154 +++- roaring/unmarshal_binary.go | 8 +- row.go | 36 +- server.go | 11 + server/config.go | 16 + server/handler_test.go | 23 +- test/holder.go | 60 +- tx.go | 344 ++++++- txfactory.go | 469 ++++++++++ utils_internal_test.go | 6 +- vprint.go | 113 +++ xrbrsupport.go | 2 +- 41 files changed, 6265 insertions(+), 424 deletions(-) create mode 100644 badger.go create mode 100644 badger_test.go create mode 100644 blake3.go create mode 100644 blake3_test.go create mode 100644 bluegreentx.go create mode 100644 catcher.go create mode 100644 txfactory.go create mode 100644 vprint.go diff --git a/Makefile b/Makefile index 511a5900d..b7a74062e 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,28 @@ docker-build: docker-test: docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) golang:$(GO_VERSION) go test -tags='$(BUILD_TAGS)' $(TESTFLAGS) ./... +# run top tests, not subdirs. print summary red/green after. +# The \-\-\- FAIL avoids counting the extra two FAIL strings at then bottom of log.topt. +topt: + go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.roar + @echo " log.topt green: \c"; cat log.topt.roar | grep PASS |wc -l + @echo " log.topt red: \c"; cat log.topt.roar | grep '\-\-\- FAIL' |wc -l + +topt-badger: + PILOSA_TXSRC=badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger + @echo " log.topt green: \c"; cat log.topt.badger | grep PASS |wc -l + @echo " log.topt red: \c"; cat log.topt.badger | grep '\-\-\- FAIL' |wc -l + +topt-rbf: + PILOSA_TXSRC=rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.rbf + @echo " log.topt green: \c"; cat log.topt.rbf | grep PASS |wc -l + @echo " log.topt red: \c"; cat log.topt.rbf | grep '\-\-\- FAIL' |wc -l + +topt-race: + go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.race + @echo " log.topt green: \c"; cat log.topt.race | grep PASS |wc -l + @echo " log.topt red: \c"; cat log.topt.race | grep '\-\-\- FAIL' |wc -l + # Run golangci-lint golangci-lint: require-golangci-lint golangci-lint run --skip-files '.*\.peg\.go' diff --git a/api.go b/api.go index 2d52676c7..b5bc89422 100644 --- a/api.go +++ b/api.go @@ -330,6 +330,7 @@ func setUpImportOptions(opts ...ImportOption) (*ImportOptions, error) { type importJob struct { ctx context.Context + tx Tx req *ImportRoaringRequest shard uint64 field *Field @@ -370,17 +371,23 @@ func importWorker(importWork chan importJob) { var doClear bool switch doAction { case RequestActionOverwrite: - tx := &RoaringTx{Field: j.field} + // TODO(jea): the question here is, why are we commiting this separately from j.tx? + // why doesn't j.tx suffice? It doesn't but why/which is correct? + tx := j.field.holder.indexes[j.field.index].Txf.NewTx(Txo{Write: true, Field: j.field}) + defer tx.Rollback() if err := j.field.importRoaringOverwrite(j.ctx, tx, viewData, j.shard, viewName, j.req.Block); err != nil { return errors.Wrap(err, "importing roaring as overwrite") } + if err := tx.Commit(); err != nil { + return errors.Wrap(err, "commit of importing roaring as overwrite") + } case RequestActionClear: doClear = true fallthrough case RequestActionSet: fileMagic := uint32(binary.LittleEndian.Uint16(viewData[0:2])) if fileMagic == roaring.MagicNumber { // if pilosa roaring format - if err := j.field.importRoaring(j.ctx, viewData, j.shard, viewName, doClear); err != nil { + if err := j.field.importRoaring(j.ctx, j.tx, viewData, j.shard, viewName, doClear); err != nil { return errors.Wrap(err, "importing pilosa roaring") } } else { @@ -388,7 +395,7 @@ func importWorker(importWork chan importJob) { // field.importRoaring changes the standard roaring run format to pilosa roaring data := make([]byte, len(viewData)) copy(data, viewData) - if err := j.field.importRoaring(j.ctx, data, j.shard, viewName, doClear); err != nil { + if err := j.field.importRoaring(j.ctx, j.tx, data, j.shard, viewName, doClear); err != nil { return errors.Wrap(err, "importing standard roaring") } } @@ -439,6 +446,10 @@ func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string, return newPreconditionFailedError(err) } + // Obtain transaction. + tx := index.Txf.NewTx(Txo{Write: true, Index: index}) + defer tx.Rollback() + nodes := api.cluster.shardNodes(indexName, shard) errCh := make(chan error, len(nodes)) for _, node := range nodes { @@ -446,6 +457,7 @@ func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string, if node.ID == api.server.nodeID { api.importWork <- importJob{ ctx: ctx, + tx: tx, req: req, shard: shard, field: field, @@ -465,9 +477,11 @@ func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string, for { select { case <-ctx.Done(): + // defered tx.Rollback() happens automatically here. return ctx.Err() case nodeErr := <-errCh: if nodeErr != nil { + // defered tx.Rollback() happens automatically here. return nodeErr } maxNode++ @@ -475,7 +489,7 @@ func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string, // Exit once all nodes are processed. if maxNode == len(nodes) { - return nil + return tx.Commit() } } } @@ -583,7 +597,8 @@ func (api *API) ExportCSV(ctx context.Context, indexName string, fieldName strin } // Obtain transaction - tx := &RoaringTx{Index: index} + tx := index.Txf.NewTx(Txo{Write: !writable, Index: index}) + defer tx.Rollback() // Wrap writer with a CSV writer. cw := csv.NewWriter(w) @@ -626,10 +641,8 @@ func (api *API) ExportCSV(ctx context.Context, indexName string, fieldName strin // Ensure data is flushed. cw.Flush() - span.LogKV("n", n) - - return nil + return tx.Commit() } // ShardNodes returns the node and all replicas which should contain a shard's data. @@ -1061,7 +1074,8 @@ func (api *API) Import(ctx context.Context, req *ImportRequest, opts ...ImportOp } // Obtain transaction. - tx := &RoaringTx{Index: index} + tx := index.Txf.NewTx(Txo{Write: true, Index: index}) + defer tx.Rollback() if err := req.ValidateWithTimestamp(index.CreatedAt(), field.CreatedAt()); err != nil { return errors.Wrap(err, "validating import value request") @@ -1165,8 +1179,12 @@ func (api *API) Import(ctx context.Context, req *ImportRequest, opts ...ImportOp err = field.Import(tx, req.RowIDs, req.ColumnIDs, timestamps, opts...) if err != nil { api.server.logger.Printf("import error: index=%s, field=%s, shard=%d, columns=%d, err=%s", req.Index, req.Field, req.Shard, len(req.ColumnIDs), err) + } else { + err = tx.Commit() } + return errors.Wrap(err, "importing") + } // ImportValue bulk imports values into a particular field. @@ -1188,7 +1206,8 @@ func (api *API) ImportValue(ctx context.Context, req *ImportValueRequest, opts . } // Obtain transaction. - tx := &RoaringTx{Index: index} + tx := index.Txf.NewTx(Txo{Write: true, Index: index}) + defer tx.Rollback() // Set up import options. options, err := setUpImportOptions(opts...) @@ -1274,7 +1293,9 @@ func (api *API) ImportValue(ctx context.Context, req *ImportValueRequest, opts . api.server.logger.Printf("import error: index=%s, field=%s, shard=%d, columns=%d, err=%s", req.Index, req.Field, req.Shard, len(req.ColumnIDs), err) } } - + if err == nil { + err = tx.Commit() + } return errors.Wrap(err, "importing value") } diff --git a/badger.go b/badger.go new file mode 100644 index 000000000..65052b525 --- /dev/null +++ b/badger.go @@ -0,0 +1,1701 @@ +// 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 ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "time" + "unsafe" + + badger "github.com/dgraph-io/badger/v2" + "github.com/pilosa/pilosa/v2/roaring" +) + +// TODO: is there a more optimal time to do badger garbage collection? +// As in: do we need to be more aggressive about cleaning in +// proportion to write activity? Space monitoring available with the +// badger.DB.Size() (lsm, vlog int64) call. +// +// See: https://godoc.org/github.com/dgraph-io/badger#DB.RunValueLogGC +// and: https://github.com/dgraph-io/badger#garbage-collection +// +// For now we run GC periodically every 1 minute or as set by the +// BadgerDBWrapper.GcEveryDur duration. +// +// Background: (quoting from docs referenced above) +// +// "Badger relies on the client to perform garbage collection at a time of +// their choosing. It provides the following method, which can be invoked +// at an appropriate time: +// +// "DB.RunValueLogGC(): This method is designed to do garbage collection while +// Badger is online. Along with randomly picking a file, it uses statistics +// generated by the LSM-tree compactions to pick files that are likely to +// lead to maximum space reclamation. It is recommended to be called during +// periods of low activity in your system, or periodically. One call would +// only result in removal of at max one log file. As an optimization, you +// could also immediately re-run it whenever it returns nil error (indicating +// a successful value log GC), as shown below." +// +// ticker := time.NewTicker(5 * time.Minute) +// defer ticker.Stop() +// for range ticker.C { +// again: +// err := db.RunValueLogGC(0.5) +// if err == nil { +// goto again +// } +// } +// + +// ========================================================= +// A note on using a recent version of badgerdb: +// +// We require a v2 release of badger after 2020 May 13, when support for +// multiple read-write iterators within one transaction was added. +// Many executor_test.go tests do foreachRow() operations, +// which call BadgerTx.ContainerIterator(), which in turn creates +// a first read-write iterator, and then OffsetRange(), which needs a +// second iterator, while still in the same read-write transaction. +// +// The most recent v2 master was pulled in and added to go.mod +// by doing go get github.com/dgraph-io/badger/v2@master +// resulting in the go.mod line +// github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 +// as of this writing, 2020 July 09. This version contains the support +// for having multiple read-write iterators. +// +// Reference on github.com/dgraph-io/badger +// +// commit af22dfd8d51317d765f0c05dcdf1d15981cca4f3 +// Author: Elliot Courant +// Date: Wed May 13 01:07:33 2020 -0500 +// +// Support multiple iterators in read-write transactions. (#1286) +// +// This adds support for multiple iterators during a read-write transaction. The +// iterators created in a read-write transaction will only be able to see writes +// that were performed before the iterator was created. Any writes that occur +// after the iterator is created will be invisible to the iterator. +// +// Fixes https://github.com/dgraph-io/badger/issues/981 +// +// +// Otherwise we'll get these panics: +// 'Only one iterator can be active at one time, for a RW txn.' +// when trying to open a second iterator on the same write transaction. +// e.g. go test -v -run TestExecutor_TranslateRowsOnBool + +var badgerDefaultLogger *BadgerLog +var badgerTestLogger *BadgerLog + +func init() { + // badger test output clutters up the screen, dump to /dev/null for now. + // TODO(jea): figure out where badger logging should go. + null, err := os.Open(os.DevNull) + panicOn(err) + badgerTestLogger = &BadgerLog{Logger: log.New(null, "badger ", log.LstdFlags)} + badgerDefaultLogger = badgerTestLogger + + // BadgerDB recommends a minimum of 128 GOMAXPROCS to make use of the IOPs + // available on the SSD. So we set that here. Details: + // + // from https://github.com/dgraph-io/badger#are-there-any-go-specific-settings-that-i-should-use + // + // "We *highly* recommend setting a high number for GOMAXPROCS, + // which allows Go to observe the full IOPS throughput provided by + // modern SSDs. In Dgraph, we have set it to 128. For more details, + // see this thread [https://groups.google.com/forum/#!topic/golang-nuts/jPb_h3TvlKE/discussion]." + // + // From that thread on golang-nuts: + // + // "Manish Rai Jain + // 8/7/17 + // Hey folks, + // During Gophercon, I happened to meet Russ Cox and asked him the same question. + // If File::Read blocks goroutines, which then spawn new OS threads, in a long running job, + // there should be plenty of OS threads created already, so the random read throughput + // should increase over time and stabilize to the maximum possible value. But, that's + // not what I see in my benchmarks. + // + // And his explanation was that the GOMAXPROCS in a way acts like a multiplexer. + // From docs, "the GOMAXPROCS variable limits the number of operating system threads + // that can execute user-level Go code simultaneously." Which basically means, all + // reads must first be run only via GOMAXPROCS number of goroutines, before switching + // over to some OS thread (not really a switch, but conceptually speaking). This + // introduces a bottleneck for throughput. + // I re-ran my benchmarks with a much higher GOMAXPROCS and was able to then + // achieve the maximum throughput. The numbers are here: + // https://github.com/dgraph-io/badger-bench/blob/master/randread/maxprocs.txt + // To summarize these benchmarks, Linux fio achieves 118K IOPS, and with GOMAXPROCS=64/128, + // I'm able to achieve 105K IOPS, which is close enough. Win! + // + // Regarding the point about using io_submit etc., instead of goroutines; I managed to + // find a library which does that, but it performed worse than just using goroutines. + // https://github.com/traetox/goaio/issues/3 + // From what I gather (talking to Russ and Ian), whatever work is going on in user space, + // the same work has to happen in kernel space; so there's not much benefit here. + // + // Overall, with GOMAXPROCS set to a higher value (as I've done in Dgraph), one can get + // the advertised SSD throughput using goroutines." + // + runtime.GOMAXPROCS(128) +} + +// BadgerLog exists because badger requires a particular logger interface, with a +// Debugf method that is not on standard library log.Logger +type BadgerLog struct { + *log.Logger +} + +// Errorf logs an error. +func (l *BadgerLog) Errorf(f string, v ...interface{}) { + l.Printf("ERROR: "+f, v...) +} + +// Warningf logs a warning. +func (l *BadgerLog) Warningf(f string, v ...interface{}) { + l.Printf("WARNING: "+f, v...) +} + +// Infof logs an informational statement. +func (l *BadgerLog) Infof(f string, v ...interface{}) { + l.Printf("INFO: "+f, v...) +} + +// Debugf logs a debug statement. +func (l *BadgerLog) Debugf(f string, v ...interface{}) { + l.Printf("DEBUG: "+f, v...) +} + +// newBadgerDBWrapper creates a new empty database, blowing away +// any prior path + "-badgerdb" directory. +func newBadgerDBWrapper(path string) (*BadgerDBWrapper, error) { + bpath := badgerPath(path) + err := os.RemoveAll(bpath) + if err != nil { + return nil, err + } + return openBadgerDBWrapper(bpath) +} + +// badgerPath is a helper for determining the full directory +// in which the badger database will be stored. +func badgerPath(path string) string { + if !strings.HasSuffix(path, "-badgerdb") { + return path + "-badgerdb" + } + return path +} + +// openBadgerDB opens the database in the bpath directoy +// without deleting any prior content. Any BadgerDB +// database directory will have the "-badgerdb" suffix. +func openBadgerDBWrapper(bpath string) (*BadgerDBWrapper, error) { + + // now that newTxFactory can call us directly, we might not + // have the -badgerdb suffix. + if !strings.HasSuffix(bpath, "-badgerdb") { + bpath += "-badgerdb" + } + + // regular: works on amd64, but 386 doesn't work. + opt := badger.DefaultOptions(bpath).WithLogger(badgerDefaultLogger) + + // to get memory only do: + //opt := badger.DefaultOptions("").WithLogger(badgerDefaultLogger).WithInMemory(true) + + db, err := badger.Open(opt) + if err != nil { + return nil, err + } + halt := make(chan bool) + w := &BadgerDBWrapper{ + path: bpath, + db: db, + halt: halt, + hasher: NewBlake3Hasher(), + } + w.startBadgerGarbageCollectionBackgroundGoro() + return w, nil +} + +// DeleteIndex deletes all the containers associated with +// the named index from the badger database. +func (w *BadgerDBWrapper) DeleteIndex(indexName string) error { + + // We use the apostrophie rune `'` to locate the end of the + // index name in the key prefix, so we cannot allow indexNames + // themselves to contain apostrophies. + if strings.Contains(indexName, "'") { + return fmt.Errorf("error: bad indexName `%v` in BadgerDBWrapper.DeleteIndex() call: indexName cannot contain apostrophes/single quotes.", indexName) + } + w.muDb.Lock() + defer w.muDb.Unlock() + + // a) do key-ony iteration, no value fetch; + // + // b) do deletes in large batches, to avoid alot of txn overhead; + // per recommendation https://github.com/dgraph-io/badger/issues/598 + // + // c) we do not, at present, try to maintain one large + // transaction with all the keys in a index in it. Because + // there can be too many keys. Hence the index will disappear + // in chucks of 100K keys, not atomically-all-at-once. + + prefix := badgerIndexOnlyPrefix(indexName) + + noMoreKeysWithPrefix := false + const maxDeletesPerTxn = 100000 + + for !noMoreKeysWithPrefix { + err := w.db.Update(func(txn *badger.Txn) error { + o := badger.DefaultIteratorOptions + o.AllVersions = false + o.PrefetchValues = false // key-only iteration, no values. + + // note: panic: Unclosed iterator at time of Txn.Discard ? panic on segfault here? + // This means we messed up and Closed() the Database already; too early. + it := txn.NewIterator(o) + + defer it.Close() + n := 0 + goners := make([][]byte, 0, maxDeletesPerTxn) + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + + // KeyCopy() is required; Key() means corruption and possible segfault. + key := it.Item().KeyCopy(nil) + goners = append(goners, key) + n++ + if n >= maxDeletesPerTxn { + break + } + } + if !it.ValidForPrefix(prefix) { + noMoreKeysWithPrefix = true // done with the full delete of up to maxDeletesPerTxn + } + for _, key := range goners { + if err := txn.Delete(key); err != nil { + return err + } + } + return nil // auto-commit happens + }) + // err back from Update can be ErrConflict in case of + // a conflict. Badger docs: "Depending on the state + // of your application, you have the option to + // retry the operation if you receive this error." + panicOn(err) + + } // end for: proceed to next bath of 100K keys + + // Finally, run a garbage collection to delete values from the value log. + // + // "Only one GC is allowed at a time. If another value log GC + // is running, or DB has been closed, this would return an ErrRejected." + // -- https://godoc.org/github.com/dgraph-io/badger#DB.RunValueLogGC + // Still, we don't see a mutex inside the RunValueLogGC code, so + // lock muGC just to be sure. + w.muGC.Lock() + defer w.muGC.Unlock() + _ = w.db.RunValueLogGC(0.5) + + return nil +} + +// startBadgerGarbageCollectionBackgroundGoro handles Badger DB +// garbage colection by regularly purging the value log from +// a background goroutine. w.GcEveryDur controls how often +// it runs. The default is after every 60 seconds. +func (w *BadgerDBWrapper) startBadgerGarbageCollectionBackgroundGoro() { + go func() { + dur := w.GcEveryDur + if dur == 0 { + dur = time.Minute + } + ticker := time.NewTicker(dur) + defer ticker.Stop() + for { + select { + case <-ticker.C: + w.muGC.Lock() + again: + err := w.db.RunValueLogGC(0.5) + if err == nil { + goto again + } + w.muGC.Unlock() + case <-w.halt: + return + } + } + }() +} + +// statically confirm that BadgerTx satisfies the Tx interface. +var _ Tx = (*BadgerTx)(nil) + +// BadgerDBWrapper provides the NewBadgerTx() method. +// The methods on BadgerDBWrapper are thread-safe, and can be called +// from different goroutines/threads. +type BadgerDBWrapper struct { + // serialize operations on BadgerDBWrapper and thus on the .db too, + // when obtaining new txns on different goroutines. + muDb sync.Mutex + + path string + db *badger.DB + + // openTx and openIt are BadgerDBWrapper 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 *BadgerTx + openTx map[*BadgerTx]bool + + // the bool value is whether the iterator is reversed + openIt map[*BadgerIterator]bool + + // protect openTx and openIt + muOpenTxIt sync.Mutex + + // close(halt) to shutdown the badger gc goroutine in Close() + halt chan bool + + // make BadgerDBWrapper.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 + + // muGC ensures we only run one Garbage Collection at a time. + muGC sync.Mutex + + hasher *Blake3Hasher + + // doAllocZero sets the corresponding flag on all new BadgerTx. + // When doAllocZero is true, we zero out any data from badger + // after transcation commit and rollback. This simulates + // what would happen if we were to use the mmap-ed data + // from badger directly. Currently we copy by default for + // safety because otherwise TestAPI_ImportColumnAttrs sees + // corrupted data. + doAllocZero bool +} + +// unprotectedListOpenTxAsString is a debugging helper. +// It is not thread safe, but is only used for debugging. Called internally while +// holding locks. +func (w *BadgerDBWrapper) 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 _ = (*BadgerDBWrapper)(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 *BadgerDBWrapper) UnprotectedListOpenItAsString() (r string) { + r = "openIt list = [" + for it, reverse := range w.openIt { + r += fmt.Sprintf("it p=%p(reverse:%v), ", it, reverse) + } + return r + "]" +} + +// NewBadgerTx produces BadgerDB 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 BadgerTx are thread-safe, and can be called from +// different goroutines. +func (w *BadgerDBWrapper) NewBadgerTx(write bool) (tx *BadgerTx) { + w.muDb.Lock() + defer w.muDb.Unlock() + + tx = &BadgerTx{ + write: write, + tx: w.db.NewTransaction(write), + Db: w, + initloc: stack(), + doAllocZero: w.doAllocZero, + } + //vv("NewBadgerTx(write=%v) top, p=%p", write, tx) + //pp("NewBadgerTx(write=%v) top, p=%p, stack=\n\n'%v'", write, tx, stack()) + + if w.openTx == nil { + w.openTx = make(map[*BadgerTx]bool) + } + //pp("NewBadgerTx(write=%v); p=%p; (currently open txn: '%v', its: '%v'). initloc:'%v'", write, tx, w.unprotectedListOpenTxAsString(), w.UnprotectedListOpenItAsString(), tx.initloc) + w.muOpenTxIt.Lock() + w.openTx[tx] = write + w.muOpenTxIt.Unlock() + return +} + +// Close shuts down the Badger database. +func (w *BadgerDBWrapper) Close() (err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + if !w.closed { + close(w.halt) + w.closed = true + } + return w.db.Close() +} + +// BadgerTx wraps a badger.Txn and provides the Tx interface +// method implementations. +// The methods on BadgerTx are thread-safe, and can be called +// from different goroutines. +type BadgerTx struct { + + // mu serializes badger operations on this single txn instance. + // + // reference: https://godoc.org/github.com/dgraph-io/badger + // "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 + + write bool + Db *BadgerDBWrapper + tx *badger.Txn + + opcount int + + initloc string // stack trace of where we were initially created. + + doAllocZero bool + + // for tracking txn boundary issues, track all the memory + // that we deploy for roaring containers, and zero it on + // transaction commit/rollback. + ourAllocs [][]byte + ourContainers []*roaring.Container +} + +func (tx *BadgerTx) UseRowCache() bool { + return false +} + +// 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 *BadgerTx) overWriteOurAllocs() { + + 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 + } + } + // 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 + //} +} + +// WholeDatabaseBlake3Hash returns the root-hash from the Merkle tree +// built by hashing all bits stored in the database backing this transaction. +func (tx *BadgerTx) WholeDatabaseBlake3Hash(index, field, view string, shard uint64) (hash string, err error) { + return +} + +// Pointer gives us a memory address for the underlying transaction for debugging. +// It is public because we use it in roaring to report invalid container memory access +// outside of a transaction. +func (tx *BadgerTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +// Rollback rolls back the transaction. +func (tx *BadgerTx) Rollback() { + tx.mu.Lock() + defer tx.mu.Unlock() + + //pp("BadgerTx.Rollback p=%p, its: '%v' initloc: '%v',\n rollbackloc:'%v'", tx, tx.Db.UnprotectedListOpenItAsString(), tx.initloc, stack()) + tx.tx.Discard() // 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() + } +} + +// Commit commits the transaction to permanent storage. +// Commits can handle up to 100k updates to fragments +// at once, but not more. This is a BadgerDB imposed limit. +func (tx *BadgerTx) Commit() error { + tx.mu.Lock() + defer tx.mu.Unlock() + + tx.Db.muOpenTxIt.Lock() + delete(tx.Db.openTx, tx) + tx.Db.muOpenTxIt.Unlock() + + //pp("BadgerTx.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 + + if tx.doAllocZero { + tx.overWriteOurAllocs() + } + return err +} + +// Readonly returns true iff the BadgerTx is read-only. +func (tx *BadgerTx) Readonly() bool { + return !tx.write +} + +// LeftShifted16MaxContainerKey is 0xffffffffffff0000. It is similar +// to the roaring.maxContainerKey 0x0000ffffffffffff, but +// shifted 16 bits to the left so its domain is the full [0, 2^64) bit space. +// It is used to match the semantics of the roaring.OffsetRange() API. +// This is the maximum endx value for Tx.OffsetRange(), because the lowbits, +// as in the roaring.OffsetRange(), are not allowed to be set. +// It is used in Tx.RoaringBitamp() to obtain the full contents of a fragment +// from a call from tx.OffsetRange() by requesting [0, LeftShifted16MaxContainerKey) +// with an offset of 0. +const LeftShifted16MaxContainerKey = uint64(0xffffffffffff0000) // or math.MaxUint64 - (1<<16 - 1), or 18446744073709486080 + +// RoaringBitmap returns the roaring.Bitmap for all bits in the fragment. +func (tx *BadgerTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + + return tx.OffsetRange(index, field, view, shard, 0, 0, LeftShifted16MaxContainerKey) +} + +// badgerKey produces the bytes that we use as a key to query badger. +// The roaringContainerKey argument is a container key into a roaring Container. +// Output examples: +// +// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@00000000000000000000" // smallest container-key +// "idx:'i';fld:'f';vw:'standard';shd:'0';ckey@18446744073709551615" // largest container-key (math.MaxUint64) +// +// NB must be kept in sync with badgerPrefix() and badgerKeyExtractContainerKey(). +// +func badgerKey(index, field, view string, shard uint64, roaringContainerKey uint64) []byte { + // The %020d which adds zero padding up to 20 runes is required to + // allow the textual sort to accurately + // reflect a numeric sort order. This is because, as a string, + // math.MaxUint64 is 20 bytes long. + // Example of such a badgerKey with a container-key that is math.MaxUint64: + // ...........................................12345678901234567890 + // idx:'i';fld:'f';vw:'standard';shd:'1';ckey@18446744073709551615 + + prefix := badgerPrefix(index, field, view, shard) + ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) + return append(prefix, ckey...) +} + +// badgerKeyAndPrefix returns the equivalent of badgerKey() and badgerPrefix() calls. +func badgerKeyAndPrefix(index, field, view string, shard uint64, roaringContainerKey uint64) (key, prefix []byte) { + prefix = badgerPrefix(index, field, view, shard) + ckey := []byte(fmt.Sprintf("%020d", roaringContainerKey)) + return append(prefix, ckey...), prefix +} + +var _ = badgerKeyAndPrefix // keep linter happy + +// badgerKeyExtractContainerKey extracts the containerKey from bkey. +func badgerKeyExtractContainerKey(bkey []byte) (containerKey uint64) { + + // The zero padding means that the container-key is always the last 20 bytes of the bkey. + // + // Be sure to catch the problematic case of a user passing in only a prefix. A prefix + // ends in 'key@' rather than a full key that has 'key@00000000000000000001' (for example) + // at the end. The ParseUint call below will fail in that case. + n := len(bkey) + if n < 20 { + panic(fmt.Sprintf("badgerKeyExtractContainerKey() error: bad bkey '%v', too short!", string(bkey))) + } + last := bkey[n-20:] // badgerKey() and badgerPrefix() always return more than 20 rune []byte. + var err error + containerKey, err = strconv.ParseUint(string(last), 10, 64) // has to be the container key + if err != nil { + panic(fmt.Sprintf("badgerKeyExtractContainerKey() error: bad bkey '%v', could not convert last 20 bytes ('%v') to a unit64: '%v'", string(bkey), string(last), err)) + } + return +} + +// badgerPrefix returns everything from badgerKey up to and +// including the '@' fune in a badger key. The prefix excludes the roaring container key itself. +// NB must be kept in sync with badgerKey() and badgerKeyExtractContainerKey(). +func badgerPrefix(index, field, view string, shard uint64) []byte { + return []byte(fmt.Sprintf("idx:'%v';fld:'%v';vw:'%v';shd:'%x';ckey@", index, field, view, shard)) +} + +// badgerIndexOnlyPrefix returns a prefix suitable for DeleteIndex and a key-scan to +// remove all storage associated with one index. +// +// The full name of the index must be provided, no partial index names will work. +// +// The provided key is terminated by `';` and so DeleteIndex("i") will not delete the index "i2". +// +func badgerIndexOnlyPrefix(indexName string) []byte { + return []byte(fmt.Sprintf("idx:'%v';", indexName)) +} + +// Container returns the requested roaring.Container, selected by fragment and ckey +func (tx *BadgerTx) Container(index, field, view string, shard uint64, ckey uint64) (c *roaring.Container, err error) { + + // values returned from Get() are only valid while the transaction + // is open. If you need to use a value outside of the transaction then + // you must use copy() to copy it to another byte slice. + // BUT here we are already inside the Txn. + + bkey := badgerKey(index, field, view, shard, ckey) + tx.mu.Lock() + var item *badger.Item + item, err = tx.tx.Get(bkey) + tx.mu.Unlock() + if err == badger.ErrKeyNotFound { + // Seems crazy, but we, for now at least, + // match what RoaringTx does by returning nil, nil. + return nil, nil + } else { + panicOn(err) + } + + err = item.Value(func(v []byte) error { + // This func with val would only be called if item.Value encounters no error + c = tx.toContainer(item.UserMeta(), v) + return nil + }) + panicOn(err) + return +} + +// PutContainer stores rc under the specified fragment and container ckey. +func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey uint64, rc *roaring.Container) error { + + bkey := badgerKey(index, field, view, shard, ckey) + var by []byte + + ct := roaring.ContainerType(rc) + + switch ct { + case containerArray: + by = fromArray16(roaring.AsArray(rc)) + case containerBitmap: + by = fromArray64(roaring.AsBitmap(rc)) + case containerRun: + by = fromInterval16(roaring.AsRuns(rc)) + case containerNil: + panic("wat? nil container is unexpected, no?!?") + default: + panic(fmt.Sprintf("unknown container type: %v", ct)) + } + entry := badger.NewEntry(bkey, by).WithMeta(ct) + tx.mu.Lock() + err := tx.tx.SetEntry(entry) + tx.mu.Unlock() + + // ErrTxnTooBig is returned if too many writes are fit into a single transaction. + // badger docs: "An ErrTxnTooBig will be reported in case the number of pending + // 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 { + // As now, we don't deal with this. The current strategy is to recommend setting lots + // of bits on your container and then change it in a single operation + // within the txn, rather than having too many SetEntry() calls in a transactions. + // The tests currently have these default limits: + // maxBatchCount:104857, maxBatchSize:10066329 + // For now, we just panic. The user should re-write their code to do + // most of the work of setting bits outside the transaction. + panic(fmt.Sprintf("error: do not do more than 100K writes in a transaction: '%v'", err)) + } + 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 := badgerKey(index, field, view, shard, ckey) + tx.mu.Lock() + err := tx.tx.Delete(bkey) + tx.mu.Unlock() + return err +} + +// 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) { + + // pure hack to match RoaringTx + defer func() { + if !batched { + if changeCount > 0 { + changeCount = 1 + } + } + }() + + // TODO: optimization: group 'a' elements into their containers, + // and then do all the Adds on that + // container at once, so we don't retrieve a container per bit. + // (maybe, for example, using ImportRoaringBits with clear=false). + + for _, v := range a { + hi, lo := highbits(v), lowbits(v) + + var rct *roaring.Container + rct, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + if err != nil { + return 0, err + } + chng := false + // TODO optimization: set all the bits in the current container at once. group by container first. + rc1, chng := rct.Add(lo) + panicOn(err) + if chng { + changeCount++ + } + if err != nil { + return changeCount, err + } + err = tx.PutContainer(index, field, view, shard, hi, rc1) + panicOn(err) + } + return +} + +// Remove clears all the specified a bits in the chosen fragment. +func (tx *BadgerTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + + // TODO: optimization: group 'a' elements into their containers, + // and then do all the Removes on that + // container at once, so we don't retrieve a container per bit. + // (maybe, for example, using ImportRoaringBits with clear=true). + for _, v := range a { + hi, lo := highbits(v), lowbits(v) + + var rct *roaring.Container + rct, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + if err != nil { + return 0, err + } + chng := false + rc1, chng := rct.Remove(lo) + panicOn(err) + if chng { + changeCount++ + } + if err != nil { + return changeCount, err + } + if rc1.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, hi) + if err != nil { + return + } + } else { + err = tx.PutContainer(index, field, view, shard, hi, rc1) + panicOn(err) + } + } + return +} + +// Contains returns exists true iff the bit chosen by key is +// hot (set to 1) in specified fragment. +func (tx *BadgerTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) { + + lo, hi := lowbits(key), highbits(key) + bkey := badgerKey(index, field, view, shard, hi) + tx.mu.Lock() + item, err := tx.tx.Get(bkey) + tx.mu.Unlock() + if err == badger.ErrKeyNotFound { + return false, nil + } + if err != nil { + return false, err + } + err = item.Value(func(v []byte) error { + // This func with val would only be called if item.Value encounters no error + c := tx.toContainer(item.UserMeta(), v) + exists = c.Contains(lo) + return nil + }) + return exists, err +} + +// 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. +// +// BadgerTx notes: We auto-stop at the end of this shard, not going beyond. +func (tx *BadgerTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) { + + // needle example: "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" + needle := badgerKey(index, field, view, shard, firstRoaringContainerKey) + + // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" + prefix := badgerPrefix(index, field, view, shard) + + bi := NewBadgerIterator(tx, prefix) + bi.Seek(needle) + if !bi.it.Valid() { + return bi, false, nil + } + + if !bi.it.ValidForPrefix(prefix) { + return bi, false, nil + } + return bi, true, nil +} + +// BadgerIterator is the iterator returned from a BadgerTx.ContainerIterator() call. +// It implements the roaring.ContainerIterator interface. +type BadgerIterator struct { + tx *BadgerTx + it *badger.Iterator + + prefix []byte + seekto []byte + + // seen counts how many Next() calls we have seen. + // It is used to match roaring.ContainerIterator semantics. + // Also useful for testing. + seen int +} + +// NewBadgerIterator creates an iterator on tx that will +// only return badgerKeys that start with prefix. +func NewBadgerIterator(tx *BadgerTx, prefix []byte) (bi *BadgerIterator) { + + tx.Db.muOpenTxIt.Lock() + defer tx.Db.muOpenTxIt.Unlock() + + defer func() { + r := recover() + if r != nil { + panic(r) + } + }() + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // else by default, pre-fetches the 1st 100 values, which would be slow. + opts.Reverse = false + + tx.mu.Lock() + it := tx.tx.NewIterator(opts) + tx.mu.Unlock() + + bi = &BadgerIterator{ + tx: tx, + it: it, + prefix: prefix, + } + if tx.Db.openIt == nil { + tx.Db.openIt = make(map[*BadgerIterator]bool) + } + tx.Db.openIt[bi] = false // true for reverse, false for forward iteration. + bi.it.Seek(prefix) + return +} + +// NewBadgerReverseIterator makes a highest-to-lowest key iterator. +// Only keys that are prefixed with prefix will be returned. +// seekto tells where to start, and should be typically shard+1 +// to start at the end of shard. Really only used in Max() at the moment. +// After creating a reverse badger iterator it, we will call it.Seek(seekto). +func NewBadgerReverseIterator(tx *BadgerTx, prefix, seekto []byte) (bi *BadgerIterator) { + + tx.Db.muOpenTxIt.Lock() + defer tx.Db.muOpenTxIt.Unlock() + + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // else by default, pre-fetches the 1st 100 values, which would be slow. + opts.Reverse = true + opts.Prefix = prefix // possible storage IOPs optimization by badger + tx.mu.Lock() + it := tx.tx.NewIterator(opts) + tx.mu.Unlock() + + bi = &BadgerIterator{ + tx: tx, + it: it, + prefix: prefix, + seekto: seekto, + } + if tx.Db.openIt == nil { + tx.Db.openIt = make(map[*BadgerIterator]bool) + } + bi.tx.Db.openIt[bi] = true // true for reverse, false for forward iteration. + bi.it.Seek(seekto) + return +} + +// Close tells the database and transaction that the user is done +// with the iterator. +// From the badger 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 *BadgerIterator) Close() { + + bi.tx.Db.muOpenTxIt.Lock() + delete(bi.tx.Db.openIt, bi) + bi.it.Close() + + bi.tx.Db.muOpenTxIt.Unlock() +} + +// Valid returns false if there are no more values in the iterator's range. +func (bi *BadgerIterator) Valid() bool { + return bi.it.Valid() +} + +// Seek allows the iterator to start at needle instead of the global begining. +func (bi *BadgerIterator) Seek(needle []byte) { + bi.it.Seek(needle) +} + +// Next advances the iterator. +func (bi *BadgerIterator) Next() bool { + + // have to skip the first bi.it.Next() call because badger iterators point to the + // first value immediately, but Pilosa iterators must have Next() called + // on a fresh iterator to get the first value. + if bi.seen > 0 { + bi.it.Next() + } + bi.seen++ + return bi.it.ValidForPrefix(bi.prefix) // does the bi.it.Valid() inside and false if not valid always. +} + +// Value retrieves what is pointed at currently by the iterator. +func (bi *BadgerIterator) Value() (containerKey uint64, c *roaring.Container) { + if !bi.it.Valid() { + panic("bi.it not valid") + } + item := bi.it.Item() + if item == nil { + panic("item was nil") + } + key := item.Key() + containerKey = badgerKeyExtractContainerKey(key) + + err := item.Value(func(v []byte) error { + c = bi.tx.toContainer(item.UserMeta(), v) + return nil + }) + panicOn(err) + return +} + +// Closer is used by badgerFinder +type Closer interface { + Close() +} + +// badgerFinder implements roaring.IteratorFinder. +// It is used by BadgerTx.ForEach() +type badgerFinder struct { + tx *BadgerTx + index string + field string + view string + shard uint64 + needClose []Closer +} + +// FindIterator lets badgerFinder implement the roaring.FindIterator interface. +func (bf *badgerFinder) FindIterator(seek uint64) (roaring.ContainerIterator, bool) { + a, found, err := bf.tx.ContainerIterator(bf.index, bf.field, bf.view, bf.shard, seek) + panicOn(err) + bf.needClose = append(bf.needClose, a) + return a, found +} + +// Close closes all bf.needClose listed Closers. +func (bf *badgerFinder) Close() { + for _, i := range bf.needClose { + i.Close() + } +} + +// NewTxIterator returns a *roaring.Iterator that MUST have Close() called on it BEFORE +// the transaction Commits or Rollsback. +func (tx *BadgerTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + bf := &badgerFinder{tx: tx, index: index, field: field, view: view, shard: shard, needClose: make([]Closer, 0)} + itr := roaring.NewIterator(bf) + return itr +} + +// ForEach applies fn to each bitmap in the fragment. +func (tx *BadgerTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + itr := tx.NewTxIterator(index, field, view, shard) + defer itr.Close() + + // Seek can create many container iterators, thus bf.Close() needClose list. + itr.Seek(0) + // v is the bit we are operating on. + for v, eof := itr.Next(); !eof; v, eof = itr.Next() { + if err := fn(v); err != nil { + return err + } + } + return nil +} + +// ForEachRange applies fn on the selected range of bits on the chosen fragment. +func (tx *BadgerTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + + itr := tx.NewTxIterator(index, field, view, shard) + defer itr.Close() + + itr.Seek(start) + + // v is the bit we are operating on. + for v, eof := itr.Next(); !eof && v < end; v, eof = itr.Next() { + if err := fn(v); err != nil { + return err + } + } + return nil +} + +// Count operates on the full bitmap level, so it sums over all the containers +// in the bitmap. +func (tx *BadgerTx) Count(index, field, view string, shard uint64) (uint64, error) { + + a, found, err := tx.ContainerIterator(index, field, view, shard, 0) + panicOn(err) + defer a.Close() + if !found { + return 0, nil + } + result := int32(0) + for a.Next() { + ckey, cont := a.Value() + _ = ckey + result += cont.N() + } + + return uint64(result), nil +} + +// Max is the maximum bit-value in your bitmap. +func (tx *BadgerTx) Max(index, field, view string, shard uint64) (uint64, error) { + + prefix := badgerPrefix(index, field, view, shard) + seekto := badgerPrefix(index, field, view, shard+1) + + it := NewBadgerReverseIterator(tx, prefix, seekto) // this iterator is still open, when we commit/discard tx. + defer it.Close() + + hb, rc := it.Value() + lb := rc.Max() + + return hb<<16 | uint64(lb), nil +} + +// Min returns the smallest bit set in the fragment. If no bit is hot, +// the second return argument is false. +func (tx *BadgerTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + + // Seek can create many container iterators, thus the bf.Close() needClose list. + bf := &badgerFinder{tx: tx, index: index, field: field, view: view, shard: shard, needClose: make([]Closer, 0)} + defer bf.Close() + itr := roaring.NewIterator(bf) + + itr.Seek(0) + + // v is the bit we are operating on. + v, eof := itr.Next() + if eof { + return 0, false, nil + } + return v, true, nil +} + +// UnionInPlace unions all the others Bitmaps into a new Bitmap, and then writes it to the +// specified fragment. +func (tx *BadgerTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + + rbm.UnionInPlace(others...) + // iterate over the containers that changed within rbm, and write them back to disk. + + it, found := rbm.Containers.Iterator(0) + _ = found // don't care about the value of found, because first containerKey might be > 0 + + for it.Next() { + containerKey, rc := it.Value() + + // TODO: only write the changed ones back, as optimization? + // Compare to ImportRoaringBits. + err := tx.PutContainer(index, field, view, shard, containerKey, rc) + panicOn(err) + } + return nil +} + +// CountRange returns the count of hot bits in the start, end range on the fragment. +func (tx *BadgerTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + skey := highbits(start) + ekey := highbits(end) + + citer, found, err := tx.ContainerIterator(index, field, view, shard, skey) + panicOn(err) + + defer citer.Close() // doesn't seem to be getting called. + if !found { + return 0, nil + } + // If range is entirely in one container then just count that range. + if skey == ekey { + citer.Next() + _, c := citer.Value() + return uint64(c.CountRange(int32(lowbits(start)), int32(lowbits(end)))), nil + } + + for citer.Next() { + k, c := citer.Value() + if k < skey { + citer.Close() + panic(fmt.Sprintf("should be impossible for k(%v) to be less than skey(%v). tx p=%p", k, skey, tx)) + } + + // k > ekey handles the case when start > end and where start and end + // are in different containers. Same container case is already handled above. + if k > ekey { + break + } + if k == skey { + n += uint64(c.CountRange(int32(lowbits(start)), roaring.MaxContainerVal+1)) + continue + } + if k < ekey { + n += uint64(c.N()) + continue + } + if k == ekey { + n += uint64(c.CountRange(0, int32(lowbits(end)))) + break + } + } + + return n, nil +} + +// OffsetRange creates a new roaring.Bitmap to return in other. For all the +// hot bits in [start, endx) of the chosen fragment, it stores +// them into other but with offset added to their bit position. +// The primary client is doing this, using ShardWidth, already; see +// fragment.rowFromStorage() in fragment.go. For example: +// +// data, err := tx.OffsetRange(f.index, f.field, f.view, f.shard, +// f.shard*ShardWidth, rowID*ShardWidth, (rowID+1)*ShardWidth) +// ^ offset ^ start ^ endx +// +// The start and endx arguments are container keys that have been shifted left by 16 bits; +// their highbits() will be taken to determine the actual container keys. This +// is done to conform to the roaring.OffsetRange() argument convention. +// +func (tx *BadgerTx) OffsetRange(index, field, view string, shard, offset, start, endx uint64) (other *roaring.Bitmap, err error) { + + // roaring does these three checks in its OffsetRange + if lowbits(offset) != 0 { + panic("offset must not contain low bits") + } + if lowbits(start) != 0 { + panic("range start must not contain low bits") + } + if lowbits(endx) != 0 { + panic("range end must not contain low bits") + } + + other = roaring.NewSliceBitmap() + off := highbits(offset) + hi0, hi1 := highbits(start), highbits(endx) + + // TODO(jea): question: do we have to account for ShardWidth here? what if the move goes + // beyond a shard? + + needle := badgerKey(index, field, view, shard, hi0) + prefix := badgerPrefix(index, field, view, shard) + + n2, pre2 := badgerKeyAndPrefix(index, field, view, shard, hi0) + if string(n2) != string(needle) { + panic(fmt.Sprintf("problem! n2(%v) != needle(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(n2), string(needle))) + } + if string(pre2) != string(prefix) { + panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), badgerKeyAndPrefix not consitent with badgerKey()", string(pre2), string(prefix))) + } + + it := NewBadgerIterator(tx, prefix) // see OffsetRange() panic 'Only one iterator can be active at one time, for a RW txn + defer it.Close() + it.Seek(needle) + for ; it.it.ValidForPrefix(prefix); it.Next() { + item := it.it.Item() + bkey := item.Key() + k := badgerKeyExtractContainerKey(bkey) + + if uint64(k) >= hi1 { + break + } + destCkey := off + (k - hi0) + err := item.Value(func(v []byte) error { + + c := tx.toContainer(item.UserMeta(), v) + other.Containers.Put(destCkey, c.Freeze()) + + return nil + }) + if err != nil { + return nil, err + } + } + return other, nil +} + +// IncrementOpN increments the tx opcount by changedN +func (tx *BadgerTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + tx.opcount += changedN +} + +// ImportRoaringBits handles deletes by setting clear=true. +// rowSet[rowID] returns the number of bit changed on that rowID. +func (tx *BadgerTx) ImportRoaringBits(index, field, view string, shard uint64, itr roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (changed int, rowSet map[uint64]int, err error) { + n := itr.Len() + if n == 0 { + return + } + rowSet = make(map[uint64]int) + + var currRow uint64 + + var oldC *roaring.Container + for itrKey, synthC := itr.NextContainer(); synthC != nil; itrKey, synthC = itr.NextContainer() { + if rowSize != 0 { + currRow = itrKey / rowSize + } + nsynth := int(synthC.N()) + if nsynth == 0 { + continue + } + // INVAR: nsynth > 0 + + oldC, err = tx.Container(index, field, view, shard, itrKey) + panicOn(err) + if err != nil { + return + } + + if oldC == nil || oldC.N() == 0 { + // no container at the itrKey in badger (or all zero container). + if clear { + // changed of 0 and empty rowSet is perfect, no need to change the defaults. + continue + } else { + + changed += nsynth + rowSet[currRow] += nsynth + + err = tx.PutContainer(index, field, view, shard, itrKey, synthC) + if err != nil { + return + } + continue + } + } + + if clear { + existN := oldC.N() // number of bits set in the old container + newC := oldC.Difference(synthC) + + // update rowSet and changes + if newC.N() == existN { + // INVAR: do changed need adjusting? nope. same bit count, + // so no change could have happened. + continue + } else { + changes := int(existN - newC.N()) + changed += changes + rowSet[currRow] -= changes + + if newC.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, itrKey) + if err != nil { + return + } + continue + } + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + return + } + continue + } + } else { + // setting bits + + existN := oldC.N() + if existN == roaring.MaxContainerVal+1 { + // completely full container already, set will do nothing. so changed of 0 default is perfect. + continue + } + if existN == 0 { + // can nsynth be zero? No, because of the continue/invariant above where nsynth > 0 + changed += nsynth + rowSet[currRow] += nsynth + err = tx.PutContainer(index, field, view, shard, itrKey, synthC) + if err != nil { + return + } + continue + } + + newC := oldC.UnionInPlace(synthC) + + if roaring.ContainerType(newC) == containerBitmap { + newC.Repair() // update the bit-count so .n is valid. b/c UnionInPlace doesn't update it. + } + if newC.N() != existN { + changes := int(newC.N() - existN) + changed += changes + rowSet[currRow] += changes + + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + panicOn(err) + return + } + continue + } + } + } + return +} + +////////////////////////////////// +// badger helper utility functions + +func highbits(v uint64) uint64 { return v >> 16 } +func lowbits(v uint64) uint16 { return uint16(v & 0xFFFF) } + +func toArray16(a []byte) []uint16 { + return (*[4096]uint16)(unsafe.Pointer(&a[0]))[: len(a)/2 : len(a)/2] +} +func toArray64(a []byte) []uint64 { + return (*[1024]uint64)(unsafe.Pointer(&a[0]))[:1024:1024] +} +func toInterval16(a []byte) []roaring.Interval16 { + return (*[2048]roaring.Interval16)(unsafe.Pointer(&a[0]))[: len(a)/4 : len(a)/4] +} + +// should really be exported from the pilosa/roaring package so we don't get out of sync... +const ( + containerNil byte = iota // no container + containerArray // slice of bit position values + containerBitmap // slice of 1024 uint64s + containerRun // container of run-encoded bits +) + +func (tx *BadgerTx) toContainer(typ byte, v []byte) (r *roaring.Container) { + + // For safety we copy v, since it lives in BadgerDB's memory-mapped vlog-file, + // and Badger will recycle it after tx ends with rollback or commit. + // We copy into Go runtime GC managed memory. Technically we don't need + // to do this if all of our use stays within the lifetime + // of the badger transaction we were started on. Hence: + // + // TODO: performance tuning might want w := v here, if we can guarantee no access to memory past the Tx lifetime. + // + // Problem is, at least some tests appear to not respect transaction boundaries... + w := make([]byte, len(v)) + copy(w, v) // green go test -v -run TestAPI_ImportColumnAttrs + //w := v // if instead of append we use v directly, it causes red: go test -v -run TestAPI_ImportColumnAttrs + + // register w so we can catch out-of-tx memory access + tx.ourAllocs = append(tx.ourAllocs, w) + + switch typ { + case containerArray: + c := roaring.NewContainerArray(toArray16(w)) + tx.ourContainers = append(tx.ourContainers, c) + return c + case containerBitmap: + c := roaring.NewContainerBitmap(-1, toArray64(w)) + tx.ourContainers = append(tx.ourContainers, c) + return c + case containerRun: + c := roaring.NewContainerRun(toInterval16(w)) + tx.ourContainers = append(tx.ourContainers, c) + return c + default: + panic(fmt.Sprintf("unknown container: %v", typ)) + } +} + +// fromArray16 converts to an 8KB page +func fromArray16(a []uint16) []byte { + return (*[8192]byte)(unsafe.Pointer(&a[0]))[: len(a)*2 : len(a)*2] +} + +// fromArray64 converts to an 8KB page +func fromArray64(a []uint64) []byte { + return (*[8192]byte)(unsafe.Pointer(&a[0]))[:8192:8192] +} + +// fromInterval16 converts to 8KB page +func fromInterval16(a []roaring.Interval16) []byte { + return (*[8192]byte)(unsafe.Pointer(&a[0]))[: len(a)*4 : len(a)*4] +} + +// badgerKey method on fragment creates a query key in the +// standard format by invoking the top level badgerKey with +// the container key being highbits(rowID * ShardWidth). +// +// Commented out for now only to keep the golangci-lint happy, +// as it has no users at the moment. +//func (f *fragment) badgerKey(rowID uint64) []byte { +// hi0 := highbits(rowID * ShardWidth) +// return badgerKey(f.index, f.field, f.view, f.shard, hi0) +//} + +// StringifiedBadgerKeys returns a string with all the container +// keys available in badger. +func (w *BadgerDBWrapper) StringifiedBadgerKeys(optionalUseThisTx Tx) (r string) { + if optionalUseThisTx == nil { + tx := w.NewBadgerTx(!writable) + defer tx.Rollback() + r = stringifiedBadgerKeysTx(tx) + return + } + + btx, ok := optionalUseThisTx.(*BadgerTx) + if !ok { + return fmt.Sprintf("", optionalUseThisTx) + } + r = stringifiedBadgerKeysTx(btx) + return +} + +// countBitsSet returns the number of bits set (or "hot") in +// the roaring container value found by the badgerKey() +// formatted bkey. +func (tx *BadgerTx) countBitsSet(bkey []byte) (n int) { + + item, err := tx.tx.Get(bkey) + if err == badger.ErrKeyNotFound { + panic(fmt.Sprintf("badger did not have value for bkey = '%v'", string(bkey))) + } + panicOn(err) + + var rc *roaring.Container + err = item.Value(func(v []byte) error { + // This func with val would only be called if item.Value encounters no error + rc = tx.toContainer(item.UserMeta(), v) + return nil + }) + panicOn(err) + + n = int(rc.N()) + return +} + +// stringifiedBadgerKeysTx reports all the badger keys and a +// corresponding blake3 hash viewable by txn within the entire +// badger 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 stringifiedBadgerKeysTx(tx *BadgerTx) (r string) { + + r = "allkeys:[\n" + it := tx.tx.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + any := false + for it.Rewind(); it.Valid(); it.Next() { + any = true + item := it.Item() + bkey := item.Key() + key := string(bkey) + ckey := badgerKeyExtractContainerKey(bkey) + hash := "" + srbm := "" + err := item.Value(func(val []byte) error { + hash = blake3sum16(val) + ct := tx.toContainer(item.UserMeta(), val) + cts := roaring.NewSliceContainers() + cts.Put(ckey, ct) + rbm := &roaring.Bitmap{Containers: cts} + srbm = bitmapAsString(rbm) + return nil + }) + panicOn(err) + r += fmt.Sprintf("%v -> %v (%v hot)\n", key, hash, tx.countBitsSet(bkey)) + r += " ......." + srbm + "\n" + } + r += "]\n all-in-blake3:" + blake3sum16([]byte(r)) + + if !any { + return "" + } + return "badger-" + r +} + +func sliceToMap(slc []uint64) (m map[uint64]bool) { + m = make(map[uint64]bool) + for _, v := range slc { + m[v] = true + } + return +} + +// return A - B +func mapDiff(mapA, mapB map[uint64]bool) (r []int) { + for a := range mapA { + _, ok := mapB[a] + if !ok { + r = append(r, int(a)) + } + } + return +} + +func asInts(a []uint64) (r []int) { + r = make([]int, len(a)) + for i, v := range a { + r[i] = int(v) + } + return +} + +func bitmapAsString(rbm *roaring.Bitmap) (r string) { + r = "c(" + slc := rbm.Slice() + width := 0 + s := "" + for _, v := range slc { + if width == 0 { + s = fmt.Sprintf("%v", v) + } else { + s = fmt.Sprintf(", %v", v) + } + width += len(s) + r += s + if width > 70 { + r += ",\n" + width = 0 + } + } + if width == 0 && len(r) > 2 { + r = r[:len(r)-2] + } + return r + ")" +} + +func containerAsString(ckey uint64, rc *roaring.Container) (r string) { + rbm := roaring.NewBitmap() + rbm.Containers.Put(ckey, rc) + return bitmapAsString(rbm) +} + +var _ = containerAsString // happy linter + +func roaringBitmapDiff(a, b *roaring.Bitmap) error { + nA := a.Count() + nB := b.Count() + + slcA := a.Slice() + slcB := b.Slice() + + mapA := sliceToMap(slcA) + mapB := sliceToMap(slcB) + + AminusB := mapDiff(mapA, mapB) + BminusA := mapDiff(mapB, mapA) + + sort.Ints(AminusB) + sort.Ints(BminusA) + + res := fmt.Sprintf("nA = %v; nB = %v;\n", nA, nB) + ndiff := 0 + if nA != nB { + ndiff++ + } + + if len(AminusB) > 0 { + res += fmt.Sprintf("==> AminusB = (len %v) '%#v'; ", len(AminusB), AminusB) + ndiff++ + } + if len(BminusA) > 0 { + res += fmt.Sprintf("\n==> BminusA = (len %v) '%#v'; ", len(BminusA), BminusA) + ndiff++ + } + if ndiff == 0 { + return nil + } + res += fmt.Sprintf("\n ==> A = '%#v'\n ==> B = '%#v'", asInts(slcA), asInts(slcB)) + return errors.New(res) +} + +func dirAsString(path string) (r string) { + r = fmt.Sprintf("dump of directory '%v':\n", path) + files, err := ioutil.ReadDir(path) + panicOn(err) + for _, f := range files { + r += f.Name() + "\n" + } + return r +} + +var _ = dirAsString // happy linter diff --git a/badger_test.go b/badger_test.go new file mode 100644 index 000000000..354afb684 --- /dev/null +++ b/badger_test.go @@ -0,0 +1,1442 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !386 + +package pilosa + +import ( + "bytes" + "fmt" + "math" + "os" + "strconv" + "testing" + + "github.com/dgraph-io/badger/v2" + "github.com/pilosa/pilosa/v2/roaring" +) + +var _ = &roaring.Bitmap{} + +// helpers, each runs their own new txn, and commits if a change/delete +// was made. The txn is rolled back if it is just viewing the data. + +func badgerDBMustHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue '%v' was NOT SET!!!", bitvalue)) + } + + tx.Rollback() +} + +func badgerDBMustNotHaveBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, bitvalue uint64) { + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG bitvalue '%v' WAS SET but should not have been.!!!", bitvalue)) + } + tx.Rollback() +} + +func badgerDBMustSetBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewBadgerTx(writable) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed != 1 { + panic("should have 1 bit changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + panicOn(tx.Commit()) +} + +func badgerDBMustDeleteBitvalueContainer(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewBadgerTx(writable) + hi := highbits(putme) + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + panicOn(tx.Commit()) +} + +func badgerDBMustDeleteBitvalue(dbwrap *BadgerDBWrapper, index, field, view string, shard uint64, putme uint64) { + tx := dbwrap.NewBadgerTx(writable) + _, err := tx.Remove(index, field, view, shard, putme) + panicOn(err) + panicOn(tx.Commit()) +} + +func mustOpenEmptyBadgerWrapper(path string) (w *BadgerDBWrapper, cleaner func()) { + var err error + fn := badgerPath(path) + panicOn(os.RemoveAll(fn)) + w, err = newBadgerDBWrapper(path) + panicOn(err) + + // verify it is empty + allkeys := w.StringifiedBadgerKeys(nil) + if allkeys != "" { + panic(fmt.Sprintf("freshly created database was not empty! had keys:'%v'", allkeys)) + } + + return w, func() { + os.RemoveAll(fn) + } +} + +// +func TestBadger_SetBitmap(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SetBitmap") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + index, field, view, shard := "i", "f", "v", uint64(0) + bitvalue := uint64(0) + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + err = tx.Commit() + panicOn(err) + + // + // commited, so should be visible outside the txn + // + + tx2 := dbwrap.NewBadgerTx(!writable) + exists, err = tx2.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!! on tx2") + } + + n, err := tx2.Count(index, field, view, shard) + panicOn(err) + if n != 1 { + panic(fmt.Sprintf("should have Count 1; instead n = %v", n)) + } + tx2.Rollback() +} + +func TestBadger_OffsetRange(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SetBitmap") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + + index, field, view, shard := "i", "f", "v", uint64(0) + bitvalue := uint64(1 << 20) + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + bitvalue2 := uint64(1<<20 + 1) + changed, err = tx.Add(index, field, view, shard, doBatched, bitvalue2) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + exists, err = tx.Contains(index, field, view, shard, bitvalue2) + panicOn(err) + if !exists { + panic("ARG bitvalue2 was NOT SET!!!") + } + + err = tx.Commit() + panicOn(err) + + offset := uint64(0 << 20) + start := uint64(0 << 16) + endx := bitvalue + 1<<16 + + tx2 := dbwrap.NewBadgerTx(!writable) + rbm2, err := tx2.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx2.Rollback() + + // should see our 1M value + s2 := bitmapAsString(rbm2) + expect2 := "c(1048576, 1048577)" + if s2 != expect2 { + panic(fmt.Sprintf("s2='%v', but expected '%v'", s2, expect2)) + } + + // now offset by 2M + offset = uint64(2 << 20) + tx3 := dbwrap.NewBadgerTx(!writable) + rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx3.Rollback() + + //expect to see 3M == 3145728 + s3 := bitmapAsString(rbm3) + expect3 := "c(3145728, 3145729)" + + if s3 != expect3 { + panic(fmt.Sprintf("s3='%v', but expected '%v'", s3, expect3)) + } +} + +func TestBadger_Count_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Count_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if int(n) != len(putmeValues) { + panic(fmt.Sprintf("expected Count of %v but got n=%v", len(putmeValues), n)) + } +} + +func TestBadger_Count_dense_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Count_dense_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + tx := dbwrap.NewBadgerTx(writable) + + expected := 0 + // can't do more than about 100k writes per badger txn by default, so + // have to keep this kind of small. + // (See maxBatchCount:104857, maxBatchSize:10066329). + for i := uint64(0); i < (1<<16)+2; i += 2 { + changed, err := tx.Add(index, field, view, shard, doBatched, i) + panicOn(err) + if changed <= 0 { + panic("wat? should have changed") + } + expected++ + } + defer tx.Rollback() + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if int(n) != expected { + panic(fmt.Sprintf("expected Count of %v but got n=%v", expected, n)) + } +} + +func TestBadger_ContainerIterator_on_empty(t *testing.T) { + // iterate on empty container, should not find anything. + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + bitvalue := uint64(0) + citer, found, err := tx.ContainerIterator(index, field, view, shard, bitvalue) + panicOn(err) + defer citer.Close() + if found { + panic("should not have found anything") + } + panicOn(err) +} + +func TestBadger_ContainerIterator_on_one_bit(t *testing.T) { + // set one bit, iterate. + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(bitvalue)) + if !found { + panic("ContainerIterator did not find the 42 bit") + } + panicOn(err) + defer citer.Close() + + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 0 { + panic("42 should have had key 0") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if !container.Contains(lowbits(bitvalue)) { + panic("container did not have our bitvalue!") + } + loopCount++ + if loopCount > 0 { // happier linter + break + } + } + if loopCount != 1 { + panic("ContainerIterator did not return a citer that scanned our set bit") + } +} + +func TestBadger_badgerKey_badgerPrefix(t *testing.T) { + + // badgerPrefix() must agree with badgerKey(), but not have the key at the end. + // This is important for iteration over containers. + + index, field, view, shard := "i", "f", "v", uint64(0) + + // needle examples with the container-key extremes: + // "index:'i';field:'f';view:'v';shard:'0';key@00000000000000000000" // smallest + // "index:'i';field:'f';view:'v';shard:'0';key@18446744073709551615" // largest + needle := badgerKey(index, field, view, shard, 0) + + // prefix example: "index:'i';field:'f';view:'v';shard:'0';key@" + prefix := badgerPrefix(index, field, view, shard) + + if !bytes.HasPrefix(needle, prefix) { + panic(fmt.Sprintf("badgerPrefix() output '%v'was not a prefix of badgerKey() '%v'", string(needle), string(prefix))) + } + if len(prefix)+20 != len(needle) { + panic(fmt.Sprintf("badgerPrefix() output '%v'was 20 characters shorter than badgerKey() '%v'", string(needle), string(prefix))) + } + + // validate assumption that badgerKeyExtractContainerKey() makes about strconv.ParseUint() error reporting; + // for distinguishing prefixes from full keys. Even if the shard number is so large that the prefix + // starts with a legitimate decimal number. + shouldNotParse := "12345123451234';key@" + containerKey, err := strconv.ParseUint(shouldNotParse, 10, 64) + if err == nil { + panic(fmt.Sprintf("strconv.ParseUint should have returned an error parsing this string '%v'; instead we got '%v'", shouldNotParse, containerKey)) + } + + // verify panic on submitting a prefix + func() { + defer func() { + r := recover() + if r == nil { + panic(fmt.Sprintf("should have seen panic on call to badgerKeyExtractContainerKey(prefix='%v')", prefix)) + } + }() + badgerKeyExtractContainerKey(prefix) // should panic. + }() +} + +func TestBadger_ContainerIterator_on_one_bit_fail_to_find(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := putme + 1 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + if !found { + panic("ContainerIterator did not find the searchme") + } + defer citer.Close() + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 1 { + panic("Containeriterator searching for highbits(searchme) should not have had a bit") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if container.Contains(lowbits(searchme)) { + panic("container should have putme but not our searchme!") + } + loopCount++ + // only want first pass. keep linter happy by avoiding raw break + if loopCount > 0 { + break + } + } + panicOn(err) +} + +func TestBadger_ContainerIterator_empty_iteration_loop(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_empty_iteration_loop") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := uint64(1 << 17) // in the next container, key:2 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + panicOn(err) + if found { + panic("ContainerIterator found the searchme, when it should not have") + } + defer citer.Close() + if citer.Next() { + panic("expected no looping, 0 iterations, b/c started searchme past our data in putme") + } + + // expect to see a blow up from the citer.Value() call, verify that we do. + func() { + defer func() { + r := recover() + if r == nil { + panic("expected a panic from citer.Value() in this case") + } + }() + citer.Value() // should panic + }() + +} + +func TestBadger_ForEach_on_one_bit(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + count := 0 + err = tx.ForEach(index, field, view, shard, func(v uint64) error { + if v != bitvalue { + panic(fmt.Sprintf("bitvalue corrupt got %v want %v", v, bitvalue)) + } + count += 1 + return nil + }) + panicOn(err) + if count != 1 { + panic(fmt.Sprintf("Expected single iteration got %v ", count)) + } +} + +func TestBadger_RemoveContainer_one_bit_test(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_RemoveContainer_one_bit_test") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 13, 77, 1511} + + for _, putme := range putmeValues { + + // a) delete of whole container in a seperate txn. Commit should establish the deletion. + + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // b) deletion + rollback on the txn should restore the deleted bit + + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // delete, but rollback instead of commit + tx := dbwrap.NewBadgerTx(writable) + hi := highbits(putme) + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + tx.Rollback() + + // verify that the rollback undid the deletion. + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // c) within one Tx, after delete it should be gone as viewed within the txn. + tx = dbwrap.NewBadgerTx(writable) + hi = highbits(putme) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG putme '%v' was NOT SET!!!", putme)) + } + + panicOn(tx.RemoveContainer(index, field, view, shard, hi)) + + exists, err = tx.Contains(index, field, view, shard, putme) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG putme '%v' was SET even after RemoveContiner in this txn.", putme)) + } + + tx.Rollback() + + // verify that the rollback undid the deletion. + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + // leave with clean slate + badgerDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + } +} + +func TestBadger_Remove_one_bit_test(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Remove_one_bit_test") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 13, 77, 1511} + + for _, putme := range putmeValues { + + // a) delete of whole container in a seperate txn. Commit should establish the deletion. + + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustDeleteBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // b) deletion + rollback on the txn should restore the deleted bit + + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // delete, but rollback instead of commit + tx := dbwrap.NewBadgerTx(writable) + hi, lo := highbits(putme), lowbits(putme) + _, _ = hi, lo + _, err := tx.Remove(index, field, view, shard, hi) + panicOn(err) + tx.Rollback() + + // verify that the rollback undid the deletion. + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + + // c) within one Tx, after delete it should be gone as viewed within the txn. + tx = dbwrap.NewBadgerTx(writable) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG putme '%v' was NOT SET!!!", putme)) + } + + mustRemove(tx.Remove(index, field, view, shard, putme)) + + exists, err = tx.Contains(index, field, view, shard, putme) + panicOn(err) + if exists { + panic(fmt.Sprintf("ARG putme '%v' was SET even after Remove in this txn.", putme)) + } + + tx.Rollback() + + // verify that the rollback undid the deletion. + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + // leave with clean slate + badgerDBMustDeleteBitvalueContainer(dbwrap, index, field, view, shard, putme) + } +} + +func TestBadger_reverse_badger_iterator(t *testing.T) { + + // sanity check our understanding of Seek()-ing on reverse iterators. + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_reverse_badger_iterator") + defer clean() + defer dbwrap.Close() + + // add 0, 1, 2 to badgerdb as keys (and same value). + err := dbwrap.db.Update(func(txn *badger.Txn) error { + for i := 0; i < 3; i++ { + kv := []byte(fmt.Sprintf("a:%v", i)) + err := txn.Set(kv, kv) + panicOn(err) + } + return nil + }) + panicOn(err) + + tx := dbwrap.db.NewTransaction(!writable) + + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // else by default, pre-fetches the 1st 100 values, which would be slow. + opts.Reverse = true + it := tx.NewIterator(opts) + it.Rewind() + if !it.Valid() { + panic("invalid reversed iterator?") + } + a := []byte("a:3") + it.Seek(a) + if !it.Valid() { + panic("invalid reversed iterator after seek") + } + + it.Next() + if !it.Valid() { + panic("invalid reversed iterator after seek and next") + } + item := it.Item() + + err = item.Value(func(val []byte) error { + // This func with val would only be called if item.Value encounters no error. + if string(val) != "a:1" { + panic(fmt.Sprintf("we are in trouble, should have gotten 'a:1' but instead got '%v'", string(val))) + } + return nil + }) + panicOn(err) +} + +func TestBadger_Max_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Max_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + max, err := tx.Max(index, field, view, shard) + panicOn(err) + expected := putmeValues[len(putmeValues)-1] + if max != expected { + panic(fmt.Sprintf("expected Max() of %v but got max=%v", expected, max)) + } +} + +func TestBadger_Min_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Min_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + // verify no containers flag works + tx := dbwrap.NewBadgerTx(!writable) + min, containersExist, err := tx.Min(index, field, view, shard) + _ = min + panicOn(err) + if containersExist { + panic("no containers should exist") + } + tx.Rollback() + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx = dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + min, containersExist, err = tx.Min(index, field, view, shard) + panicOn(err) + if !containersExist { + panic("containers should exist") + } + expected := putmeValues[0] + if min != expected { + panic(fmt.Sprintf("expected Min() of %v but got min=%v", expected, min)) + } +} + +func TestBadger_CountRange_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_CountRange_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + // verify no containers flag works + tx := dbwrap.NewBadgerTx(!writable) + n, err := tx.CountRange(index, field, view, shard, 0, math.MaxUint64) + panicOn(err) + if n != 0 { + panic("no containers should exist") + } + tx.Rollback() + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx = dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + n, err = tx.CountRange(index, field, view, shard, 0, math.MaxUint64) + panicOn(err) + if n == 0 { + panic("containers should exist") + } + expected := uint64(len(putmeValues)) + if n != expected { + panic(fmt.Sprintf("expected CountRange() of %v but got n=%v", expected, n)) + } +} + +func TestBadger_CountRange_middle_container(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_CountRange_middle_container") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + // pick out just the middle container with the 1 bit set on it. + n, err := tx.CountRange(index, field, view, shard, 4, (2<<16)+1) + panicOn(err) + if n != 1 { + panic("middle 1 bit container should exist") + } +} + +func TestBadger_CountRange_many_middle_container(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_CountRange_many_middle_container") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + // get them all + n, err := tx.CountRange(index, field, view, shard, 0, (4<<16)+1) + panicOn(err) + if n != 3 { + panic("count should have been all 3 bits") + } +} + +func TestBadger_UnionInPlace(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_UnionInPlace") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{3, 2 << 16} + + others := roaring.NewBitmap() + others2 := roaring.NewBitmap() + others3 := roaring.NewBitmap() + // populate others with putmeValues +1 into others + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx2 := dbwrap.NewBadgerTx(!writable) + n, err := tx2.Count(index, field, view, shard) + panicOn(err) + if n != 2 { + panic("should have 2 bits set") + } + tx2.Rollback() + + for _, putme := range putmeValues { + mustAddR(others.Add(putme)) // should not change count, b/c putme already in the rbm + mustAddR(others.Add(putme + 1)) + mustAddR(others2.Add(putme + 2)) + } + mustAddR(others3.Add(4 << 16)) // outside the 2<<16 container + + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + err = tx.UnionInPlace(index, field, view, shard, others, others2, others3) + panicOn(err) + + // end game, check we got the union. + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + n = rbm.Count() + if n != 7 { + panic("should have a total 3 + 3 +1 = 7 bits set on the containers") + } +} + +func TestBadger_RoaringBitmap(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_RoaringBitmap") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + expected := uint64(3) + putme := expected + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + + tx := dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + panicOn(err) + + slc := rbm.Slice() + if slc[0] != uint64(expected) { + panic(fmt.Sprintf("should have gotten %v back", expected)) + } +} + +func TestBadger_reverse_badger_iterator_and_prefix_valid(t *testing.T) { + + // does a reverse iterator and ValidForPrefix behave like we expect it too? + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_reverse_badger_iterator_and_prefix_valid") + defer clean() + defer dbwrap.Close() + + // add 0, 1, 2 to badgerdb as keys (and same value). + err := dbwrap.db.Update(func(txn *badger.Txn) error { + for _, prefix := range []string{"a", "b", "c"} { + for i := 0; i < 3; i++ { + kv := []byte(fmt.Sprintf("%v:%v", prefix, i)) + err := txn.Set(kv, kv) + panicOn(err) + } + } + return nil + }) + panicOn(err) + //vv("stringifiedBadgerKeys(db) = '%v'", stringifiedBadgerKeys(dbwrap.db)) + // allkeys:["a:0", "a:1", "a:2", "b:0", "b:1", "b:2", "c:0", "c:1", "c:2", ]' + + tx := dbwrap.NewBadgerTx(!writable) + + prefix := []byte("b:") + it := NewBadgerIterator(tx, prefix) + + if !it.it.Valid() { + panic("why is underlying badger it not valid here?") + } + results := "" + for it.Next() { + item := it.it.Item() + sk := string(item.Key()) + results += sk + ", " + } + expected := `b:0, b:1, b:2, ` + if results != expected { + panic(fmt.Sprintf("observed: '%v' but expected: '%v'", results, expected)) + } + it.Close() + + // now reversed + seekto := []byte("c:") + rit := NewBadgerReverseIterator(tx, prefix, seekto) // Seeks("b:") goes to b:0 + defer rit.Close() + + if !rit.it.Valid() { + panic("why is underlying badger it not valid here?") + } + results = "" + for rit.Next() { + item := rit.it.Item() + sk := string(item.Key()) + results += sk + ", " + } + expected = `b:2, b:1, b:0, ` + if results != expected { + panic(fmt.Sprintf("observed: '%v' but expected: '%v'", results, expected)) + } + rit.Close() +} + +func TestBadger_just_reverse_badger_iterator_and_prefix_valid(t *testing.T) { + + // does a reverse iterator and ValidForPrefix behave like we expect it too? + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_reverse_badger_iterator_and_prefix_valid") + defer clean() + defer dbwrap.Close() + + // add 0, 1, 2 to badgerdb as keys (and same value). + err := dbwrap.db.Update(func(txn *badger.Txn) error { + for _, prefix := range []string{"a", "b", "c"} { + for i := 0; i < 3; i++ { + kv := []byte(fmt.Sprintf("%v:%v", prefix, i)) + err := txn.Set(kv, kv) + panicOn(err) + } + } + return nil + }) + panicOn(err) + //vv("stringifiedBadgerKeys(db) = '%v'", stringifiedBadgerKeys(dbwrap.db)) + // allkeys:["a:0", "a:1", "a:2", "b:0", "b:1", "b:2", "c:0", "c:1", "c:2", ]' + + tx := dbwrap.NewBadgerTx(!writable) + + seekto := []byte("c:") + prefix := []byte("b:") + // now reversed + rit := NewBadgerReverseIterator(tx, prefix, seekto) // Seeks("b:") goes to b:0 + defer rit.Close() + + if !rit.it.Valid() { + panic("why is underlying badger rit not valid here?") + } + results := "" + for rit.Next() { + item := rit.it.Item() + sk := string(item.Key()) + results += sk + ", " + } + expected := `b:2, b:1, b:0, ` + if results != expected { + panic(fmt.Sprintf("observed: '%v' but expected: '%v'", results, expected)) + } + rit.Close() +} + +func TestBadger_ImportRoaringBits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ImportRoaringBits") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + //bitvalue := uint64(42) + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the union in place with the same set gives no change. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize) + _ = rowSet + if changed != 0 { + panic(fmt.Sprintf("should have not changed any bits on the second import, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the clear path + clear = true + + for _, v := range bits { + // clear 1 bit at a time + data := getTestBitmapAsRawRoaring(v) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize) + _ = rowSet + if changed != 1 { + panic(fmt.Sprintf("should have changed 1 bit: '%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + } + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 0 { + panic(fmt.Sprintf("n = %v not zero so the clearbits didn't happen!", n)) + } + allkeys := stringifiedBadgerKeysTx(tx) + + // should have no keys + if allkeys != "" { + panic("badger should have no keys now") + } +} + +func TestBadger_ImportRoaringBits_set_nonoverlapping_bits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ImportRoaringBits_set_nonoverlapping_bits") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd, overlapping set and set them. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize) + _ = rowSet + if changed != 4 { + panic(fmt.Sprintf("should have changed 2 bits: the 1 and the 3, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) +} + +func TestBadger_ImportRoaringBits_clear_nonoverlapping_bits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ImportRoaringBits_clear_nonoverlapping_bits") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + defer tx.Rollback() + index, field, view, shard := "i", "f", "v", uint64(0) + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} //, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd overlapping set and clear them. + clear = true + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize) + _ = rowSet + if changed != 2 { + panic(fmt.Sprintf("should have changed 1 bit: the 2, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 2 { // just the 0 and the 1<<16 bits should be left set. + panic(fmt.Sprintf("n = %v not 2 so the clearbits didn't happen!", n)) + } + +} + +func getTestBitmapAsRawRoaring(bitsToSet ...uint64) []byte { + b := roaring.NewBitmap() + changed := b.DirectAddN(bitsToSet...) + n := len(bitsToSet) + if changed != n { + panic(fmt.Sprintf("changed=%v but bitsToSet len = %v", changed, n)) + } + buf := bytes.NewBuffer(make([]byte, 0, 100000)) + _, err := b.WriteTo(buf) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func TestBadger_DeleteIndex(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteIndex") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + bitvalue := uint64(777) + index, field, view, shard := "i", "f", "v", uint64(0) + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + for _, v := range bits { + changed, err := tx.Add(index, field, view, shard, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + + index2 := "i2" // should not be deleted, even though it shares a prefix with 'i' + changed, err := tx.Add(index2, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + exists, err := tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!! on index2") + } + err = tx.Commit() + panicOn(err) + + // end of setup + err = dbwrap.DeleteIndex(index) + panicOn(err) + + tx = dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + exists, err = tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("after delete of '%v', another index '%v' was gone too?!?", index, index2)) + } + + for _, v := range bits { + exists, err = tx.Contains(index, field, view, shard, v) + panicOn(err) + if exists { + allkeys := stringifiedBadgerKeysTx(tx) + panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) + } + } +} + +func TestBadger_DeleteIndex_over100k(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteIndex_over100k") + defer clean() + defer dbwrap.Close() + tx := dbwrap.NewBadgerTx(writable) + bitvalue := uint64(777) + index, field, view, shard := "i", "f", "v", uint64(0) + limit := uint64(100002) // default batch size in DeleteIndex is 100k keys per delete transaction. + //limit := uint64(101) + for v := uint64(1); v < limit; v++ { + // shift by << 16 to get into a different shard + changed, err := tx.Add(index, field, view, shard, doBatched, v<<16) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + if v%100000 == 0 { + panicOn(tx.Commit()) + tx = dbwrap.NewBadgerTx(writable) + } + } + + index2 := "i2" // should not be deleted, even though it shares a prefix with 'i' + changed, err := tx.Add(index2, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + err = tx.Commit() + panicOn(err) + + // end of setup + err = dbwrap.DeleteIndex(index) + panicOn(err) + + tx = dbwrap.NewBadgerTx(!writable) + defer tx.Rollback() + exists, err := tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic(fmt.Sprintf("after delete of '%v', another index '%v' was gone too?!?", index, index2)) + } + + for v := uint64(0); v < limit; v++ { + exists, err = tx.Contains(index, field, view, shard, v<<16) + panicOn(err) + if exists { + allkeys := stringifiedBadgerKeysTx(tx) + panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) + } + } +} + +func TestBitmapDiff(t *testing.T) { + a := roaring.NewBitmap() + b := roaring.NewBitmap() + err := roaringBitmapDiff(a, b) + panicOn(err) + err = roaringBitmapDiff(b, a) + panicOn(err) + + a = roaring.NewBitmap(0) + err = roaringBitmapDiff(a, b) + if err == nil { + panic("diff should have been noticed") + } + err = roaringBitmapDiff(b, a) + if err == nil { + panic("diff should have been noticed") + } + + b = roaring.NewBitmap(0) + err = roaringBitmapDiff(a, b) + panicOn(err) + err = roaringBitmapDiff(b, a) + panicOn(err) + + a = roaring.NewBitmap() + + err = roaringBitmapDiff(a, b) + if err == nil { + panic("diff should have been noticed") + } + err = roaringBitmapDiff(b, a) + if err == nil { + panic("diff should have been noticed") + } + + a = roaring.NewBitmap(1) + + err = roaringBitmapDiff(a, b) + if err == nil { + panic("diff should have been noticed") + } + err = roaringBitmapDiff(b, a) + if err == nil { + panic("diff should have been noticed") + } + + b = roaring.NewBitmap(1, 2) + a = roaring.NewBitmap(0, 1) + + err = roaringBitmapDiff(a, b) + if err == nil { + panic("diff should have been noticed") + } + err = roaringBitmapDiff(b, a) + if err == nil { + panic("diff should have been noticed") + } + + b = roaring.NewBitmap(1, 2, 3) + a = roaring.NewBitmap(1, 2) + + err = roaringBitmapDiff(a, b) + if err == nil { + panic("diff should have been noticed") + } + err = roaringBitmapDiff(b, a) + if err == nil { + panic("diff should have been noticed") + } +} + +// mustAddR is a helper for calling roaring.Container.Add() in tests to +// keep the linter happy that we are checking the error. +func mustAddR(changed bool, err error) { + panicOn(err) +} + +// mustRemove is a helper for calling Tx.Remove() in tests to +// keep the linter happy that we are checking the error. +func mustRemove(changeCount int, err error) { + panicOn(err) +} diff --git a/blake3.go b/blake3.go new file mode 100644 index 000000000..22e3da86b --- /dev/null +++ b/blake3.go @@ -0,0 +1,95 @@ +// 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 ( + "encoding/binary" + "fmt" + "sync" + + cryptorand "crypto/rand" + "github.com/zeebo/blake3" +) + +// Blake3Hasher is a thread/goroutine safe way to +// obtain a blake3 cryptographic hash of input []byte. +// Reference https://github.com/BLAKE3-team/BLAKE3 +// suggests it is 6x faster than BLAKE2B. +// The Go github.com/zeebo/blake3 version is +// AVX2 and SSE4.1 accelerated. +type Blake3Hasher struct { + hasher *blake3.Hasher + hasherMu sync.Mutex +} + +// NewBlake3Hasher returns a new Blake3Hasher. +func NewBlake3Hasher() *Blake3Hasher { + return &Blake3Hasher{ + hasher: blake3.New(), + } +} + +// CryptoHash writes the blake3 cryptographic hash of +// input into buffer and returns it. +// Like the standard libary's hash.Hash interface's Sum() method, +// the buffer is re-used and overwritten +// to avoid allocation. The caller determines the byte length of +// the outputCryptohash by the size of the supplied buffer +// slice, and this will be exactly equal to the supplies bytes. +// In this way, shorter or longer hashes can be provided as +// needed. +func (w *Blake3Hasher) CryptoHash(input []byte, buffer []byte) (outputCryptohash []byte) { + w.hasherMu.Lock() + w.hasher.Reset() + + // "Write implements part of the hash.Hash interface. It never returns an error." + // -- https://godoc.org/github.com/zeebo/blake3#Hasher.Write + _, _ = w.hasher.Write(input) + + // Digest.Read reads data from the hasher into buffer. + // "It always fills the entire buffer and never errors." + // -- https://godoc.org/github.com/zeebo/blake3#Digest + _, _ = w.hasher.Digest().Read(buffer) + + // no chance of panic, so avoid any defer cost. + w.hasherMu.Unlock() + + return buffer +} + +// blake3sum16 might be slower because we allocate a new hasher every time, but +// it is more conenient for writing debug code. It returns +// a 16 byte hash as a hexidecimal string. +func blake3sum16(input []byte) string { + hasher := blake3.New() + + _, _ = hasher.Write(input) + var buf [16]byte + _, _ = hasher.Digest().Read(buf[0:]) + + return fmt.Sprintf("%x", buf) +} + +// cryptoRandInt64 uses crypto/rand to get an random int64 +func cryptoRandInt64() int64 { + c := 8 + b := make([]byte, c) + _, err := cryptorand.Read(b) + if err != nil { + panic(err) + } + r := int64(binary.LittleEndian.Uint64(b)) + return r +} diff --git a/blake3_test.go b/blake3_test.go new file mode 100644 index 000000000..f2c85cb93 --- /dev/null +++ b/blake3_test.go @@ -0,0 +1,47 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "fmt" + "testing" + + "encoding/hex" +) + +func TestBlake3Hasher(t *testing.T) { + + hasher := NewBlake3Hasher() + hash := make([]byte, 16) + input := []byte("hello world") + hash = hasher.CryptoHash(input, hash) + expected := "d74981efa70a0c880b8d8c1985d075db" + observed := hex.EncodeToString(hash) + if observed != expected { + panic(fmt.Sprintf("expected hash:'%v' but observed hash '%v'", expected, observed)) + } + + obs2 := blake3sum16(input) + if obs2 != expected { + panic(fmt.Sprintf("expected hash:'%v' but observed hash from blake2sum16: '%v'", expected, obs2)) + } +} + +func TestCryptoRandInt64(t *testing.T) { + rnd := cryptoRandInt64() + if rnd == 0 { + panic("cryptoRandInt64() gave 0, very high odds it has broken") + } +} diff --git a/bluegreentx.go b/bluegreentx.go new file mode 100644 index 000000000..dd58c69fd --- /dev/null +++ b/bluegreentx.go @@ -0,0 +1,421 @@ +// 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" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// blueGreenTx runs two Tx together and notices differences in their output. +// By convention, the 'b' Tx is the output that is returned to caller. +type blueGreenTx struct { + a Tx + b Tx // b's output is returned + + idx *Index +} + +func newBlueGreenTx(a, b Tx, idx *Index) *blueGreenTx { + return &blueGreenTx{a: a, b: b, idx: idx} +} + +var _ = newBlueGreenTx // keep linter happy + +var _ Tx = (*blueGreenTx)(nil) + +func (c *blueGreenTx) Readonly() bool { + a := c.a.Readonly() + b := c.b.Readonly() + if a != b { + panic(fmt.Sprintf("a=%v, but b =%v", a, b)) + } + return b +} + +func (c *blueGreenTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + return c.b.NewTxIterator(index, field, view, shard) +} + +func (c *blueGreenTx) Pointer() string { + return fmt.Sprintf("%p", c) +} + +func (c *blueGreenTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + c.a.IncrementOpN(index, field, view, shard, changedN) + c.b.IncrementOpN(index, field, view, shard, changedN) +} + +func (c *blueGreenTx) Rollback() { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + c.a.Rollback() + c.b.Rollback() +} + +func (c *blueGreenTx) Commit() error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Commit() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.Commit() + _ = errA + errB := c.b.Commit() + + compareErrors(errA, errB) + return errB +} + +func (c *blueGreenTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RoaringBitmap() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + a, errA := c.a.RoaringBitmap(index, field, view, shard) + _, _ = a, errA + b, errB := c.b.RoaringBitmap(index, field, view, shard) + compareErrors(errA, errB) + return b, errB +} + +func (c *blueGreenTx) 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) + } + }() + a, errA := c.a.Container(index, field, view, shard, key) + b, errB := c.b.Container(index, field, view, shard, key) + compareErrors(errA, errB) + err = a.BitwiseCompare(b) + panicOn(err) + + return b, errB +} + +func (c *blueGreenTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see PutContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.PutContainer(index, field, view, shard, key, rc) + errB := c.b.PutContainer(index, field, view, shard, key, rc) + compareErrors(errA, errB) + + /* draft idea of how to check the full databases afterwards: + hashA := c.a.RootHashString() + hashB := c.b.RootHashString() + if hashA != hashB { + panic(fmt.Sprintf("hashA = '%v' but hashB = '%v'", hashA, hashB)) + } + */ + return errB +} + +func (c *blueGreenTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (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) + } + }() + + // remember where the iterator started, so we can replay it a second time. + rit2 := rit.Clone() + + changedA, rowSetA, errA := c.a.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize) + + changedB, rowSetB, errB := c.b.ImportRoaringBits(index, field, view, shard, rit2, clear, log, rowSize) + + if changedA != changedB { + panic(fmt.Sprintf("changedA = %v, but changedB = %v", changedA, changedB)) + } + if len(rowSetA) != len(rowSetB) { + panic(fmt.Sprintf("rowSetA = %#v, but rowSetB = %#v", rowSetA, rowSetB)) + } + for k, va := range rowSetA { + vb, ok := rowSetB[k] + if !ok { + panic(fmt.Sprintf("diff on key '%v': present in rowSetA, but not in rowSet B. rowSetA = %#v, but rowSetB = %#v", k, rowSetA, rowSetB)) + } + if va != vb { + panic(fmt.Sprintf("diff on key '%v', rowSetA has value '%v', but rowSetB has value '%v'", k, va, vb)) + } + } + + compareErrors(errA, errB) + + //compareDatabases(c.a, c.b) + return changedB, rowSetB, errB +} + +/* // TODO: get a database-wide checksum working +func compareDatabases(a, b Tx) { + + index, field, view, shard := "i", "f", "v", uint64(0) + + ha, errA := a.WholeDatabaseBlake3Hash(index, field, view, shard) + panicOn(errA) + hb, errB := b.WholeDatabaseBlake3Hash(index, field, view, shard) + panicOn(errB) + + if ha != hb { + panic(fmt.Sprintf("a.WholeDatabaseBlake3Hash(%T) = '%v' but b.WholeDatabaseBlake3Hash(%T) = '%v'", a, ha, b, hb)) + } +} +*/ + +func (c *blueGreenTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see RemoveContainer() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.RemoveContainer(index, field, view, shard, key) + errB := c.b.RemoveContainer(index, field, view, shard, key) + compareErrors(errA, errB) + return errB +} + +func (c *blueGreenTx) UseRowCache() bool { + return c.b.UseRowCache() +} + +func (c *blueGreenTx) 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' for index='%v', field='%v', view='%v', shard='%v' at '%v'", r, index, field, view, shard, stack()) + panic(r) + } + }() + + // must copy a before calling Add(), since RoaringTx.Add() uses roaring.DirectAddN() + // which modifies the input array a. + a2 := make([]uint64, len(a)) + copy(a2, a) + + ach, errA := c.a.Add(index, field, view, shard, batched, a...) + _, _ = ach, errA + + bch, errB := c.b.Add(index, field, view, shard, batched, a2...) + + if ach != bch { + panic(fmt.Sprintf("Add() difference, ach=%v, but bch=%v; errA='%v'; errB='%v'", ach, bch, errA, errB)) + } + compareErrors(errA, errB) + return bch, errB +} + +func compareErrors(errA, errB error) { + switch { + case errA == nil && errB == nil: + // OK + case errA == nil: + panic(fmt.Sprintf("errA is nil, but errB = %#v", errB)) + case errB == nil: + panic(fmt.Sprintf("errB is nil, but errA = %#v", errA)) + default: + ae := errA.Error() + be := errB.Error() + if ae != be { + panic(fmt.Sprintf("errA is '%v', but errB is '%v'", ae, be)) + } + } +} + +func (c *blueGreenTx) 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) + } + }() + ach, errA := c.a.Remove(index, field, view, shard, a...) + _, _ = ach, errA + bch, errB := c.b.Remove(index, field, view, shard, a...) + compareErrors(errA, errB) + return bch, errB +} + +func (c *blueGreenTx) 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) + } + }() + ax, errA := c.a.Contains(index, field, view, shard, key) + _, _ = ax, errA + bx, errB := c.b.Contains(index, field, view, shard, key) + + compareErrors(errA, errB) + return bx, errB +} + +func (c *blueGreenTx) 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) + } + }() + // TODO: need to return a blueGreenIterator too, that does close/next operations on both A and B. + ait, afound, errA := c.a.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) + _, _, _ = ait, afound, errA + bit, bfound, errB := c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) + + compareErrors(errA, errB) + return bit, bfound, errB +} + +func (c *blueGreenTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEach() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.ForEach(index, field, view, shard, fn) + _ = errA + errB := c.b.ForEach(index, field, view, shard, fn) + _ = errB + + compareErrors(errA, errB) + return errB +} + +func (c *blueGreenTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see ForEachRange() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.ForEachRange(index, field, view, shard, start, end, fn) + _ = errA + errB := c.b.ForEachRange(index, field, view, shard, start, end, fn) + _ = errB + + compareErrors(errA, errB) + return errB +} + +func (c *blueGreenTx) Count(index, field, view string, shard uint64) (uint64, error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Count() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + a, errA := c.a.Count(index, field, view, shard) + _, _ = a, errA + b, errB := c.b.Count(index, field, view, shard) + _, _ = b, errB + + compareErrors(errA, errB) + return b, errB +} + +func (c *blueGreenTx) Max(index, field, view string, shard uint64) (uint64, error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Max() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + a, errA := c.a.Max(index, field, view, shard) + _, _ = a, errA + b, errB := c.b.Max(index, field, view, shard) + _, _ = b, errB + + compareErrors(errA, errB) + return b, errB +} + +func (c *blueGreenTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Min() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + amin, afound, errA := c.a.Min(index, field, view, shard) + _, _, _ = amin, afound, errA + bmin, bfound, errB := c.b.Min(index, field, view, shard) + _, _, _ = bmin, bfound, errB + + compareErrors(errA, errB) + return bmin, bfound, errB +} + +func (c *blueGreenTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see UnionInPlace() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + errA := c.a.UnionInPlace(index, field, view, shard, others...) + errB := c.b.UnionInPlace(index, field, view, shard, others...) + compareErrors(errA, errB) + return errB +} + +func (c *blueGreenTx) 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) + } + }() + a, errA := c.a.CountRange(index, field, view, shard, start, end) + b, errB := c.b.CountRange(index, field, view, shard, start, end) + + if a != b { + panic(fmt.Sprintf("a = %v, but b = %v", a, b)) + } + + compareErrors(errA, errB) + return b, errB +} + +func (c *blueGreenTx) 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) + } + }() + a, errA := c.a.OffsetRange(index, field, view, shard, offset, start, end) + b, errB := c.b.OffsetRange(index, field, view, shard, offset, start, end) + + err = roaringBitmapDiff(a, b) + panicOn(err) + compareErrors(errA, errB) + return b, errB +} diff --git a/cache.go b/cache.go index ec7b3aa85..9195f63c9 100644 --- a/cache.go +++ b/cache.go @@ -350,6 +350,13 @@ type PairField struct { Field string } +func (p PairField) Clone() (r PairField) { + return PairField{ + Pair: p.Pair, + Field: p.Field, + } +} + // ToTable implements the ToTabler interface. func (p PairField) ToTable() (*pb.TableResponse, error) { return pb.RowsToTable(p, 1) @@ -469,6 +476,15 @@ type PairsField struct { Field string } +func (p *PairsField) Clone() (r *PairsField) { + r = &PairsField{ + Pairs: make([]Pair, len(p.Pairs)), + Field: p.Field, + } + copy(r.Pairs, p.Pairs) + return +} + // ToTable implements the ToTabler interface. func (p *PairsField) ToTable() (*pb.TableResponse, error) { return pb.RowsToTable(p, len(p.Pairs)) diff --git a/catcher.go b/catcher.go new file mode 100644 index 000000000..a4d832c20 --- /dev/null +++ b/catcher.go @@ -0,0 +1,277 @@ +// 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" + + "github.com/pilosa/pilosa/v2/roaring" +) + +// catcher is useful to report error locations with a +// stack dump before the complexity +// of the executor_test swallows up +// the location of a panic. +type catcherTx struct { + b *BadgerTx +} + +func newCatcherTx(b *BadgerTx) *catcherTx { + return &catcherTx{b: b} +} + +func init() { + // keep golangci-lint happy + _ = newCatcherTx +} + +var _ Tx = (*catcherTx)(nil) + +func (c *catcherTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + c.b.IncrementOpN(index, field, view, shard, changedN) +} + +func (c *catcherTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + return c.b.NewTxIterator(index, field, view, shard) +} + +func (c *catcherTx) WholeDatabaseBlake3Hash(index, field, view string, shard uint64) (hash string, err error) { + return c.b.WholeDatabaseBlake3Hash(index, field, view, shard) +} + +func (c *catcherTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (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) + } + }() + return c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize) +} + +func (c *catcherTx) 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 *catcherTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +func (c *catcherTx) Rollback() { + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + c.b.Rollback() +} + +func (c *catcherTx) Commit() error { + + defer func() { + if r := recover(); r != nil { + AlwaysPrintf("see Commit() panic '%v' at '%v'", r, stack()) + panic(r) + } + }() + return c.b.Commit() +} + +func (c *catcherTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + + 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 *catcherTx) 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) + } + }() + return c.b.Container(index, field, view, shard, key) +} + +func (c *catcherTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) error { + + 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 *catcherTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { + + 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 *catcherTx) UseRowCache() bool { + return c.b.UseRowCache() +} + +func (c *catcherTx) 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) + } + }() + return c.b.Add(index, field, view, shard, batched, a...) +} + +func (c *catcherTx) 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) + } + }() + return c.b.Remove(index, field, view, shard, a...) +} + +func (c *catcherTx) 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) + } + }() + return c.b.Contains(index, field, view, shard, key) +} + +func (c *catcherTx) 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) + } + }() + return c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) +} + +func (c *catcherTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { + + 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 *catcherTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { + + 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 *catcherTx) Count(index, field, view string, shard uint64) (uint64, error) { + + 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 *catcherTx) Max(index, field, view string, shard uint64) (uint64, error) { + + 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 *catcherTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { + + 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 *catcherTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { + + 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 *catcherTx) 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) + } + }() + return c.b.CountRange(index, field, view, shard, start, end) +} + +func (c *catcherTx) 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) + } + }() + return c.b.OffsetRange(index, field, view, shard, offset, start, end) +} diff --git a/cluster_internal_test.go b/cluster_internal_test.go index 00492d068..c31b9f294 100644 --- a/cluster_internal_test.go +++ b/cluster_internal_test.go @@ -161,7 +161,7 @@ func TestFragSources(t *testing.T) { // Obtain transaction. tx := &RoaringTx{Index: idx} - defer func() { _ = tx.Rollback() }() + defer tx.Rollback() field, err := idx.CreateFieldIfNotExists("f", OptFieldTypeDefault()) if err != nil { @@ -787,6 +787,7 @@ func TestCluster_ResizeStates(t *testing.T) { if err := tc.CreateField("i", "f", OptFieldTypeDefault()); err != nil { t.Fatalf("creating field: %v", err) } + // Each tc.SetBit starts and commits its own Tx. if err := tc.SetBit("i", "f", 1, 101, nil); err != nil { t.Fatalf("setting bit: %v", err) } @@ -804,6 +805,12 @@ func TestCluster_ResizeStates(t *testing.T) { t.Fatal(err) } + idx0 := node0.holder.Index("i") + if idx0 == nil { + t.Fatal(`idx0 was nil, could not retrieve Index("i")`) + } + //idx0.Dump("node0") + // addNode needs to block until the resize process has completed. if err := tc.addNode(); err != nil { t.Fatalf("adding node: %v", err) @@ -816,6 +823,7 @@ func TestCluster_ResizeStates(t *testing.T) { } else if node1.State() != ClusterStateNormal { t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, node1.State()) } + // INVAR: after node1.State() is normal, the rebalancing should have been done. expectedTop := &Topology{ nodeIDs: []string{node0.Node.ID, node1.Node.ID}, @@ -834,11 +842,19 @@ func TestCluster_ResizeStates(t *testing.T) { node1View := node1Field.view("standard") node1Fragment := node1View.Fragment(1) + idx1 := node1.holder.Index("i") + if idx1 == nil { + t.Fatal(`idx1 was nil, could not retrieve Index("i")`) + } + //idx0.Dump("after rebalance, node0") + //idx1.Dump("after rebalance, node1") + // Ensure checksums are the same. if chksum, err := node1Fragment.Checksum(); err != nil { t.Fatal(err) } else if !bytes.Equal(chksum, node0Checksum) { t.Fatalf("expected standard view checksum to match: %x - %x", chksum, node0Checksum) + // badger red: TestCluster_ResizeStates/Multiple_nodes,_with_data: cluster_internal_test.go:841: expected standard view checksum to match: ef46db3751d8e999 - fad4de25ee696ca0 } // Close TestCluster. diff --git a/ctl/server.go b/ctl/server.go index 8aa0edeb9..aa0b9a077 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -85,4 +85,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { // Profiling flags.IntVar(&srv.Config.Profile.BlockRate, "profile.block-rate", srv.Config.Profile.BlockRate, "Sampling rate for goroutine blocking profiler. One sample per ns.") flags.IntVar(&srv.Config.Profile.MutexFraction, "profile.mutex-fraction", srv.Config.Profile.MutexFraction, "Sampling fraction for mutex contention profiling. Sample 1/ of events.") + + // Transactional storage engine + flags.StringVarP(&srv.Config.Txsrc, "tx", "", "roaring", "transaction/storage to use: one of roaring, rbf, badger, rbf_roaring, roaring_rbf, badger_roaring, roaring_badger, badger_rbf, or rbf_badger") } diff --git a/executor.go b/executor.go index 3812c8451..62b98b5be 100644 --- a/executor.go +++ b/executor.go @@ -180,8 +180,14 @@ func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shar return resp, ErrIndexNotFound } + needWriteTxn := false + nw := q.WriteCallN() + if nw > 0 { + needWriteTxn = true + } + // Verify that the number of writes do not exceed the maximum. - if e.MaxWritesPerRequest > 0 && q.WriteCallN() > e.MaxWritesPerRequest { + if e.MaxWritesPerRequest > 0 && nw > e.MaxWritesPerRequest { return resp, ErrTooManyWrites } @@ -210,12 +216,11 @@ func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shar } } - // TODO: Determine if query is read-only. - tx, err := e.Holder.Begin(true) + tx, err := e.Holder.BeginTx(needWriteTxn, idx) if err != nil { return resp, err } - defer func() { _ = tx.Rollback() }() + defer tx.Rollback() results, err := e.execute(ctx, tx, index, q, shards, opt) if err != nil { @@ -267,19 +272,75 @@ func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shar // Translate response objects from ids to keys, if necessary. // No need to translate a remote call. if !opt.Remote { + // only translateResults if this local node is the final destination. only string/column keys. if err := e.translateResults(ctx, index, idx, q.Calls, results); err != nil { return resp, err } else if err := validateQueryContext(ctx); err != nil { return resp, err } } + // Must copy out of Tx data before Commiting, because it will become invalid afterwards. + respSafeNoTxData := e.safeCopy(resp) // Commit transaction. if err := tx.Commit(); err != nil { - return resp, err + return respSafeNoTxData, err } + return respSafeNoTxData, nil +} - return resp, nil +// safeCopy copies everything in resp that has Bitmap material, +// to avoid anything coming from the mmap-ed Tx storage. +func (e *executor) safeCopy(resp QueryResponse) (out QueryResponse) { + out = QueryResponse{ + // not transactional, from attribute storage so no need to clone these: + ColumnAttrSets: resp.ColumnAttrSets, // []*ColumnAttrSet + Err: resp.Err, // error + Profile: resp.Profile, // *tracing.Profile + } + // Results can contain *roaring.Bitmap, so need to copy from Tx mmap-ed memory. + for _, v := range resp.Results { + switch x := v.(type) { + case *Row: + rowSafe := x.Clone() + out.Results = append(out.Results, rowSafe) + case bool: + out.Results = append(out.Results, x) + case nil: + out.Results = append(out.Results, nil) + case uint64: + out.Results = append(out.Results, x) // for counts + case *PairsField: + // no bitmap material, so should be ok to skip Clone() + out.Results = append(out.Results, x) + case PairField: // not PairsField but PairField + // no bitmap material, so should be ok to skip Clone() + out.Results = append(out.Results, x) + case ValCount: + // no bitmap material, so should be ok to skip Clone() + out.Results = append(out.Results, x) + case SignedRow: + // has *Row in it, so has Bitmap material, and very likely needs Clone. + y := x.Clone() + out.Results = append(out.Results, *y) + case GroupCount: + // no bitmap material, so should be ok to skip Clone() + out.Results = append(out.Results, x) + case []GroupCount: + out.Results = append(out.Results, x) + case RowIdentifiers: + // no bitmap material, so should be ok to skip Clone() + out.Results = append(out.Results, x) + case RowIDs: + // defined as: type RowIDs []uint64 + // so does not contain bitmap material, and + // should not need to be cloned. + out.Results = append(out.Results, x) + default: + panic(fmt.Sprintf("handle %T here", v)) + } + } + return } // readColumnAttrSets returns a list of column attribute objects by id. @@ -308,6 +369,7 @@ func (e *executor) readColumnAttrSets(index *Index, ids []uint64) ([]*ColumnAttr // handlePreCalls traverses the call tree looking for calls that need // precomputed values. Right now, that's just Distinct. func (e *executor) handlePreCalls(ctx context.Context, tx Tx, index string, c *pql.Call, shards []uint64, opt *execOptions) error { + if c.Name == "Precomputed" { idx := c.Args["valueidx"].(int64) if idx >= 0 && idx < int64(len(opt.EmbeddedData)) { @@ -457,6 +519,7 @@ func (e *executor) execute(ctx context.Context, tx Tx, index string, q *pql.Quer // Execute each call serially. results := make([]interface{}, 0, len(q.Calls)) for i, call := range q.Calls { + if err := validateQueryContext(ctx); err != nil { return nil, err } @@ -1019,6 +1082,7 @@ func (e *executor) executeAllCallMapReduce(ctx context.Context, tx Tx, index str // executeIncludesColumnCallShard func (e *executor) executeIncludesColumnCallShard(ctx context.Context, tx Tx, index string, c *pql.Call, shard uint64, column uint64) (bool, error) { + span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executeIncludesColumnCallShard") defer span.Finish() @@ -1308,7 +1372,8 @@ func (e *executor) executeBitmapCall(ctx context.Context, tx Tx, index string, c reduceFn := func(ctx context.Context, prev, v interface{}) interface{} { other, _ := prev.(*Row) if other == nil { - other = NewRow() + + other = NewRow() // bug! this row ends up containing Badger Txn data that should be accessed outside the Txn. } if err := ctx.Err(); err != nil { return err @@ -1823,6 +1888,21 @@ type RowIdentifiers struct { field string } +func (r *RowIdentifiers) Clone() (clone *RowIdentifiers) { + clone = &RowIdentifiers{ + field: r.field, + } + if r.Rows != nil { + clone.Rows = make([]uint64, len(r.Rows)) + copy(clone.Rows, r.Rows) + } + if r.Keys != nil { + clone.Keys = make([]string, len(r.Keys)) + copy(clone.Keys, r.Keys) + } + return +} + // ToTable implements the ToTabler interface. func (r RowIdentifiers) ToTable() (*pb.TableResponse, error) { var n int @@ -2041,6 +2121,20 @@ type FieldRow struct { Value *int64 `json:"value,omitempty"` } +func (fr *FieldRow) Clone() (clone *FieldRow) { + clone = &FieldRow{ + Field: fr.Field, + RowID: fr.RowID, + RowKey: fr.RowKey, + } + if fr.Value != nil { + // deep copy, for safety. + v := *fr.Value + clone.Value = &v + } + return +} + // MarshalJSON marshals FieldRow to JSON such that // either a Key or an ID is included. func (fr FieldRow) MarshalJSON() ([]byte, error) { @@ -2138,6 +2232,18 @@ type GroupCount struct { Sum int64 `json:"sum"` } +func (g *GroupCount) Clone() (r *GroupCount) { + r = &GroupCount{ + Group: make([]FieldRow, len(g.Group)), + Count: g.Count, + Sum: g.Sum, + } + for i := range g.Group { + r.Group[i] = *(g.Group[i].Clone()) + } + return +} + // mergeGroupCounts merges two slices of GroupCounts throwing away any that go // beyond the limit. It assume that the two slices are sorted by the row ids in // the fields of the group counts. It may modify its arguments. @@ -2642,6 +2748,7 @@ func (e *executor) executeRowShard(ctx context.Context, tx Tx, index string, c * // Simply return row if times are not set. if c.Name == "Row" && timeNotSet { + frag := e.Holder.fragment(index, fieldName, viewStandard, shard) if frag == nil { return NewRow(), nil @@ -3771,6 +3878,7 @@ func (e *executor) executeSetColumnAttrs(ctx context.Context, tx Tx, index strin delete(attrs, "field") // Set attributes. + if err := idx.ColumnAttrStore().SetAttrs(col, attrs); err != nil { return err } @@ -4654,6 +4762,15 @@ type SignedRow struct { field string } +func (s *SignedRow) Clone() (r *SignedRow) { + r = &SignedRow{ + Neg: s.Neg.Clone(), // Row.Clone() returns nil for nil. + Pos: s.Pos.Clone(), + field: s.field, + } + return +} + // Field returns the field name associated to the signed row. func (s *SignedRow) Field() string { return s.field @@ -4768,6 +4885,18 @@ type ValCount struct { Count int64 `json:"count"` } +func (v *ValCount) Clone() (r *ValCount) { + r = &ValCount{ + Val: v.Val, + FloatVal: v.FloatVal, + Count: v.Count, + } + if v.DecimalVal != nil { + r.DecimalVal = v.DecimalVal.Clone() + } + return +} + // ToTable implements the ToTabler interface. func (v ValCount) ToTable() (*pb.TableResponse, error) { return pb.RowsToTable(&v, 1) diff --git a/executor_internal_test.go b/executor_internal_test.go index 28dd369ef..8ae31da98 100644 --- a/executor_internal_test.go +++ b/executor_internal_test.go @@ -137,12 +137,6 @@ func TestExecutor_TranslateRowsOnBool(t *testing.T) { holder := NewHolder(DefaultPartitionN) defer holder.Close() - tx, err := holder.Begin(true) - if err != nil { - t.Fatal(err) - } - defer func() { _ = tx.Rollback() }() - e := &executor{ Holder: holder, Cluster: NewTestCluster(1), @@ -157,6 +151,12 @@ func TestExecutor_TranslateRowsOnBool(t *testing.T) { t.Fatalf("creating index: %v", err) } + tx, err := holder.BeginTx(writable, idx) + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + fb, errb := idx.CreateField("b", OptFieldTypeBool()) _, errbk := idx.CreateField("bk", OptFieldTypeBool(), OptFieldKeys()) if errb != nil || errbk != nil { diff --git a/executor_test.go b/executor_test.go index 32fc01b27..9a78b96c0 100644 --- a/executor_test.go +++ b/executor_test.go @@ -41,6 +41,9 @@ import ( "github.com/pkg/errors" ) +// writable initializes Tx that update, use !writable for read-only. +const writable = true + var ( TempDir = getTempDirString() ) @@ -142,6 +145,7 @@ func TestExecutor_Execute_Difference(t *testing.T) { c := test.MustRunCluster(t, 1) defer c.Close() hldr := test.Holder{Holder: c[0].Server.Holder()} + hldr.SetBit("i", "general", 10, 1) hldr.SetBit("i", "general", 10, 2) hldr.SetBit("i", "general", 10, 3) @@ -223,7 +227,6 @@ func TestExecutor_Execute_Intersect(t *testing.T) { hldr.SetBit("i", "general", 10, 1) hldr.SetBit("i", "general", 10, ShardWidth+1) hldr.SetBit("i", "general", 10, ShardWidth+2) - hldr.SetBit("i", "general", 11, 1) hldr.SetBit("i", "general", 11, 2) hldr.SetBit("i", "general", 11, ShardWidth+2) @@ -904,11 +907,11 @@ func TestExecutor_Execute_SetValue(t *testing.T) { } // Obtain transaction. - tx, err := hldr.Begin(false) + tx, err := hldr.BeginTx(!writable, index.Index) if err != nil { t.Fatal(err) } - defer func() { _ = tx.Rollback() }() + defer tx.Rollback() f := hldr.Field("i", "f") if value, exists, err := f.Value(tx, 10); err != nil { @@ -3435,7 +3438,6 @@ func TestExecutor_Execute_Existence(t *testing.T) { } else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{ShardWidth + 2}) { t.Fatalf("unexpected columns after Not: %+v", bits) } - // Reopen cluster to ensure existence field is reloaded. if err := c[0].Reopen(); err != nil { t.Fatal(err) @@ -3820,7 +3822,6 @@ func TestExecutor_Execute_ClearRow(t *testing.T) { if res := responses[1].Results[0].(bool); !res { t.Fatalf("unexpected clear row result: %+v", res) } - // Clear the row again and ensure we get a `false` response. if res := responses[2].Results[0].(bool); res { t.Fatalf("unexpected clear row result: %+v", res) @@ -4023,6 +4024,7 @@ func TestExecutor_Execute_ClearRow(t *testing.T) { // Ensure a row can be set. func TestExecutor_Execute_SetRow(t *testing.T) { + t.Run("Set_NewRow", func(t *testing.T) { c := test.MustRunCluster(t, 1) defer c.Close() @@ -4824,6 +4826,7 @@ func TestExecutor_ForeignIndex(t *testing.T) { } join := c.Query(t, "parent", `Intersect(Row(general=3), Distinct(Row(color="blue"), index="child", field="parent_id"))`).Results[0].(*pilosa.Row) + if !reflect.DeepEqual(join.Keys, []string{"one"}) { t.Fatalf("unexpected keys: %v", join.Keys) } @@ -4870,6 +4873,7 @@ func TestExecutor_Execute_GroupBy(t *testing.T) { {12, 2}, {12, ShardWidth + 2}, }) + c.ImportBits(t, "i", "sub", [][2]uint64{ {100, 0}, {100, 1}, diff --git a/field.go b/field.go index 956fbd452..a9a80e2fe 100644 --- a/field.go +++ b/field.go @@ -1737,7 +1737,7 @@ func (f *Field) importValue(tx Tx, columnIDs []uint64, values []int64, options * return nil } -func (f *Field) importRoaring(ctx context.Context, data []byte, shard uint64, viewName string, clear bool) error { +func (f *Field) importRoaring(ctx context.Context, tx Tx, data []byte, shard uint64, viewName string, clear bool) error { span, ctx := tracing.StartSpanFromContext(ctx, "Field.importRoaring") defer span.Finish() @@ -1754,7 +1754,7 @@ func (f *Field) importRoaring(ctx context.Context, data []byte, shard uint64, vi if err != nil { return errors.Wrap(err, "creating fragment") } - if err := frag.importRoaring(ctx, data, clear); err != nil { + if err := frag.importRoaring(ctx, tx, data, clear); err != nil { return err } diff --git a/fragment.go b/fragment.go index c7bd1e241..b0bf22e05 100644 --- a/fragment.go +++ b/fragment.go @@ -129,7 +129,14 @@ type fragment struct { // Cache for row counts. CacheType string // passed in by field - cache cache + + // cache keeps a local rowid,count ranking: + // telling us which is the most populated rows in that field. + // Is only on "set fields" with rowCache enabled. So + // BSI, mutex, bool fields do not have this. + // Good: it Only has a string and a count, so cannot use Tx memory. + cache cache + CacheSize uint32 // Stats reporting. @@ -160,6 +167,15 @@ type fragment struct { stats stats.StatsClient bitmapInfo *roaring.BitmapInfo + + // txTestingOnly: this looks gross. + // Nonetheless, it allowed us to + // integrate Tx into the + // fragment_internal_test.go suite + // and not break the world all at once. + // + // Only for testing, obviously. + txTestingOnly Tx } // newFragment returns a new instance of Fragment. @@ -192,6 +208,10 @@ type FragmentInfo struct { BlockChecksums []FragmentBlock `json:"BlockChecksums,omitempty"` } +func (f *fragment) Index() *Index { + return f.holder.Index(f.index) +} + func (f *fragment) inspect(params InspectRequestParams) (fi FragmentInfo) { if f.bitmapInfo == nil { fi.BitmapInfo = f.storage.Info(params.Containers) @@ -529,16 +549,20 @@ func (f *fragment) mustRow(tx Tx, rowID uint64) *Row { // unprotectedRow returns a row from the row cache if available or from storage // (updating the cache). func (f *fragment) unprotectedRow(tx Tx, rowID uint64) (*Row, error) { - r, ok := f.rowCache.Fetch(rowID) - if ok && r != nil { - return r, nil + useRowCache := tx.UseRowCache() + if useRowCache { + r, ok := f.rowCache.Fetch(rowID) + if ok && r != nil { + return r, nil + } } - row, err := f.rowFromStorage(tx, rowID) if err != nil { return nil, err } - f.rowCache.Add(rowID, row) + if useRowCache { + f.rowCache.Add(rowID, row) + } return row, nil } @@ -559,7 +583,7 @@ func (f *fragment) rowFromStorage(tx Tx, rowID uint64) (*Row, error) { row := &Row{ segments: []rowSegment{{ - data: data, + data: data, // this data contains BadgerTx data, which should not survive Txn commit. shard: f.shard, writable: true, }}, @@ -602,7 +626,7 @@ func (f *fragment) handleMutex(tx Tx, rowID, columnID uint64) error { // unprotectedSetBit TODO should be replaced by an invocation of importPositions with a single bit to set. func (f *fragment) unprotectedSetBit(tx Tx, rowID, columnID uint64) (changed bool, err error) { - changed = false + // Determine the position of the bit in the storage. pos, err := f.pos(rowID, columnID) if err != nil { @@ -610,7 +634,10 @@ func (f *fragment) unprotectedSetBit(tx Tx, rowID, columnID uint64) (changed boo } // Write to storage. - if changed, err = tx.Add(f.index, f.field, f.view, f.shard, pos); err != nil { + changeCount := 0 + changeCount, err = tx.Add(f.index, f.field, f.view, f.shard, doBatched, pos) + changed = changeCount > 0 + if err != nil { return false, errors.Wrap(err, "writing") } @@ -623,7 +650,7 @@ func (f *fragment) unprotectedSetBit(tx Tx, rowID, columnID uint64) (changed boo delete(f.checksums, int(rowID/HashBlockSize)) // Increment number of operations until snapshot is required. - f.incrementOpN(1) + tx.IncrementOpN(f.index, f.field, f.view, f.shard, 1) // If we're using a cache, update it. Otherwise skip the // possibly-expensive count operation. @@ -671,20 +698,23 @@ func (f *fragment) unprotectedClearBit(tx Tx, rowID, columnID uint64) (changed b } // Write to storage. - if changed, err = tx.Remove(f.index, f.field, f.view, f.shard, pos); err != nil { + changeCount := 0 + if changeCount, err = tx.Remove(f.index, f.field, f.view, f.shard, pos); err != nil { return false, errors.Wrap(err, "writing") } // Don't update the cache if nothing changed. - if !changed { - return changed, nil + if changeCount <= 0 { + return false, nil + } else { + changed = true } // Invalidate block checksum. delete(f.checksums, int(rowID/HashBlockSize)) // Increment number of operations until snapshot is required. - f.incrementOpN(1) + tx.IncrementOpN(f.index, f.field, f.view, f.shard, 1) // If we're using a cache, update it. Otherwise skip the // possibly-expensive count operation. @@ -997,15 +1027,17 @@ func (f *fragment) importSetValue(tx Tx, columnID uint64, bitDepth uint, value i } if uvalue&(1< 0 { changed++ } } else { - if c, err := tx.Remove(f.index, f.field, f.view, f.shard, bit); err != nil { + changeCount, err := tx.Remove(f.index, f.field, f.view, f.shard, bit) + if err != nil { return changed, errors.Wrap(err, "removing") - } else if c { + } else if changeCount > 0 { changed++ } } @@ -1017,13 +1049,13 @@ func (f *fragment) importSetValue(tx Tx, columnID uint64, bitDepth uint, value i } else if clear { if c, err := tx.Remove(f.index, f.field, f.view, f.shard, p); err != nil { return changed, errors.Wrap(err, "removing not-null from storage") - } else if c { + } else if c > 0 { changed++ } } else { - if c, err := tx.Add(f.index, f.field, f.view, f.shard, p); err != nil { + if c, err := tx.Add(f.index, f.field, f.view, f.shard, !doBatched, p); err != nil { return changed, errors.Wrap(err, "adding not-null to storage") - } else if c { + } else if c > 0 { changed++ } } @@ -1034,13 +1066,13 @@ func (f *fragment) importSetValue(tx Tx, columnID uint64, bitDepth uint, value i } else if value >= 0 || clear { if c, err := tx.Remove(f.index, f.field, f.view, f.shard, p); err != nil { return changed, errors.Wrap(err, "removing sign from storage") - } else if c { + } else if c > 0 { changed++ } } else { - if c, err := tx.Add(f.index, f.field, f.view, f.shard, p); err != nil { + if c, err := tx.Add(f.index, f.field, f.view, f.shard, !doBatched, p); err != nil { return changed, errors.Wrap(err, "adding sign to storage") - } else if c { + } else if c > 0 { changed++ } } @@ -1889,8 +1921,17 @@ func (f *fragment) Blocks() ([]FragmentBlock, error) { var a []FragmentBlock - // Initialize the iterator. - itr := f.storage.Iterator() + idx := f.holder.Index(f.index) + if idx == nil { + panic(fmt.Sprintf("index was nil in fragment.Blocks(): f.index='%v'; f.holder.indexes='%#v'\n", f.index, f.holder.indexes)) + } + tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f}) + defer tx.Rollback() + // no Commit below, b/c is read-only. + + itr := tx.NewTxIterator(f.index, f.field, f.view, f.shard) + defer itr.Close() + itr.Seek(0) // Initialize block hasher. @@ -1967,7 +2008,13 @@ func (f *fragment) readContiguousChecksums(a *[]FragmentBlock, blockID int) (n i func (f *fragment) blockData(id int) (rowIDs, columnIDs []uint64, err error) { f.mu.Lock() defer f.mu.Unlock() - if err := f.storage.ForEachRange(uint64(id)*HashBlockSize*ShardWidth, (uint64(id)+1)*HashBlockSize*ShardWidth, func(i uint64) error { + + idx := f.holder.Index(f.index) + tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx}) + defer tx.Rollback() + // readonly, so no Commit() + + if err := tx.ForEachRange(f.index, f.field, f.view, f.shard, uint64(id)*HashBlockSize*ShardWidth, (uint64(id)+1)*HashBlockSize*ShardWidth, func(i uint64) error { rowIDs = append(rowIDs, i/ShardWidth) columnIDs = append(columnIDs, i%ShardWidth) return nil @@ -2101,7 +2148,7 @@ func (f *fragment) mergeBlock(tx Tx, id int, data []pairSet) (sets, clears []pai rowSet[clears[0].rowIDs[i]] = struct{}{} clears[0].columnIDs[i] += clears[0].rowIDs[i] * ShardWidth } - err = f.importPositions(sets[0].columnIDs, clears[0].columnIDs, rowSet) + err = f.importPositions(tx, sets[0].columnIDs, clears[0].columnIDs, rowSet) return sets[1:], clears[1:], err } @@ -2149,9 +2196,9 @@ func (f *fragment) bulkImportStandard(tx Tx, rowIDs, columnIDs []uint64, options f.mu.Lock() defer f.mu.Unlock() if options.Clear { - err = f.importPositions(nil, positions, rowSet) + err = f.importPositions(tx, nil, positions, rowSet) } else { - err = f.importPositions(positions, nil, rowSet) + err = f.importPositions(tx, positions, nil, rowSet) } return errors.Wrap(err, "bulkImportStandard") } @@ -2164,26 +2211,31 @@ func (f *fragment) bulkImportStandard(tx Tx, rowIDs, columnIDs []uint64, options // importPositions tries to intelligently decide whether or not to do a full // snapshot of the fragment or just do in-memory updates while appending // operations to the op log. -func (f *fragment) importPositions(set, clear []uint64, rowSet map[uint64]struct{}) error { +func (f *fragment) importPositions(tx Tx, set, clear []uint64, rowSet map[uint64]struct{}) error { + //tx.AddN() err := f.gen.Transaction(&f.storage.OpWriter, func() error { if len(set) > 0 { f.stats.Count(MetricImportingN, int64(len(set)), 1) - changedN, err := f.storage.AddN(set...) // TODO benchmark Add/RemoveN behavior with sorted/unsorted positions + + // TODO benchmark Add/RemoveN behavior with sorted/unsorted positions + // Note: AddN() avoids writing to the op-log. While Add() does. + changedN, err := tx.Add(f.index, f.field, f.view, f.shard, !doBatched, set...) + if err != nil { return errors.Wrap(err, "adding positions") } f.stats.Count(MetricImportedN, int64(changedN), 1) - f.incrementOpN(changedN) + tx.IncrementOpN(f.index, f.field, f.view, f.shard, changedN) } if len(clear) > 0 { f.stats.Count(MetricClearingN, int64(len(clear)), 1) - changedN, err := f.storage.RemoveN(clear...) + changedN, err := tx.Remove(f.index, f.field, f.view, f.shard, clear...) if err != nil { return errors.Wrap(err, "clearing positions") } f.stats.Count(MetricClearedN, int64(changedN), 1) - f.incrementOpN(changedN) + tx.IncrementOpN(f.index, f.field, f.view, f.shard, changedN) } // Update cache counts for all affected rows. @@ -2192,7 +2244,14 @@ func (f *fragment) importPositions(set, clear []uint64, rowSet map[uint64]struct delete(f.checksums, int(rowID/HashBlockSize)) if f.CacheType != CacheTypeNone { - n := f.storage.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth) + start := rowID * ShardWidth + end := (rowID + 1) * ShardWidth + + n, err := tx.CountRange(f.index, f.field, f.view, f.shard, start, end) + if err != nil { + return errors.Wrap(err, "CountRange") + } + f.cache.BulkAdd(rowID, n) } @@ -2275,10 +2334,10 @@ func (f *fragment) bulkImportMutex(tx Tx, rowIDs, columnIDs []uint64) error { toSet := rowIDs[:i] toClear := columnIDs[:clearIdx] - return errors.Wrap(f.importPositions(toSet, toClear, rowSet), "importing positions") + return errors.Wrap(f.importPositions(tx, toSet, toClear, rowSet), "importing positions") } -func (f *fragment) importValueSmallWrite(columnIDs []uint64, values []int64, bitDepth uint, clear bool) error { +func (f *fragment) importValueSmallWrite(tx Tx, columnIDs []uint64, values []int64, bitDepth uint, clear bool) error { // TODO figure out how to avoid re-allocating these each time. Probably // possible to store them on the fragment with a capacity based on // MaxOpN. For now, we know that the total number of bits to be @@ -2310,7 +2369,7 @@ func (f *fragment) importValueSmallWrite(columnIDs []uint64, values []int64, bit for i := uint(0); i < bitDepth+1; i++ { rowSet[uint64(i)] = struct{}{} } - err := f.importPositions(toSet, toClear, rowSet) + err := f.importPositions(tx, toSet, toClear, rowSet) if err != nil { return errors.Wrap(err, "importing positions") } @@ -2332,7 +2391,7 @@ func (f *fragment) importValue(tx Tx, columnIDs []uint64, values []int64, bitDep } if len(columnIDs)*int(bitDepth+1)+f.opN < f.MaxOpN { - return errors.Wrap(f.importValueSmallWrite(columnIDs, values, bitDepth, clear), "import small write") + return errors.Wrap(f.importValueSmallWrite(tx, columnIDs, values, bitDepth, clear), "import small write") } // Process every value. @@ -2374,7 +2433,7 @@ func (f *fragment) importValue(tx Tx, columnIDs []uint64, values []int64, bitDep // importRoaring imports from the official roaring data format defined at // https://github.com/RoaringBitmap/RoaringFormatSpec or from pilosa's version // of the roaring format. The cache is updated to reflect the new data. -func (f *fragment) importRoaring(ctx context.Context, data []byte, clear bool) error { +func (f *fragment) importRoaring(ctx context.Context, tx Tx, data []byte, clear bool) error { span, ctx := tracing.StartSpanFromContext(ctx, "fragment.importRoaring") defer span.Finish() span, ctx = tracing.StartSpanFromContext(ctx, "importRoaring.AcquireFragmentLock") @@ -2382,16 +2441,22 @@ func (f *fragment) importRoaring(ctx context.Context, data []byte, clear bool) e defer f.mu.Unlock() span.Finish() - return f.unprotectedImportRoaring(ctx, data, clear) + return f.unprotectedImportRoaring(ctx, tx, data, clear) } -func (f *fragment) unprotectedImportRoaring(ctx context.Context, data []byte, clear bool) error { +func (f *fragment) unprotectedImportRoaring(ctx context.Context, tx Tx, data []byte, clear bool) error { rowSize := uint64(1 << shardVsContainerExponent) span, ctx := tracing.StartSpanFromContext(ctx, "importRoaring.ImportRoaringBits") var changed int var rowSet map[uint64]int err := f.gen.Transaction(&f.storage.OpWriter, func() (err error) { - changed, rowSet, err = f.storage.ImportRoaringBits(data, clear, true, rowSize) + var rit roaring.RoaringIterator + rit, err = roaring.NewRoaringIterator(data) + if err != nil { + return err + } + + changed, rowSet, err = tx.ImportRoaringBits(f.index, f.field, f.view, f.shard, rit, clear, true, rowSize) return err }) @@ -2428,7 +2493,9 @@ func (f *fragment) unprotectedImportRoaring(ctx context.Context, data []byte, cl } span, _ = tracing.StartSpanFromContext(ctx, "importRoaring.incrementOpN") - f.incrementOpN(changed) + + tx.IncrementOpN(f.index, f.field, f.view, f.shard, changed) + span.Finish() return nil } @@ -2444,7 +2511,7 @@ func (f *fragment) importRoaringOverwrite(ctx context.Context, tx Tx, data []byt } // Union the new block data with the fragment data. - return f.unprotectedImportRoaring(ctx, data, false) + return f.unprotectedImportRoaring(ctx, tx, data, false) } // incrementOpN increase the operation count by one. @@ -2486,7 +2553,6 @@ func (f *fragment) snapshot() (err error) { defer func() { debug.SetPanicOnFault(wouldPanic) if r := recover(); r != nil { - fmt.Printf("snapshot panic!\n") if e2, ok := r.(error); ok { err = e2 // special case: if we caught a page fault, we diagnose that directly. sadly, @@ -2869,6 +2935,7 @@ func (f *fragment) unprotectedRows(ctx context.Context, tx Tx, start uint64, fil if err != nil { return nil, err } + defer i.Close() // must close iterators allocated on a Tx rows := make([]uint64, 0) var lastRow uint64 = math.MaxUint64 @@ -3074,6 +3141,8 @@ func (f *fragment) foreachRow(tx Tx, filters []rowFilter, fn func(rid uint64) er if err != nil { return err } + defer i.Close() // must close tx allocated iterators when done. + // Loop over the existing containers. for i.Next() { key, c := i.Value() @@ -3263,7 +3332,7 @@ func (s *fragmentSyncer) syncFragment() error { for _, node := range nodes { // Read local blocks. if node.ID == s.Node.ID { - b, err := s.Fragment.Blocks() + b, err := s.Fragment.Blocks() // comes from Tx store, creates its own Tx. if err != nil { return err } @@ -3404,7 +3473,6 @@ func (s *fragmentSyncer) syncBlock(id int) error { defer span.Finish() f := s.Fragment - tx := &RoaringTx{fragment: f} // Read pairs from each remote block. var uris []*URI @@ -3423,6 +3491,7 @@ func (s *fragmentSyncer) syncBlock(id int) error { uris = append(uris, uri) // Only sync the standard block. + // Does a remote fetch rowIDs, columnIDs, err := s.Cluster.InternalClient.BlockData(ctx, &node.URI, f.index, f.field, f.view, f.shard, id) if err != nil { return errors.Wrap(err, "getting block") @@ -3439,12 +3508,24 @@ func (s *fragmentSyncer) syncBlock(id int) error { return nil } + idx := f.holder.Index(f.index) + tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx}) + defer tx.Rollback() + // Merge blocks together. sets, clears, err := f.mergeBlock(tx, id, pairSets) if err != nil { return errors.Wrap(err, "merging") } + // no safeCopy needed here. We are not leaking data outside the tx, because + // sets and clears only contain columnIDs. + + err = tx.Commit() + if err != nil { + return err + } + // Write updates to remote blocks. for i := 0; i < len(uris); i++ { set, clear := sets[i], clears[i] diff --git a/fragment_internal_test.go b/fragment_internal_test.go index 661f8430a..16fca1dfc 100644 --- a/fragment_internal_test.go +++ b/fragment_internal_test.go @@ -28,7 +28,6 @@ import ( "runtime" "runtime/debug" "sort" - "sync/atomic" "testing" "testing/quick" @@ -51,11 +50,12 @@ var ( // Ensure a fragment can set a bit and retrieve it. func TestFragment_SetBit(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set bits on the fragment. if _, err := f.setBit(tx, 120, 1); err != nil { @@ -65,6 +65,7 @@ func TestFragment_SetBit(t *testing.T) { } else if _, err := f.setBit(tx, 121, 0); err != nil { t.Fatal(err) } + // should have two containers set in the fragment. // Verify counts on rows. if n := f.mustRow(tx, 120).Count(); n != 2 { @@ -73,10 +74,18 @@ func TestFragment_SetBit(t *testing.T) { t.Fatalf("unexpected count: %d", n) } + // commit the change, and verify it is still there + panicOn(tx.Commit()) + // Close and reopen the fragment & verify the data. - if err := f.Reopen(); err != nil { + err := f.Reopen() // roaring data not being flushed? red on roaring + if err != nil { t.Fatal(err) - } else if n := f.mustRow(tx, 120).Count(); n != 2 { + } + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + + if n := f.mustRow(tx, 120).Count(); n != 2 { t.Fatalf("unexpected count (reopen): %d", n) } else if n := f.mustRow(tx, 121).Count(); n != 1 { t.Fatalf("unexpected count (reopen): %d", n) @@ -85,11 +94,13 @@ func TestFragment_SetBit(t *testing.T) { // Ensure a fragment can clear a set bit. func TestFragment_ClearBit(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set and then clear bits on the fragment. if _, err := f.setBit(tx, 1000, 1); err != nil { @@ -104,6 +115,12 @@ func TestFragment_ClearBit(t *testing.T) { if n := f.mustRow(tx, 1000).Count(); n != 1 { t.Fatalf("unexpected count: %d", n) } + // The Reopen below implies this test is looking at storage consistency. + // In that spirit, we will check that the Tx Commit is visible afterwards. + panicOn(tx.Commit()) + + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() // Close and reopen the fragment & verify the data. if err := f.Reopen(); err != nil { @@ -113,20 +130,28 @@ func TestFragment_ClearBit(t *testing.T) { } } +/* We suspect this test is no longer valid under the new Tx + framework in which we always copy mmap-ed rows before + returning them. So we will comment it out for now. + If someone knows any reason for this to stick around, + let us know; we couldn't figure out how to adapt + to do a meaningful test under Tx. - jaten / tgruben + // What about rowcache timing. func TestFragment_RowcacheMap(t *testing.T) { var done int64 - f := mustOpenFragment("i", "f", viewStandard, 0, "") - + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Under -race, this test turns out to take a fairly long time // to run with larger OpN, because we write 50,000 bits to // the bitmap, and everything is being race-detected, and we don't // actually need that many to get the result we care about. f.MaxOpN = 2000 - defer f.Clean(t) + defer f.Clean(t) // failing here with TestFragment_RowcacheMap: fragment_internal_test.go:2859: fragment /var/folders/2x/hm9gp5ys3k9gmm5f_vzm_6wc0000gn/T/pilosa-fragment-001943331: unmarshalled bitmap different: differing containers for key 0: vs ch := make(chan struct{}) @@ -136,6 +161,7 @@ func TestFragment_RowcacheMap(t *testing.T) { // force snapshot so we get a mmapped row... _ = f.Snapshot() row := f.mustRow(tx, 0) + tx.Commit(0) segment := row.Segments()[0] bitmap := segment.data @@ -159,14 +185,17 @@ func TestFragment_RowcacheMap(t *testing.T) { atomic.StoreInt64(&done, 1) <-ch } +*/ // Ensure a fragment can clear a row. func TestFragment_ClearRow(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set and then clear bits on the fragment. if _, err := f.setBit(tx, 1000, 1); err != nil { @@ -181,6 +210,9 @@ func TestFragment_ClearRow(t *testing.T) { if n := f.mustRow(tx, 1000).Count(); n != 0 { t.Fatalf("unexpected count: %d", n) } + panicOn(tx.Commit()) + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() // Close and reopen the fragment & verify the data. if err := f.Reopen(); err != nil { @@ -192,11 +224,13 @@ func TestFragment_ClearRow(t *testing.T) { // Ensure a fragment can set a row. func TestFragment_SetRow(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 7, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 7, "") + _ = idx defer f.Clean(t) // Obtain transction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() rowID := uint64(1000) @@ -224,6 +258,10 @@ func TestFragment_SetRow(t *testing.T) { t.Fatalf("expected changed value: %v", changed) } + panicOn(tx.Commit()) + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + // Verify data on row. if cols := f.mustRow(tx, rowID).Columns(); !reflect.DeepEqual(cols, []uint64{7*ShardWidth + 1, 7*ShardWidth + 65537, 7*ShardWidth + 140000}) { t.Fatalf("unexpected columns after set row: %+v", cols) @@ -233,6 +271,10 @@ func TestFragment_SetRow(t *testing.T) { t.Fatalf("unexpected count after set row: %d", n) } + panicOn(tx.Commit()) + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + // Close and reopen the fragment & verify the data. if err := f.Reopen(); err != nil { t.Fatal(err) @@ -244,11 +286,13 @@ func TestFragment_SetRow(t *testing.T) { // Ensure a fragment can set & read a value. func TestFragment_SetValue(t *testing.T) { t.Run("OK", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set value. if changed, err := f.setValue(tx, 100, 16, 3829); err != nil { @@ -272,14 +316,39 @@ func TestFragment_SetValue(t *testing.T) { } else if changed { t.Fatal("expected no change") } + + // same after Commit + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + + // Read value. + if value, exists, err := f.value(tx, 100, 16); err != nil { + t.Fatal(err) + } else if value != 3829 { + t.Fatalf("unexpected value: %d", value) + } else if !exists { + t.Fatal("expected to exist") + } + + // Setting value should return no change. + if changed, err := f.setValue(tx, 100, 16, 3829); err != nil { + t.Fatal(err) + } else if changed { + t.Fatal("expected no change") + } }) t.Run("Overwrite", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set value. if changed, err := f.setValue(tx, 100, 16, 3829); err != nil { @@ -303,14 +372,32 @@ func TestFragment_SetValue(t *testing.T) { } else if !exists { t.Fatal("expected to exist") } + + // Read value after commit. + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + + if value, exists, err := f.value(tx, 100, 16); err != nil { + t.Fatal(err) + } else if value != 2028 { + t.Fatalf("unexpected value: %d", value) + } else if !exists { + t.Fatal("expected to exist") + } + }) t.Run("Clear", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Set value. if changed, err := f.setValue(tx, 100, 16, 3829); err != nil { @@ -334,14 +421,31 @@ func TestFragment_SetValue(t *testing.T) { } else if exists { t.Fatal("expected to not exist") } + + // Same after Commit + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + + if value, exists, err := f.value(tx, 100, 16); err != nil { + t.Fatal(err) + } else if value != 0 { + t.Fatalf("unexpected value: %d", value) + } else if exists { + t.Fatal("expected to not exist") + } }) t.Run("NotExists", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set value. if changed, err := f.setValue(tx, 100, 10, 20); err != nil { @@ -358,6 +462,7 @@ func TestFragment_SetValue(t *testing.T) { } else if exists { t.Fatal("expected to not exist") } + }) t.Run("QuickCheck", func(t *testing.T) { @@ -370,11 +475,13 @@ func TestFragment_SetValue(t *testing.T) { values[i] = values[i] % (1 << bitDepth) } - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. m := make(map[uint64]int64) @@ -400,6 +507,25 @@ func TestFragment_SetValue(t *testing.T) { } } + // Same after Commit + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() + + // Ensure values are set. + for columnID, value := range m { + v, exists, err := f.value(tx, columnID, bitDepth) + if err != nil { + t.Fatal(err) + } else if value != int64(v) { + t.Fatalf("value mismatch: columnID=%d, bitdepth=%d, value: %d != %d", columnID, bitDepth, value, v) + } else if !exists { + t.Fatalf("value should exist: columnID=%d", columnID) + } + } + return true }, nil); err != nil { t.Fatal(err) @@ -411,11 +537,13 @@ func TestFragment_SetValue(t *testing.T) { func TestFragment_Sum(t *testing.T) { const bitDepth = 16 - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. vals := []struct { @@ -473,11 +601,13 @@ func TestFragment_Sum(t *testing.T) { func TestFragment_MinMax(t *testing.T) { const bitDepth = 16 - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -553,11 +683,13 @@ func TestFragment_Range(t *testing.T) { const bitDepth = 16 t.Run("EQ", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -579,11 +711,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("NEQ", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -605,11 +739,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("LT", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -656,11 +792,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("LTRegression", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME if _, err := f.setValue(tx, 1, 1, 1); err != nil { t.Fatal(err) @@ -674,11 +812,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("LTMaxRegression", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME if _, err := f.setValue(tx, 1, 2, 3); err != nil { t.Fatal(err) @@ -694,11 +834,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("GT", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -745,11 +887,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("GTMinRegression", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME if _, err := f.setValue(tx, 1, 2, 0); err != nil { t.Fatal(err) @@ -765,11 +909,13 @@ func TestFragment_Range(t *testing.T) { }) t.Run("BETWEEN", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set values. if _, err := f.setValue(tx, 1000, bitDepth, 382); err != nil { @@ -820,7 +966,8 @@ func TestFragment_Range(t *testing.T) { // of setting values. func benchmarkSetValues(b *testing.B, bitDepth uint, f *fragment, cfunc func(uint64) uint64) { // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME column := uint64(0) for i := 0; i < b.N; i++ { @@ -836,13 +983,15 @@ func BenchmarkFragment_SetValue(b *testing.B) { depths := []uint{4, 8, 16} for _, bitDepth := range depths { name := fmt.Sprintf("Depth%d", bitDepth) - f := mustOpenFragment("i", "f", viewBSIGroupPrefix+"foo", 0, "none") + f, idx := mustOpenFragment("i", "f", viewBSIGroupPrefix+"foo", 0, "none") + _ = idx b.Run(name+"_Sparse", func(b *testing.B) { benchmarkSetValues(b, bitDepth, f, func(u uint64) uint64 { return (u + 70000) & (ShardWidth - 1) }) }) f.Clean(b) - f = mustOpenFragment("i", "f", viewBSIGroupPrefix+"foo", 0, "none") + f, idx = mustOpenFragment("i", "f", viewBSIGroupPrefix+"foo", 0, "none") + _ = idx b.Run(name+"_Dense", func(b *testing.B) { benchmarkSetValues(b, bitDepth, f, func(u uint64) uint64 { return (u + 1) & (ShardWidth - 1) }) }) @@ -854,7 +1003,8 @@ func BenchmarkFragment_SetValue(b *testing.B) { // of setting values using the special setter used for imports. func benchmarkImportValues(b *testing.B, bitDepth uint, f *fragment, cfunc func(uint64) uint64) { // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME column := uint64(0) b.StopTimer() @@ -877,12 +1027,14 @@ func BenchmarkFragment_ImportValue(b *testing.B) { depths := []uint{4, 8, 16} for _, bitDepth := range depths { name := fmt.Sprintf("Depth%d", bitDepth) - f := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + f, idx := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + _ = idx b.Run(name+"_Sparse", func(b *testing.B) { benchmarkImportValues(b, bitDepth, f, func(u uint64) uint64 { return (u + 70000) & (ShardWidth - 1) }) }) f.Clean(b) - f = mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + f, idx = mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + _ = idx b.Run(name+"_Dense", func(b *testing.B) { benchmarkImportValues(b, bitDepth, f, func(u uint64) uint64 { return (u + 1) & (ShardWidth - 1) }) }) @@ -916,14 +1068,16 @@ func BenchmarkFragment_RepeatedSmallImports(b *testing.B) { updateRows[i] = uint64(rand.Int63n(int64(numRows))) // row id updateCols[i] = uint64(rand.Int63n(ShardWidth)) // column id } - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx f.MaxOpN = opN defer f.Clean(b) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME - err := f.importRoaringT(getZipfRowsSliceRoaring(uint64(numRows), 1, 0, ShardWidth), false) + err := f.importRoaringT(tx, getZipfRowsSliceRoaring(uint64(numRows), 1, 0, ShardWidth), false) if err != nil { b.Fatalf("importing base data for benchmark: %v", err) } @@ -956,18 +1110,21 @@ func BenchmarkFragment_RepeatedSmallImportsRoaring(b *testing.B) { b.StopTimer() // build the update data set all at once - this will get applied // to a fragment in numUpdates batches - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx f.MaxOpN = opN defer f.Clean(b) + tx := f.txTestingOnly + defer tx.Rollback() - err := f.importRoaringT(getZipfRowsSliceRoaring(numRows, 1, 0, ShardWidth), false) + err := f.importRoaringT(tx, getZipfRowsSliceRoaring(numRows, 1, 0, ShardWidth), false) if err != nil { b.Fatalf("importing base data for benchmark: %v", err) } for i := 0; i < numUpdates; i++ { data := getUpdataRoaring(numRows, bitsPerUpdate, int64(i)) b.StartTimer() - err := f.importRoaringT(data, false) + err := f.importRoaringT(tx, data, false) b.StopTimer() if err != nil { b.Fatalf("doing small roaring import: %v", err) @@ -1006,11 +1163,13 @@ func BenchmarkFragment_RepeatedSmallValueImports(b *testing.B) { b.Run(fmt.Sprintf("Updates%dVals%dOpN%d", numUpdates, valsPerUpdate, opN), func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - f := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + f, idx := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + _ = idx f.MaxOpN = opN // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME err := f.importValue(tx, initialCols, initialVals, 21, false) if err != nil { @@ -1038,11 +1197,13 @@ func BenchmarkFragment_RepeatedSmallValueImports(b *testing.B) { // Ensure a fragment can snapshot correctly. func TestFragment_Snapshot(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set and then clear bits on the fragment. if _, err := f.setBit(tx, 1000, 1); err != nil { @@ -1070,11 +1231,13 @@ func TestFragment_Snapshot(t *testing.T) { // Ensure a fragment can iterate over all bits in order. func TestFragment_ForEachBit(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on the fragment. if _, err := f.setBit(tx, 100, 20); err != nil { @@ -1102,11 +1265,13 @@ func TestFragment_ForEachBit(t *testing.T) { // Ensure a fragment can return the top n results. func TestFragment_Top(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on the rows 100, 101, & 102. f.mustSetBits(tx, 100, 1, 3, 200) @@ -1128,11 +1293,13 @@ func TestFragment_Top(t *testing.T) { // Ensure a fragment can filter rows when retrieving the top n rows. func TestFragment_Top_Filter(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on the rows 100, 101, & 102. f.mustSetBits(tx, 100, 1, 3, 200) @@ -1167,11 +1334,13 @@ func TestFragment_Top_Filter(t *testing.T) { // Ensure a fragment can return top rows that intersect with an input row. func TestFragment_TopN_Intersect(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Create an intersecting input row. src := NewRow(1, 2, 3) @@ -1201,11 +1370,13 @@ func TestFragment_TopN_Intersect_Large(t *testing.T) { t.Skip("short mode") } - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Create an intersecting input row. src := NewRow( @@ -1225,7 +1396,7 @@ func TestFragment_TopN_Intersect_Large(t *testing.T) { if err != nil { t.Fatalf("writing to bytes: %v", err) } - err = f.importRoaringT(b.Bytes(), false) + err = f.importRoaringT(tx, b.Bytes(), false) if err != nil { t.Fatalf("importing data: %v", err) } @@ -1252,11 +1423,13 @@ func TestFragment_TopN_Intersect_Large(t *testing.T) { // Ensure a fragment can return top rows when specified by ID. func TestFragment_TopN_IDs(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on various rows. f.mustSetBits(tx, 100, 1, 2, 3) @@ -1276,11 +1449,13 @@ func TestFragment_TopN_IDs(t *testing.T) { // Ensure a fragment return none if CacheTypeNone is set func TestFragment_TopN_NopCache(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeNone) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeNone) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on various rows. f.mustSetBits(tx, 100, 1, 2, 3) @@ -1331,7 +1506,10 @@ func TestFragment_TopN_CacheSize(t *testing.T) { defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + + tx := index.Txf.NewTx(Txo{Write: writable, Index: index, Fragment: f}) + f.txTestingOnly = tx + defer tx.Rollback() // okay to call 2x. f.Clean(t) will do Rollback() too. // Set bits on various rows. f.mustSetBits(tx, 100, 1, 2, 3) @@ -1363,11 +1541,13 @@ func TestFragment_TopN_CacheSize(t *testing.T) { // Ensure fragment can return a checksum for its blocks. func TestFragment_Checksum(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // Retrieve checksum and set bits. orig, err := f.Checksum() @@ -1379,6 +1559,7 @@ func TestFragment_Checksum(t *testing.T) { } else if _, err := f.setBit(tx, HashBlockSize*2, 200); err != nil { t.Fatal(err) } + panicOn(tx.Commit()) // Ensure new checksum is different. if chksum, err := f.Checksum(); err != nil { @@ -1390,11 +1571,12 @@ func TestFragment_Checksum(t *testing.T) { // Ensure fragment can return a checksum for a given block. func TestFragment_Blocks(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly // Retrieve initial checksum. var prev []FragmentBlock @@ -1403,7 +1585,8 @@ func TestFragment_Blocks(t *testing.T) { if _, err := f.setBit(tx, 0, 0); err != nil { t.Fatal(err) } - blocks, err := f.Blocks() + panicOn(tx.Commit()) + blocks, err := f.Blocks() // FAIL: TestFragment_Blocks b/c 0 blocks back if err != nil { t.Fatal(err) } else if blocks[0].Checksum == nil { @@ -1411,10 +1594,12 @@ func TestFragment_Blocks(t *testing.T) { } prev = blocks + tx = idx.Txf.NewTx(Txo{Write: true, Index: idx, Fragment: f}) // Set bit on different row. if _, err := f.setBit(tx, 20, 0); err != nil { t.Fatal(err) } + panicOn(tx.Commit()) blocks, err = f.Blocks() if err != nil { t.Fatal(err) @@ -1424,9 +1609,12 @@ func TestFragment_Blocks(t *testing.T) { prev = blocks // Set bit on different column. + tx = idx.Txf.NewTx(Txo{Write: true, Index: idx, Fragment: f}) + f.txTestingOnly = tx // let the Clean do the Rollback if _, err := f.setBit(tx, 20, 100); err != nil { t.Fatal(err) } + panicOn(tx.Commit()) blocks, err = f.Blocks() if err != nil { t.Fatal(err) @@ -1437,16 +1625,19 @@ func TestFragment_Blocks(t *testing.T) { // Ensure fragment returns an empty checksum if no data exists for a block. func TestFragment_Blocks_Empty(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly //&RoaringTx{fragment: f} + defer tx.Rollback() // Set bits on a different block. if _, err := f.setBit(tx, 100, 1); err != nil { t.Fatal(err) } + panicOn(tx.Commit()) // f.Blocks() will start a new Tx, so the SetBit needs to be visible before that. // Ensure checksum for block 1 is blank. if blocks, err := f.Blocks(); err != nil { @@ -1460,11 +1651,13 @@ func TestFragment_Blocks_Empty(t *testing.T) { // Ensure a fragment's cache can be persisted between restarts. func TestFragment_LRUCache_Persistence(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeLRU) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeLRU) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on the fragment. for i := uint64(0); i < 1000; i++ { @@ -1517,7 +1710,7 @@ func TestFragment_RankCache_Persistence(t *testing.T) { } // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := index.Txf.NewTx(Txo{Write: writable, Index: index, Fragment: f}) // Set bits on the fragment. for i := uint64(0); i < 1000; i++ { @@ -1551,7 +1744,8 @@ func TestFragment_RankCache_Persistence(t *testing.T) { // Ensure a fragment can be copied to another fragment. func TestFragment_WriteTo_ReadFrom(t *testing.T) { - f0 := mustOpenFragment("i", "f", viewStandard, 0, "") + f0, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f0.Clean(t) // Obtain transaction. @@ -1579,7 +1773,8 @@ func TestFragment_WriteTo_ReadFrom(t *testing.T) { } // Read into another fragment. - f1 := mustOpenFragment("i", "f", viewStandard, 0, "") + f1, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f1.Clean(t) if rn, err := f1.ReadFrom(&buf); err != nil { @@ -1632,12 +1827,14 @@ func BenchmarkFragment_Blocks(b *testing.B) { } func BenchmarkFragment_IntersectionCount(b *testing.B) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(b) f.MaxOpN = math.MaxInt32 // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Generate some intersecting data. for i := 0; i < 10000; i += 2 { @@ -1666,11 +1863,13 @@ func BenchmarkFragment_IntersectionCount(b *testing.B) { } func TestFragment_Tanimoto(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME src := NewRow(1, 2, 3) @@ -1692,11 +1891,13 @@ func TestFragment_Tanimoto(t *testing.T) { } func TestFragment_Zero_Tanimoto(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME src := NewRow(1, 2, 3) @@ -1720,11 +1921,13 @@ func TestFragment_Zero_Tanimoto(t *testing.T) { } func TestFragment_Snapshot_Run(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set bits on the fragment. for i := uint64(1); i < 3; i++ { @@ -1750,11 +1953,13 @@ func TestFragment_Snapshot_Run(t *testing.T) { // Ensure a fragment can set mutually exclusive values. func TestFragment_SetMutex(t *testing.T) { - f := mustOpenMutexFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenMutexFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME var cols []uint64 @@ -1867,11 +2072,13 @@ func TestFragment_ImportSet(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("importset%d", i), func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set import. err := f.bulkImport(tx, test.setRowIDs, test.setColIDs, &ImportOptions{}) @@ -1906,11 +2113,13 @@ func TestFragment_ImportSet(t *testing.T) { func TestFragment_ConcurrentImport(t *testing.T) { t.Run("bulkImportStandard", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME eg := errgroup.Group{} eg.Go(func() error { return f.bulkImportStandard(tx, []uint64{1, 2}, []uint64{1, 2}, &ImportOptions{}) }) @@ -2006,11 +2215,13 @@ func TestFragment_ImportMutex(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("importmutex%d", i), func(t *testing.T) { - f := mustOpenMutexFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenMutexFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set import. err := f.bulkImport(tx, test.setRowIDs, test.setColIDs, &ImportOptions{}) @@ -2128,11 +2339,13 @@ func TestFragment_ImportBool(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("importmutex%d", i), func(t *testing.T) { - f := mustOpenBoolFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenBoolFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Set import. err := f.bulkImport(tx, test.setRowIDs, test.setColIDs, &ImportOptions{}) @@ -2191,11 +2404,13 @@ func BenchmarkFragment_Snapshot(b *testing.B) { } func BenchmarkFragment_FullSnapshot(b *testing.B) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(b) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // Generate some intersecting data. maxX := ShardWidth / 2 @@ -2258,9 +2473,10 @@ func BenchmarkFragment_Import(b *testing.B) { // since bulkImport modifies the input slices, we make new copies for each round copy(rowsUse, rows) copy(colsUse, cols) - f := mustOpenFragment("i", "f", viewStandard, 0, "") - // Obtain transaction. - tx := &RoaringTx{fragment: f} + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx // Obtain transaction. + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME b.StartTimer() if err := f.bulkImport(tx, rowsUse, colsUse, options); err != nil { b.Errorf("Error Building Sample: %s", err) @@ -2285,9 +2501,12 @@ func BenchmarkImportRoaring(b *testing.B) { b.Run(fmt.Sprintf("Rows%dCache_%s", numRows, cacheType), func(b *testing.B) { b.StopTimer() for i := 0; i < b.N; i++ { - f := mustOpenFragment("i", fmt.Sprintf("r%dc%s", numRows, cacheType), viewStandard, 0, cacheType) + f, _ := mustOpenFragment("i", fmt.Sprintf("r%dc%s", numRows, cacheType), viewStandard, 0, cacheType) b.StartTimer() - err := f.importRoaringT(data, false) + tx := f.txTestingOnly + defer tx.Rollback() + + err := f.importRoaringT(tx, data, false) if err != nil { // we don't actually particularly // care whether this succeeds, @@ -2324,14 +2543,17 @@ func BenchmarkImportRoaringConcurrent(b *testing.B) { frags := make([]*fragment, concurrency) for i := 0; i < b.N; i++ { for j := 0; j < concurrency; j++ { - frags[j] = mustOpenFragment("i", "f", viewStandard, uint64(j), cacheType) + frags[j], _ = mustOpenFragment("i", "f", viewStandard, uint64(j), cacheType) } eg := errgroup.Group{} b.StartTimer() for j := 0; j < concurrency; j++ { j := j eg.Go(func() error { - err := frags[j].importRoaringT(data[j], false) + tx := frags[j].txTestingOnly + defer tx.Rollback() + + err := frags[j].importRoaringT(tx, data[j], false) // error unimportant if it happened, but we want // any snapshots to have finished. _ = defaultSnapshotQueue.Await(frags[j]) @@ -2367,7 +2589,7 @@ func BenchmarkImportRoaringUpdateConcurrent(b *testing.B) { frags := make([]*fragment, concurrency) for i := 0; i < b.N; i++ { for j := 0; j < concurrency; j++ { - frags[j] = mustOpenFragment("i", "f", viewStandard, uint64(j), cacheType) + frags[j], _ = mustOpenFragment("i", "f", viewStandard, uint64(j), cacheType) // the cost of actually doing the op log for the large initial data set // is excessive. force storage into snapshotted state, then use import @@ -2386,7 +2608,10 @@ func BenchmarkImportRoaringUpdateConcurrent(b *testing.B) { for j := 0; j < concurrency; j++ { j := j eg.Go(func() error { - err := frags[j].importRoaringT(updata, false) + tx := frags[j].txTestingOnly + defer tx.Rollback() + + err := frags[j].importRoaringT(tx, updata, false) err2 := defaultSnapshotQueue.Await(frags[j]) if err == nil { err = err2 @@ -2420,10 +2645,11 @@ func BenchmarkImportStandard(b *testing.B) { for i := 0; i < b.N; i++ { copy(rowIDs, rowIDsOrig) copy(columnIDs, columnIDsOrig) - f := mustOpenFragment("i", fmt.Sprintf("r%dc%s", numRows, cacheType), viewStandard, 0, cacheType) - + f, idx := mustOpenFragment("i", fmt.Sprintf("r%dc%s", numRows, cacheType), viewStandard, 0, cacheType) + _ = idx // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME b.StartTimer() err := f.bulkImport(tx, rowIDs, columnIDs, &ImportOptions{}) @@ -2451,12 +2677,17 @@ func BenchmarkImportRoaringUpdate(b *testing.B) { b.Run(name, func(b *testing.B) { b.StopTimer() for i := 0; i < b.N; i++ { - f := mustOpenFragment("i", fmt.Sprintf("r%dc%dcache_%s", numRows, numCols, cacheType), viewStandard, 0, cacheType) + f, idx := mustOpenFragment("i", fmt.Sprintf("r%dc%dcache_%s", numRows, numCols, cacheType), viewStandard, 0, cacheType) + _ = idx + tx := f.txTestingOnly + defer tx.Rollback() // the cost of actually doing the op log for the large initial data set // is excessive. force storage into snapshotted state, then use import // to generate an op log and/or snapshot. - _, _, err := f.storage.ImportRoaringBits(data, false, false, 0) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + _, _, err = tx.ImportRoaringBits(f.index, f.field, f.view, f.shard, itr, false, false, 0) if err != nil { b.Errorf("import error: %v", err) } @@ -2465,7 +2696,7 @@ func BenchmarkImportRoaringUpdate(b *testing.B) { b.Errorf("snapshot after import error: %v", err) } b.StartTimer() - err = f.importRoaringT(updata, false) + err = f.importRoaringT(tx, updata, false) if err != nil { f.Clean(b) b.Errorf("import error: %v", err) @@ -2512,13 +2743,16 @@ func BenchmarkUpdatePathological(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { b.StopTimer() - f := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) - err := f.importRoaringT(exists, false) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) + _ = idx + tx := f.txTestingOnly + defer tx.Rollback() + err := f.importRoaringT(tx, exists, false) if err != nil { b.Fatalf("importing roaring: %v", err) } b.StartTimer() - err = f.importRoaringT(inc, false) + err = f.importRoaringT(tx, inc, false) if err != nil { b.Fatalf("importing second: %v", err) } @@ -2531,11 +2765,14 @@ var bigFrag string func initBigFrag() { if bigFrag == "" { - f := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) + _ = idx + tx := f.txTestingOnly + defer tx.Rollback() for i := int64(0); i < 10; i++ { // 10 million rows, 1 bit per column, random seeded by i data := getZipfRowsSliceRoaring(10000000, i, 0, ShardWidth) - err := f.importRoaringT(data, false) + err := f.importRoaringT(tx, data, false) if err != nil { panic(fmt.Sprintf("setting up fragment data: %v", err)) } @@ -2545,6 +2782,7 @@ func initBigFrag() { panic(fmt.Sprintf("closing fragment: %v", err)) } bigFrag = f.path + panicOn(tx.Commit()) } } @@ -2610,13 +2848,24 @@ func BenchmarkImportRoaringIntoLargeFragment(b *testing.B) { } origF.Close() fi.Close() - nf := newFragment(NewHolder(DefaultPartitionN), fi.Name(), "i", "f", viewStandard, 0, 0) + + // want to do this, but no path argument. + //nf, idx := mustOpenFragmentFlags(index, field, view string, shard uint64, cacheType string, flags byte) + + th := newTestHolder() + idx := fragTestMustOpenIndex("i", th, IndexOptions{}) + nf := newFragment(th, fi.Name(), "i", "f", viewStandard, 0, 0) + + tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: nf}) + defer tx.Rollback() + + //nf := newFragment(NewHolder(DefaultPartitionN), fi.Name(), "i", "f", viewStandard, 0, 0) err = nf.Open() if err != nil { b.Fatalf("opening fragment: %v", err) } b.StartTimer() - err = nf.importRoaringT(updata, false) + err = nf.importRoaringT(tx, updata, false) b.StopTimer() if err != nil { b.Fatalf("bulkImport: %v", err) @@ -2627,13 +2876,15 @@ func BenchmarkImportRoaringIntoLargeFragment(b *testing.B) { } func TestGetZipfRowsSliceRoaring(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, DefaultCacheType) + _ = idx // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() data := getZipfRowsSliceRoaring(10, 1, 0, ShardWidth) - err := f.importRoaringT(data, false) + err := f.importRoaringT(tx, data, false) if err != nil { t.Fatalf("importing roaring: %v", err) } @@ -2813,6 +3064,9 @@ func (f *fragment) Clean(t testing.TB) { } } }() + if f.txTestingOnly != nil { + f.txTestingOnly.Rollback() + } errc := f.Close() // prevent double-closes of generation during testing. f.gen = nil @@ -2828,8 +3082,9 @@ func (f *fragment) Clean(t testing.TB) { } // importRoaringT calls importRoaring with context.Background() for convenience -func (f *fragment) importRoaringT(data []byte, clear bool) error { - return f.importRoaring(context.Background(), data, clear) +func (f *fragment) importRoaringT(tx Tx, data []byte, clear bool) error { + + return f.importRoaring(context.Background(), tx, data, clear) } // CleanKeep is just like Clean(), but it doesn't remove the @@ -2847,11 +3102,11 @@ func (f *fragment) CleanKeep(t testing.TB) { } // mustOpenFragment returns a new instance of Fragment with a temporary path. -func mustOpenFragment(index, field, view string, shard uint64, cacheType string) *fragment { +func mustOpenFragment(index, field, view string, shard uint64, cacheType string) (*fragment, *Index) { return mustOpenFragmentFlags(index, field, view, shard, cacheType, 0) } -func mustOpenBSIFragment(index, field, view string, shard uint64) *fragment { +func mustOpenBSIFragment(index, field, view string, shard uint64) (*fragment, *Index) { return mustOpenFragmentFlags(index, field, view, shard, "", 1) } @@ -2861,8 +3116,33 @@ func init() { testHolder.SnapshotQueue = newSnapshotQueue(1, 1, nil) } +func newTestHolder() *Holder { + h := NewHolder(DefaultPartitionN) + h.SnapshotQueue = newSnapshotQueue(1, 1, nil) + return h +} + +// fragTestMustOpenIndex returns a new, opened index at a temporary path. Panic on error. +func fragTestMustOpenIndex(index string, holder *Holder, opt IndexOptions) *Index { + path, err := ioutil.TempDir(*TempDir, "pilosa-index-") + if err != nil { + panic(err) + } + holder.Path = path + idx, err := holder.createIndex(index, opt) + panicOn(err) + + idx.keys = opt.Keys + idx.trackExistence = opt.TrackExistence + + if err := idx.Open(); err != nil { + panic(err) + } + return idx +} + // mustOpenFragment returns a new instance of Fragment with a temporary path. -func mustOpenFragmentFlags(index, field, view string, shard uint64, cacheType string, flags byte) *fragment { +func mustOpenFragmentFlags(index, field, view string, shard uint64, cacheType string, flags byte) (*fragment, *Index) { file, err := ioutil.TempFile(*TempDir, "pilosa-fragment-") if err != nil { panic(err) @@ -2873,7 +3153,15 @@ func mustOpenFragmentFlags(index, field, view string, shard uint64, cacheType st cacheType = DefaultCacheType } - f := newFragment(testHolder, file.Name(), index, field, view, shard, flags) + // new: + th := newTestHolder() + idx := fragTestMustOpenIndex(index, th, IndexOptions{}) + f := newFragment(th, file.Name(), index, field, view, shard, flags) + + tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + f.txTestingOnly = tx + + //old: f := newFragment(testHolder, file.Name(), index, field, view, shard, flags) f.CacheType = cacheType f.RowAttrStore = &memAttrStore{ @@ -2883,21 +3171,21 @@ func mustOpenFragmentFlags(index, field, view string, shard uint64, cacheType st if err := f.Open(); err != nil { panic(err) } - return f + return f, idx } // mustOpenMutexFragment returns a new instance of Fragment for a mutex field. -func mustOpenMutexFragment(index, field, view string, shard uint64, cacheType string) *fragment { - frag := mustOpenFragment(index, field, view, shard, cacheType) +func mustOpenMutexFragment(index, field, view string, shard uint64, cacheType string) (*fragment, *Index) { + frag, idx := mustOpenFragment(index, field, view, shard, cacheType) frag.mutexVector = newRowsVector(frag) - return frag + return frag, idx } // mustOpenBoolFragment returns a new instance of Fragment for a bool field. -func mustOpenBoolFragment(index, field, view string, shard uint64, cacheType string) *fragment { - frag := mustOpenFragment(index, field, view, shard, cacheType) +func mustOpenBoolFragment(index, field, view string, shard uint64, cacheType string) (*fragment, *Index) { + frag, idx := mustOpenFragment(index, field, view, shard, cacheType) frag.mutexVector = newBoolVector(frag) - return frag + return frag, idx } // Reopen closes the fragment and reopens it as a new instance. @@ -2932,9 +3220,11 @@ func addToBitmap(bm *roaring.Bitmap, rowID uint64, columnIDs ...uint64) { // Test Various methods of retrieving RowIDs func TestFragment_RowsIteration(t *testing.T) { t.Run("firstContainer", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME expectedAll := make([]uint64, 0) expectedOdd := make([]uint64, 0) @@ -2964,9 +3254,11 @@ func TestFragment_RowsIteration(t *testing.T) { }) t.Run("secondRow", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME expected := []uint64{1, 2} if _, err := f.setBit(tx, 1, 66000); err != nil { @@ -2993,9 +3285,11 @@ func TestFragment_RowsIteration(t *testing.T) { }) t.Run("combinations", func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME expectedRows := make([]uint64, 0) for r := uint64(1); r < uint64(10000); r += 250 { @@ -3045,9 +3339,11 @@ func TestFragment_RoaringImport(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("importroaring%d", i), func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() for num, input := range test { buf := &bytes.Buffer{} @@ -3056,7 +3352,7 @@ func TestFragment_RoaringImport(t *testing.T) { if err != nil { t.Fatalf("writing to buffer: %v", err) } - err = f.importRoaringT(buf.Bytes(), false) + err = f.importRoaringT(tx, buf.Bytes(), false) if err != nil { t.Fatalf("importing roaring: %v", err) } @@ -3093,9 +3389,12 @@ func TestFragment_RoaringImportTopN(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("importroaring%d", i), func(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + + tx := f.txTestingOnly + defer tx.Rollback() options := &ImportOptions{} err := f.bulkImport(tx, test.rowIDs, test.colIDs, options) @@ -3132,7 +3431,7 @@ func TestFragment_RoaringImportTopN(t *testing.T) { if err != nil { t.Fatalf("writing to buffer: %v", err) } - err = f.importRoaringT(buf.Bytes(), false) + err = f.importRoaringT(tx, buf.Bytes(), false) if err != nil { t.Fatalf("importing roaring: %v", err) } @@ -3231,9 +3530,11 @@ func calcExpected(inputs ...[]uint64) [][]uint64 { func TestFragmentRowIterator(t *testing.T) { t.Run("basic", func(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME f.mustSetBits(tx, 0, 0) f.mustSetBits(tx, 1, 0) @@ -3275,9 +3576,11 @@ func TestFragmentRowIterator(t *testing.T) { }) t.Run("skipped rows", func(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME f.mustSetBits(tx, 1, 0) f.mustSetBits(tx, 3, 0) @@ -3319,9 +3622,11 @@ func TestFragmentRowIterator(t *testing.T) { }) t.Run("basic wrapped", func(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME f.mustSetBits(tx, 0, 0) f.mustSetBits(tx, 1, 0) @@ -3352,9 +3657,11 @@ func TestFragmentRowIterator(t *testing.T) { }) t.Run("skipped rows wrapped", func(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeRanked) + _ = idx defer f.Clean(t) - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME f.mustSetBits(tx, 1, 0) f.mustSetBits(tx, 3, 0) @@ -3386,10 +3693,11 @@ func TestFragmentRowIterator(t *testing.T) { } func TestUnionInPlaceMapped(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) // note: clean has to be deferred first, because it has to run with // the lock *not* held, because it is sometimes so it has to grab the // lock... + _ = idx defer f.Clean(t) f.mu.Lock() @@ -3461,7 +3769,8 @@ func randPositions(n int, r *rand.Rand) []uint64 { } func TestFragmentPositionsForValue(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) + _ = idx defer f.Clean(t) tests := []struct { @@ -3543,11 +3852,13 @@ func TestFragmentPositionsForValue(t *testing.T) { } func TestIntLTRegression(t *testing.T) { - f := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) + f, idx := mustOpenFragment("i", "f", "v", 0, CacheTypeNone) + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME _, err := f.setValue(tx, 1, 6, 33) if err != nil { @@ -3611,11 +3922,13 @@ func TestImportClearRestart(t *testing.T) { } } - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx f.MaxOpN = maxOpN // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() err := f.bulkImport(tx, testrows, testcols, &ImportOptions{}) if err != nil { @@ -3624,14 +3937,17 @@ func TestImportClearRestart(t *testing.T) { if expOpN <= maxOpN && f.opN != expOpN { t.Errorf("unexpected opN - %d is not %d", f.opN, expOpN) } - check(t, f, exp) + check(t, tx, f, exp) err = f.Close() if err != nil { t.Fatalf("closing fragment: %v", err) } + panicOn(tx.Commit()) err = f.Open() + tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() if err != nil { t.Fatalf("reopening fragment: %v", err) } @@ -3640,16 +3956,20 @@ func TestImportClearRestart(t *testing.T) { t.Errorf("unexpected opN after close/open %d is not %d", f.opN, expOpN) } - check(t, f, exp) + check(t, tx, f, exp) f2 := newFragment(NewHolder(DefaultPartitionN), f.path, "i", "f", viewStandard, 0, 0) f2.MaxOpN = maxOpN f2.CacheType = f.CacheType + tx2 := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f2}) + defer tx2.Rollback() + err = f.closeStorage() if err != nil { t.Fatalf("closing storage: %v", err) } + panicOn(tx.Commit()) // match the f.closeStorage which overlaps the f2 creation. err = f2.Open() if err != nil { @@ -3660,11 +3980,11 @@ func TestImportClearRestart(t *testing.T) { t.Errorf("unexpected opN after close/open %d is not %d", f2.opN, expOpN) } - check(t, f2, exp) + check(t, tx2, f2, exp) copy(testrows, test.rows) copy(testcols, test.cols) - err = f2.bulkImport(tx, testrows, testcols, &ImportOptions{Clear: true}) + err = f2.bulkImport(tx2, testrows, testcols, &ImportOptions{Clear: true}) if err != nil { t.Fatalf("clearing imported data: %v", err) } @@ -3674,24 +3994,29 @@ func TestImportClearRestart(t *testing.T) { exp[row] = nil } - check(t, f2, exp) + check(t, tx2, f2, exp) f3 := newFragment(NewHolder(DefaultPartitionN), f2.path, "i", "f", viewStandard, 0, 0) f3.MaxOpN = maxOpN f3.CacheType = f.CacheType + tx3 := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f3}) + defer tx3.Rollback() + err = f2.closeStorage() if err != nil { t.Fatalf("f2 closing storage: %v", err) } + panicOn(tx2.Commit()) err = f3.Open() if err != nil { + // TODO(jea): might be a flaky test? when run from make test t.Fatalf("opening f3: %v", err) } defer f3.Clean(t) - check(t, f3, exp) + check(t, tx3, f3, exp) }) @@ -3699,8 +4024,7 @@ func TestImportClearRestart(t *testing.T) { } } -func check(t *testing.T, f *fragment, exp map[uint64]map[uint64]struct{}) { - tx := &RoaringTx{fragment: f} +func check(t *testing.T, tx Tx, f *fragment, exp map[uint64]map[uint64]struct{}) { for rowID, colsExp := range exp { colsAct := f.mustRow(tx, rowID).Columns() @@ -3729,13 +4053,29 @@ func check(t *testing.T, f *fragment, exp map[uint64]map[uint64]struct{}) { } func TestImportValueConcurrent(t *testing.T) { - f := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + + 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.")) + } + + // Since eg.Go gets called multiple times below, each + // time needs its own Tx. So close the default one and + // make a new one each time. + tx := f.txTestingOnly + tx.Rollback() + defer f.Clean(t) eg := &errgroup.Group{} for i := 0; i < 4; i++ { i := i eg.Go(func() error { - tx := &RoaringTx{fragment: f} + tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f}) + defer tx.Rollback() for j := uint64(0); j < 10; j++ { err := f.importValue(tx, []uint64{j}, []int64{int64(rand.Int63n(1000))}, 10, i%2 == 0) if err != nil { @@ -3771,12 +4111,14 @@ func TestImportMultipleValues(t *testing.T) { for i, test := range tests { for _, maxOpN := range []int{0, 10000} { // test small/large write t.Run(fmt.Sprintf("%dLowOpN", i), func(t *testing.T) { - f := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + f, idx := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + _ = idx f.MaxOpN = maxOpN defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME err := f.importValue(tx, test.cols, test.vals, test.depth, false) if err != nil { @@ -3832,12 +4174,14 @@ func TestImportValueRowCache(t *testing.T) { for i, test := range tests { for _, maxOpN := range []int{1, 10000} { t.Run(fmt.Sprintf("%dMaxOpN%d", i, maxOpN), func(t *testing.T) { - f := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) + f, idx := mustOpenBSIFragment("i", "f", viewBSIGroupPrefix+"foo", 0) f.MaxOpN = maxOpN + _ = idx defer f.Clean(t) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // First import (tc1) if err := f.importValue(tx, test.tc1.cols, test.tc1.vals, test.tc1.depth, false); err != nil { @@ -3866,13 +4210,15 @@ func TestImportValueRowCache(t *testing.T) { } func TestFragmentConcurrentReadWrite(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + f, idx := mustOpenFragment("i", "f", viewStandard, 0, CacheTypeRanked) + _ = idx defer f.Clean(t) eg := &errgroup.Group{} eg.Go(func() error { // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME for i := uint64(0); i < 1000; i++ { _, err := f.setBit(tx, i%4, i) @@ -3884,7 +4230,8 @@ func TestFragmentConcurrentReadWrite(t *testing.T) { }) // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME acc := uint64(0) for i := uint64(0); i < 100; i++ { @@ -3899,7 +4246,9 @@ func TestFragmentConcurrentReadWrite(t *testing.T) { } func TestRemapCache(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx + // request a panic that doesn't kill the program on fault wouldFault := debug.SetPanicOnFault(true) defer func() { @@ -3917,7 +4266,8 @@ func TestRemapCache(t *testing.T) { }() // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() //LOOKATME // create a container _, err := f.storage.Add(65537) @@ -3957,10 +4307,11 @@ func TestRemapCache(t *testing.T) { } func TestFragment_Bug_Q2DoubleDelete(t *testing.T) { - f := mustOpenFragment("i", "f", viewStandard, 0, "") - + f, idx := mustOpenFragment("i", "f", viewStandard, 0, "") + _ = idx // Obtain transaction. - tx := &RoaringTx{fragment: f} + tx := f.txTestingOnly + defer tx.Rollback() // byShardWidth is a map of the same roaring (fragment) data generated // with different shard widths. @@ -3976,11 +4327,14 @@ func TestFragment_Bug_Q2DoubleDelete(t *testing.T) { b = data } + _ = idx defer f.Clean(t) - err := f.importRoaringT(b, false) + + err := f.importRoaringT(tx, b, false) if err != nil { t.Fatalf("importing roaring: %v", err) } + //check the bit res := f.mustRow(tx, 1).Columns() if len(res) < 1 || f.mustRow(tx, 1).Columns()[0] != 1 { @@ -3991,25 +4345,30 @@ func TestFragment_Bug_Q2DoubleDelete(t *testing.T) { if !changed { t.Fatalf("expected change got %v", changed) } + //check missing res = f.mustRow(tx, 1).Columns() if len(res) != 0 { t.Fatalf("expected nothing got %v", res) } + // import again - err = f.importRoaringT(b, false) + err = f.importRoaringT(tx, b, false) if err != nil { t.Fatalf("importing roaring: %v", err) } + //check res = f.mustRow(tx, 1).Columns() if len(res) < 1 || f.mustRow(tx, 1).Columns()[0] != 1 { t.Fatalf("again expecting 1 got: %v", res) } + changed, _ = f.clearBit(tx, 1, 1) if !changed { - t.Fatalf("again expected change got %v", changed) + t.Fatalf("again expected change got %v", changed) // again expected change got false } + //check missing res = f.mustRow(tx, 1).Columns() if len(res) != 0 { diff --git a/go.mod b/go.mod index 485e2a5be..acc83bb52 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,12 @@ require ( github.com/benbjohnson/immutable v0.2.0 github.com/boltdb/bolt v1.3.1 github.com/cespare/xxhash v1.1.0 - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/davecgh/go-spew v1.1.1 + github.com/dchest/blake2b v1.0.0 // indirect + github.com/dgraph-io/badger v1.6.1-0.20191025180844-32a2548a9d85 // indirect + github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 github.com/go-ole/go-ole v1.2.4 // indirect github.com/gogo/protobuf v1.2.0 github.com/golang/protobuf v1.3.3 @@ -31,17 +34,17 @@ require ( github.com/satori/go.uuid v1.2.0 github.com/shirou/gopsutil v2.18.12+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect - github.com/spf13/cobra v0.0.3 + github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 - github.com/spf13/viper v1.3.1 + github.com/spf13/viper v1.3.2 github.com/uber-go/atomic v1.4.0 // indirect github.com/uber/jaeger-client-go v2.16.0+incompatible github.com/uber/jaeger-lib v2.2.0+incompatible // indirect + github.com/willoch/tago v0.0.0-20180311150625-8f2f8e8900dc // indirect + github.com/zeebo/blake3 v0.0.4 go.uber.org/atomic v1.4.0 // indirect golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 // indirect - golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect golang.org/x/sync v0.0.0-20190423024810-112230192c58 - golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 // indirect golang.org/x/text v0.3.2 // indirect google.golang.org/grpc v1.28.0 modernc.org/mathutil v1.0.0 diff --git a/go.sum b/go.sum index 997603c23..fbed4774f 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d h1:n0G4ckjMEj7bWu github.com/CAFxX/gcnotifier v0.0.0-20190112062741-224a280d589d/go.mod h1:Rn2zM2MnHze07LwkneP48TWt6UiZhzQTwCvw6djVGfE= github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895 h1:dmc/C8bpE5VkQn65PNbbyACDC8xw8Hpp/NEurdPmQDQ= github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= +github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= @@ -24,6 +26,7 @@ github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx2 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -33,9 +36,29 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/blake2b v1.0.0 h1:KK9LimVmE0MjRl9095XJmKqZ+iLxWATvlcpVFRtaw6s= +github.com/dchest/blake2b v1.0.0/go.mod h1:U034kXgbJpCle2wSk5ybGIVhOSHCVLMDqOzcPEA0F7s= +github.com/dgraph-io/badger v1.6.1-0.20191025180844-32a2548a9d85 h1:oEqDRoxpep5ZlTxrAFc2yg+f0uBdUtkZE0uWsOru5bc= +github.com/dgraph-io/badger v1.6.1-0.20191025180844-32a2548a9d85/go.mod h1:cEjdIw+iaGXuQdsDymXPRcpp8yHXZ6PmwmDJajnVyJc= +github.com/dgraph-io/badger v1.6.1 h1:w9pSFNSdq/JPM1N12Fz/F/bzo993Is1W+Q7HjPzi7yg= +github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 h1:JBNM90aGLCiF9iJYvpvayMpYeW498v5ZDZqE2chqZ2A= +github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= +github.com/dgraph-io/badger/v2 v2.0.3 h1:inzdf6VF/NZ+tJ8RwwYMjJMvsOALTHYdozn0qSl6XJI= +github.com/dgraph-io/badger/v2 v2.0.3/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM= +github.com/dgraph-io/ristretto v0.0.0-20191010170704-2ba187ef9534/go.mod h1:edzKIzGvqUCMzhTVWbiTSe75zD9Xxq0GtSBtFmaUTZs= +github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3 h1:MQLRM35Pp0yAyBYksjbj1nZI/w6eyRY/mWoM1sFf4kU= +github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -61,6 +84,8 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= @@ -90,12 +115,16 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/molecula/apophenia v0.0.0-20190827192002-68b7a14a478b h1:cZADDaNYM7xn/nklO3g198JerGQjadFuA0ofxBJgK0Y= @@ -139,6 +168,7 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= @@ -150,21 +180,30 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/uber-go/atomic v1.4.0 h1:yOuPqEq4ovnhEjpHmfFwsqBXDYbQeT6Nb0bwD6XnD5o= github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY= @@ -172,7 +211,16 @@ github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMW github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/GfSYVCjK7dyaw= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/willoch/tago v0.0.0-20180311150625-8f2f8e8900dc h1:Jsemerl8qK30jGNdYlxGZpZk9RjB4pqvezJgxqUgy30= +github.com/willoch/tago v0.0.0-20180311150625-8f2f8e8900dc/go.mod h1:9WHA/f8A/TRK+WQQZhqx47In4pnIhMTH6UrsgqqgsVQ= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/zeebo/assert v0.0.0-20181109011804-10f827ce2ed6/go.mod h1:yssERNPivllc1yU3BvpjYI5BUW+zglcz6QWqeVRL5t0= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.0.4-0.20200428182842-252974700486 h1:yh0zEy8it58x/IPNtKKuvKUkxSIaq4s5XiRSd40JuYs= +github.com/zeebo/blake3 v0.0.4-0.20200428182842-252974700486/go.mod h1:YOZo8A49yNqM0X/Y+JmDUZshJWLt1laHsNSn5ny2i34= +github.com/zeebo/blake3 v0.0.4 h1:vtZ4X8B2lKXZFg2Xyg6Wo36mvmnJvc2VQYTtA4RDCkI= +github.com/zeebo/blake3 v0.0.4/go.mod h1:YOZo8A49yNqM0X/Y+JmDUZshJWLt1laHsNSn5ny2i34= +github.com/zeebo/pcg v0.0.0-20181207190024-3cdc6b625a05/go.mod h1:Gr+78ptB0MwXxm//LBaEvBiaXY7hXJ6KGe2V32X2F6E= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -197,6 +245,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -215,6 +265,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -238,6 +292,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/holder.go b/holder.go index 30fb7ede0..58dca7f2c 100644 --- a/holder.go +++ b/holder.go @@ -115,6 +115,10 @@ type HolderOpts struct { // If Inspect is set, we'll try to obtain additional information // about fragments when opening them. Inspect bool + + // Txsrc controls the tx/storage engine we instatiate. Set by + // server.go OptServerTxsrc + Txsrc string } func (h *Holder) StartTransaction(ctx context.Context, id string, timeout time.Duration, exclusive bool) (*Transaction, error) { @@ -507,6 +511,10 @@ func (h *Holder) Open() error { if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") { continue } + // Skip badgerdb files too. + if strings.HasSuffix(fi.Name(), "badgerdb") { + continue + } h.Logger.Printf("opening index: %s", filepath.Base(fi.Name())) @@ -614,8 +622,8 @@ func (h *Holder) Close() error { } // Begin starts a transaction on the holder. -func (h *Holder) Begin(writable bool) (Tx, error) { - return NewMultiTx(writable, h), nil +func (h *Holder) BeginTx(writable bool, index *Index) (Tx, error) { + return index.Txf.NewTx(Txo{Write: writable, Index: index}), nil } // HasData returns true if Holder contains at least one index. @@ -908,6 +916,11 @@ func (h *Holder) DeleteIndex(name string) error { return errors.Wrap(err, "closing") } + // remove any backing store. + if err := index.Txf.DeleteIndex(name); err != nil { + return errors.Wrap(err, "index.Txf.DeleteIndex") + } + // Delete index directory. if err := os.RemoveAll(h.IndexPath(name)); err != nil { return errors.Wrap(err, "removing directory") diff --git a/holder_internal_test.go b/holder_internal_test.go index 28a18fbe8..008770fbc 100644 --- a/holder_internal_test.go +++ b/holder_internal_test.go @@ -79,21 +79,22 @@ func makeHolder() (*Holder, string, error) { } h := NewHolder(DefaultPartitionN) h.Path = path - - return h, h.Path, nil + return h, path, nil } func testSetBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) { - tx, err := h.Begin(true) - if err != nil { - t.Fatal(err) - } - defer func() { _ = tx.Rollback() }() idx, err := h.CreateIndexIfNotExists(index, IndexOptions{}) if err != nil { t.Fatalf("creating index: %v", err) } + + tx, err := h.BeginTx(writable, idx) + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + f, err := idx.CreateFieldIfNotExists(field, OptFieldTypeDefault()) if err != nil { t.Fatalf("setting bit: %v", err) diff --git a/holder_test.go b/holder_test.go index 45df98852..a527307ba 100644 --- a/holder_test.go +++ b/holder_test.go @@ -122,9 +122,14 @@ func TestHolder_Open(t *testing.T) { h := test.MustOpenHolder() defer h.Close() - if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { + var idx *pilosa.Index + var err error + + if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { t.Fatal(err) - } else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { + } + + if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if err := h.Holder.Close(); err != nil { t.Fatal(err) @@ -140,9 +145,13 @@ func TestHolder_Open(t *testing.T) { h := test.MustOpenHolder() defer h.Close() - if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { + var idx *pilosa.Index + var err error + if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { t.Fatal(err) - } else if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { + } + + if _, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if err := h.Holder.Close(); err != nil { t.Fatal(err) @@ -162,15 +171,18 @@ func TestHolder_Open(t *testing.T) { h := test.MustOpenHolder() defer h.Close() - tx, err := h.Begin(true) + var idx *pilosa.Index + var err error + if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { + t.Fatal(err) + } + tx, err := h.BeginTx(writable, idx) if err != nil { t.Fatal(err) } - defer func() { _ = tx.Rollback() }() + defer tx.Rollback() - if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { - t.Fatal(err) - } else if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { + if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if _, err := field.SetBit(tx, 0, 0, nil); err != nil { t.Fatal(err) @@ -192,15 +204,19 @@ func TestHolder_Open(t *testing.T) { h := test.MustOpenHolder() defer h.Close() - tx, err := h.Begin(true) + var idx *pilosa.Index + var err error + if idx, err = h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { + t.Fatal(err) + } + + tx, err := h.BeginTx(writable, idx) if err != nil { t.Fatal(err) } - defer func() { _ = tx.Rollback() }() + defer tx.Rollback() - if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { - t.Fatal(err) - } else if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { + if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if _, err := field.SetBit(tx, 0, 0, nil); err != nil { t.Fatal(err) @@ -220,15 +236,17 @@ func TestHolder_Open(t *testing.T) { h := test.MustOpenHolder() defer h.Close() - tx, err := h.Begin(true) + idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}) if err != nil { t.Fatal(err) } - defer func() { _ = tx.Rollback() }() - - if idx, err := h.CreateIndex("foo", pilosa.IndexOptions{}); err != nil { + tx, err := h.BeginTx(writable, idx) + if err != nil { t.Fatal(err) - } else if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { + } + defer tx.Rollback() + + if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } else if _, err := field.SetBit(tx, 0, 0, nil); err != nil { t.Fatal(err) @@ -686,7 +704,10 @@ func TestHolderSyncer_IntField(t *testing.T) { } defer c.Close() - _, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) + var idx0 *pilosa.Index + _ = idx0 + idx0, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) + _ = idx0 if err != nil { t.Fatalf("creating index i: %v", err) } @@ -698,24 +719,40 @@ func TestHolderSyncer_IntField(t *testing.T) { hldr0 := &test.Holder{Holder: c[0].Server.Holder()} hldr1 := &test.Holder{Holder: c[1].Server.Holder()} - // Set data on the local holder for node0. + // Set data on the local holder for node0. columnID=1, value=1 hldr0.SetValue("i", "f", 1, 1) - // Set data on node1. - hldr1.SetValue("i", "f", 2, 2) + // in c0 expect the 1 bit + //idx0.Dump("in c0, before SyncData") + // Set data on node1. columnID=2, value=2 + idx1 := hldr1.SetValue("i", "f", 2, 2) + _ = idx1 + + //idx1.Dump("in c1, before SyncData") + + //vv("before c[0] SyncData") err = c[0].Server.SyncData() if err != nil { t.Fatalf("syncing node 0: %v", err) } + //vv("after c[0] SyncData") + + // expect 3 rows, the 1 bit + 2 rows for the 2 value as BSI. But, we only see that c0 overwrote c1. + //idx0.Dump("in c0, after syncData") + //idx1.Dump("in c1, after syncData") + + // Problem is: data at c1 was replaced by c0, instead of being merged with existing c1. + // Problem is: data at c0 did not receive and merge the c1 data. // Verify data is the same on both nodes. for i, hldr := range []*test.Holder{hldr0, hldr1} { if a, exists := hldr.Value("i", "f", 1); !exists || a != 1 { - t.Errorf("unexpected value(node%d/0): %d, exists: %v", i, a, exists) + // expects exists==true, a==1 + t.Errorf("unexpected value(node%d/0): a:%d, exists: %v", i, a, exists) // failing TestHolderSyncer_IntField under Badger, unexpected value(node1/0): a:0, exists: true } if a, exists := hldr.Value("i", "f", 2); exists { - t.Errorf("unexpected value(node%d/1): %d, exists: %v", i, a, exists) + t.Errorf("unexpected value(node%d/1): a:%d, exists: %v", i, a, exists) } } }) @@ -732,7 +769,10 @@ func TestHolderSyncer_IntField(t *testing.T) { } defer c.Close() - _, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) + var idx0 *pilosa.Index + _ = idx0 + idx0, err = c[0].API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}) + _ = idx0 if err != nil { t.Fatalf("creating index i: %v", err) } @@ -769,6 +809,10 @@ func TestHolderSyncer_IntField(t *testing.T) { t.Fatalf("syncing node 1: %v", err) } + // dump the badger keys for both c0 and c1 + //vv("in c0, allkeys = '%v'", idx0.StringifiedBadgerKeys(nil)) + //vv("in c1, allkeys = '%v'", c[1].index.StringifiedBadgerKeys()) + // Verify data is the same on both nodes. for i, hldr := range []*test.Holder{hldr0, hldr1} { if a := hldr.Range("i", "f", pql.GT, 0); !reflect.DeepEqual(a.Columns(), []uint64{2 * pilosa.ShardWidth, 3 * pilosa.ShardWidth, 4 * pilosa.ShardWidth}) { diff --git a/index.go b/index.go index 41dfeb645..b1e2a03b2 100644 --- a/index.go +++ b/index.go @@ -67,16 +67,32 @@ type Index struct { // Instantiates new translation stores OpenTranslateStore OpenTranslateStoreFunc + + // txf chooses the transaction and storage strategy + Txf *TxFactory } // NewIndex returns a new instance of Index. func NewIndex(holder *Holder, path, name string) (*Index, error) { - err := validateName(name) + + // Emulate what the spf13/cobra does, letting env vars override + // the defaults, because we may be under a simple "go test" run where + // not all that command line machinery has been spun up. + txsrc := os.Getenv("PILOSA_TXSRC") + if txsrc == "" { + txsrc = DefaultTxsrc + } + txf, err := newTxFactory(txsrc, path) + if err != nil { + return nil, errors.Wrap(err, "creating newTxFactory") + } + + err = validateName(name) if err != nil { return nil, errors.Wrap(err, "validating name") } - return &Index{ + idx := &Index{ path: path, name: name, fields: make(map[string]*Field), @@ -94,7 +110,11 @@ func NewIndex(holder *Holder, path, name string) (*Index, error) { translationSyncer: NopTranslationSyncer, OpenTranslateStore: OpenInMemTranslateStore, - }, nil + + Txf: txf, + } + idx.Txf.idx = idx + return idx, nil } // CreatedAt is an timestamp for a specific version of an index. @@ -326,6 +346,11 @@ func (i *Index) Close() error { i.mu.Lock() defer i.mu.Unlock() + err := i.Txf.CloseIndex(i) + if err != nil { + return errors.Wrap(err, "closing index") + } + // Close the attribute store. i.columnAttrs.Close() @@ -367,9 +392,8 @@ func (i *Index) AvailableShards() *roaring.Bitmap { } // Begin starts a transaction on a shard of the index. -func (i *Index) Begin(writable bool, shard uint64) (Tx, error) { - // TODO(bbj): Check for underlying storage as RBF or roaring. - return &RoaringTx{Index: i}, nil +func (i *Index) BeginTx(writable bool, shard uint64) (Tx, error) { + return i.Txf.NewTx(Txo{Write: writable, Index: i, Shard: shard}), nil } // fieldPath returns the path to a field in the index. diff --git a/license.exceptions b/license.exceptions index ef44dcacc..e703b7c2a 100644 --- a/license.exceptions +++ b/license.exceptions @@ -9,3 +9,4 @@ ./proto/pilosa.pb.go ./logger/filewriter.go ./logger/filewriter_test.go +./vprint.go diff --git a/mmap_test.go b/mmap_test.go index a41830fc5..20fbd7685 100644 --- a/mmap_test.go +++ b/mmap_test.go @@ -31,7 +31,8 @@ type cv struct { func forceSnapshotsCheckMapping(t *testing.T) { depth := uint(6) - f := mustOpenBSIFragment("i", "f", viewStandard, 0) + f, idx := mustOpenBSIFragment("i", "f", viewStandard, 0) + _ = idx f.Logger = logger.NewLogfLogger(t) defer f.Clean(t) diff --git a/pql/ast.go b/pql/ast.go index 9d65bb3c5..8ebfaf122 100644 --- a/pql/ast.go +++ b/pql/ast.go @@ -244,7 +244,7 @@ func (q *Query) WriteCallN() int { var n int for _, call := range q.Calls { switch call.Name { - case "Set", "Clear", "SetRowAttrs", "SetColumnAttrs": + case "Set", "Clear", "SetRowAttrs", "SetColumnAttrs", "ClearRow", "Store", "SetBit": n++ } } diff --git a/pql/decimal.go b/pql/decimal.go index c6a7f834c..46ef82eb7 100644 --- a/pql/decimal.go +++ b/pql/decimal.go @@ -68,6 +68,14 @@ type Decimal struct { Scale int64 } +func (d Decimal) Clone() (r *Decimal) { + r = &Decimal{ + Value: d.Value, + Scale: d.Scale, + } + return +} + // NewDecimal returns a Decimal based on the provided arguments. func NewDecimal(value, scale int64) Decimal { return Decimal{ diff --git a/roaring/containers_btree.go b/roaring/containers_btree.go index 257a756fa..72837dcb4 100644 --- a/roaring/containers_btree.go +++ b/roaring/containers_btree.go @@ -210,6 +210,8 @@ type btcIterator struct { val *Container } +func (i *btcIterator) Close() {} + func (i *btcIterator) Next() bool { k, v, err := i.e.Next() if err == io.EOF { diff --git a/roaring/containers_slice.go b/roaring/containers_slice.go index 1eaa65cf0..79798adb0 100644 --- a/roaring/containers_slice.go +++ b/roaring/containers_slice.go @@ -91,11 +91,16 @@ func (sc *sliceContainers) GetOrCreate(key uint64) *Container { } func (sc *sliceContainers) Clone() Containers { + other := newSliceContainers() other.keys = make([]uint64, len(sc.keys)) other.containers = make([]*Container, len(sc.containers)) copy(other.keys, sc.keys) for i, c := range sc.containers { + if c == nil { + other.containers[i] = nil + continue + } other.containers[i] = c.Clone() } return other @@ -234,6 +239,8 @@ type sliceIterator struct { value *Container // current value } +func (si *sliceIterator) Close() {} + func (si *sliceIterator) Next() bool { if si.e == nil { return false diff --git a/roaring/roaring.go b/roaring/roaring.go index 496bdffa6..7f755f995 100644 --- a/roaring/roaring.go +++ b/roaring/roaring.go @@ -174,6 +174,7 @@ type Containers interface { type ContainerIterator interface { Next() bool Value() (uint64, *Container) + Close() } // Bitmap represents a roaring bitmap. @@ -235,6 +236,7 @@ var NewFileBitmap = NewBTreeBitmap // Clone returns a heap allocated copy of the bitmap. // Note: The OpWriter IS NOT copied to the new bitmap. func (b *Bitmap) Clone() *Bitmap { + if b == nil { return nil } @@ -749,6 +751,8 @@ func (it *mutableContainersIterator) Value() (uint64, *Container) { return it.cit.Value() } +func (it *mutableContainersIterator) Close() {} + // IntersectInPlace returns the bitwise intersection of b and others, // modifying b in place. func (b *Bitmap) IntersectInPlace(others ...*Bitmap) { @@ -1716,10 +1720,10 @@ func (b *Bitmap) writeToUnoptimized(w io.Writer) (n int64, err error) { return n, nil } -// roaringIterator represents something which can iterate through a roaring +// RoaringIterator represents something which can iterate through a roaring // bitmap and yield information about containers, including type, size, and // the location of their data structures. -type roaringIterator interface { +type RoaringIterator interface { // Len reports the number of containers total. Len() (count int64) // Next yields the information about the next container @@ -1728,6 +1732,19 @@ type roaringIterator interface { // which is typically an ops log in our case, and also its offset in case // we need to talk about truncation. Remaining() ([]byte, int64) + + // NextContainer is a helper that is used in place of Next(). It will + // allocate a Container from the output of its internal call to Next(), + // and return the key and container rc. If Next returns an error, then + // NextContainer will return 0, nil. + NextContainer() (key uint64, rc *Container) + + // Data returns the underlying data, esp for the Ops log. + Data() []byte + + // Clone copies the iterator, preserving it at this point in the iteration. + // It may well share much underlying data. + Clone() RoaringIterator } // baseRoaringIterator holds values used by both Pilosa and official Roaring @@ -1763,6 +1780,16 @@ func (b *baseRoaringIterator) SilenceLint() { _ = b.prevOffset32 } +func (b *pilosaRoaringIterator) Clone() (clone RoaringIterator) { + cp := *b + return &cp +} + +func (b *officialRoaringIterator) Clone() (clone RoaringIterator) { + cp := *b + return &cp +} + type pilosaRoaringIterator struct { baseRoaringIterator } @@ -1859,7 +1886,7 @@ func newPilosaRoaringIterator(data []byte) (*pilosaRoaringIterator, error) { return r, nil } -func newRoaringIterator(data []byte) (roaringIterator, error) { +func NewRoaringIterator(data []byte) (RoaringIterator, error) { if len(data) < headerBaseSize { return nil, errors.New("invalid data: not long enough to be a roaring header") } @@ -1891,6 +1918,10 @@ func (r *baseRoaringIterator) Len() int64 { return r.keys } +func (r *baseRoaringIterator) Data() []byte { + return r.data +} + func (r *baseRoaringIterator) Remaining() ([]byte, int64) { if r.lastDataOffset == 0 { return nil, 0 @@ -1898,6 +1929,20 @@ func (r *baseRoaringIterator) Remaining() ([]byte, int64) { return r.data[r.lastDataOffset:], r.lastDataOffset } +func (r *pilosaRoaringIterator) NextContainer() (key uint64, rc *Container) { + itrKey, itrCType, itrN, itrLen, itrPointer, itrErr := r.Next() + if itrErr != nil { + return 0, nil + } + rc = &Container{} + rc.typeID = itrCType + rc.n = int32(itrN) + rc.len = int32(itrLen) + rc.cap = int32(itrLen) + rc.pointer = itrPointer + return itrKey, rc +} + func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length int, pointer *uint16, err error) { if r.currentIdx >= r.keys { // we're already done @@ -1954,6 +1999,20 @@ func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length in return r.Current() } +func (r *officialRoaringIterator) NextContainer() (key uint64, rc *Container) { + itrKey, itrCType, itrN, itrLen, itrPointer, itrErr := r.Next() + if itrErr != nil { + return 0, nil + } + rc = &Container{} + rc.typeID = itrCType + rc.n = int32(itrN) + rc.len = int32(itrLen) + rc.cap = int32(itrLen) + rc.pointer = itrPointer + return itrKey, rc +} + func (r *officialRoaringIterator) Next() (key uint64, cType byte, n int, length int, pointer *uint16, err error) { if r.currentIdx >= r.keys { // we're already done @@ -2066,7 +2125,7 @@ func (b *Bitmap) RemapRoaringStorage(data []byte) (mappedAny bool, returnErr err if b.Containers == nil { return false, nil } - var itr roaringIterator + var itr RoaringIterator var err error var itrKey uint64 var itrCType byte @@ -2079,7 +2138,7 @@ func (b *Bitmap) RemapRoaringStorage(data []byte) (mappedAny bool, returnErr err // map to the data. We still need to do the UpdateEvery loop, we // just won't have an iterator for it. if data != nil && b.preferMapping { - itr, err = newRoaringIterator(data) + itr, err = NewRoaringIterator(data) } // don't return early: we still have to do the unmapping if err != nil { @@ -2144,7 +2203,16 @@ func (b *Bitmap) ImportRoaringBits(data []byte, clear bool, log bool, rowSize ui if data == nil { return 0, nil, errors.New("no roaring bitmap provided") } - var itr roaringIterator + var itr RoaringIterator + + itr, err = NewRoaringIterator(data) + if err != nil { + return 0, nil, err + } + return b.ImportRoaringRawIterator(itr, clear, log, rowSize) +} + +func (b *Bitmap) ImportRoaringRawIterator(itr RoaringIterator, clear bool, log bool, rowSize uint64) (changed int, rowSet map[uint64]int, err error) { var itrKey uint64 var itrCType byte var itrN int @@ -2152,10 +2220,6 @@ func (b *Bitmap) ImportRoaringBits(data []byte, clear bool, log bool, rowSize ui var itrPointer *uint16 var itrErr error - itr, err = newRoaringIterator(data) - if err != nil { - return 0, nil, err - } if itr == nil { return 0, nil, errors.New("failed to create roaring iterator, but don't know why") } @@ -2225,7 +2289,7 @@ func (b *Bitmap) ImportRoaringBits(data []byte, clear bool, log bool, rowSize ui } err = nil if log && changed > 0 { - op := op{opN: changed, roaring: data} + op := op{opN: changed, roaring: itr.Data()} if clear { op.typ = opTypeRemoveRoaring } else { @@ -2257,13 +2321,13 @@ func (b *Bitmap) writeOp(op *op) error { // Iterator returns a new iterator for the bitmap. func (b *Bitmap) Iterator() *Iterator { - itr := &Iterator{bitmap: b} + itr := NewIterator(&BitmapIteratorFinder{b}) itr.Seek(0) return itr } func (b *Bitmap) IteratorAt(start uint64) *Iterator { - itr := &Iterator{bitmap: b} + itr := NewIterator(&BitmapIteratorFinder{b}) itr.Seek(start) return itr } @@ -2287,7 +2351,7 @@ func RoaringToBitmaps(data []byte, shardWidth uint64) ([]*Bitmap, []uint64) { if data == nil { return nil, nil } - var itr roaringIterator + var itr RoaringIterator var itrKey uint64 var itrCType byte var itrN int @@ -2300,7 +2364,7 @@ func RoaringToBitmaps(data []byte, shardWidth uint64) ([]*Bitmap, []uint64) { var shards []uint64 keysPerShard := shardWidth >> 16 - itr, err := newRoaringIterator(data) + itr, err := NewRoaringIterator(data) if err != nil || itr == nil { return nil, nil } @@ -2524,15 +2588,38 @@ type BitmapInfo struct { From, To uintptr // if set, indicates the address range used when unpacking } +type IteratorFinder interface { + FindIterator(uint64) (ContainerIterator, bool) + Close() +} +type BitmapIteratorFinder struct { + bitmap *Bitmap +} + +func (bif *BitmapIteratorFinder) FindIterator(seek uint64) (ContainerIterator, bool) { + return bif.bitmap.Containers.Iterator(seek) +} +func (bif *BitmapIteratorFinder) Close() {} + // Iterator represents an iterator over a Bitmap. type Iterator struct { - bitmap *Bitmap + finder IteratorFinder citer ContainerIterator key uint64 c *Container j, k int32 // i: container; j: array index, bit index, or run index; k: offset within the run } +// NewIterator requires f as an IteratorFinder, it will +// crash if f is nil. +func NewIterator(f IteratorFinder) *Iterator { + return &Iterator{finder: f} +} + +func (itr *Iterator) Close() { + itr.finder.Close() +} + // Seek moves to the first value equal to or greater than `seek`. func (itr *Iterator) Seek(seek uint64) { // k should always be -1 unless we're seeking into a run container. Then the @@ -2540,7 +2627,7 @@ func (itr *Iterator) Seek(seek uint64) { itr.k = -1 // Move to the correct container. - itr.citer, _ = itr.bitmap.Containers.Iterator(highbits(seek)) + itr.citer, _ = itr.finder.FindIterator(highbits(seek)) if !itr.citer.Next() { itr.c = nil return // eof @@ -6114,10 +6201,10 @@ func (b *Bitmap) BitwiseEqual(c *Bitmap) (bool, error) { cn = biter.Next() } if bn { - return false, fmt.Errorf("container mismatch: %d vs %d containers, first bitmap has extra container %d [%d bits]", bct, cct, bk, bc) + return false, fmt.Errorf("container mismatch: %d vs %d containers, first bitmap has extra container %d [%v bits]", bct, cct, bk, bc) } if cn { - return false, fmt.Errorf("container mismatch: %d vs %d containers, second bitmap has extra container %d [%d bits]", bct, cct, ck, cc) + return false, fmt.Errorf("container mismatch: %d vs %d containers, second bitmap has extra container %d [%v bits]", bct, cct, ck, cc) } return true, nil } @@ -6783,6 +6870,35 @@ func Optimize(c *Container) { func Union(a, b *Container) *Container { return union(a, b) } + func Difference(a, b *Container) *Container { return difference(a, b) } + +func (c *Container) Add(v uint16) (newC *Container, added bool) { + return c.add(v) +} + +func (c *Container) Remove(v uint16) (c2 *Container, removed bool) { + return c.remove(v) +} + +func (c *Container) Max() uint16 { + return c.max() +} + +func (c *Container) CountRange(start, end int32) (n int32) { + return c.countRange(start, end) +} + +func (c *Container) UnionInPlace(other *Container) *Container { + return c.unionInPlace(other) +} + +func (c *Container) Difference(other *Container) *Container { + return difference(c, other) +} + +func NewSliceContainers() *sliceContainers { + return newSliceContainers() +} diff --git a/roaring/unmarshal_binary.go b/roaring/unmarshal_binary.go index 53aff37ee..39a0762f9 100644 --- a/roaring/unmarshal_binary.go +++ b/roaring/unmarshal_binary.go @@ -27,7 +27,7 @@ func (b *Bitmap) UnmarshalBinary(data []byte) (err error) { if data == nil { return errors.New("no roaring bitmap provided") } - var itr roaringIterator + var itr RoaringIterator var itrKey uint64 var itrCType byte var itrN int @@ -35,7 +35,7 @@ func (b *Bitmap) UnmarshalBinary(data []byte) (err error) { var itrPointer *uint16 var itrErr error - itr, err = newRoaringIterator(data) + itr, err = NewRoaringIterator(data) if err != nil { return err } @@ -110,7 +110,7 @@ func InspectBinary(data []byte, mapped bool, info *BitmapInfo) (b *Bitmap, mappe if data == nil { return b, mappedAny, errors.New("no roaring bitmap provided") } - var itr roaringIterator + var itr RoaringIterator var itrKey uint64 var itrCType byte var itrN int @@ -118,7 +118,7 @@ func InspectBinary(data []byte, mapped bool, info *BitmapInfo) (b *Bitmap, mappe var itrPointer *uint16 var itrErr error - itr, err = newRoaringIterator(data) + itr, err = NewRoaringIterator(data) if err != nil { return b, mappedAny, err } diff --git a/row.go b/row.go index d76dcc73c..30c11d2fe 100644 --- a/row.go +++ b/row.go @@ -45,6 +45,40 @@ func NewRow(columns ...uint64) *Row { return r } +func (r *Row) Clone() (clone *Row) { + if r == nil { + return nil + } + var keyClone []string + if len(r.Keys) > 0 { + keyClone = make([]string, len(r.Keys)) + copy(keyClone, r.Keys) + } + + attrClone := make(map[string]interface{}) + for k, v := range r.Attrs { + attrClone[k] = v + } + clone = &Row{ + Keys: keyClone, + Attrs: attrClone, + } + + for _, seg := range r.segments { + segClone := rowSegment{ + shard: seg.shard, + writable: true, // we know it is safe; it is a copy. + n: seg.n, + } + if seg.data != nil { + segClone.data = seg.data.Clone() // *roaring.Bitmap + } + //segClone.InvalidateCount() // not needed? + clone.segments = append(clone.segments, segClone) + } + return clone +} + // NewRowFromBitmap divides a bitmap into rows, which it now calls shards. This // transposes; data that was in any shard for Row 0 is now considered shard 0, // etcetera. @@ -520,7 +554,7 @@ func (r *Row) MarshalJSON() ([]byte, error) { func (r *Row) Columns() []uint64 { a := make([]uint64, 0, r.Count()) for i := range r.segments { - a = append(a, r.segments[i].Columns()...) + a = append(a, r.segments[i].Columns()...) // Accessing Tx memory that is now invalid. } return a } diff --git a/server.go b/server.go index 0afd2fa43..f0974c0d5 100644 --- a/server.go +++ b/server.go @@ -326,6 +326,17 @@ func OptServerOpenTranslateReader(fn OpenTranslateReaderFunc) ServerOption { } } +// OptServerTxsrc is a functional option on Server +// used to specify the transactional-storage to use, +// resulting in RoaringTx, RbfTx, BadgerTx, or a blueGreen* Tx +// being used for all Tx interface calls. +func OptServerTxsrc(txsrc string) ServerOption { + return func(s *Server) error { + s.holder.Opts.Txsrc = txsrc + return nil + } +} + // NewServer returns a new instance of Server. func NewServer(opts ...ServerOption) (*Server, error) { cluster := newCluster() diff --git a/server/config.go b/server/config.go index 6d0181756..ca5817fd0 100644 --- a/server/config.go +++ b/server/config.go @@ -24,6 +24,7 @@ import ( "strings" "time" + "github.com/pilosa/pilosa/v2" "github.com/pilosa/pilosa/v2/gossip" "github.com/pilosa/pilosa/v2/toml" "github.com/pkg/errors" @@ -166,6 +167,19 @@ type Config struct { // MutexFraction is passed directly to runtime.SetMutexProfileFraction MutexFraction int `toml:"mutex-fraction"` } `toml:"profile"` + + // Txsrc determines which Tx implementation the holder/Index will use; one + // of the available transactional-storage engines. Choices are listed + // in the string constants below. Should be one of + // "roaring","badger", "rbf", "badger_roaring", "roaring_badger", "rbf_roaring", + // "roaring_rbf", "badger_rbf", "rbf_badger", or any later addition. The + // engines with _ underscore indicate use of a blueGreenTx with a comparison + // of values back from each Tx method, and a panic if they differ. This + // is an effective test for consistency. If "rbf_roaring" is specified, then + // the roaring values are the ones actually returned from the blueGreenTx. + // If "roaring_rbf" is chosen, then the RBF values are the ones actually + // returned from the blueGreenTx. + Txsrc string `toml:"txsrc"` } // NewConfig returns an instance of Config with default options. @@ -222,6 +236,8 @@ func NewConfig() *Config { c.Profile.BlockRate = 10000000 // 1 sample per 10 ms c.Profile.MutexFraction = 100 // 1% sampling + c.Txsrc = pilosa.DefaultTxsrc + return c } diff --git a/server/handler_test.go b/server/handler_test.go index c318056a1..d4648c3c3 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -173,29 +173,38 @@ func TestHandler_Endpoints(t *testing.T) { } }) - tx, err := holder.Begin(true) + i0 := hldr.MustCreateIndexIfNotExists("i0", pilosa.IndexOptions{}) + tx0, err := holder.BeginTx(true, i0.Index) if err != nil { t.Fatal(err) } - defer func() { _ = tx.Rollback() }() + defer tx0.Rollback() - i0 := hldr.MustCreateIndexIfNotExists("i0", pilosa.IndexOptions{}) i1 := hldr.MustCreateIndexIfNotExists("i1", pilosa.IndexOptions{}) + tx1, err := holder.BeginTx(true, i1.Index) + if err != nil { + t.Fatal(err) + } + defer tx1.Rollback() + if f, err := i0.CreateFieldIfNotExists("f1", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) - } else if _, err := f.SetBit(tx, 0, 0, nil); err != nil { + } else if _, err := f.SetBit(tx0, 0, 0, nil); err != nil { t.Fatal(err) } if f, err := i1.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) - } else if _, err := f.SetBit(tx, 0, 0, nil); err != nil { + } else if _, err := f.SetBit(tx1, 0, 0, nil); err != nil { t.Fatal(err) } if _, err := i0.CreateFieldIfNotExists("f0", pilosa.OptFieldTypeDefault()); err != nil { t.Fatal(err) } - if err := tx.Commit(); err != nil { + if err := tx0.Commit(); err != nil { + t.Fatal(err) + } + if err := tx1.Commit(); err != nil { t.Fatal(err) } @@ -638,7 +647,7 @@ func TestHandler_Endpoints(t *testing.T) { t.Run("Query empty", func(t *testing.T) { w := httptest.NewRecorder() h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader(""))) - if body := w.Body.String(); body != `{"results":[]}`+"\n" { + if body := w.Body.String(); body != `{"results":[]}`+"\n" && body != `{"results":null}`+"\n" { t.Fatalf("unexpected body: %q", body) } }) diff --git a/test/holder.go b/test/holder.go index b48016f9f..8db4af11c 100644 --- a/test/holder.go +++ b/test/holder.go @@ -25,6 +25,8 @@ import ( "github.com/pilosa/pilosa/v2/pql" ) +var panicOn = pilosa.PanicOn + // Holder is a test wrapper for pilosa.Holder. type Holder struct { *pilosa.Holder @@ -86,29 +88,40 @@ func (h *Holder) Row(index, field string, rowID uint64) *pilosa.Row { if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: false, Index: idx.Index}) + defer tx.Rollback() row, err := f.Row(tx, rowID) if err != nil { panic(err) } - return row + // clone it so that mmapped storage doesn't disappear from under it + // once the tx goes away. + return row.Clone() } // ReadRow returns a Row for a given field. If the field does not exist, // it panics rather than creating the field. func (h *Holder) ReadRow(index, field string, rowID uint64) *pilosa.Row { - f := h.Holder.Field(index, field) + idx := h.Holder.Index(index) + if idx == nil { + panic(pilosa.ErrIndexNotFound) + } + f := idx.Field(field) if f == nil { panic(pilosa.ErrFieldNotFound) } - tx := &pilosa.RoaringTx{Field: f} + tx := idx.Txf.NewTx(pilosa.Txo{Write: false, Field: f}) + defer tx.Rollback() row, err := f.Row(tx, rowID) if err != nil { panic(err) } - return row + + // clone it so that mmapped storage doesn't disappear from under it + // once the tx goes away. + return row.Clone() } func (h *Holder) RowAttrStore(index, field string) pilosa.AttrStore { @@ -126,13 +139,17 @@ func (h *Holder) RowTime(index, field string, rowID uint64, t time.Time, quantum if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: false, Index: idx.Index}) + defer tx.Rollback() row, err := f.RowTime(tx, rowID, t, quantum) if err != nil { panic(err) } - return row + + // clone it so that mmapped storage doesn't disappear from under it + // once the tx goes away. + return row.Clone() } // SetBit sets a bit on the given field. @@ -147,12 +164,15 @@ func (h *Holder) SetBitTime(index, field string, rowID, columnID uint64, t *time if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index}) + defer tx.Rollback() _, err = f.SetBit(tx, rowID, columnID, t) if err != nil { panic(err) } + panicOn(tx.Commit()) } // ClearBit clears a bit on the given field. @@ -162,12 +182,14 @@ func (h *Holder) ClearBit(index, field string, rowID, columnID uint64) { if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index}) + defer tx.Rollback() _, err = f.ClearBit(tx, rowID, columnID) if err != nil { panic(err) } + panicOn(tx.Commit()) } // MustSetBits sets columns on a row. Panic on error. @@ -179,18 +201,23 @@ func (h *Holder) MustSetBits(index, field string, rowID uint64, columnIDs ...uin } // SetValue sets an integer value on the given field. -func (h *Holder) SetValue(index, field string, columnID uint64, value int64) { +func (h *Holder) SetValue(index, field string, columnID uint64, value int64) *Index { idx := h.MustCreateIndexIfNotExists(index, pilosa.IndexOptions{}) f, err := idx.CreateFieldIfNotExists(field, pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64)) if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index}) + defer tx.Rollback() _, err = f.SetValue(tx, columnID, value) if err != nil { panic(err) } + if err := tx.Commit(); err != nil { + panic(err) + } + return idx } // Value returns the integer value for a given column. @@ -200,7 +227,8 @@ func (h *Holder) Value(index, field string, columnID uint64) (int64, bool) { if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: false, Index: idx.Index}) + defer tx.Rollback() val, exists, err := f.Value(tx, columnID) if err != nil { @@ -217,11 +245,15 @@ func (h *Holder) Range(index, field string, op pql.Token, predicate int64) *pilo if err != nil { panic(err) } - tx := &pilosa.RoaringTx{Index: idx.Index} + tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: false, Index: idx.Index}) + defer tx.Rollback() row, err := f.Range(tx, field, op, predicate) if err != nil { panic(err) } - return row + + // clone it so that mmapped storage doesn't disappear from under it + // once the tx goes away. + return row.Clone() } diff --git a/tx.go b/tx.go index b8b74d427..9fcf6fd99 100644 --- a/tx.go +++ b/tx.go @@ -22,30 +22,160 @@ import ( "github.com/pkg/errors" ) +// batch operations want Tx.Add(batched=doBatch), while bit-at-a-time want Tx.Add(batched=!doBatched) +// Used in Tx.Add() to get consistency between RoaringTx and other Tx implementations on +// the changeCount returned. +const doBatched = false // must be false, do not change this without adjusting the Add() implementations correspondingly. + +// writable initializes Tx that update, use !writable for read-only. +const writable = true + +// Tx providers offer transactional storage for high-level roaring.Bitmaps and +// low-level roaring.Containers. +// +// The common 4-tuple of (index, field, view, shard) jointly specify a fragment. +// A fragment conceptually holds one roaring.Bitmap. +// +// Within the fragment, the ckey or container-key is the uint64 that specifies +// the high 48-bits of the roaring.Bitmap 64-bit space. +// The ckey is used to retreive a specific roaring.Container that +// is either a run, array, or raw-bitmap. The roaring.Container is the +// low 16-bits of the roaring.Bitmap space. Its size is at most +// 8KB (2^16 bits / (8 bits / byte) == 8192 bytes). +// +// The grain of the transaction is guaranteed to be at least at the shard +// within one index. Therefore updates to the any of the fields within +// the same shard will be atomically visible only once the transaction commits. +// Reads from another, concurrently open, transaction will not see updates +// that have not been committed. type Tx interface { - Rollback() error + + // Rollback must be called the end of read-only transactions. Either + // Rollback or Commit must be called at the end of writable transactions. + // It is safe to call Rollback multiple times, but it must be + // called at least once to release resources. Any Rollback after + // a Commit is ignored, so 'defer tx.Rollback()' should be commonly + // written after starting a new transaction. + // + // If there is an error during internal Rollback processing, + // this would be quite serious, and the underlying storage is + // expected to panic. Hence there is no explicit error returned + // from Rollback that needs to be checked. + Rollback() + + // Commit makes the updates in the Tx visible to subsequent transactions. Commit() error + // Readonly returns the flag this transaction was created with + // during NewTx. If the transaction is writable, it will return false. + Readonly() bool + + // UseRowCache is used by fragment.go unprotectedRow() to determine + // dynamically at runtime if RoaringTx + // are in use, which for continuity want to continue to use the + // rowCache, or if other storage engines (RBF, Badger) are in + // use, which will mean that the bitmap data stored by the + // rowCache can disappear as it is un-mmap-ed, causing crashes. + UseRowCache() bool + + // IncrementOpN updates internal statistics with the changedN provided. + IncrementOpN(index, field, view string, shard uint64, changedN int) + + // 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. + Pointer() string + + // NewTxIterator returns it, a *roaring.Iterator whose it.Next() will + // successively return each uint64 stored in the conceptual roaring.Bitmap + // for the specified fragment. + NewTxIterator(index, field, view string, shard uint64) (it *roaring.Iterator) + + // ContainerIterator loops over the containers in the conceptual + // roaring.Bitmap for the specified fragment. + // Calling Next() on the returned roaring.ContainerIterator gives + // you a roaring.Container that is either run, array, or raw bitmap. + // Return value 'found' is true when the ckey container was present. + ContainerIterator(index, field, view string, shard uint64, ckey uint64) (citer roaring.ContainerIterator, found bool, err error) + + // RoaringBitmap retreives the roaring.Bitmap for the entire shard. RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) - Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) - PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error - RemoveContainer(index, field, view string, shard uint64, key uint64) error + // Container returns the roaring.Container for the given ckey + // (container-key or highbits), in the chosen fragment. + Container(index, field, view string, shard uint64, ckey uint64) (*roaring.Container, error) - Add(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) - Remove(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) + // PutContainer stores c under the given ckey (container-key), in the specified fragment. + PutContainer(index, field, view string, shard uint64, ckey uint64, c *roaring.Container) error + + // RemoveContainer deletes the roaring.Container under the given ckey (container-key), + // in the specified fragment. + RemoveContainer(index, field, view string, shard uint64, ckey uint64) error + + // Add adds the 'a' bits to the specified fragment. + // + // Using batched=true allows efficient bulk-import. + // + // Notes on the RoaringTx implementation: + // If the batched flag is true, then the roaring.Bitmap.AddN() is used, which does oplog batches. + // If the batched flag is false, then the roaring.Bitmap.Add() is used, which does simple opTypeAdd single adds. + // + // Beware: if batched is true, then changeCount will only ever be 0 or 1, + // because it calls roaring.Add(). + // If batched is false, we call roaring.DirectAddN() and then changeCount + // will be accurate if the changeCount is greater than 0. + // + // Hence: only ever call Add(batched=false) if changeCount is expected to be 0 or 1. + // Or, must use Add(batched=true) if changeCount can be > 1. + // + Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) + + // Remove removes the 'a' values from the Bitmap for the fragment. + Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) + + // Contains tests if the uint64 v is stored in the fragment's Bitmap. Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) - ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) + // ForEach ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error + + // ForEachRange ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error + // Count Count(index, field, view string, shard uint64) (uint64, error) + + // Max Max(index, field, view string, shard uint64) (uint64, error) + + // Min Min(index, field, view string, shard uint64) (uint64, bool, error) + + // UnionInPlace UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error + + // CountRange CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) + + // OffsetRange OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) + + // ImportRoaringBits does efficient bulk import using rit, a roaring.RoaringIterator. + // See the roaring package for details of the RoaringIterator. + // If clear is true, the bits from rit are cleared, otherwise they are set in the + // specifed fragment. + ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (changed int, rowSet map[uint64]int, err error) +} + +// RawRoaringData used by ImportRoaringBits. +// must be consumable by roaring.newRoaringIterator() +type RawRoaringData struct { + data []byte +} + +func (rr *RawRoaringData) Iterator() (roaring.RoaringIterator, error) { + return roaring.NewRoaringIterator(rr.data) } // MultiTx implements the transaction interface to combine multiple transactions. @@ -77,14 +207,40 @@ func NewMultiTxWithIndex(writable bool, index *Index) *MultiTx { var _ Tx = (*MultiTx)(nil) +func (mtx *MultiTx) UseRowCache() bool { + return true +} + +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 (tx *MultiTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (changed int, rowSet map[uint64]int, err error) { + panic("not done") +} + +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() (err error) { +func (mtx *MultiTx) Rollback() { for _, tx := range mtx.txs { - if e := tx.Rollback(); e != nil && err == nil { - err = e - } + tx.Rollback() } - return err } // Commit commits all underlying transactions. @@ -129,18 +285,18 @@ func (mtx *MultiTx) RemoveContainer(index, field, view string, shard uint64, key return tx.RemoveContainer(index, field, view, shard, key) } -func (mtx *MultiTx) Add(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) { +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 false, err + return 0, err } - return tx.Add(index, field, view, shard, a...) + return tx.Add(index, field, view, shard, batched, a...) } -func (mtx *MultiTx) Remove(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) { +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 false, err + return 0, err } return tx.Remove(index, field, view, shard, a...) } @@ -231,8 +387,10 @@ 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[multiTxKey{index, shard}] + tx := mtx.txs[mkey] if tx != nil { return tx, nil } @@ -246,10 +404,10 @@ func (mtx *MultiTx) tx(index string, shard uint64) (_ Tx, err error) { } // Begin tranaction & cache it. - if tx, err = idx.Begin(mtx.writable, shard); err != nil { + if tx, err = idx.BeginTx(mtx.writable, shard); err != nil { return nil, err } - mtx.txs[multiTxKey{index, shard}] = tx + mtx.txs[mkey] = tx return tx, nil } @@ -257,31 +415,66 @@ func (mtx *MultiTx) tx(index string, shard uint64) (_ Tx, err error) { type multiTxKey struct { index string shard uint64 + write bool } // RoaringTx represents a fake transaction object for Roaring storage. type RoaringTx struct { + write bool Index *Index Field *Field fragment *fragment } -// Rollback is a no-op as Roaring does not support transactions. -func (tx *RoaringTx) Rollback() error { - return nil +func (tx *RoaringTx) UseRowCache() bool { + return true } +func (tx *RoaringTx) Pointer() string { + return fmt.Sprintf("%p", tx) +} + +// NewTxIterator returns a *roaring.Iterator that MUST have Close() called on it BEFORE +// the transaction Commits or Rollsback. +func (tx *RoaringTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator { + b, err := tx.bitmap(index, field, view, shard) + panicOn(err) + return b.Iterator() +} + +func (tx *RoaringTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64) (changed int, rowSet map[uint64]int, err error) { + b, err := tx.bitmap(index, field, view, shard) + panicOn(err) + if err != nil { + return 0, nil, err + } + return b.ImportRoaringRawIterator(rit, clear, true, rowSize) +} + +func (tx *RoaringTx) Readonly() bool { + return !tx.write +} + +func (tx *RoaringTx) IncrementOpN(index, field, view string, shard uint64, changedN int) { + frag, err := tx.getFragment(index, field, view, shard) + panicOn(err) + frag.incrementOpN(changedN) +} + +// Rollback is a no-op as Roaring does not support transactions. +func (tx *RoaringTx) Rollback() {} + // Commit is a no-op as Roaring does not support transactions. func (tx *RoaringTx) Commit() error { return nil } func (tx *RoaringTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { - return tx.bitmap(field, view, shard) + return tx.bitmap(index, field, view, shard) } func (tx *RoaringTx) Container(index, field, view string, shard uint64, key uint64) (*roaring.Container, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return nil, err } @@ -289,7 +482,7 @@ func (tx *RoaringTx) Container(index, field, view string, shard uint64, key uint } func (tx *RoaringTx) PutContainer(index, field, view string, shard uint64, key uint64, c *roaring.Container) error { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return err } @@ -298,7 +491,7 @@ func (tx *RoaringTx) PutContainer(index, field, view string, shard uint64, key u } func (tx *RoaringTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return err } @@ -306,24 +499,48 @@ func (tx *RoaringTx) RemoveContainer(index, field, view string, shard uint64, ke return nil } -func (tx *RoaringTx) Add(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) { - b, err := tx.bitmap(field, view, shard) +func (tx *RoaringTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { + b, err := tx.bitmap(index, field, view, shard) if err != nil { - return false, err + return 0, err } - return b.Add(a...) + if !batched { + changed, err := b.Add(a...) + if changed { + return 1, err + } + return 0, err + } + + // Note: do not replace b.AddN() with b.DirectAddN(). + // DirectAddN() does not do op-log operations inside roaring + // This creates a problem because RoaringTx needs the op-log + // to know when to flush the fragment to disk. + count, err := b.AddN(a...) // AddN does oplog batches. needed to keep op-log up to date. + + return count, err } -func (tx *RoaringTx) Remove(index, field, view string, shard uint64, a ...uint64) (changed bool, err error) { - b, err := tx.bitmap(field, view, shard) +func (tx *RoaringTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + b, err := tx.bitmap(index, field, view, shard) if err != nil { - return false, err + return 0, err } - return b.Remove(a...) + changed, err := b.Remove(a...) // green TestFragment_Bug_Q2DoubleDelete + panicOn(err) + if changed { + return 1, err + } else { + return 0, err + } + + // Note: don't replace b.Remove(a...) with b.RemoveN(a...) or + // with b.DirectRemoveN(a...). If you do, you'll see + // TestFragment_Bug_Q2DoubleDelete go red. } func (tx *RoaringTx) Contains(index, field, view string, shard uint64, v uint64) (exists bool, err error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return false, err } @@ -331,7 +548,7 @@ func (tx *RoaringTx) Contains(index, field, view string, shard uint64, v uint64) } func (tx *RoaringTx) ContainerIterator(index, field, view string, shard uint64, key uint64) (citer roaring.ContainerIterator, found bool, err error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return nil, false, err } @@ -340,7 +557,7 @@ func (tx *RoaringTx) ContainerIterator(index, field, view string, shard uint64, } func (tx *RoaringTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return err } @@ -348,7 +565,7 @@ func (tx *RoaringTx) ForEach(index, field, view string, shard uint64, fn func(i } func (tx *RoaringTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return err } @@ -356,7 +573,7 @@ func (tx *RoaringTx) ForEachRange(index, field, view string, shard uint64, start } func (tx *RoaringTx) Count(index, field, view string, shard uint64) (uint64, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return 0, err } @@ -364,7 +581,7 @@ func (tx *RoaringTx) Count(index, field, view string, shard uint64) (uint64, err } func (tx *RoaringTx) Max(index, field, view string, shard uint64) (uint64, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return 0, err } @@ -372,7 +589,7 @@ func (tx *RoaringTx) Max(index, field, view string, shard uint64) (uint64, error } func (tx *RoaringTx) Min(index, field, view string, shard uint64) (uint64, bool, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return 0, false, err } @@ -381,7 +598,7 @@ func (tx *RoaringTx) Min(index, field, view string, shard uint64) (uint64, bool, } func (tx *RoaringTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return err } @@ -390,7 +607,7 @@ func (tx *RoaringTx) UnionInPlace(index, field, view string, shard uint64, other } func (tx *RoaringTx) CountRange(index, field, view string, shard uint64, start, end uint64) (uint64, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return 0, err } @@ -398,27 +615,43 @@ func (tx *RoaringTx) CountRange(index, field, view string, shard uint64, start, } func (tx *RoaringTx) OffsetRange(index, field, view string, shard uint64, offset, start, end uint64) (*roaring.Bitmap, error) { - b, err := tx.bitmap(field, view, shard) + b, err := tx.bitmap(index, field, view, shard) if err != nil { return nil, err } return b.OffsetRange(offset, start, end), nil } -func (tx *RoaringTx) bitmap(field, view string, shard uint64) (*roaring.Bitmap, error) { +// getFragment is used by IncrementOpN() and by bitmap() +func (tx *RoaringTx) getFragment(index, field, view string, shard uint64) (*fragment, error) { + // If a fragment is attached, always use it. if tx.fragment != nil { - return tx.fragment.storage, nil + return tx.fragment, nil } // If a field is attached, start from there. // Otherwise look up the field from the index. f := tx.Field + if f == nil { - if f = tx.Index.Field(field); f == nil { - return nil, ErrFieldNotFound + // we cannot assume that the tx.Index that we "started" on is the same + // as the index we are being queried; it might be foreign: TestExecutor_ForeignIndex + // So go through the holder + idx := tx.Index.holder.Index(index) + if idx == nil { + // only thing we can try is the cached index, and hope we aren't being asked for a foreign index. + f = tx.Index.Field(field) + if f == nil { + return nil, ErrFieldNotFound + } + } else { + if f = idx.Field(field); f == nil { + return nil, ErrFieldNotFound + } } } + // INVAR: f is not nil. v := f.view(view) if v == nil { @@ -429,5 +662,18 @@ func (tx *RoaringTx) bitmap(field, view string, shard uint64) (*roaring.Bitmap, if frag == nil { panic(fmt.Sprintf("fragment not found: %q / %q / %d", field, view, shard)) } + + // Note: we cannot cache frag into tx.fragment. + // Empirically, it breaks 245 top-level pilosa tests. + // tx.fragment = frag // breaks the world. + + return frag, nil +} + +func (tx *RoaringTx) bitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { + frag, err := tx.getFragment(index, field, view, shard) + if err != nil { + return nil, err + } return frag.storage, nil } diff --git a/txfactory.go b/txfactory.go new file mode 100644 index 000000000..6d31f43ce --- /dev/null +++ b/txfactory.go @@ -0,0 +1,469 @@ +// 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" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pkg/errors" +) + +// public strings that pilosa/server/config.go can reference +const ( + RoaringTxn string = "roaring" + BadgerTxn string = "badger" + 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 +// to set the default for pilosa server exeutable. +// Can be overridden with env variable PILOSA_TXSRC for testing. +const DefaultTxsrc = RoaringTxn + +var sep = string(os.PathSeparator) + +// TxFactory abstracts the creation of Tx interface-level +// 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 + + bw *BadgerDBWrapper + + // could have more than one *Index, but for now keep it simple, + // and allow blueGreenTx to report badger contents via idx + idx *Index + + // TODO: put RBF database handle here. +} + +// integer types for fast switch{} +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 +) + +func txsrcToTxtype(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 + } + panic(fmt.Sprintf("unknown txsrc '%v'", txsrc)) +} + +func newTxFactory(txsrc string, path string) (f *TxFactory, err error) { + ty := txsrcToTxtype(txsrc) + if ty < 1 || ty > 9 { + panic(fmt.Sprintf("invalid txtype '%v'", int(ty))) + } + var bw *BadgerDBWrapper + if ty == badgerTxn || ty == 4 || ty == 5 || ty == 8 || ty == 9 { + bw, err = openBadgerDBWrapper(path) + // TODO(jea): figure out what the appropriate error path is here. + //fmt.Printf("warning: could not open badgerdb on path '%v': '%v'. For safety, we are opening a new '%v-fallback' instead\n", path, err, path+"-fallback") + if err != nil { + //bw, err = newBadgerDBWrapper(path + "-fallback") + bw, err = newBadgerDBWrapper(path) + } + panicOn(err) + + bw.doAllocZero = true + } + return &TxFactory{ + typeOfTx: ty, + bw: bw, + }, err +} + +// Txo holds the transaction options +type Txo struct { + Write bool + Field *Field + Index *Index + Fragment *fragment + Shard uint64 +} + +func (f *TxFactory) TxType() txtype { + 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.bw.DeleteIndex(name) + case rbfTxn: + panic("todo rbfTxn DeleteIndex(name)") + case blueGreenBadgerRoaring: + return f.bw.DeleteIndex(name) + case blueGreenRoaringBadger: + return f.bw.DeleteIndex(name) + } + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) +} + +func (f *TxFactory) Close() error { + switch f.typeOfTx { + case roaringFragmentFilesTxn: + return nil + case badgerTxn: + // note cannot actually close Badger here. + // causes problems b/c tries holder.DeleteIndex tries to delete the index after db is closed. + //return f.bw.Close() + return nil + case rbfTxn: + panic("todo rbfTxn Close()") + case blueGreenBadgerRoaring: + return nil + case blueGreenRoaringBadger: + return nil + } + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) +} + +func (f *TxFactory) CloseIndex(idx *Index) error { + switch f.typeOfTx { + case roaringFragmentFilesTxn: + return nil + case badgerTxn: + return nil + case rbfTxn: + panic("todo rbfTxn CloseIndex()") + + case blueGreenBadgerRoaring: + return nil + case blueGreenRoaringBadger: + return nil + } + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) +} + +func (f *TxFactory) NewTx(o Txo) Tx { + + switch f.typeOfTx { + case roaringFragmentFilesTxn: + return &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} + case badgerTxn: + btx := f.bw.NewBadgerTx(o.Write) + return btx + case rbfTxn: + panic("todo rbfTxn creation") + + case blueGreenBadgerRoaring: + btx := f.bw.NewBadgerTx(o.Write) + rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} + return newBlueGreenTx(btx, rtx, f.idx) + case blueGreenRoaringBadger: + btx := f.bw.NewBadgerTx(o.Write) + rtx := &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment} + return newBlueGreenTx(rtx, btx, f.idx) + } + panic(fmt.Sprintf("unknown f.typeOfTx type: '%v'", f.typeOfTx)) +} + +func (ty txtype) String() string { + switch ty { + case noneTxn: + return "noneTxn" + case roaringFragmentFilesTxn: + return "roaringFragmentFilesTxn" + 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" + } + panic(fmt.Sprintf("unhandled ty '%v' in txtype.String()", int(ty))) +} + +// StringifiedBadgerKeys displays the keys visible in BadgerDB for the idx *Index. +// If optionalUseThisTx is nil, it will start a new read-only transaction to +// do this query. Otherwise it will piggy back on the provided transaction. +// Hence to view uncommited keys, you must provide in optionalUseThisTx the +// Tx in which they have been added. +func (idx *Index) StringifiedBadgerKeys(optionalUseThisTx Tx) string { + return idx.Txf.bw.StringifiedBadgerKeys(optionalUseThisTx) +} + +// fragmentSpecFromRoaringPath takes a path releative to the +// index directory, not including the name of the index itself. +// The path should not start with the path separator sep ('/' or '\\') rune. +func fragmentSpecFromRoaringPath(path string) (field, view string, shard uint64, err error) { + + if len(path) == 0 { + err = fmt.Errorf("fragmentSpecFromRoaringPath error: path '%v' too short", path) + return + } + if path[:1] == sep { + err = fmt.Errorf("fragmentSpecFromRoaringPath error: path '%v' cannot start with separator '%v'; must be relative to the index base directory", path, sep) + return + } + + // sample path: + // field view shard + // myfield/views/standard/fragments/0 + s := strings.Split(path, "/") + n := len(s) + if n != 5 { + err = fmt.Errorf("len(s)=%v, but expected 5. path='%v'", n, path) + return + } + field = s[0] + view = s[2] + shard, err = strconv.ParseUint(s[4], 10, 64) + if err != nil { + err = fmt.Errorf("fragmentSpecFromRoaringPath(path='%v') could not parse shard '%v' as uint: '%v'", path, s[4], err) + } + return +} + +func (idx *Index) StringifiedRoaringKeys() (r string) { + + paths, err := listFilesUnderDir(idx.path, false, "", true) + panicOn(err) + index := idx.name + + r = "allkeys:[\n" + for _, relpath := range paths { + field, view, shard, err := fragmentSpecFromRoaringPath(relpath) + if err != nil { + continue // ignore .meta paths + } + abspath := idx.path + sep + relpath + s, err := stringifiedRawRoaringFragment(abspath, index, field, view, shard) + panicOn(err) + //r += fmt.Sprintf("path:'%v' fragment contains:\n") + s + r += s + } + r += "]\n all-in-blake3:" + blake3sum16([]byte(r)) + "\n" + + return "roaring-" + r +} + +func stringifiedRawRoaringFragment(path string, index, field, view string, shard uint64) (r string, err error) { + + var info roaring.BitmapInfo + _ = info + var f *os.File + f, err = os.Open(path) + panicOn(err) + if err != nil { + return + } + + var fi os.FileInfo + fi, err = f.Stat() + panicOn(err) + if err != nil { + return + } + // Memory map the file. + data, err := syscall.Mmap(int(f.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + err = errors.Wrap(err, "mmapping") + return + } + defer func() { + err := syscall.Munmap(data) + if err != nil { + panic(fmt.Errorf("loadRawRoaringContainer: munmap failed: %v", err)) + } + panicOn(f.Close()) + }() + + // Attach the mmap file to the bitmap. + var rbm *roaring.Bitmap + rbm, _, err = roaring.InspectBinary(data, true, &info) + if err != nil { + err = errors.Wrap(err, "inspecting") + return + } + + citer, found := rbm.Containers.Iterator(0) + _ = found // probably gonna use just the Ops log instead, so don't panic if !found. + + for citer.Next() { + ckey, ct := citer.Value() + by := containerToBytes(ct) + hash := blake3sum16(by) + + cts := roaring.NewSliceContainers() + cts.Put(ckey, ct) + rbm := &roaring.Bitmap{Containers: cts} + srbm := bitmapAsString(rbm) + panicOn(err) + + bkey := string(badgerKey(index, field, view, shard, ckey)) + + r += fmt.Sprintf("%v -> %v (%v hot)\n", bkey, hash, ct.N()) + r += " ......." + srbm + "\n" + } + + return +} + +// listFilesUnderDir returns the paths of files found under directory root. +// If includeRoot is true, it returns the full path, otherwise paths are relative to root. +// If requriedSuffix is supplied, the returned file paths will end in that, +// and any other files found during the walk of the directory tree will be ignored. +// If ignoreEmpty is true, files of size 0 will be excluded. +func listFilesUnderDir(root string, includeRoot bool, requiredSuffix string, ignoreEmpty bool) (files []string, err error) { + if !dirExists(root) { + return nil, fmt.Errorf("listFilesUnderDir error: root directory '%v' not found", root) + } + n := len(root) + 1 + if includeRoot { + n = 0 + } + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if len(path) < n { + // ignore + } else { + if info == nil { + panic(fmt.Sprintf("info was nil for path = '%v'", path)) + } + if info.IsDir() { + // skip directories. + } else { + if ignoreEmpty && info.Size() == 0 { + return nil + } + if requiredSuffix == "" || strings.HasSuffix(path, requiredSuffix) { + files = append(files, path[n:]) + } + } + } + return nil + }) + return +} + +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 +} + +var _ = fileSize // happy linter + +// Dump prints to stdout the contents of the roaring Containers +// stored in idx. Its format may vary depending of the type of +// idx.Txf transaction factory that is in use. +// Mostly for debugging. +func (idx *Index) Dump(label string) { + ty := idx.Txf.TxType() + fileline := FileLine(2) + switch ty { + case badgerTxn: + fmt.Printf("%v Index.Dump('%v') for index '%v':\n%v\n", fileline, label, idx.name, idx.StringifiedBadgerKeys(nil)) + return + case blueGreenRoaringBadger, blueGreenBadgerRoaring: + fmt.Printf("%v Index.Dump('%v') for index '%v', RoaringTx:\n%v\n", fileline, label, idx.name, idx.StringifiedRoaringKeys()) + fmt.Printf("%v Index.Dump('%v') for index '%v', BadgerTx :\n%v\n", fileline, label, idx.name, idx.StringifiedBadgerKeys(nil)) + return + case roaringFragmentFilesTxn: + fmt.Printf("%v Index.Dump('%v') for index '%v', BadgerTx :\n%v\n", fileline, label, idx.name, idx.StringifiedRoaringKeys()) + return + } + panic(fmt.Errorf("%v Index.Dump('%v') for index '%v': no implementation for txtype '%v'\n", fileline, label, idx.name, ty)) +} + +func containerToBytes(ct *roaring.Container) []byte { + ty := roaring.ContainerType(ct) + switch ty { + case containerNil: + panic("nil container") + case containerArray: + return fromArray16(roaring.AsArray(ct)) + case containerBitmap: + return fromArray64(roaring.AsBitmap(ct)) + case containerRun: + return fromInterval16(roaring.AsRuns(ct)) + } + panic(fmt.Sprintf("unknown container type '%v'", int(ty))) +} diff --git a/utils_internal_test.go b/utils_internal_test.go index 386282adb..e9603b222 100644 --- a/utils_internal_test.go +++ b/utils_internal_test.go @@ -135,11 +135,13 @@ func (t *ClusterCluster) SetBit(index, field string, rowID, colID uint64, x *tim } if err := func() error { - tx, err := c.holder.Begin(true) + tx, err := c.holder.BeginTx(writable, c.holder.indexes[f.index]) + if tx != nil { + defer tx.Rollback() + } if err != nil { return err } - defer func() { _ = tx.Rollback() }() if _, err := f.SetBit(tx, rowID, colID, x); err != nil { return err diff --git a/vprint.go b/vprint.go new file mode 100644 index 000000000..bc40dfe45 --- /dev/null +++ b/vprint.go @@ -0,0 +1,113 @@ +// 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. + +package pilosa + +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()) +} diff --git a/xrbrsupport.go b/xrbrsupport.go index b06d94cc5..ebfd9db27 100644 --- a/xrbrsupport.go +++ b/xrbrsupport.go @@ -59,7 +59,7 @@ func (rbc *RBFConverter) Convert(index, field, view string, shard uint64, rb *ro if err != nil { return err } - tx, err := db.Begin(true) + tx, err := db.Begin(writable) if err != nil { return err }