From 2eb097c14d8986fe38e613ee95b8587c3e4ff15c Mon Sep 17 00:00:00 2001 From: Jason Aten Date: Sat, 5 Sep 2020 07:52:46 -0500 Subject: [PATCH] blue_green migration. holdbkg.go holder goroutine. - blue_green for doing migration. Called before Holder.Open finishes. - holdbkg.go added for index lookup. Less wedging between a deadlock and a race. - fix fault under read-only map under lmdb at TestExecutor_Execute_Row_Range/RowIDColumnID by doing cow in roaring. - roaring -tags gofuzz builds again - roaringparanoia build tag added to make test targets in Makefile - add rbf.NewDBWithAllocZero for out-of-bounds memory checks - .circleci/config.yml test-shardwidth-22 with large run container, kept OOM-ing we suspect. Fixes #819 --- .circleci/config.yml | 1 + Makefile | 51 +- api.go | 4 + badger.go | 1919 +++++++++++++++++++++++++++++++++++ badger_test.go | 1756 ++++++++++++++++++++++++++++++++ bluegreentx.go | 334 +++--- bluegreentx_test.go | 7 + catcher.go | 8 +- cluster.go | 6 +- cluster_internal_test.go | 9 +- dbshard.go | 320 +++++- dbshard_internal_test.go | 88 +- dbshard_test.go | 35 +- diagnostics.go | 2 +- executor.go | 14 +- executor_internal_test.go | 2 +- executor_test.go | 2 +- field.go | 30 +- field_internal_test.go | 23 +- field_test.go | 8 +- fragment.go | 19 +- fragment_internal_test.go | 130 +-- go.mod | 4 +- go.sum | 21 + gossip/gossip.go | 2 +- holdbkg.go | 135 +++ holder.go | 255 +++-- holder_internal_test.go | 79 +- holder_test.go | 6 +- index.go | 66 +- index_internal_test.go | 2 +- license.exceptions | 1 + lmdb.go | 101 +- lmdb_other.go | 10 +- lmdb_test.go | 38 +- mmap_test.go | 2 +- rbf.go | 99 +- rbf/cursorx.go | 9 +- rbf/db.go | 70 ++ rbf/db_test.go | 80 ++ rbf/tx.go | 14 +- rbf/tx_test.go | 2 +- roaring/fuzzer.go | 8 +- roaring/roaring.go | 28 +- rrtx.go | 292 ++++-- rrtx_internal_test.go | 63 ++ server/server.go | 3 + stattx.go | 8 +- synthload/synthload_test.go | 361 +++++++ synthload/testindex.tar.gz | Bin 0 -> 234061 bytes synthload/vprint.go | 170 ++++ test/holder.go | 8 +- test/index.go | 2 +- tx.go | 7 +- txfactory.go | 219 +++- txfactory_internal_test.go | 263 ++++- util.go | 120 +++ utils_internal_test.go | 8 +- view.go | 24 +- view_internal_test.go | 1 + 60 files changed, 6703 insertions(+), 646 deletions(-) create mode 100644 badger.go create mode 100644 badger_test.go create mode 100644 holdbkg.go create mode 100644 rrtx_internal_test.go create mode 100644 synthload/synthload_test.go create mode 100644 synthload/testindex.tar.gz create mode 100644 synthload/vprint.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 16180ae16..3a54b16b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -201,6 +201,7 @@ workflows: - test: name: test-shardwidth-22 shard_width: "22" + resource_class: large requires: - setup - cluster-tests: diff --git a/Makefile b/Makefile index b082fceeb..65840bf4d 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ RELEASE_ENABLED = $(subst 0,,$(RELEASE)) NOCHECKPTR=$(shell go version | grep -q 'go1.1[4,5,6,7]' && echo \"-gcflags=all=-d=checkptr=0\" ) BUILD_TAGS += $(if $(RELEASE_ENABLED),release) BUILD_TAGS += shardwidth$(SHARD_WIDTH) +TEST_TAGS = roaringparanoia define LICENSE_HASH_CODE head -13 $1 | sed -e 's/Copyright 20[0-9][0-9]/Copyright 20XX/g' | shasum | cut -f 1 -d " " endef @@ -38,11 +39,11 @@ vendor: go.mod # Run test suite test: - go test ./... -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) + go test ./... -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v # Run test suite with race flag test-race: - go test ./... -tags='$(BUILD_TAGS)' $(TESTFLAGS) -race $(NOCHECKPTR) -timeout 60m -v + go test ./... -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) -race $(NOCHECKPTR) -timeout 60m -v testv: topt testvsub @@ -57,7 +58,7 @@ testvsub: set -e; for i in ctl http pg pql rbf roaring server sql txkey; do \ echo; echo "___ testing subpkg $$i"; \ cd $$i; pwd; \ - go test -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -timeout 60m || break; \ + go test -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -timeout 60m || break; \ echo; echo "999 done testing subpkg $$i"; \ cd ..; \ done @@ -66,7 +67,7 @@ testvsub-race: set -e; for i in ctl http pg pql rbf roaring server sql txkey; do \ echo; echo "___ testing subpkg $$i -race"; \ cd $$i; pwd; \ - go test -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -race -timeout 60m || break; \ + go test -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -v -race -timeout 60m || break; \ echo; echo "999 done testing subpkg $$i -race"; \ cd ..; \ done @@ -194,55 +195,55 @@ pilosa-chk: # Run Pilosa tests inside Docker container 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) ./... + docker run --rm -v $(PWD):/go/src/$(CLONE_URL) -w /go/src/$(CLONE_URL) golang:$(GO_VERSION) go test -tags='$(BUILD_TAGS) $(TEST_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: mv log.topt.roar log.topt.roar.prev || true - go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.roar + go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.roar @echo " log.topt.roar green: \c"; cat log.topt.roar | grep PASS |wc -l @echo " log.topt.roar red: \c"; cat log.topt.roar | grep '\-\-\- FAIL' |wc -l topt-badger: mv log.topt.badger log.topt.badger.prev || true - PILOSA_TXSRC=badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger + PILOSA_TXSRC=badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger @echo " log.topt.badger green: \c"; cat log.topt.badger | grep PASS |wc -l @echo " log.topt.badger red: \c"; cat log.topt.badger | grep '\-\-\- FAIL' |wc -l topt-badger-race: mv log.topt.badger-race log.topt.badger-race.prev || true - PILOSA_TXSRC=badger go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger-race + PILOSA_TXSRC=badger go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.badger-race @echo " log.topt.badger-race green: \c"; cat log.topt.badger-race | grep PASS |wc -l @echo " log.topt.badger-race red: \c"; cat log.topt.badger-race | grep '\-\-\- FAIL' |wc -l topt-rbf: mv log.topt.rbf log.topt.rbf.prev || true - PILOSA_TXSRC=rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.rbf + PILOSA_TXSRC=rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.rbf @echo " log.topt.rbf green: \c"; cat log.topt.rbf | grep PASS |wc -l @echo " log.topt.rbf red: \c"; cat log.topt.rbf | grep '\-\-\- FAIL' |wc -l topt-rbf-race: mv log.topt.rbf-race log.topt.rbf-race.prev || true - PILOSA_TXSRC=rbf go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -timeout 120m 2>&1 | tee log.topt.rbf-race + PILOSA_TXSRC=rbf go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) -timeout 120m 2>&1 | tee log.topt.rbf-race @echo " log.topt.rbf-race green: \c"; cat log.topt.rbf-race | grep PASS |wc -l @echo " log.topt.rbf-race red: \c"; cat log.topt.rbf-race | grep '\-\-\- FAIL' |wc -l topt-lmdb: mv log.topt.lmdb log.topt.lmdb.prev || true - PILOSA_TXSRC=lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb + PILOSA_TXSRC=lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb @echo " log.topt.lmdb green: \c"; cat log.topt.lmdb | grep PASS |wc -l @echo " log.topt.lmdb red: \c"; cat log.topt.lmdb | grep '\-\-\- FAIL' |wc -l topt-lmdb-race: mv log.topt.lmdb log.topt.lmdb.prev || true - PILOSA_TXSRC=lmdb go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb-race + PILOSA_TXSRC=lmdb go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lmdb-race @echo " log.topt.lmdb-race green: \c"; cat log.topt.lmdb-race | grep PASS |wc -l @echo " log.topt.lmdb-race red: \c"; cat log.topt.lmdb-race | grep '\-\-\- FAIL' |wc -l topt-race: mv log.topt.race log.topt.race.prev || true - go test -race -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.race + go test -race -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.race @echo " log.topt.race green: \c"; cat log.topt.race | grep PASS |wc -l @echo " log.topt.race red: \c"; cat log.topt.race | grep '\-\-\- FAIL' |wc -l @@ -251,73 +252,73 @@ topt-race: bg-rr: # shorthand for bluegreen test with A:badger; B:roaring mv log.bg-rr log.bg-rr.prev || true - PILOSA_TXSRC=badger_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rr + PILOSA_TXSRC=badger_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rr @echo " log.bg-rr green: \c"; cat log.bg-rr | grep PASS |wc -l @echo " log.bg-rr red: \c"; cat log.bg-rr | grep '\-\-\- FAIL' |wc -l rr-bg: # bluegreen with A:roaring; B:badger (B's values are returned). mv log.bg.roar_bg log.bg.roar_bg.prev || true - PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg + PILOSA_TXSRC=roaring_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-bg @echo " log.rr-bg green: \c"; cat log.rr-bg | grep PASS |wc -l @echo " log.rr-bg red: \c"; cat log.rr-bg | grep '\-\-\- FAIL' |wc -l rbf-rr: mv log.rbf-rr log.rbf-rr.prev || true - PILOSA_TXSRC=rbf_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-rr + PILOSA_TXSRC=rbf_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-rr @echo " log.rbf-rr green: \c"; cat log.rbf-rr | grep PASS |wc -l @echo " log.rbf-rr red: \c"; cat log.rbf-rr | grep '\-\-\- FAIL' |wc -l rr-rbf: mv log.rr-rbf log.rr-rbf.prev || true - PILOSA_TXSRC=roaring_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-rbf + PILOSA_TXSRC=roaring_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-rbf @echo " log.rr-rbf green: \c"; cat log.rr-rbf | grep PASS |wc -l @echo " log.rr-rbf red: \c"; cat log.rr-rbf | grep '\-\-\- FAIL' |wc -l rbf-bg: mv log.rbf-bg log.rbf-bg.prev || true - PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-bg + PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-bg @echo " log.rbf-bg green: \c"; cat log.rbf-bg | grep PASS |wc -l @echo " log.rbf-bg red: \c"; cat log.rbf-bg | grep '\-\-\- FAIL' |wc -l bg-rbf: mv log.bg-rbf log.bg-rbf.prev || true - PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rbf + PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.bg-rbf @echo " log.bg-rbf green: \c"; cat log.bg-rbf | grep PASS |wc -l @echo " log.bg-rbf red: \c"; cat log.bg-rbf | grep '\-\-\- FAIL' |wc -l rbf-lm: mv log.rbf-lm log.rbf-lm.prev || true - PILOSA_TXSRC=rbf_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-lm + PILOSA_TXSRC=rbf_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rbf-lm @echo " log.rbf-lm green: \c"; cat log.rbf-lm | grep PASS |wc -l @echo " log.rbf-lm red: \c"; cat log.rbf-lm | grep '\-\-\- FAIL' |wc -l lm-rbf: mv log.lm-rbf log.lm-rbf.prev || true - PILOSA_TXSRC=badger_rbf go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rbf + PILOSA_TXSRC=lmdb_rbf go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rbf @echo " log.lm-rbf green: \c"; cat log.lm-rbf | grep PASS |wc -l @echo " log.lm-rbf red: \c"; cat log.lm-rbf | grep '\-\-\- FAIL' |wc -l lm-rr: mv log.lm-rr log.lm-rr.prev || true - PILOSA_TXSRC=lmdb_roaring go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rr + PILOSA_TXSRC=lmdb_roaring go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.lm-rr @echo " log.lm-rr green: \c"; cat log.lm-rr | grep PASS |wc -l @echo " log.lm-rr red: \c"; cat log.lm-rr | grep '\-\-\- FAIL' |wc -l rr-lm: mv log.rr-lm log.rr-lm.prev || true - PILOSA_TXSRC=roaring_lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-lm + PILOSA_TXSRC=roaring_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.rr-lm @echo " log.rr-lm green: \c"; cat log.rr-lm | grep PASS |wc -l @echo " log.rr-lm red: \c"; cat log.rr-lm | grep '\-\-\- FAIL' |wc -l bg-lm: mv log.topt.bg-lm log.topt.bg-lm.prev || true - PILOSA_TXSRC=badger_lmdb go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.bg-lm + PILOSA_TXSRC=badger_lmdb go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.bg-lm @echo " log.topt.bg-lm green: \c"; cat log.topt.bg-lm | grep PASS |wc -l @echo " log.topt.bg-lm red: \c"; cat log.topt.bg-lm | grep '\-\-\- FAIL' |wc -l lm-bg: mv log.topt.lm-bg log.topt.lm-bg.prev || true - PILOSA_TXSRC=lmdb_badger go test -v -tags='$(BUILD_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lm-bg + PILOSA_TXSRC=lmdb_badger go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) $(NOCHECKPTR) 2>&1 | tee log.topt.lm-bg @echo " log.topt.lm-bg green: \c"; cat log.topt.lm-bg | grep PASS |wc -l @echo " log.topt.lm-bg red: \c"; cat log.topt.lm-bg | grep '\-\-\- FAIL' |wc -l diff --git a/api.go b/api.go index 260c40c6f..f462830ff 100644 --- a/api.go +++ b/api.go @@ -54,6 +54,10 @@ type API struct { Serializer Serializer } +func (api *API) Holder() *Holder { + return api.holder +} + // apiOption is a functional option type for pilosa.API type apiOption func(*API) error diff --git a/badger.go b/badger.go new file mode 100644 index 000000000..66941bbb3 --- /dev/null +++ b/badger.go @@ -0,0 +1,1919 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "math" + "os" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + badger "github.com/dgraph-io/badger/v2" + badgeroptions "github.com/dgraph-io/badger/v2/options" + "github.com/pilosa/pilosa/v2/hash" + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/testhook" + "github.com/pilosa/pilosa/v2/txkey" + "github.com/pkg/errors" +) + +// 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 + +const BadgerLogToStderr = false + +func init() { + // badger test output clutters up the screen, dump to /dev/null for now. + null, err := os.Open(os.DevNull) + panicOn(err) + var out io.Writer = null + if BadgerLogToStderr { + // view badger logs + out = os.Stderr + } + badgerTestLogger = &BadgerLog{Logger: log.New(out, "badger ", log.LstdFlags)} + badgerDefaultLogger = badgerTestLogger + + // BadgerDB recommends a minimum of 128 GOMAXPROCS to make use of the IOPs + // 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." + // + // moved to holder init + //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...) +} + +// badgerRegistrar facilitates shutdown +// of all the badger databases started under +// tests. Its needed because most tests don't cleanup +// the *Index(es) they create. But we still +// want to shutdown badgerDB goroutines +// after tests run. +// +// It also allows opening the same path twice to +// result in sharing the same open database handle, and +// thus the same transactional guarantees. +// +type badgerRegistrar struct { + mu sync.Mutex + mp map[*BadgerDBWrapper]bool + + path2db map[string]*BadgerDBWrapper +} + +func (r *badgerRegistrar) Size() int { + r.mu.Lock() + defer r.mu.Unlock() + nmp := len(r.mp) + npa := len(r.path2db) + if nmp != npa { + panic(fmt.Sprintf("nmp=%v, vs npa=%v", nmp, npa)) + } + return nmp +} + +var globalBadgerReg *badgerRegistrar = newBadgerTestRegistrar() + +func newBadgerTestRegistrar() *badgerRegistrar { + return &badgerRegistrar{ + mp: make(map[*BadgerDBWrapper]bool), + path2db: make(map[string]*BadgerDBWrapper), + } +} + +// register each badger created under tests, so we +// can clean them up. This is called by openBadgerDBWrapper() while +// holding the r.mu.Lock, since it needs to atomically +// check the registry and make a new instance only +// if one does not exist for its path, and otherwise +// return the existing instance. +func (r *badgerRegistrar) unprotectedRegister(w *BadgerDBWrapper) { + r.mp[w] = true + r.path2db[w.path] = w +} + +// unregister removes w from r +func (r *badgerRegistrar) unregister(w *BadgerDBWrapper) { + r.mu.Lock() + delete(r.mp, w) + delete(r.path2db, w.path) + r.mu.Unlock() +} + +func DumpAllBadger() { + globalBadgerReg.mu.Lock() + defer globalBadgerReg.mu.Unlock() + for w := range globalBadgerReg.mp { + _ = w + AlwaysPrintf("this badger path='%v' has: \n%v\n", w.path, w.StringifiedBadgerKeys(nil, true)) + } +} + +// 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. +// +// openBadgerDB will check the registry and make a new instance only +// if one does not exist for its bpath. Otherwise it returns +// the existing instance. This insures only one badgerDB +// per bpath in this pilosa node. +func (r *badgerRegistrar) OpenDBWrapper(bpath string, doAllocZero bool) (DBWrapper, error) { + // now that newTxFactory can call us directly, we might not + // have the -badgerdb suffix. + bpath = badgerPath(bpath) + + r.mu.Lock() + defer r.mu.Unlock() + w, ok := r.path2db[bpath] + if ok { + // creates the effect of having only one badger open per pilosa node. + return w, nil + } + // otherwise, make a new badger and store it in globalBadgerReg + + // regular: works on amd64, but 386 doesn't work. + opt := badger.DefaultOptions(bpath).WithLogger(badgerDefaultLogger) + + opt.Compression = badgeroptions.None // turn off compression. + opt.ZSTDCompressionLevel = 0 // really, just in case. + opt.SyncWrites = true // default is true, safe. + //opt.KeepL0InMemory = true // speedup? + + // MaxCacheSize docs: + // + // how much data cache should hold in memory. A small size of + // cache means lower memory consumption and lookups/iterations + // would take longer. It is recommended to use a cache if you're + // using compression or encryption. If compression and + // encryption both are disabled, adding a cache will lead to + // unnecessary overhead which will affect the read performance. + // Setting size to zero disables the cache altogether. + //opt.MaxCacheSize = 1 << 30 // slows down 135 sec vs 113 sec on our benchmark + //opt.MaxCacheSize = 0 + opt.LoadBloomsOnOpen = false // should speed up start-up time. + + //opt.KeepBlocksInCache = true // default false + //opt.KeepBlockIndicesInCache = true // default false + + opt.BlockSize = 8 * 1024 // default 4 * 1024 + + // to get memory only do: + //opt := badger.DefaultOptions("").WithLogger(badgerDefaultLogger).WithInMemory(true) + + //db, err := badger.Open(opt) + db, err := badger.OpenManaged(opt) + if err != nil { + return nil, err + } + halt := make(chan bool) + w = &BadgerDBWrapper{ + reg: r, + path: bpath, + db: db, + halt: halt, + hasher: hash.NewBlake3Hasher(), + doAllocZero: doAllocZero, + } + _ = testhook.Opened(NewAuditor(), w, nil) + r.unprotectedRegister(w) + + w.startBadgerGarbageCollectionBackgroundGoro() + return w, nil +} + +func (w *BadgerDBWrapper) DeleteDBPath(dbs *DBShard) error { + panic("TODO") +} + +// 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) + } + prefix := txkey.IndexOnlyPrefix(indexName) + return w.DeletePrefix(prefix) +} + +// 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) + +var globalNextTxSnBadger int64 + +// BadgerDBWrapper provides the NewTx() 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 + + // track our registrar for Close / goro leak reporting purposes. + reg *badgerRegistrar + + // 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 *hash.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 + + // stack() from our creation point, to track tests + // that haven't closed us. + startStack string + + DeleteEmptyContainer bool + + writeBatch *badger.WriteBatch +} + +func (w *BadgerDBWrapper) Path() string { + return w.path +} + +func (w *BadgerDBWrapper) SetHolder(h *Holder) { + // don't need it at the moment + //w.h = h +} + +// EntryCount for Badger returns a count of either 0 or 1. A 1 +// means it +func (w *BadgerDBWrapper) HasData() (has bool, err error) { + tx, err := w.NewTx(!writable, "", Txo{Write: false}) + if err != nil { + return false, err + } + defer tx.Rollback() + bi := NewBadgerIterator(tx.(*BadgerTx), nil) + defer bi.Close() + bi.Seek(nil) + if bi.it.Valid() { + return true, nil + } + return false, nil +} + +func (w *BadgerDBWrapper) CleanupTx(tx Tx) { + r := tx.(*BadgerTx) + r.mu.Lock() + defer r.mu.Unlock() + w.unprotectedCleanupTx(r) +} + +func (w *BadgerDBWrapper) unprotectedCleanupTx(btx *BadgerTx) { + if btx.done { + return + } + btx.done = true + btx.o.dbs.Cleanup(btx) // release the read/write lock. +} + +// 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 + "]" +} + +// NewTx 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. +// +// initialIndexName is optional. It is set by the TxFactory from the Txo +// options provided at the Tx creation point. It allows us to recognize +// and isolate cross-index queries more quickly. It can always be empty "" +// but when set is highly useful for debugging. It has no impact +// on transaction behavior. +// +func (w *BadgerDBWrapper) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) { + + sn := atomic.AddInt64(&globalNextTxSnBadger, 1) + + tx = &BadgerTx{ + frag: o.Fragment, + write: write, + tx: w.db.NewTransactionAt(1, write), + Db: w, + doAllocZero: w.doAllocZero, + initialIndexName: initialIndexName, + DeleteEmptyContainer: w.DeleteEmptyContainer, + o: o, + sn: sn, + } + return +} + +// Close shuts down the Badger database. +func (w *BadgerDBWrapper) Close() (err error) { + + w.muDb.Lock() + defer w.muDb.Unlock() + if !w.closed { + w.reg.unregister(w) + close(w.halt) + w.closed = true + } + _ = testhook.Closed(NewAuditor(), w, nil) + 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 + + frag *fragment + opcount int + + doAllocZero bool + + // for tracking txn boundary issues, track all the memory + // that we deploy for roaring containers, and zero it on + // transaction commit/rollback. + acMu sync.Mutex // protect ourAllocs and ourContainers + ourAllocs [][]byte + ourContainers []*roaring.Container + + initialIndexName string + + DeleteEmptyContainer bool + + // We must avoid writing more than 10MB to badger in + // one transaction. If we go over, then + // we'll get a ErrTxnTooBig error. At that point + // we can't commit more, because the transaction + // will "conflict". So we must monitor + // totals written and auto-commit before going + // over the limits to avoid wedging into an + // unrecoverable state. + // + // We now use the approach described in https://github.com/dgraph-io/badger/issues/539 + // wherein "I fixed it by using a ManagedDB and using a commtTs of 1 always." + // But see also + // https://github.com/dgraph-io/badger/issues/409 + // wrt to "Write skew anomaly with concurrent transactions". + // which means we probably still only want one writeTx at a time + // even for badger. + writeCount int + writeByteCount int + + o Txo + sn int64 // serial number + + done bool +} + +func (tx *BadgerTx) IsDone() (done bool) { + tx.mu.Lock() + done = tx.done + tx.mu.Unlock() + return +} + +func (tx *BadgerTx) Group() *TxGroup { + return tx.o.Group +} + +func (tx *BadgerTx) Options() Txo { + return tx.o +} + +func (tx *BadgerTx) Type() string { + return BadgerTxn +} + +func (tx *BadgerTx) UseRowCache() bool { + //the row cache speeds up queries. + 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() { + + tx.acMu.Lock() + defer tx.acMu.Unlock() + for _, s := range tx.ourAllocs { + + // The Go compiler recognizes the following pattern and inserts + // an efficient memclr instruction. + // See https://github.com/golang/go/issues/5373 + // and https://codereview.appspot.com/137880043 + for i := range s { + s[i] = 0 + // or + // Seebs suggested we might see even more crashes :) + // but since it will be slow (no memclr), we'll leave the default 0 for now. + //s[i] = -2 + } + } + // keep this around if we need to activate out-of-mmap memory access again. + //for _, v := range tx.ourContainers { + //v.Invalid = true + //v.Tx = tx + //} +} + +// Pointer gives us a memory address for the underlying transaction for debugging. +// It is public because we use it in roaring to report invalid container memory access +// outside of a transaction. +func (tx *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() + } + + tx.Db.unprotectedCleanupTx(tx) +} + +// 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.CommitAt(1, nil) // must hold tx.mu mutex lock + + if tx.doAllocZero { + tx.overWriteOurAllocs() + } + + tx.Db.unprotectedCleanupTx(tx) + return err +} + +// Readonly returns true iff the BadgerTx is read-only. +func (tx *BadgerTx) Readonly() bool { + return !tx.write +} + +// 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) +} + +// 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 := txkey.Key(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 +} + +func (w *BadgerDBWrapper) NewWriteBatch() { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch != nil { + panic("must FlushWriteBatch() before calling NewWriteBatch()") + } + w.writeBatch = w.db.NewWriteBatch() +} + +// Flush any remaining un-committed writes in progress. +func (w *BadgerDBWrapper) FlushWriteBatch() (err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch == nil { + panic("CommitWriteBatch error: no batch in progress") + } + err = w.writeBatch.Flush() + w.writeBatch = nil + return +} + +// Cancel any remaining un-committed writes in progress. +func (w *BadgerDBWrapper) CancelWriteBatch() { + w.muDb.Lock() + defer w.muDb.Unlock() + if w.writeBatch == nil { + panic("CancelWriteBatch error: no batch in progress") + } + w.writeBatch.Cancel() +} + +// PutContainer stores rc under the specified fragment and container ckey. +func (tx *BadgerTx) PutContainer(index, field, view string, shard uint64, ckey uint64, rc *roaring.Container) error { + + bkey := txkey.Key(index, field, view, shard, ckey) + var by []byte + + ct := roaring.ContainerType(rc) + + switch ct { + case roaring.ContainerArray: + by = fromArray16(roaring.AsArray(rc)) + case roaring.ContainerBitmap: + by = fromArray64(roaring.AsBitmap(rc)) + case roaring.ContainerRun: + by = fromInterval16(roaring.AsRuns(rc)) + case roaring.ContainerNil: + panic("wat? nil roaring.Container is unexpected, no?!?") + default: + panic(fmt.Sprintf("unknown roaring.Container type: %v", ct)) + } + + entry := badger.NewEntry(bkey, by).WithMeta(ct) + + tx.Db.muDb.Lock() + if tx.Db.writeBatch != nil { + err := tx.Db.writeBatch.SetEntry(entry) // Will create txns as needed. + tx.Db.muDb.Unlock() + return err + } + tx.Db.muDb.Unlock() + + tx.mu.Lock() + defer tx.mu.Unlock() + + tx.writeCount++ + sz := len(by) + len(bkey) + 2 + tx.writeByteCount += sz + + // The integration tests do large bit level loads that exceed 10MB. + // So we autocommit and start a new Txn if we are about to + // write too much into one Txn. + // + // The badger defaults limits are currently: + // maxBatchCount:104857, maxBatchSize:10066329 + // + // However, emprirically we still get ErrTnTooBig when + // tx.writeByteCount=5884222; or when tx.writeCount=16197. + // So duck under both those thresholds by some margin. + if tx.writeCount > 100 || tx.writeByteCount > 2000000 { + // avoid ErrTxnTooBig by commiting before going over the limits, + // because then we get a error: "Transaction Conflict. Please retry." + err := tx.tx.CommitAt(1, nil) + panicOn(err) + // badger docs: + // `ErrConflict is returned when a transaction conflicts with another transaction. This can + // happen if the read rows had been updated concurrently by another transaction. + // ErrConflict = errors.New("Transaction Conflict. Please retry")` + //if err == badger.ErrConflict { + // problem is, we don't have the previous entry handy now. + //} + if err != nil { + panic(fmt.Sprintf("commit failed on bkey '%v': err '%v'", string(bkey), err)) + } + tx.tx = tx.Db.db.NewTransactionAt(1, tx.write) + + tx.writeCount = 1 + tx.writeByteCount = sz + } + err := tx.tx.SetEntry(entry) + + // 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 { + + err = tx.tx.CommitAt(1, nil) + if err != nil { + panic(fmt.Sprintf("commit after TooBig failed on bkey '%v': err '%v'", string(bkey), err)) + } + + tx.tx = tx.Db.db.NewTransactionAt(1, tx.write) + //vv("NewTx write txn (p=%p) on gid=%v. b/c TooBig writeCount=%v; writeByteCount=%v", tx.tx, curGID(), tx.writeCount, tx.writeByteCount) + tx.writeCount = 1 + tx.writeByteCount = sz + + err = tx.tx.SetEntry(entry) + panicOn(err) + //panic(fmt.Sprintf("got error badger.ErrTxnTooBig, but we shoud never get this now; len(by) = %v; len(bkey)=%v; vs limit is 10MB. tx.writeCount=%v; tx.writeByteCount=%v;", len(by), len(bkey), tx.writeCount, tx.writeByteCount)) + } + return err +} + +// RemoveContainer deletes the container specified by the shard and container key ckey +func (tx *BadgerTx) RemoveContainer(index, field, view string, shard uint64, ckey uint64) error { + bkey := txkey.Key(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) { + return tx.addOrRemove(index, field, view, shard, batched, false, a...) +} + +// Remove clears all the specified a bits in the chosen fragment. +func (tx *BadgerTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) { + const batched = false + const remove = true + return tx.addOrRemove(index, field, view, shard, batched, remove, a...) +} + +func (tx *BadgerTx) addOrRemove(index, field, view string, shard uint64, batched, remove bool, a ...uint64) (changeCount int, err error) { + // pure hack to match RoaringTx + defer func() { + if !remove && !batched { + if changeCount > 0 { + changeCount = 1 + } + } + }() + + if len(a) == 0 { + return 0, nil + } + + // have to sort, b/c input is not always sorted. + sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) + + var lastHi uint64 = math.MaxUint64 // highbits is always less than this starter. + var rc *roaring.Container + var hi uint64 + var lo uint16 + + for i, v := range a { + + hi, lo = highbits(v), lowbits(v) + if hi != lastHi { + // either first time through, or changed to a different container. + // do we need put the last updated container now? + if i > 0 { + // not first time through, write what we got. + if remove && (rc == nil || rc.N() == 0) { + err = tx.RemoveContainer(index, field, view, shard, lastHi) + panicOn(err) + } else { + err = tx.PutContainer(index, field, view, shard, lastHi, rc) + panicOn(err) + } + } + // get the next container + rc, err = tx.Container(index, field, view, shard, hi) + panicOn(err) + } // else same container, keep adding bits to rct. + chng := false + // rc can be nil before, and nil after, in both Remove/Add below. + // The roaring container add() and remove() methods handle this. + if remove { + rc, chng = rc.Remove(lo) + } else { + rc, chng = rc.Add(lo) + } + if chng { + changeCount++ + } + lastHi = hi + } + // write the last updates. + if remove { + if rc == nil || rc.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, hi) + panicOn(err) + } else { + err = tx.PutContainer(index, field, view, shard, hi, rc) + panicOn(err) + } + } else { + if rc == nil || rc.N() == 0 { + panic("there should be no way to have an empty bitmap AFTER an Add() operation") + } + err = tx.PutContainer(index, field, view, shard, hi, rc) + panicOn(err) + } + return +} + +// 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 := txkey.Key(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 +} + +func (tx *BadgerTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { + + prefix := txkey.AllShardPrefix(index, field, view) + + bi := NewBadgerIterator(tx, prefix) + defer bi.Close() + bi.Seek(prefix) + if !bi.it.Valid() { + return + } + lastShard := uint64(0) + firstDone := false + for bi.Next() { + item := bi.it.Item() + key := item.Key() + shard := txkey.ShardFromKey(key) + if firstDone { + if shard != lastShard { + sliceOfShards = append(sliceOfShards, shard) + } + lastShard = shard + } else { + // first time + lastShard = shard + firstDone = true + sliceOfShards = append(sliceOfShards, shard) + } + + } + return +} + +// key is the container key for the first roaring Container +// roaring docs: Iterator returns a ContainterIterator which *after* a call to Next(), a call to Value() will +// return the first container at or after key. found will be true if a +// container is found at key. +// +// 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: "idx:'i';fld:'f';vw:'v';shd:'00000000000000000000';key@00000000000000000000" + needle := txkey.Key(index, field, view, shard, firstRoaringContainerKey) + + // prefix example: "idx:'i';fld:'f';vw:'v';shard:'00000000000000000000';key@" + prefix := txkey.Prefix(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 + } + item := bi.it.Item() + // have to compare b/c badger might give us valid iterator + // that is past our needle if needle isn't present. + return bi, bytes.Equal(item.Key(), needle), 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 txkey.Keys that start with prefix. +func NewBadgerIterator(tx *BadgerTx, prefix []byte) (bi *BadgerIterator) { + + 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, + } + tx.Db.muOpenTxIt.Lock() + if tx.Db.openIt == nil { + tx.Db.openIt = make(map[*BadgerIterator]bool) + } + tx.Db.openIt[bi] = false // true for reverse, false for forward iteration. + tx.Db.muOpenTxIt.Unlock() + + bi.it.Seek(prefix) + return +} + +// 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 = txkey.KeyExtractContainerKey(key) + + err := item.Value(func(v []byte) error { + c = bi.tx.toContainer(item.UserMeta(), v) + return nil + }) + panicOn(err) + return +} + +// 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. +// Returns zero if the bitmap is empty. Odd, but this is what roaring.Max does. +func (tx *BadgerTx) Max(index, field, view string, shard uint64) (uint64, error) { + + prefix := txkey.Prefix(index, field, view, shard) + seekto := txkey.Prefix(index, field, view, shard+1) + + it := NewBadgerReverseIterator(tx, prefix, seekto) // this iterator is still open, when we commit/discard tx. + defer it.Close() + + if !it.it.Valid() { + return 0, nil + } + hb, rc := it.Value() // getting it returns invalid, as in empty iterator + 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. +// roaring.countRange counts the number of bits set between [start, end). +func (tx *BadgerTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + if tx.frag == nil { + return tx.countRangeNoFrag(index, field, view, shard, start, end) + } + + // For speed, exploit the fact that on startup the rowCache will + // have already loaded fragments. + rowID := start / ShardWidth + row, err := tx.frag.unprotectedRow(tx, rowID) + if err != nil { + return 0, err + } + return row.Count(), nil +} + +// CountRange returns the count of hot bits in the start, end range on the fragment. +// roaring.countRange counts the number of bits set between [start, end). +func (tx *BadgerTx) countRangeNoFrag(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) { + + if start >= end { + return 0, nil + } + + skey := highbits(start) + ekey := highbits(end) + + citer, found, err := tx.ContainerIterator(index, field, view, shard, skey) + _ = found + panicOn(err) + + defer citer.Close() + + // If range is entirely in one container then just count that range. + if skey == ekey { + citer.Next() + _, c := citer.Value() + return uint64(c.CountRange(int32(lowbits(start)), int32(lowbits(end)))), nil + } + + for citer.Next() { + k, c := citer.Value() + if k < skey { + citer.Close() + panic(fmt.Sprintf("should be impossible for k(%v) to be less than skey(%v). tx p=%p", k, skey, tx)) + } + + // k > ekey handles the case when start > end and where start and end + // are in different containers. Same container case is already handled above. + if k > ekey { + break + } + if k == skey { + n += uint64(c.CountRange(int32(lowbits(start)), roaring.MaxContainerVal+1)) + continue + } + if k < ekey { + n += uint64(c.N()) + continue + } + if k == ekey { + n += uint64(c.CountRange(0, int32(lowbits(end)))) + break + } + } + + return n, nil +} + +// OffsetRange creates a new roaring.Bitmap to return in other. For all the +// hot bits in [start, endx) of the chosen fragment, it stores +// them into other but with offset added to their bit position. +// The primary client is doing this, using ShardWidth, already; see +// fragment.rowFromStorage() in fragment.go. For example: +// +// data, err := tx.OffsetRange(f.index, f.field, f.view, f.shard, +// f.shard*ShardWidth, rowID*ShardWidth, (rowID+1)*ShardWidth) +// ^ offset ^ start ^ endx +// +// The start and endx arguments are container keys that have been shifted left by 16 bits; +// their highbits() will be taken to determine the actual container keys. This +// is done to conform to the roaring.OffsetRange() argument convention. +// +func (tx *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) + + needle := txkey.Key(index, field, view, shard, hi0) + prefix := txkey.Prefix(index, field, view, shard) + + n2, pre2 := txkey.KeyAndPrefix(index, field, view, shard, hi0) + if string(n2) != string(needle) { + panic(fmt.Sprintf("problem! n2(%v) != needle(%v), txkey.KeyAndPrefix not consitent with txkey.Key()", string(n2), string(needle))) + } + if string(pre2) != string(prefix) { + panic(fmt.Sprintf("problem! pre2(%v) != prefix(%v), txkey.KeyAndPrefix not consitent with txkey.Key()", string(pre2), string(prefix))) + } + + it := NewBadgerIterator(tx, prefix) + defer it.Close() + it.Seek(needle) + for ; it.it.ValidForPrefix(prefix); it.Next() { + item := it.it.Item() + bkey := item.Key() + k := txkey.KeyExtractContainerKey(bkey) + + // >= hi1 is correct b/c endx cannot have any lowbits set. + 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, data []byte) (changed int, rowSet map[uint64]int, err error) { + n := itr.Len() + if n == 0 { + return + } + rowSet = make(map[uint64]int) + + var currRow uint64 + + var oldC *roaring.Container + for itrKey, synthC := itr.NextContainer(); synthC != nil; itrKey, synthC = itr.NextContainer() { + if rowSize != 0 { + currRow = itrKey / rowSize + } + nsynth := int(synthC.N()) + if nsynth == 0 { + continue + } + // INVAR: nsynth > 0 + + oldC, err = tx.Container(index, field, view, shard, itrKey) + panicOn(err) + if err != nil { + return + } + + if oldC == nil || oldC.N() == 0 { + // no container at the itrKey in 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 tx.DeleteEmptyContainer && newC.N() == 0 { + err = tx.RemoveContainer(index, field, view, shard, itrKey) + if err != nil { + return + } + continue + } + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + return + } + continue + } + } else { + // setting bits + + existN := oldC.N() + if existN == roaring.MaxContainerVal+1 { + // completely full container already, set will do nothing. so changed of 0 default is perfect. + continue + } + if existN == 0 { + // can nsynth be zero? No, because of the continue/invariant above where nsynth > 0 + changed += nsynth + rowSet[currRow] += nsynth + err = tx.PutContainer(index, field, view, shard, itrKey, synthC) + if err != nil { + return + } + continue + } + + newC := roaring.Union(oldC, synthC) // UnionInPlace was giving us crashes on overly large containers. + + if roaring.ContainerType(newC) == roaring.ContainerBitmap { + newC.Repair() // update the bit-count so .n is valid. b/c UnionInPlace doesn't update it. + } + if newC.N() != existN { + changes := int(newC.N() - existN) + changed += changes + rowSet[currRow] += changes + + err = tx.PutContainer(index, field, view, shard, itrKey, newC) + if err != nil { + panicOn(err) + return + } + continue + } + } + } + return +} + +func (tx *BadgerTx) toContainer(typ byte, v []byte) (r *roaring.Container) { + + if len(v) == 0 { + return nil + } + + var w []byte + useRowCache := tx.UseRowCache() + if tx.doAllocZero || useRowCache { + // Do electric fence-inspired bad-memory read detection. + // + // The v []byte lives in BadgerDB's memory-mapped vlog-file, + // and Badger will recycle it after tx ends with rollback or commit. + // + // Problem is, at least some operations were not respecting transaction boundaries. + // This technique helped us find them. The rowCache was an example. + // + // See the global const DetectMemAccessPastTx + // at the top of txfactory.go to activate/deactivate this. + // + // Seebs suggested this nice variation: we could use individual mmaps for these + // copies, which would be unusable in production, but workable for testing, and then unmap them, + // which would get us probable segfaults on future accesses to them. + // + // The go runtime also has an -efence flag which may be similarly useful if really pressed. + // + w = make([]byte, len(v)) + copy(w, v) + + if !useRowCache { + // register w so we can catch out-of-tx memory access + tx.acMu.Lock() + defer tx.acMu.Unlock() + tx.ourAllocs = append(tx.ourAllocs, w) + } + } else { + w = v + } + + switch typ { + case roaring.ContainerArray: + c := roaring.NewContainerArray(toArray16(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + case roaring.ContainerBitmap: + c := roaring.NewContainerBitmap(-1, toArray64(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + case roaring.ContainerRun: + c := roaring.NewContainerRun(toInterval16(w)) + if tx.doAllocZero { + // tx.acMu was acquired above, and Unlock deferred. + tx.ourContainers = append(tx.ourContainers, c) + } + return c + default: + panic(fmt.Sprintf("unknown container: %v", typ)) + } +} + +// StringifiedBadgerKeys returns a string with all the container +// keys available in badger. +func (w *BadgerDBWrapper) StringifiedBadgerKeys(optionalUseThisTx Tx, short bool) (r string) { + if optionalUseThisTx == nil { + tx, _ := w.NewTx(!writable, "", Txo{}) + defer tx.Rollback() + r = stringifiedBadgerKeysTx(tx.(*BadgerTx), short) + return + } + + btx, ok := optionalUseThisTx.(*BadgerTx) + if !ok { + return fmt.Sprintf("", optionalUseThisTx) + } + r = stringifiedBadgerKeysTx(btx, short) + return +} + +// countBitsSet returns the number of bits set (or "hot") in +// the roaring container value found by the txkey.Key() +// 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 +} + +func (tx *BadgerTx) Dump(short bool) { + fmt.Printf("BadgerTx %p Dump: %v\n", tx, stringifiedBadgerKeysTx(tx, short)) +} + +// 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, short bool) (r string) { + + r = "allkeys:[\n" + it := tx.tx.NewIterator(badger.DefaultIteratorOptions) // PrefetchValues true okay here. + defer it.Close() + any := false + for it.Rewind(); it.Valid(); it.Next() { + any = true + item := it.Item() + bkey := item.Key() + key := txkey.ToString(bkey) + ckey := txkey.KeyExtractContainerKey(bkey) + h := "" + srbm := "" + err := item.Value(func(val []byte) error { + h = 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, h, tx.countBitsSet(bkey)) + if !short { + r += " ......." + srbm + "\n" + } + } + r += "]\n all-in-blake3:" + hash.Blake3sum16([]byte(r)) + + if !any { + return "" + } + return "badger-" + r +} + +func (w *BadgerDBWrapper) DeleteField(index, field, fieldPath string) error { + + // under blue-green roaring_badger, the directory will not be found, b/c roaring will have + // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: + // "If the path does not exist, RemoveAll returns nil (no error)" + err := os.RemoveAll(fieldPath) + if err != nil { + return errors.Wrap(err, "removing directory") + } + prefix := txkey.FieldPrefix(index, field) + return w.DeletePrefix(prefix) +} + +func (w *BadgerDBWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { + prefix := txkey.Prefix(index, field, view, shard) + return w.DeletePrefix(prefix) +} + +func (w *BadgerDBWrapper) DeletePrefix(prefix []byte) error { + 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. + + noMoreKeysWithPrefix := false + const maxDeletesPerTxn = 100000 + + for !noMoreKeysWithPrefix { + + txn := w.db.NewTransactionAt(1, true) + //defer txn.Discard() + + 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. For + // example in TxFactor.CloseIndex() in txfactory.go:331. + 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 { + it.Close() + txn.Discard() + return err + } + } + it.Close() + err := txn.CommitAt(1, nil) + + // 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 +} + +func (tx *BadgerTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { + + rbm, err := tx.RoaringBitmap(index, field, view, shard) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader RoaringBitmap") + } + var buf bytes.Buffer + sz, err = rbm.WriteTo(&buf) + if err != nil { + return nil, -1, errors.Wrap(err, "RoaringBitmapReader rbm.WriteTo(buf)") + } + return ioutil.NopCloser(&buf), sz, err +} + +func (w *BadgerDBWrapper) OpenListString() (r string) { + return "BadgerDBWrapper.OpenListString not yet implemented" +} +func (w *BadgerDBWrapper) OpenSnList() (sns []int64) { + return nil // panic("BadgerDBWrapper.OpenSnList not yet implemented") +} + +// Sn retreives the serial number of the Tx. +func (tx *BadgerTx) Sn() int64 { + return tx.sn +} + +func (w *BadgerDBWrapper) Update(fn func(txn *badger.Txn) error) error { + txn := w.db.NewTransactionAt(1, true) + defer txn.Discard() + + if err := fn(txn); err != nil { + return err + } + + return txn.CommitAt(1, nil) +} diff --git a/badger_test.go b/badger_test.go new file mode 100644 index 000000000..a4ad6ab71 --- /dev/null +++ b/badger_test.go @@ -0,0 +1,1756 @@ +// 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. + +// explanation of build tags: +// +// badgerdb builds but won't run in 32-bit 386 world, as of 2020 July 20. +// See https://github.com/dgraph-io/badger/issues/1384 for any progress. +// What we see is that the value-log allocations immediately run out of +// memory. So we turn off 386 with a build tag to keep the .circleci happy. + +// +build !386 + +package pilosa + +import ( + "fmt" + "math" + "os" + "testing" + + "github.com/dgraph-io/badger/v2" + "github.com/pilosa/pilosa/v2/roaring" + "github.com/pilosa/pilosa/v2/testhook" + "github.com/pkg/errors" +) + +var _ = &roaring.Bitmap{} + +func init() { + testhook.RegisterPostTestHook(reportTestBadgersNeedingClose) +} + +// 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + + // 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.NewTx(writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + _, 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)) + ww, err := globalBadgerReg.OpenDBWrapper(path, DetectMemAccessPastTx) + panicOn(err) + w = ww.(*BadgerDBWrapper) + + // verify it is empty + allkeys := w.StringifiedBadgerKeys(nil, true) + if allkeys != "" { + panic(fmt.Sprintf("freshly created database was not empty! had keys:'%v'", allkeys)) + } + + return w, func() { + w.Close() // stop any started background GC goroutine. + os.RemoveAll(fn) + } +} + +// end of helper utilities +////////////////////////// + +////////////////////////// +// begin Tx method tests + +func TestBadger_DeleteFragment(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteFragment") + defer clean() + defer dbwrap.Close() + index, field, view, shard0 := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + + shard1 := uint64(1) + + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + shards := []uint64{shard0, shard1} + for _, s := range shards { + for _, v := range bits { + changed, err := tx.Add(index, field, view, s, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + } + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + } + err := tx.Commit() + panicOn(err) + + // end of setup + + survivor := shard0 + victim := shard1 + err = dbwrap.DeleteFragment(index, field, view, victim, nil) + panicOn(err) + + tx, _ = dbwrap.NewTx(!writable, index, Txo{}) + defer tx.Rollback() + + for _, s := range shards { + for _, v := range bits { + exists, err := tx.Contains(index, field, view, s, v) + panicOn(err) + if s == survivor { + if !exists { + panic(fmt.Sprintf("ARG survivor died : bit %v", v)) + } + } else if s == victim { // victim, should have been deleted + if exists { + panic(fmt.Sprintf("ARG victim lived : bit %v", v)) + } + } + } + } +} + +func TestBadger_Max_on_many_containers(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_Max_on_many_containers") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + + putmeValues := []uint64{0, 2 << 16, 4 << 16} + + for _, putme := range putmeValues { + badgerDBMustNotHaveBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + badgerDBMustHaveBitvalue(dbwrap, index, field, view, shard, putme) + } + + tx, _ := dbwrap.NewTx(!writable, index, Txo{}) + defer tx.Rollback() + + max, err := tx.Max(index, field, view, shard) + panicOn(err) + expected := putmeValues[len(putmeValues)-1] + if max != expected { + panic(fmt.Sprintf("expected Max() of %v but got max=%v", expected, max)) + } +} + +// and the rest + +func TestBadger_SetBitmap(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SetBitmap") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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_OffsetRange") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + + 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() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(!writable, index, Txo{}) + defer tx.Rollback() + bitvalue := uint64(0) + citer, found, err := tx.ContainerIterator(index, field, view, shard, bitvalue) + panicOn(err) + defer citer.Close() + if found { + panic("should not have found anything") + } + panicOn(err) +} + +func 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() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(bitvalue)) + if !found { + panic("ContainerIterator did not find the 42 bit") + } + panicOn(err) + defer citer.Close() + + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 0 { + panic("42 should have had key 0") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if !container.Contains(lowbits(bitvalue)) { + panic("container did not have our bitvalue!") + } + loopCount++ + if loopCount > 0 { // happier linter + break + } + } + if loopCount != 1 { + panic("ContainerIterator did not return a citer that scanned our set bit") + } +} + +func TestBadger_ContainerIterator_on_one_bit_fail_to_find(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := putme + 1 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + if !found { + panic("ContainerIterator did not find the searchme") + } + defer citer.Close() + loopCount := 0 + for citer.Next() { + key, container := citer.Value() + if key != 1 { + panic("Containeriterator searching for highbits(searchme) should not have had a bit") + } + if container == nil { + panic("container was nil") + } + if container.N() != 1 { + panic("put a bit in, but size of container was not 1") + } + if container.Contains(lowbits(searchme)) { + panic("container should have putme but not our searchme!") + } + loopCount++ + // only want first pass. keep linter happy by avoiding raw break + if loopCount > 0 { + break + } + } + panicOn(err) +} + +func TestBadger_ContainerIterator_empty_iteration_loop(t *testing.T) { + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_empty_iteration_loop") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + putme := uint64(1<<16) + 3 // in the key:1 container + searchme := uint64(1 << 17) // in the next container, key:2 + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, putme) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, putme) + panicOn(err) + if !exists { + panic("ARG putme was NOT SET!!!") + } + + // same Tx, continues in use. + + citer, found, err := tx.ContainerIterator(index, field, view, shard, highbits(searchme)) + panicOn(err) + if found { + panic("ContainerIterator found the searchme, when it should not have") + } + defer citer.Close() + if citer.Next() { + panic("expected no looping, 0 iterations, b/c started searchme past our data in putme") + } + + // expect to see a blow up from the citer.Value() call, verify that we do. + func() { + defer func() { + r := recover() + if r == nil { + panic("expected a panic from citer.Value() in this case") + } + }() + citer.Value() // should panic + }() + +} + +func TestBadger_ForEach_on_one_bit(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ContainerIterator_on_one_bit") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + bitvalue := uint64(42) + + // add a bit + changed, err := tx.Add(index, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + exists, err := tx.Contains(index, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + + // same Tx, continues in use. + count := 0 + err = tx.ForEach(index, field, view, shard, func(v uint64) error { + if v != bitvalue { + panic(fmt.Sprintf("bitvalue corrupt got %v want %v", v, bitvalue)) + } + count += 1 + return nil + }) + panicOn(err) + if count != 1 { + panic(fmt.Sprintf("Expected single iteration got %v ", count)) + } +} + +func 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.NewTx(writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + + 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.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.NewTransactionAt(1, !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_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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + 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.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) + + tx0, _ := dbwrap.NewTx(!writable, "no-index-avail", Txo{}) + tx := tx0.(*BadgerTx) + + 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.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) + + tx0, _ := dbwrap.NewTx(!writable, "no-index-avail", Txo{}) + tx := tx0.(*BadgerTx) + + 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() + index, field, view, shard := "i", "f", "v", uint64(0) + tx0, _ := dbwrap.NewTx(writable, index, Txo{}) + tx := tx0.(*BadgerTx) + defer tx.Rollback() + tx.DeleteEmptyContainer = true // traditional badger Tx behavior, but not Roaring. + + //bitvalue := uint64(42) + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the union in place with the same set gives no change. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != 0 { + panic(fmt.Sprintf("should have not changed any bits on the second import, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now test the clear path + clear = true + + for _, v := range bits { + // clear 1 bit at a time + data := getTestBitmapAsRawRoaring(v) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != 1 { + panic(fmt.Sprintf("should have changed 1 bit: '%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + } + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 0 { + panic(fmt.Sprintf("n = %v not zero so the clearbits didn't happen!", n)) + } + allkeys := stringifiedBadgerKeysTx(tx, false) + + // 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() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd, overlapping set and set them. + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize, nil) + _ = rowSet + if changed != 4 { + panic(fmt.Sprintf("should have changed 2 bits: the 1 and the 3, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) +} + +func TestBadger_ImportRoaringBits_clear_nonoverlapping_bits(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ImportRoaringBits_clear_nonoverlapping_bits") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + defer tx.Rollback() + + // get some roaring bits, get an itr RoaringIterator from them + rowSize := uint64(0) + //bits := []uint64{0} + bits := []uint64{0, 2, 1 << 16, 1<<16 + 2} //, 5, 1<<16 + 1, 2 << 16} + data := getTestBitmapAsRawRoaring(bits...) + itr, err := roaring.NewRoaringIterator(data) + panicOn(err) + + bits2 := []uint64{1, 2, 3, 1<<16 + 1, 1<<16 + 2, 1<<16 + 3} //, 5, 1<<16 + 1, 2 << 16} + data2 := getTestBitmapAsRawRoaring(bits2...) + itr2, err := roaring.NewRoaringIterator(data2) + panicOn(err) + + clear := false + logme := false + + changed, rowSet, err := tx.ImportRoaringBits(index, field, view, shard, itr, clear, logme, rowSize, nil) + _ = rowSet + if changed != len(bits) { + panic(fmt.Sprintf("should have changed %v bits: changed='%v', rowSet='%#v', err='%v'", len(bits), changed, rowSet, err)) + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic(fmt.Sprintf("ARG bitvalue was NOT SET!!! '%v'", v)) + } + } + + // now import the 2nd overlapping set and clear them. + clear = true + + changed, rowSet, err = tx.ImportRoaringBits(index, field, view, shard, itr2, clear, logme, rowSize, nil) + _ = rowSet + if changed != 2 { + panic(fmt.Sprintf("should have changed 1 bit: the 2, but we see changed='%v', rowSet='%#v', err='%v'", changed, rowSet, err)) + } + panicOn(err) + + n, err := tx.Count(index, field, view, shard) + panicOn(err) + if n != 2 { // just the 0 and the 1<<16 bits should be left set. + panic(fmt.Sprintf("n = %v not 2 so the clearbits didn't happen!", n)) + } + +} + +/* +func TestBadger_AutoCommit(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_AutoCommit") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + + // if we go over 100K writes, we should autocommit + // rather than panic. + for v := 0; v < 133444; v++ { + changed, err := tx.Add(index, field, view, shard, doBatched, uint64(v)) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + + err := tx.Commit() + panicOn(err) +} + +func TestBadger_BigWritesAvoidTxnTooLargeWithAutoCommit(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_BigWritesAvoidTxnTooLargeWithAutoCommit") + defer clean() + defer dbwrap.Close() + + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + + containerKey := uint64(0) + // setup + bits := make([]uint64, 1024) + n := 0 + for i := range bits { + bits[i] = ^uint64(0) + n += 64 + } + rc := roaring.NewContainerBitmap(n, bits) + + // if we go over 100K big writes, we should autocommit + // rather than panic. + for v := 0; v < 133444; v++ { + containerKey++ + err := tx.PutContainer(index, field, view, shard, containerKey, rc) + panicOn(err) + } + + err := tx.Commit() + panicOn(err) +} +*/ + +func TestBadger_DeleteIndex(t *testing.T) { + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteIndex") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx0, _ := dbwrap.NewTx(writable, index, Txo{}) + tx := tx0.(*BadgerTx) + + bitvalue := uint64(777) + bits := []uint64{0, 3, 1 << 16, 1<<16 + 3, 8 << 16} + for _, v := range bits { + changed, err := tx.Add(index, field, view, shard, doBatched, v) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + } + + index2 := "i2" // should not be deleted, even though it shares a prefix with 'i' + changed, err := tx.Add(index2, field, view, shard, doBatched, bitvalue) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + + for _, v := range bits { + exists, err := tx.Contains(index, field, view, shard, v) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!!") + } + } + exists, err := tx.Contains(index2, field, view, shard, bitvalue) + panicOn(err) + if !exists { + panic("ARG bitvalue was NOT SET!!! on index2") + } + err = tx.Commit() + panicOn(err) + + // end of setup + err = dbwrap.DeleteIndex(index) + panicOn(err) + + tx0, _ = dbwrap.NewTx(!writable, index2, Txo{}) + tx = tx0.(*BadgerTx) + + 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, false) + 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) { + t.Skip("test big and long running, skip") + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_DeleteIndex_over100k") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx0, _ := dbwrap.NewTx(writable, index, Txo{}) + tx := tx0.(*BadgerTx) + + bitvalue := uint64(777) + limit := uint64(100002) // default batch size in DeleteIndex is 100k keys per delete transaction. + //limit := uint64(101) + for v := uint64(1); v < limit; v++ { + // shift by << 16 to get into a different shard + changed, err := tx.Add(index, field, view, shard, doBatched, v<<16) + if changed <= 0 { + panic("should have changed") + } + panicOn(err) + if v%100000 == 0 { + panicOn(tx.Commit()) + tx0, _ := dbwrap.NewTx(writable, index, Txo{}) + tx = tx0.(*BadgerTx) + } + } + + 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) + + tx0, _ = dbwrap.NewTx(!writable, index2, Txo{}) + tx = tx0.(*BadgerTx) + + 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, false) + 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") + } +} + +func TestBadger_SliceOfShards(t *testing.T) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_SliceOfShards") + defer clean() + defer dbwrap.Close() + index, field, view := "i", "f", "v" + shards := []uint64{0, 1, 2, 3, 1000001, 2000001} + putme := uint64(179) + for _, shard := range shards { + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + } + tx, _ := dbwrap.NewTx(!writable, index, Txo{}) + defer tx.Rollback() + + slc, err := tx.SliceOfShards(index, field, view, "") + panicOn(err) + for i := range shards { + if shards[i] != slc[i] { + panic(fmt.Sprintf("expected at i=%v that slc[i]=%v = shards[i]=%v", i, slc[i], shards[i])) + } + } +} + +func TestBadger_HasData(t *testing.T) { + + db, clean := mustOpenEmptyBadgerWrapper("TestBadger_HasData") + defer clean() + defer db.Close() + + // HasData should start out false. + hasAnything, err := db.HasData() + if err != nil { + t.Fatal(err) + } + if hasAnything { + t.Fatalf("HasData reported existing data on an empty database") + } + + // check that HasData sees a committed record. + + index, field, view, shard, putme := "i", "f", "v", uint64(123), uint64(42) + badgerDBMustSetBitvalue(db, index, field, view, shard, putme) + + // HasData(false) should now report data + hasAnything, err = db.HasData() + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData() reported no data on a database that has bits written to it") + } +} + +// Benchmark performance of setValue for BSI ranges. +func BenchmarkBadger_Write(b *testing.B) { + + dbwrap, clean := mustOpenEmptyBadgerWrapper("BenchmarkBadger_Write") + //defer clean() + _ = clean + defer dbwrap.Close() + + putmeValues := []uint64{3, 2 << 16} + index, field, view, shard := "i", "f", "v", uint64(0) + + for _, putme := range putmeValues { + badgerDBMustSetBitvalue(dbwrap, index, field, view, shard, putme) + } + /* + + dbwrap, clean := mustOpenEmptyBadgerWrapper("BenchmarkBadger_Write") + defer clean() + defer dbwrap.Close() + index, field, view, shard := "i", "f", "v", uint64(0) + tx, _ := dbwrap.NewTx(writable, index, Txo{}) + + 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.NewTx(!writable, index, Txo{}) + 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.NewTx(!writable, index, Txo{}) + rbm3, err := tx3.OffsetRange(index, field, view, shard, offset, start, endx) + panicOn(err) + tx3.Rollback() + + //expect to see 3M == 3145728 + s3 := bitmapAsString(rbm3) + expect3 := "c(3145728, 3145729)" + + if s3 != expect3 { + panic(fmt.Sprintf("s3='%v', but expected '%v'", s3, expect3)) + } + */ +} + +func reportTestBadgersNeedingClose() error { + globalBadgerReg.mu.Lock() + defer globalBadgerReg.mu.Unlock() + n := len(globalBadgerReg.mp) + if n == 0 { + return nil + } + AlwaysPrintf("*** these badgers are still open (n=%v):", n) + i := 0 + for w := range globalBadgerReg.mp { + AlwaysPrintf("i=%v, w p=%p stack:\n%v\n\n", i, w, w.startStack) + i++ + } + return errors.New("unclosed badgers, contact Animal Control") +} + +/* +func TestBadger_ConflictWriteWriteResolution(t *testing.T) { + + // 1) when do we get write-write conflicts (probably different goroutines) but + // can we get them on different keys? + + // 2) does having a lock registry that insures we are only ever writing + // different keys at once avoid write-write conflicts? + + // 3) how should write-write conflicts be resolved? + // presumably just retying the write? + + // setup + dbwrap, clean := mustOpenEmptyBadgerWrapper("TestBadger_ConflictWriteWriteResolution") + defer clean() + defer dbwrap.Close() + + concur := 10 + //bkey := []byte("a") + //by := []byte("value-for-a") + + // read-loop: + for i := 0; i < concur*2; i++ { + go func() { + tx, _ := dbwrap.NewTx(!writable, "", Txo{}) + + for j := 0; true; j++ { + + bkey := []byte(fmt.Sprintf("key-for-a j=%v", j)) + //by := []byte(fmt.Sprintf("value-for-a j=%v", -1)) + + _, err := tx.tx.Get(bkey) + if err != badger.ErrKeyNotFound { + panicOn(err) + } + + if j%10 == 0 { + vv("committing after 10, gid=%v", curGID()) + tx.Rollback() + tx, _ = dbwrap.NewTx(!writable, "", Txo{}) + } + + } + }() + } + + // write-loop: + for i := 0; i < concur; i++ { + go func() { + tx, _ := dbwrap.NewTx(writable, "", Txo{}) + + for j := 0; true; j++ { + + bkey := []byte(fmt.Sprintf("key-for-a j=%v", j)) + by := []byte(fmt.Sprintf("value-for-a j=%v", j)) + + entry := badger.NewEntry(bkey, by) + err := tx.tx.SetEntry(entry) + panicOn(err) + + if j%5 == 0 { + panicOn(tx.tx.Delete(bkey)) + } + + _, err = tx.tx.Get(bkey) + if err != badger.ErrKeyNotFound { + panicOn(err) + } + + if j%10 == 0 { + ///vv("committing after 10, gid=%v", curGID()) + err = tx.tx.Commit() + panicOn(err) + tx, _ = dbwrap.NewTx(writable, "", Txo{}) + } + + } + }() + } + select {} +} +*/ diff --git a/bluegreentx.go b/bluegreentx.go index 1b7ad72c7..033f2809c 100644 --- a/bluegreentx.go +++ b/bluegreentx.go @@ -32,7 +32,8 @@ import ( // The checkDatabase() call will do reads of the fragments at Commit/Rollback, // while the snapshotqueue may be doing writes. // -// Do not run with go test -race and expect it to be race free. +// Do not run with go test -race and expect it to be race free with RoaringTx +// on one arm. // type blueGreenTx struct { a Tx @@ -42,7 +43,8 @@ type blueGreenTx struct { as string bs string - types []txtype + types []txtype + hasRoaring bool // roaring will not create as many Tx (they are // psuedo Tx anyway), espcially when deleting @@ -57,6 +59,8 @@ type blueGreenTx struct { rollbackOrCommitDone bool txf *TxFactory + + short bool // short Dump or long } // blueGreenRegistry is used to force checking of (read) transactions @@ -65,19 +69,29 @@ type blueGreenTx struct { // roaring will show up, while writes to the DB won't show up on // readTx that have already started. type blueGreenRegistry struct { - mu sync.Mutex - m map[int64]*blueGreenTx + mu sync.Mutex + m map[int64]*blueGreenTx + types []txtype + hasRoaring bool } -func newBlueGreenReg() *blueGreenRegistry { +// if we have raoring in the mix we cannot expect reads +// to match up, but otherwise do. +func newBlueGreenReg(types []txtype) *blueGreenRegistry { + + hasRoaring := false + if types[0] == roaringTxn || types[1] == roaringTxn { + hasRoaring = true + } return &blueGreenRegistry{ - m: make(map[int64]*blueGreenTx), + m: make(map[int64]*blueGreenTx), + types: types, + hasRoaring: hasRoaring, } } -// add remembers the tx so we can check it -// should we see a write after creation -// but before rollback/commit. +// add remembers the tx so we can check that +// all tx were finished before Close(). func (b *blueGreenRegistry) add(c *blueGreenTx) { b.mu.Lock() defer b.mu.Unlock() @@ -100,26 +114,33 @@ func (b *blueGreenRegistry) Close() { b.mu.Lock() defer b.mu.Unlock() if len(b.m) > 0 { - panic(fmt.Sprintf("still have unchecked blueGreenTx: '%#v'", b.m)) + panic(fmt.Sprintf("still have open/unchecked blueGreenTx: '%#v'", b.m)) //AlwaysPrintf("still have unchecked blueGreenTx: '%#v'", b.m) } } func (txf *TxFactory) newBlueGreenTx(a, b Tx, idx *Index, o Txo) *blueGreenTx { - //func (reg *blueGreenRegistry) newBlueGreenTx(a, b Tx, idx *Index, o Txo, openTxSn []int64) *blueGreenTx { as := a.Type() bs := b.Type() - c := &blueGreenTx{a: a, b: b, idx: idx, as: as, bs: bs, txf: txf, types: txf.types} + c := &blueGreenTx{a: a, + b: b, + idx: idx, + as: as, + bs: bs, + txf: txf, + types: txf.types, + hasRoaring: txf.blueGreenReg.hasRoaring, + short: true, + } if c.types[1] == roaringTxn { c.useSnA = true } + //vv("newBlueGreenTx with a.sn=%v with o.Shard=%v", c.Sn(), int(o.Shard)) c.checker.c = c c.o = o - if o.Write { - txf.blueGreenReg.add(c) - } + txf.blueGreenReg.add(c) return c } @@ -131,24 +152,27 @@ func (c *blueGreenTx) Type() string { var blueGreenTxDumpMut sync.Mutex -func (c *blueGreenTx) Dump() { +func (c *blueGreenTx) Dump(short bool) { + blueGreenTxDumpMut.Lock() defer blueGreenTxDumpMut.Unlock() fmt.Printf("%v blueGreenTx.Dump ============== \n", FileLine(2)) fmt.Printf("A(%v) Dump:\n", c.as) - c.a.Dump() + c.a.Dump(short) fmt.Printf("B(%v) Dump:\n", c.bs) - c.b.Dump() + c.b.Dump(short) - fmt.Printf("dbPerShard.DumpAll(): idx=%p\n", c.idx) - c.idx.Txf.dbPerShard.DumpAll() + if !short { + fmt.Printf("dbPerShard.DumpAll(): idx=%p\n", c.idx) + c.idx.holder.txf.dbPerShard.DumpAll() + } } 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)) + panic(fmt.Sprintf("Readonly difference, a=%v, but b =%v", a, b)) } return b } @@ -172,6 +196,9 @@ func (c *blueGreenTx) IncrementOpN(index, field, view string, shard uint64, chan // compareTxState is called for the first Commit or Rollback a blueGreenTx sees. func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { + if c.o.blueGreenOff { + return + } here := fmt.Sprintf("%v/%v/%v/%v", index, field, view, shard) //vv("compareTxState here = '%v', _sn_ %v gid=%v", here, c.Sn(), curGID()) aIter, aFound, aErr := c.a.ContainerIterator(index, field, view, shard, 0) @@ -184,21 +211,21 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { } if aFound != bFound { - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: A(%v) ContainerIterator had aFound=%v, but B(%v) had bFound=%v; at '%v'", here, c.as, aFound, c.bs, bFound, stack())) } if aErr != nil || bErr != nil { if aErr != nil && bErr != nil { - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: A(%v) reported err '%v'; B(%v) reported err '%v' at %v", here, c.as, aErr, c.bs, bErr, stack())) } if aErr != nil { - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: A(%v) reported err %v at %v; but B(%v) did not", here, c.as, aErr, c.bs, stack())) } if bErr != nil { - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: B(%v) reported err %v at %v; but A(%v) did not", here, c.bs, bErr, c.as, stack())) } } @@ -207,17 +234,17 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { if !bIter.Next() { AlwaysPrintf("compareTxState[%v]: A(%v) found key %v, B(%v) didn't, dump to follow, stack=\n %v\n\n and here is dump:", here, c.as, aKey, c.bs, stack()) - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: A(%v) found key %v, B(%v) didn't, at %v", here, c.as, aKey, c.bs, stack())) } bKey, bValue := bIter.Value() if bKey != aKey { AlwaysPrintf("problem in caller %v", Caller(2)) - c.Dump() + c.Dump(c.short) panic(fmt.Sprintf("compareTxState[%v]: A(%v) found key %v, B(%v) found %v, at %v", here, c.as, aKey, c.bs, bKey, stack())) } if err := aValue.BitwiseCompare(bValue); err != nil { - c.Dump() + c.Dump(c.short) //vv("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at stack=%v", here, aKey, err, c.as, c.bs, stack()) panic(fmt.Sprintf("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at stack=%v", here, aKey, err, c.as, c.bs, stack())) } @@ -226,7 +253,7 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { // end checking everything in A, but does B have more? if bIter.Next() { AlwaysPrintf("bIter has more than it should. problem in caller %v. _sn_ %v", Caller(2), c.Sn()) - c.Dump() + c.Dump(c.short) bKey, _ := bIter.Value() panic(fmt.Sprintf("compareTxState[%v]: B(%v) found key %v, A(%v) didn't, (a.sn=%v) (b.sn=%v) at %v", here, c.bs, bKey, c.as, c.a.Sn(), c.b.Sn(), stack())) } @@ -234,10 +261,17 @@ func (c *blueGreenTx) compareTxState(index, field, view string, shard uint64) { } func (c *blueGreenTx) checkDatabase() { - if !c.o.Write { - // We only need to check the we are A/B consistent after every write. - // Then reads can only see that consitent state, and don't need - // to be checked themselves. Sketch of proof by induction: + if c.o.blueGreenOff { + return + } + if c.hasRoaring && !c.o.Write { + // With roaring on one arm, we only check the we are A/B + // consistent after every write. + // + // Ideally reads can only see that consitent state, and don't need + // to be checked themselves-- but we do try if both A and B + // are transactional. Sketch of proof by induction that + // write checking should, theoretically, suffice: // Starting with zero data, if we have agreement in both A/B // database state after each write, then // because there is only ever a single @@ -245,7 +279,8 @@ func (c *blueGreenTx) checkDatabase() { // data state between A and B as long as every prior // A/B check of the serialized writes suceeded. // - // This avoids a key problem we discovered when A/B checking reads. + // This avoids a key problem we discovered when A/B checking reads + // with roaring on one arm. // The MVCC of the transactional engines means that reads that // start before a write commit will look very different // when comparing to roaring's non-transactional state. @@ -259,8 +294,6 @@ func (c *blueGreenTx) checkDatabase() { } c.checker.checkDone = true - // seen() returns nil on 2nd or any further call, - // so only the first Commit() or Rollback() does this. for index, fields := range c.checker.seen() { for field, views := range fields { for view, shards := range views { @@ -272,6 +305,10 @@ func (c *blueGreenTx) checkDatabase() { } } +func (c *blueGreenTx) IsDone() bool { + return c.b.IsDone() +} + func (c *blueGreenTx) Rollback() { c.mu.Lock() defer c.mu.Unlock() @@ -308,7 +345,9 @@ func (c *blueGreenTx) Commit() error { //vv("blueGreenTx.Commit() called. bgtx p=%p", c) c.rollbackOrCommitDone = true if c.o.Write { - c.checkDatabase() + if !c.o.blueGreenOff { + c.checkDatabase() + } } defer func() { if r := recover(); r != nil { @@ -336,14 +375,15 @@ func (c *blueGreenTx) RoaringBitmap(index, field, view string, shard uint64) (*r a, errA := c.a.RoaringBitmap(index, field, view, shard) _, _ = a, errA b, errB := c.b.RoaringBitmap(index, field, view, shard) - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) - slcA := a.Slice() - slcB := b.Slice() - if !reflect.DeepEqual(slcA, slcB) { - panic("blueGreenTx.RoaringBitmap() returning different roaring.Bitmaps!") + slcA := a.Slice() + slcB := b.Slice() + if !reflect.DeepEqual(slcA, slcB) { + panic("blueGreenTx.RoaringBitmap() returning different roaring.Bitmaps!") + } } - return b, errB } @@ -357,10 +397,12 @@ func (c *blueGreenTx) Container(index, field, view string, shard uint64, key uin }() 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) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + err = a.BitwiseCompare(b) + panicOn(err) + } return b, errB } @@ -374,8 +416,10 @@ func (c *blueGreenTx) PutContainer(index, field, view string, shard uint64, key }() errA := c.a.PutContainer(index, field, view, shard, key, rc) errB := c.b.PutContainer(index, field, view, shard, key, rc) - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return errB } @@ -386,7 +430,7 @@ func (c *blueGreenTx) ImportRoaringBits(index, field, view string, shard uint64, // ================== begin save comments. //c.checkDatabase() ////vv("got past database check at TOP of ImportRoaringBits") - //c.Dump() + //c.Dump(c.short) ////vv("done with top dump; clear=%v", clear) // ================== end save comments. defer func() { @@ -403,28 +447,31 @@ func (c *blueGreenTx) ImportRoaringBits(index, field, view string, shard uint64, changedA, rowSetA, errA := c.a.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) changedB, rowSetB, errB := c.b.ImportRoaringBits(index, field, view, shard, rit2, clear, log, rowSize, data) - if len(data) == 0 { - // okay to check! otherwise we are in the fragment.fillFragmentFromArchive - // case where we know that RoaringTx.ImportRoaringBits changed and rowSet will - // be inaccurate. - 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 !c.o.blueGreenOff { + + if len(data) == 0 { + // okay to check! otherwise we are in the fragment.fillFragmentFromArchive + // case where we know that RoaringTx.ImportRoaringBits changed and rowSet will + // be inaccurate. + if changedA != changedB { + panic(fmt.Sprintf("changedA = %v, but changedB = %v", changedA, changedB)) } - if va != vb { - panic(fmt.Sprintf("diff on key '%v', rowSetA has value '%v', but rowSetB has value '%v'", k, va, vb)) + 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) + c.checkDatabase() } - compareErrors(errA, errB) - c.checkDatabase() return changedB, rowSetB, errB } @@ -438,7 +485,10 @@ func (c *blueGreenTx) RemoveContainer(index, field, view string, shard uint64, k }() errA := c.a.RemoveContainer(index, field, view, shard, key) errB := c.b.RemoveContainer(index, field, view, shard, key) - compareErrors(errA, errB) + + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return errB } @@ -481,10 +531,14 @@ func (c *blueGreenTx) Add(index, field, view string, shard uint64, batched bool, 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)) + if !c.o.blueGreenOff { + + if ach != bch { + panic(fmt.Sprintf("Add() difference, ach=%v, but bch=%v; errA='%v'; errB='%v'", ach, bch, errA, errB)) + } + compareErrors(errA, errB) } - compareErrors(errA, errB) + return bch, errB } @@ -516,7 +570,10 @@ func (c *blueGreenTx) Remove(index, field, view string, shard uint64, a ...uint6 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) + + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return bch, errB } @@ -532,7 +589,9 @@ func (c *blueGreenTx) Contains(index, field, view string, shard uint64, key uint _, _ = ax, errA bx, errB := c.b.Contains(index, field, view, shard, key) - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return bx, errB } @@ -550,8 +609,10 @@ func (c *blueGreenTx) ContainerIterator(index, field, view string, shard uint64, bit, bfound, errB := c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey) - compareErrors(errA, errB) - // INVAR: errA == errB, so only need to check one. + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } + if errB != nil { // RoaringTx can return an iterator and an error, so be sure Close it we have it. if ait != nil { @@ -562,6 +623,13 @@ func (c *blueGreenTx) ContainerIterator(index, field, view string, shard uint64, } return nil, bfound, errB } + if errA != nil { + // RoaringTx can return an iterator and an error, so be sure Close it we have it. + if ait != nil { + ait.Close() + } + } + // INVAR: errA == errB == nil bgi := NewBlueGreenIterator(c, ait, bit) return bgi, bfound, errB @@ -598,11 +666,14 @@ func (bgi *blueGreenIterator) Next() bool { func (bgi *blueGreenIterator) Value() (uint64, *roaring.Container) { ka, ca := bgi.ait.Value() kb, cb := bgi.bit.Value() - if ka != kb { - panic(fmt.Sprintf("ka=%v != kb=%v", ka, kb)) + + if !bgi.tx.o.blueGreenOff { + if ka != kb { + panic(fmt.Sprintf("ka=%v != kb=%v", ka, kb)) + } + err := ca.BitwiseCompare(cb) + panicOn(err) } - err := ca.BitwiseCompare(cb) - panicOn(err) return kb, cb } func (bgi *blueGreenIterator) Close() { @@ -611,7 +682,7 @@ func (bgi *blueGreenIterator) Close() { } // ForEach is read-only on the database, and so we only pass through to B. -// Avoids the side-effects of calling fn too many times. +// Avoids the side-effects of calling fn too many times, which can cause serious false alarms. func (c *blueGreenTx) ForEach(index, field, view string, shard uint64, fn func(i uint64) error) error { defer func() { if r := recover(); r != nil { @@ -654,7 +725,9 @@ func (c *blueGreenTx) Count(index, field, view string, shard uint64) (uint64, er b, errB := c.b.Count(index, field, view, shard) _, _ = b, errB - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return b, errB } @@ -671,7 +744,9 @@ func (c *blueGreenTx) Max(index, field, view string, shard uint64) (uint64, erro b, errB := c.b.Max(index, field, view, shard) _, _ = b, errB - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return b, errB } @@ -688,7 +763,9 @@ func (c *blueGreenTx) Min(index, field, view string, shard uint64) (uint64, bool bmin, bfound, errB := c.b.Min(index, field, view, shard) _, _, _ = bmin, bfound, errB - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return bmin, bfound, errB } @@ -702,7 +779,9 @@ func (c *blueGreenTx) UnionInPlace(index, field, view string, shard uint64, othe }() errA := c.a.UnionInPlace(index, field, view, shard, others...) errB := c.b.UnionInPlace(index, field, view, shard, others...) - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } return errB } @@ -710,7 +789,7 @@ func (c *blueGreenTx) CountRange(index, field, view string, shard uint64, start, c.checker.see(index, field, view, shard) defer func() { if r := recover(); r != nil { - c.Dump() + c.Dump(c.short) AlwaysPrintf("see CountRange() panic '%v' at '%v'", r, stack()) panic(r) } @@ -718,11 +797,13 @@ func (c *blueGreenTx) CountRange(index, field, view string, shard uint64, start, 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) = %v, but b(%v) = %v", c.as, a, c.bs, b)) - } + if !c.o.blueGreenOff { + if a != b { + panic(fmt.Sprintf("a(%v) = %v, but b(%v) = %v", c.as, a, c.bs, b)) + } - compareErrors(errA, errB) + compareErrors(errA, errB) + } return b, errB } @@ -730,19 +811,22 @@ func (c *blueGreenTx) OffsetRange(index, field, view string, shard, offset, star c.checker.see(index, field, view, shard) defer func() { if r := recover(); r != nil { - AlwaysPrintf("see OffsetRange() panic '%v' at '%v'", r, stack()) + AlwaysPrintf("see OffsetRange() on _sn_ %v, panic '%v' at '%v'", c.Sn(), 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) - if err != nil { - c.Dump() - panicOn(err) + if !c.o.blueGreenOff { + + err = roaringBitmapDiff(a, b) + if err != nil { + c.Dump(false) + panicOn(fmt.Errorf("on _sn_ %v OffsetRange(index='%v', field='%v', view='%v', shard='%v', offset: %v start: %v, end: %v) err: %v", c.Sn(), index, field, view, int(shard), offset, start, end, err)) + } + compareErrors(errA, errB) } - compareErrors(errA, errB) return b, errB } @@ -751,7 +835,7 @@ func (c *blueGreenTx) RoaringBitmapReader(index, field, view string, shard uint6 c.checker.see(index, field, view, shard) defer func() { if r := recover(); r != nil { - c.Dump() + c.Dump(c.short) AlwaysPrintf("see RoaringBitmapReader() panic '%v' at '%v'", r, stack()) panic(r) } @@ -760,14 +844,19 @@ func (c *blueGreenTx) RoaringBitmapReader(index, field, view string, shard uint6 rcA, szA, errA := c.a.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) rcB, szB, errB := c.b.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring) - compareErrors(errA, errB) + if !c.o.blueGreenOff { + compareErrors(errA, errB) + } // We are seeing Roaring vs Badger size differences on // server/ test TestClusterResize_AddNode/ContinuousShards, // so turn off the szA vs szB checks and MutliReaderB use. But keep them if we want to // check RBF vs Badger for byte-for-byte compatiblity (we // suspect the ops log or optimized bitmaps are accounting for the difference). - sizeMustMatch := false + sizeMustMatch := false // !c.hasRoaring + if c.o.blueGreenOff { + sizeMustMatch = false + } if sizeMustMatch { if szA != szB { panic(fmt.Sprintf("szA(%v) = %v, but szB(%v) = %v; fragmentPathForRoaring='%v'", c.as, szA, c.bs, szB, fragmentPathForRoaring)) @@ -806,41 +895,44 @@ func (c *blueGreenTx) SliceOfShards(index, field, view, optionalViewPath string) //c.checker.see(index, field, view, shard) // don't have shard. defer func() { if r := recover(); r != nil { - c.Dump() + c.Dump(c.short) AlwaysPrintf("see SliceOfShards() panic '%v' at '%v'", r, stack()) panic(r) } }() slcA, errA := c.a.SliceOfShards(index, field, view, optionalViewPath) slcB, errB := c.b.SliceOfShards(index, field, view, optionalViewPath) - compareErrors(errA, errB) - // sort order may be different, and that's ok. - cpa := append([]uint64{}, slcA...) - cpb := append([]uint64{}, slcB...) - sort.Slice(cpa, func(i, j int) bool { return cpa[i] < cpa[j] }) - sort.Slice(cpb, func(i, j int) bool { return cpb[i] < cpb[j] }) + if !c.o.blueGreenOff { + compareErrors(errA, errB) - if !reflect.DeepEqual(cpa, cpb) { - // report the first difference - ma := make(map[uint64]bool) - for _, ka := range slcA { - ma[ka] = true - } - for _, kb := range slcB { - if !ma[kb] { - //vv("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb) - c.Dump() - panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb)) + // sort order may be different, and that's ok. + cpa := append([]uint64{}, slcA...) + cpb := append([]uint64{}, slcB...) + sort.Slice(cpa, func(i, j int) bool { return cpa[i] < cpa[j] }) + sort.Slice(cpb, func(i, j int) bool { return cpb[i] < cpb[j] }) + + if !reflect.DeepEqual(cpa, cpb) { + // report the first difference + ma := make(map[uint64]bool) + for _, ka := range slcA { + ma[ka] = true } - delete(ma, kb) - } - if len(ma) != 0 { - for firstDifference := range ma { - panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! A(%v) had %v, but B(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.as, firstDifference, c.bs, cpa, cpb)) + for _, kb := range slcB { + if !ma[kb] { + //vv("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb) + c.Dump(c.short) + panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! B(%v) had shard %v, but A(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.bs, kb, c.as, cpa, cpb)) + } + delete(ma, kb) } + if len(ma) != 0 { + for firstDifference := range ma { + panic(fmt.Sprintf("blueGreenTx SliceOfShards diference! A(%v) had %v, but B(%v) did not. cpa='%#v'; cpb='%#v'; in the SliceOfShards returned slice.", c.as, firstDifference, c.bs, cpa, cpb)) + } + } + panic(fmt.Sprintf("blueGreenTx SliceOfShards diference \n slcA(%v)='%#v';\n slcB(%v)='%#v';\n", c.as, cpa, c.bs, cpb)) } - panic(fmt.Sprintf("blueGreenTx SliceOfShards diference \n slcA(%v)='%#v';\n slcB(%v)='%#v';\n", c.as, cpa, c.bs, cpb)) } return slcB, errB } diff --git a/bluegreentx_test.go b/bluegreentx_test.go index 131712450..7455f2c6d 100644 --- a/bluegreentx_test.go +++ b/bluegreentx_test.go @@ -16,13 +16,20 @@ package pilosa import ( "bytes" + "context" "io" "io/ioutil" + "os" + "strings" "testing" cryrand "crypto/rand" ) +var _ = context.Background +var _ = os.Open +var _ = strings.Split + func TestMultiReaderB(t *testing.T) { // MultiReaderB should read identical chunks of bytes from both its "a" and "b" // member io.Readers, else it should panic. This should hold for diff --git a/catcher.go b/catcher.go index 8ff120f57..e2b4766e5 100644 --- a/catcher.go +++ b/catcher.go @@ -58,8 +58,8 @@ func (c *catcherTx) ImportRoaringBits(index, field, view string, shard uint64, r return c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) } -func (c *catcherTx) Dump() { - c.b.Dump() +func (c *catcherTx) Dump(short bool) { + c.b.Dump(short) } func (c *catcherTx) Readonly() bool { @@ -145,6 +145,10 @@ func (c *catcherTx) UseRowCache() bool { return c.b.UseRowCache() } +func (c *catcherTx) IsDone() bool { + return c.b.IsDone() +} + func (c *catcherTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { defer func() { diff --git a/cluster.go b/cluster.go index 56cb417ab..61a2babc7 100644 --- a/cluster.go +++ b/cluster.go @@ -62,7 +62,7 @@ const ( resizeJobActionAdd = "ADD" resizeJobActionRemove = "REMOVE" - defaultConfirmDownRetries = 10 + defaultConfirmDownRetries = 120 defaultConfirmDownSleep = 1 * time.Second ) @@ -758,7 +758,7 @@ func (c *cluster) fragsByHost(idx *Index) fragsByHost { fieldViews.addView(field.Name(), view.name) } } - return c.fragCombos(idx.Name(), idx.AvailableShards(), fieldViews) + return c.fragCombos(idx.Name(), idx.AvailableShards(includeRemote), fieldViews) } // fragCombos returns a map (by uri) of lists of fragments for a given index @@ -2218,7 +2218,7 @@ func (c *cluster) nodeStatus() *NodeStatus { is := &IndexStatus{Name: idx.Name, CreatedAt: idx.CreatedAt} for _, f := range idx.Fields { if field := c.holder.Field(idx.Name, f.Name); field != nil { - availableShards = field.AvailableShards() + availableShards = field.AvailableShards(includeRemote) } else { availableShards = roaring.NewBitmap() } diff --git a/cluster_internal_test.go b/cluster_internal_test.go index e7c246fc7..003e7f668 100644 --- a/cluster_internal_test.go +++ b/cluster_internal_test.go @@ -97,6 +97,7 @@ func newIndexWithTempPath(tb testing.TB, name string) *Index { panic(err) } h := NewHolder(path, nil) + panicOn(h.Open()) index, err := h.CreateIndex(name, IndexOptions{}) testhook.Cleanup(tb, func() { h.Close() @@ -170,7 +171,7 @@ func TestFragSources(t *testing.T) { // Obtain transaction. var shard uint64 - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() _, err = field.SetBit(tx, 1, 101, nil) @@ -180,7 +181,7 @@ func TestFragSources(t *testing.T) { panicOn(tx.Commit()) shard = 1 - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() _, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil) if err != nil { @@ -189,7 +190,7 @@ func TestFragSources(t *testing.T) { panicOn(tx.Commit()) shard = 2 - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() _, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil) @@ -199,7 +200,7 @@ func TestFragSources(t *testing.T) { panicOn(tx.Commit()) shard = 3 - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() _, err = field.SetBit(tx, 1, ShardWidth*shard+1, nil) diff --git a/dbshard.go b/dbshard.go index 50d74e372..f7f629ba1 100644 --- a/dbshard.go +++ b/dbshard.go @@ -22,6 +22,9 @@ import ( "strconv" "strings" "sync" + "sync/atomic" + + "github.com/pkg/errors" ) var _ = sort.Sort @@ -50,6 +53,9 @@ type DBWrapper interface { DeleteField(index, field, fieldPath string) error OpenListString() string OpenSnList() (sns []int64) + Path() string + HasData() (has bool, err error) + SetHolder(h *Holder) } type DBRegistry interface { @@ -68,7 +74,9 @@ type DBShard struct { // by a reader to start with. mut sync.RWMutex - types []txtype + types []txtype + hasRoaring bool // if either of the types is roaringTxn + W []DBWrapper ParentDBIndex *DBIndex @@ -76,6 +84,7 @@ type DBShard struct { per *DBPerShard useOpenList int + closed bool } func (dbs *DBShard) DeleteFragment(index, field, view string, shard uint64, frag interface{}) (err error) { @@ -105,6 +114,7 @@ func (dbs *DBShard) Close() (err error) { return err } } + dbs.closed = true return } @@ -112,10 +122,49 @@ func (dbs *DBShard) String() string { return dbs.Path } -func (dbs *DBShard) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) { - dbs.mut.Lock() - defer dbs.mut.Unlock() +// Cleanup must be called at every commit/rollback of a Tx, in +// order to release the read-write mutex that guarantees a single +// writer at a time. Each tx must take care to call cleanup() +// exactly once. examples: +// tx.o.dbs.Cleanup(tx) +// tx.Options().dbs.Cleanup(tx) +// +func (dbs *DBShard) Cleanup(tx Tx) { + if dbs == nil { + return // some tests are using Tx only, no dbs available. + } + if useRWLock { + if !dbs.hasRoaring { + if tx.Readonly() { + dbs.mut.RUnlock() + } else { + dbs.mut.Unlock() + } + } + } +} +// experimental feature, off for now. +const useRWLock = false + +func (dbs *DBShard) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) { + if useRWLock { + // enforce only one writer at a time. The dbs.mut is held until + // the Tx finishes. + if !dbs.hasRoaring { + if write { + dbs.mut.Lock() + } else { + dbs.mut.RLock() + } + } + } + if o.dbs != dbs { + panic(fmt.Sprintf("TxFactory.NewTx() should have set o.dbs(%p) to equal dbs(%p)", o.dbs, dbs)) + } + if o.Shard != dbs.Shard { + panic(fmt.Sprintf("shard disagreement! o.Shard='%v' but dbs.Shard='%v'", int(o.Shard), int(dbs.Shard))) + } var txns []Tx for _, w := range dbs.W { @@ -153,7 +202,8 @@ type DBPerShard struct { // Easily see how many we have. Flatmap map[*DBShard]struct{} - types []txtype + types []txtype + hasRoaring bool txf *TxFactory @@ -163,6 +213,25 @@ type DBPerShard struct { useOpenList int } +// HasData returns true if the database has at least one key. +// For roaring it returns the number of fragments stored. +// The `which` argument is the index into the per.W slice. 0 for blue, 1 for green. +// If you pass 1, be sure you have a blue-green configuration. +func (per *DBPerShard) HasData(which int) (hasData bool, err error) { + // has to aggregate across all available DBShard for each index and shard. + + for v := range per.Flatmap { + hasData, err = v.W[which].HasData() + if err != nil { + return + } + if hasData { + return + } + } + return +} + func (per *DBPerShard) ListOpenString() (r string) { for v := range per.Flatmap { r += v.Path + " -> " + v.W[per.useOpenList].OpenListString() + "\n" @@ -173,12 +242,19 @@ func (per *DBPerShard) ListOpenString() (r string) { func (txf *TxFactory) NewDBPerShard(types []txtype, holderDir string) (d *DBPerShard) { useOpenList := 0 + hasRoaring := false + if types[0] == roaringTxn { + hasRoaring = true + } if len(types) == 2 { // blue-green, avoid the empty roaring Tx open list. // Prefer B's open list if neither is roaring. if types[0] == roaringTxn || types[1] != roaringTxn { useOpenList = 1 } + if types[1] == roaringTxn { + hasRoaring = true + } } d = &DBPerShard{ @@ -188,6 +264,7 @@ func (txf *TxFactory) NewDBPerShard(types []txtype, holderDir string) (d *DBPerS Flatmap: make(map[*DBShard]struct{}), txf: txf, useOpenList: useOpenList, + hasRoaring: hasRoaring, } return } @@ -239,28 +316,33 @@ func (per *DBPerShard) DeleteFieldFromStore(index, field, fieldPath string) (err func (per *DBPerShard) DeleteFragment(index, field, view string, shard uint64, frag *fragment) error { - dbs, err := per.GetDBShard(index, shard, nil) + idx := per.txf.holder.Index(index) + dbs, err := per.GetDBShard(index, shard, idx) panicOn(err) return dbs.DeleteFragment(index, field, view, shard, frag) } func (dbs *DBShard) DumpAll() { - + short := false + fmt.Printf("\n============= begin DumpAll dbs=%p index='%v', shard=%v ========\n", dbs, dbs.Index, int(dbs.Shard)) for i, ty := range dbs.types { _ = i tx, err := dbs.W[i].NewTx(!writable, "", Txo{Index: dbs.idx}) panicOn(err) defer tx.Rollback() - tx.Dump() + fmt.Printf("\n============= dumping dbs.W[%v] %v ========\n", i, ty) + tx.Dump(short) switch ty { case roaringTxn: case rbfTxn: case lmdbTxn: + case badgerTxn: default: panic(fmt.Sprintf("unknown txtyp: '%v'", ty)) } } + fmt.Printf("\n============= end of DumpAll index='%v', shard=%v ========\n", dbs.Index, int(dbs.Shard)) } func (per *DBPerShard) DumpAll() { @@ -285,6 +367,7 @@ func (per *DBPerShard) Path(index string, shard uint64) string { } func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs *DBShard, err error) { + per.Mu.Lock() defer per.Mu.Unlock() @@ -296,8 +379,10 @@ func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs * per.dbh.Index[index] = dbi } dbs, ok = dbi.Shard[shard] + if dbs != nil && dbs.closed { + panic(fmt.Sprintf("cannot retain closed dbs across holder ReOpen dbs='%p'", dbs)) + } if !ok { - dbs = &DBShard{ types: per.types, ParentDBIndex: dbi, @@ -307,6 +392,7 @@ func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs * idx: idx, per: per, useOpenList: per.useOpenList, + hasRoaring: per.hasRoaring, } dbi.Shard[shard] = dbs } @@ -320,11 +406,15 @@ func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs * registry = globalRbfDBReg case lmdbTxn: registry = globalLMDBReg + case badgerTxn: + registry = globalBadgerReg default: panic(fmt.Sprintf("unknown txtyp: '%v'", ty)) } w, err := registry.OpenDBWrapper(dbs.Path, DetectMemAccessPastTx) panicOn(err) + h := idx.Holder() + w.SetHolder(h) dbs.Open = true if w != nil && len(dbs.W) == 0 { per.Flatmap[dbs] = struct{}{} @@ -364,18 +454,39 @@ func (per *DBPerShard) Close() (err error) { return } -// requiredSuffix should be "-badgerdb" for badger, etc. +// DBPerShardGetShardsForIndex returns the indexes from the B (green) database if +// blue-green comparison is in use, rather than from the A (blue) database. func DBPerShardGetShardsForIndex(idx *Index, roaringViewPath string) (sliceOfShards []uint64, err error) { // follow the blueGreen convention of returning the answer for 'B' or // the last wrapper type. types := idx.holder.txf.Types() ty := types[len(types)-1] + return TypedDBPerShardGetLocalShardsForIndex(ty, idx, roaringViewPath) +} + +// if roaringViewPath is "" then for ty == roaringTxn we go to disk to discover +// all the view paths under idx for type ty. +func TypedDBPerShardGetLocalShardsForIndex(ty txtype, idx *Index, roaringViewPath string) (sliceOfShards []uint64, err error) { if ty == roaringTxn { rx := &RoaringTx{ Index: idx, } + if roaringViewPath == "" { + for _, field := range idx.Fields() { + for _, view := range field.views() { + sos, err := rx.SliceOfShards("", "", "", view.path) + if err != nil { + return nil, + errors.Wrap(err, fmt.Sprintf( + "TypedDBPerShardGetLocalShardsForIndex roaringTxn view.path='%v'", view.path)) + } + sliceOfShards = append(sliceOfShards, sos...) + } + } + return dedupShardSlice(sliceOfShards), nil + } return rx.SliceOfShards("", "", "", roaringViewPath) } requiredSuffix := ty.FileSuffix() @@ -435,3 +546,192 @@ func listDirUnderDir(root string, includeRoot bool, requiredSuffix string, ignor }) return } + +// populateBlueFromGreen prepares for a blue_green run at startup time. +// +// It is called at the end of Holder.Open(). This allows the application +// of blue-green checking to pilosa instances that +// were previously run only with a single (solo) backend. +// +// PRE: This operation requires, at its start, either: +// +// (1) an empty blue database -- this allows transitioning from +// a solo database to blue_green checking where the solo +// becomes the green; or +// +// (2) that the blue data, if present, be logically +// identical to the green data -- this allows one to restart +// a pilosa that was already running in blue_green mode +// and remain in blue_green mode. +// +// In either case, the goal to to finish populateBlueFromGreen() +// and have the exact same logical set of data in both backends. +// +// Why must the data be identical after Holder.Open() finishes? +// Otherwise subsequent blue-green checks have no hope of +// being accurate. +// +// The blue is the destination -- this is always types[0]. +// The green source is always types[1]. The mnemonic is blue_geen. +// The blue is first, so it is in types[0]. The green +// is second, in types[1]. For example, with PILOSA_TXSRC=lmdb_roaring +// we have lmdb as blue, and roaring as green. The contents of +// lmdb must be empty or exactly match roaring. If lmdb +// starts empty, it will be populated from roaring by +// populateBlueFromGreen(). +// +func (dbs *DBShard) populateBlueFromGreen() (err error) { + n := len(dbs.W) + if n != 2 { + panic(fmt.Sprintf("copyGreenToBlue did not find 2 open DBs: have %v", n)) + } + + dest := dbs.W[0] // blue + src := dbs.W[1] // green + + // copy all the key/container pairs. + // Since a shard is fairly small, we think one Tx will suffice. + + readtx, err := src.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard}) + panicOn(err) + defer readtx.Rollback() + + writetx, err := dest.NewTx(writable, dbs.Index, Txo{Write: writable, Index: dbs.idx, Shard: dbs.Shard}) + panicOn(err) + defer writetx.Rollback() + + for _, fld := range dbs.idx.Fields() { + field := fld.Name() + for _, vw := range fld.views() { + view := vw.name + citer, _, err := readtx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0) + if err != nil { + return errors.Wrap(err, "DBShard.copyGreenToBlue readtx.ContainerIterator") + } + + for citer.Next() { + ckey, rc := citer.Value() + err := writetx.PutContainer(dbs.Index, field, view, dbs.Shard, ckey, rc) + if err != nil { + citer.Close() + return errors.Wrap(err, "DBShard.copyGreenToBlue writetx.PutContainer") + } + } + citer.Close() + } + } + err = writetx.Commit() + if err != nil { + return errors.Wrap(err, "writetx.Commit()") + } + return nil +} + +// verifyBlueEqualsGreen checks that blue and green are identical. +func (dbs *DBShard) verifyBlueEqualsGreen(numCtVerified *int64) (err error) { + + n := len(dbs.W) + if n != 2 { + panic(fmt.Sprintf("verifyBlueEqualsGreen did not find 2 open DBs: have %v", n)) + } + + blue := dbs.W[0] + green := dbs.W[1] + + greentx, err := green.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard}) + panicOn(err) + defer greentx.Rollback() + + bluetx, err := blue.NewTx(!writable, dbs.Index, Txo{Write: !writable, Index: dbs.idx, Shard: dbs.Shard}) + panicOn(err) + defer bluetx.Rollback() + + for _, fld := range dbs.idx.Fields() { + field := fld.Name() + for _, vw := range fld.views() { + + view := vw.name + gCiter, _, err := greentx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0) + if err != nil { + return errors.Wrap(err, "DBShard.verifyBlueEqualsGreen greentx.ContainerIterator") + } + + bCiter, _, err := bluetx.ContainerIterator(dbs.Index, field, view, dbs.Shard, 0) + if err != nil { + gCiter.Close() + if bCiter != nil { + bCiter.Close() + } + return errors.Wrap(err, "DBShard.verifyBlueEqualsGreen bluetx.ContainerIterator") + } + + for gCiter.Next() { + greenCkey, greenc := gCiter.Value() + + if !bCiter.Next() { + bCiter.Close() + gCiter.Close() + return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+ + "sees missing blue container at index: '%v' field: '%v' view: '%v' "+ + "shard: '%v' the greenCkey: '%v'", + dbs.Index, field, view, dbs.Shard, greenCkey)) + } + blueCkey, bluec := bCiter.Value() + + if blueCkey != greenCkey { + bCiter.Close() + gCiter.Close() + return fmt.Errorf("DBShard.verifyBlueEqualsGreen sees sequence-of-ckey "+ + "difference: blueCkey %v not equal to greenCkey %v at index: '%v' field: '%v' view: '%v' "+ + "shard: '%v'", + blueCkey, greenCkey, dbs.Index, field, view, dbs.Shard) + } + nGreen := greenc.N() + nBlue := bluec.N() + if nBlue != nGreen { + bCiter.Close() + gCiter.Close() + return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+ + "sees variation in blue at index: '%v' field: '%v' view: '%v' "+ + "shard: '%v' ckey: '%v' nHotGreen= %v nHotBlue= %v", + dbs.Index, field, view, dbs.Shard, greenCkey, nGreen, nBlue)) + } + err = bluec.BitwiseCompare(greenc) + if err != nil { + bCiter.Close() + gCiter.Close() + return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+ + "sees variation in blue at index: '%v' field: '%v' view: '%v' "+ + "shard: '%v' ckey: '%v' nHotGreen= %v nHotBlue= %v ; BitwiseCompare response: '%v'", + dbs.Index, field, view, dbs.Shard, greenCkey, nGreen, nBlue, err)) + } + atomic.AddInt64(numCtVerified, 1) + + } + if bCiter.Next() { + blueCkey, _ := bCiter.Value() + bCiter.Close() + gCiter.Close() + return errors.Wrap(err, fmt.Sprintf("DBShard.verifyBlueEqualsGreen "+ + "sees extra blue container (not present in green) at index: '%v' field: '%v' view: '%v' "+ + "shard: '%v' the ckey: '%v'", + dbs.Index, field, view, dbs.Shard, blueCkey)) + } + bCiter.Close() + gCiter.Close() + } + } + + return nil +} + +func dedupShardSlice(sos []uint64) (r []uint64) { + m := make(map[uint64]struct{}) + for _, s := range sos { + m[s] = struct{}{} + } + for k := range m { + r = append(r, k) + } + return +} diff --git a/dbshard_internal_test.go b/dbshard_internal_test.go index 96b7602a1..175c24f65 100644 --- a/dbshard_internal_test.go +++ b/dbshard_internal_test.go @@ -54,7 +54,7 @@ func TestShardPerDB_SetBit(t *testing.T) { if err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if n := f.mustRow(tx, 120).Count(); n != 2 { @@ -72,12 +72,13 @@ func Test_DBPerShard_GetShardsForIndex(t *testing.T) { orig := os.Getenv("PILOSA_TXSRC") defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests! - for _, src := range []string{"lmdb", "roaring", "rbf"} { - makeSampleRoaringDir(tmpdir, src) + for _, src := range []string{"lmdb", "roaring", "badger", "rbf"} { + makeSampleRoaringDir(tmpdir, src, 0) os.Setenv("PILOSA_TXSRC", src) // must make Holder AFTER setting src. holder := NewHolder(tmpdir, nil) + idx, err := NewIndex(holder, tmpdir, "rick") panicOn(err) estd := "rick/_exists/views/standard" @@ -100,6 +101,7 @@ func Test_DBPerShard_GetShardsForIndex(t *testing.T) { } } } + holder.Close() } } @@ -135,36 +137,62 @@ rick/_exists/views/standard/fragments/219 rick/_exists/views/standard/fragments/223 `, "lmdb": ` -rick/0219-lmdb/data.mdb -rick/0219-lmdb/lock.mdb -rick/0093-lmdb/data.mdb -rick/0093-lmdb/lock.mdb -rick/0223-lmdb/data.mdb -rick/0223-lmdb/lock.mdb -rick/0215-lmdb/data.mdb -rick/0215-lmdb/lock.mdb -rick/0217-lmdb/data.mdb -rick/0217-lmdb/lock.mdb -rick/0221-lmdb/data.mdb -rick/0221-lmdb/lock.mdb +rick/0219-lmdb@/data.mdb +rick/0219-lmdb@/lock.mdb +rick/0093-lmdb@/data.mdb +rick/0093-lmdb@/lock.mdb +rick/0223-lmdb@/data.mdb +rick/0223-lmdb@/lock.mdb +rick/0215-lmdb@/data.mdb +rick/0215-lmdb@/lock.mdb +rick/0217-lmdb@/data.mdb +rick/0217-lmdb@/lock.mdb +rick/0221-lmdb@/data.mdb +rick/0221-lmdb@/lock.mdb +`, + "badger": ` +rick/0219-badgerdb@/000000.vlog +rick/0219-badgerdb@/KEYREGISTRY +rick/0219-badgerdb@/MANIFEST +rick/0219-badgerdb@/LOCK +rick/0221-badgerdb@/000000.vlog +rick/0221-badgerdb@/KEYREGISTRY +rick/0221-badgerdb@/MANIFEST +rick/0221-badgerdb@/LOCK +rick/0223-badgerdb@/000000.vlog +rick/0223-badgerdb@/KEYREGISTRY +rick/0223-badgerdb@/MANIFEST +rick/0223-badgerdb@/LOCK +rick/0093-badgerdb@/000000.vlog +rick/0093-badgerdb@/KEYREGISTRY +rick/0093-badgerdb@/MANIFEST +rick/0093-badgerdb@/LOCK +rick/0217-badgerdb@/000000.vlog +rick/0217-badgerdb@/KEYREGISTRY +rick/0217-badgerdb@/MANIFEST +rick/0217-badgerdb@/LOCK +rick/0215-badgerdb@/000000.vlog +rick/0215-badgerdb@/KEYREGISTRY +rick/0215-badgerdb@/MANIFEST +rick/0215-badgerdb@/LOCK `, "rbf": ` -rick/0223-rbfdb/wal/0000000000000001.wal -rick/0223-rbfdb/data -rick/0093-rbfdb/wal/0000000000000001.wal -rick/0093-rbfdb/data -rick/0217-rbfdb/wal/0000000000000001.wal -rick/0217-rbfdb/data -rick/0215-rbfdb/wal/0000000000000001.wal -rick/0215-rbfdb/data -rick/0221-rbfdb/wal/0000000000000001.wal -rick/0221-rbfdb/data -rick/0219-rbfdb/wal/0000000000000001.wal -rick/0219-rbfdb/data +rick/0223-rbfdb@/wal/0000000000000001.wal +rick/0223-rbfdb@/data +rick/0093-rbfdb@/wal/0000000000000001.wal +rick/0093-rbfdb@/data +rick/0217-rbfdb@/wal/0000000000000001.wal +rick/0217-rbfdb@/data +rick/0215-rbfdb@/wal/0000000000000001.wal +rick/0215-rbfdb@/data +rick/0221-rbfdb@/wal/0000000000000001.wal +rick/0221-rbfdb@/data +rick/0219-rbfdb@/wal/0000000000000001.wal +rick/0219-rbfdb@/data `, } -func makeSampleRoaringDir(root, txsrc string) { +func makeSampleRoaringDir(root, txsrc string, minBytes int) { fns := strings.Split(sampleRoaringDirList[txsrc], "\n") for _, fn := range fns { if fn == "" { @@ -174,6 +202,10 @@ func makeSampleRoaringDir(root, txsrc string) { panicOn(os.MkdirAll(path, 0755)) fd, err := os.Create(root + sep + fn) panicOn(err) + if minBytes > 0 { + _, err := fd.Write(make([]byte, minBytes)) + panicOn(err) + } fd.Close() } } diff --git a/dbshard_test.go b/dbshard_test.go index 380412d8d..d69aa7943 100644 --- a/dbshard_test.go +++ b/dbshard_test.go @@ -18,9 +18,7 @@ import ( "context" "fmt" "os" - "path/filepath" "reflect" - "strings" "testing" "github.com/pilosa/pilosa/v2" @@ -39,7 +37,8 @@ func skipForNonLMDB(t *testing.T) { } } -var _ = skipForNonLMDB // happy linter +var _ = skipForNonLMDB // happy linter +var _ = skipForNonBadger // happy linter func skipForNonBadger(t *testing.T) { src := os.Getenv("PILOSA_TXSRC") @@ -50,7 +49,7 @@ func skipForNonBadger(t *testing.T) { // Can't write it all to one shard like we do (did). func Test_DBPerShard_multiple_shards_used(t *testing.T) { - skipForNonBadger(t) + skipForNonLMDB(t) c := test.MustRunCluster(t, 1) defer c.Close() hldr := c.GetHolder(0) @@ -62,8 +61,8 @@ func Test_DBPerShard_multiple_shards_used(t *testing.T) { hldr.SetBit(index, "general", 11, 2) hldr.SetBit(index, "general", 11, ShardWidth+2) - //tx_suffix := "-lmdb" - tx_suffix := "-badgerdb" + types := pilosa.MustTxsrcToTxtype("lmdb") + tx_suffix := types[0].FileSuffix() root := hldr.Path() + sep + index shards := []string{"0000", "0001", "0002"} pathShard := []string{} @@ -75,7 +74,7 @@ func Test_DBPerShard_multiple_shards_used(t *testing.T) { if !DirExists(pathShard[i]) { panic(fmt.Sprintf("no shard made for pathShard[%v]='%v'", i, pathShard[i])) } - sz, err := DiskUse(pathShard[i], "") + sz, err := pilosa.DiskUse(pathShard[i], "") panicOn(err) if sz < 100 { @@ -90,28 +89,6 @@ func Test_DBPerShard_multiple_shards_used(t *testing.T) { } } -func DiskUse(root string, requiredSuffix string) (tot int, err error) { - if !DirExists(root) { - return -1, fmt.Errorf("listFilesUnderDir error: root directory '%v' not found", root) - } - - err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if info == nil { - panic(fmt.Sprintf("info was nil for path = '%v'", path)) - } - if info.IsDir() { - // skip directories. - } else { - sz := info.Size() - if requiredSuffix == "" || strings.HasSuffix(path, requiredSuffix) { - tot += int(sz) - } - } - return nil - }) - return -} - func TestAPI_SimplerOneNode_ImportColumnKey(t *testing.T) { c := test.MustRunCluster(t, 1, diff --git a/diagnostics.go b/diagnostics.go index b3ab1dd1f..9c380c1b8 100644 --- a/diagnostics.go +++ b/diagnostics.go @@ -229,7 +229,7 @@ func (d *diagnosticsCollector) EnrichWithSchemaProperties() { timeQuantumEnabled := false for _, index := range d.server.holder.Indexes() { - numShards += index.AvailableShards().Count() + numShards += index.AvailableShards(includeRemote).Count() numIndexes++ for _, field := range index.Fields() { numFields++ diff --git a/executor.go b/executor.go index 3b5062fe2..4ef7e90a4 100644 --- a/executor.go +++ b/executor.go @@ -506,7 +506,7 @@ func (e *executor) execute(ctx context.Context, qcx *Qcx, index string, q *pql.Q if idx == nil { return nil, newNotFoundError(ErrIndexNotFound, index) } - shards = idx.AvailableShards().Slice() + shards = idx.AvailableShards(includeRemote).Slice() if len(shards) == 0 { shards = []uint64{0} } @@ -780,7 +780,7 @@ func (e *executor) executeCall(ctx context.Context, qcx *Qcx, index string, c *p if idx == nil { return nil, newNotFoundError(ErrIndexNotFound, index) } - shards = idx.AvailableShards().Slice() + shards = idx.AvailableShards(includeRemote).Slice() if len(shards) == 0 { shards = []uint64{0} } @@ -4808,7 +4808,7 @@ func (e *executor) translateCalls(ctx context.Context, defaultIndexName string, // Perform a separate batch translation for each separate index used. keyMaps := make(map[string]map[string]uint64) for indexName, keySet := range keySets { - idx := e.Holder.indexes[indexName] + idx := e.Holder.Index(indexName) if idx == nil { return fmt.Errorf("cannot find index %q", indexName) } @@ -4851,8 +4851,8 @@ func (e *executor) collectCallKeySets(ctx context.Context, indexName string, c * // Collect foreign index keys. if fieldName != "" { - idx, exists := e.Holder.indexes[indexName] - if !exists { + idx := e.Holder.Index(indexName) + if idx == nil { return newNotFoundError(ErrIndexNotFound, indexName) } if field := idx.Field(fieldName); field != nil && field.ForeignIndex() != "" { @@ -4899,8 +4899,8 @@ func (e *executor) translateCall(ctx context.Context, indexName string, c *pql.C // Translate column key. colKey, rowKey, fieldName := c.TranslateInfo(columnLabel, rowLabel) - idx, exists := e.Holder.indexes[indexName] - if !exists { + idx := e.Holder.Index(indexName) + if idx == nil { return newNotFoundError(ErrIndexNotFound, indexName) } if idx.Keys() { diff --git a/executor_internal_test.go b/executor_internal_test.go index cd7347869..622037032 100644 --- a/executor_internal_test.go +++ b/executor_internal_test.go @@ -155,7 +155,7 @@ func TestExecutor_TranslateRowsOnBool(t *testing.T) { } shard := uint64(0) - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() fb, errb := idx.CreateField("b", OptFieldTypeBool()) diff --git a/executor_test.go b/executor_test.go index eb9dad815..e79a0e301 100644 --- a/executor_test.go +++ b/executor_test.go @@ -936,7 +936,7 @@ func TestExecutor_Execute_SetValue(t *testing.T) { // Obtain transaction. idx := index.Index shard := uint64(0) - tx := idx.Txf.NewTx(pilosa.Txo{Write: !writable, Index: idx, Shard: shard}) + tx := idx.Txf().NewTx(pilosa.Txo{Write: !writable, Index: idx, Shard: shard}) defer tx.Rollback() f := hldr.Field("i", "f") diff --git a/field.go b/field.go index 2073bf8ac..9e0b13ad2 100644 --- a/field.go +++ b/field.go @@ -412,11 +412,31 @@ func (f *Field) TranslateStore() TranslateStore { func (f *Field) RowAttrStore() AttrStore { return f.rowAttrStore } // AvailableShards returns a bitmap of shards that contain data. -func (f *Field) AvailableShards() *roaring.Bitmap { +func (f *Field) AvailableShards(localOnly bool) *roaring.Bitmap { f.mu.RLock() defer f.mu.RUnlock() - b := f.remoteAvailableShards.Clone() + var b *roaring.Bitmap + if localOnly { + b = roaring.NewBitmap() + } else { + b = f.remoteAvailableShards.Clone() + } + for _, view := range f.viewMap { + //b.Union(view.availableShards()) + b.UnionInPlace(view.availableShards()) + } + return b +} + +// LocalAvailableShards returns a bitmap of shards that contain data, but +// only from the local node. This prevents txfactory from making +// db-per-shard for remote shards. +func (f *Field) LocalAvailableShards() *roaring.Bitmap { + f.mu.RLock() + defer f.mu.RUnlock() + + b := roaring.NewBitmap() for _, view := range f.viewMap { //b.Union(view.availableShards()) b.UnionInPlace(view.availableShards()) @@ -687,7 +707,7 @@ func (f *Field) applyTranslateStore() error { // In the case where the field has a foreign index, set // the usesKeys value accordingly. if foreignIndexName := f.ForeignIndex(); foreignIndexName != "" { - if foreignIndex := f.holder.indexes[foreignIndexName]; foreignIndex != nil { + if foreignIndex := f.holder.Index(foreignIndexName); foreignIndex != nil { f.usesKeys = foreignIndex.Keys() } } @@ -1483,7 +1503,7 @@ func (f *Field) SetValue(tx Tx, columnID uint64, value int64) (changed bool, err if view.idx == nil { panic("view.idx should not be nil") } - view.holder.addIndexFromField(view.idx) + view.holder.addIndex(view.idx) return view.setValue(tx, columnID, bsig.BitDepth, baseValue) } @@ -1571,7 +1591,7 @@ func (f *Field) MinForShard(tx Tx, shard uint64, filter *Row) (ValCount, error) var localTx Tx if NilInside(tx) { - localTx = f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Fragment: fragment, Shard: fragment.shard}) + localTx = f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Fragment: fragment, Shard: fragment.shard}) defer localTx.Rollback() } else { localTx = tx diff --git a/field_internal_test.go b/field_internal_test.go index 1bb96c617..ad0f5faaf 100644 --- a/field_internal_test.go +++ b/field_internal_test.go @@ -208,6 +208,8 @@ func NewTestField(t *testing.T, opts FieldOption) *TestField { t.Fatal(err) } h := NewHolder(path, nil) + panicOn(h.Open()) + idx, err := h.CreateIndex("i", IndexOptions{}) if err != nil { panic(err) @@ -232,7 +234,7 @@ func OpenField(t *testing.T, opts FieldOption) *TestField { // Close closes the field and removes the underlying data. func (f *TestField) Close() error { if f.idx != nil { - panicOn(f.idx.Txf.CloseIndex(f.idx)) + panicOn(f.idx.holder.txf.CloseIndex(f.idx)) } defer os.RemoveAll(f.Path()) return f.Field.Close() @@ -245,7 +247,7 @@ func (f *TestField) Reopen() error { f.parent = nil return err } - if err := f.parent.Open(false); err != nil { + if err := f.parent.Open(); err != nil { f.parent = nil return err } @@ -318,7 +320,7 @@ func TestField_RowTime(t *testing.T) { defer f.Close() // Obtain transaction. - tx := f.idx.Txf.NewTx(Txo{Write: writable, Index: f.idx, Field: f.Field, Shard: 0}) + tx := f.idx.holder.txf.NewTx(Txo{Write: writable, Index: f.idx, Field: f.Field, Shard: 0}) defer tx.Rollback() if err := f.setTimeQuantum(TimeQuantum("YMDH")); err != nil { @@ -334,7 +336,7 @@ func TestField_RowTime(t *testing.T) { panicOn(tx.Commit()) // obtain 2nd transaction to read it back. - tx = f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: 0}) + tx = f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: 0}) defer tx.Rollback() if r, err := f.RowTime(tx, 1, time.Date(2010, time.November, 5, 12, 0, 0, 0, time.UTC), "Y"); err != nil { @@ -574,7 +576,7 @@ func TestBSIGroup_importValue(t *testing.T) { f := OpenField(t, OptFieldTypeInt(-100, 200)) defer f.Close() - qcx := f.idx.Txf.NewQcx() + qcx := f.idx.holder.txf.NewQcx() defer qcx.Abort() options := &ImportOptions{} @@ -620,7 +622,7 @@ func TestIntField_MinMaxForShard(t *testing.T) { f := OpenField(t, OptFieldTypeInt(-100, 200)) defer f.Close() - qcx := f.idx.Txf.NewQcx() + qcx := f.idx.holder.txf.NewQcx() defer qcx.Abort() options := &ImportOptions{} @@ -679,7 +681,7 @@ func TestIntField_MinMaxForShard(t *testing.T) { panicOn(qcx.Finish()) shard := uint64(0) - tx := f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard}) + tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard}) // Rollback below manually, because we are in a loop. maxvc, err := f.MaxForShard(tx, shard, nil) @@ -705,6 +707,7 @@ func TestIntField_MinMaxForShard(t *testing.T) { // Ensure we get errors when they are expected. func TestDecimalField_MinMaxBoundaries(t *testing.T) { th := newTestHolder(t) + defer th.Close() for i, test := range []struct { scale int64 min pql.Decimal @@ -785,7 +788,7 @@ func TestDecimalField_MinMaxForShard(t *testing.T) { f := OpenField(t, OptFieldTypeDecimal(3)) defer f.Close() - qcx := f.idx.Txf.NewQcx() + qcx := f.idx.holder.txf.NewQcx() defer qcx.Abort() options := &ImportOptions{} @@ -843,7 +846,7 @@ func TestDecimalField_MinMaxForShard(t *testing.T) { } shard := uint64(0) - tx := f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard}) + tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Field: f.Field, Shard: shard}) defer tx.Rollback() maxvc, err := f.MaxForShard(tx, shard, nil) @@ -869,7 +872,7 @@ func TestBSIGroup_TxReopenDB(t *testing.T) { f := OpenField(t, OptFieldTypeInt(-100, 200)) defer f.Close() - qcx := f.idx.Txf.NewQcx() + qcx := f.idx.holder.txf.NewQcx() defer qcx.Abort() options := &ImportOptions{} diff --git a/field_test.go b/field_test.go index ea0c3e1e2..af58039a1 100644 --- a/field_test.go +++ b/field_test.go @@ -205,6 +205,8 @@ func TestField_NameValidation(t *testing.T) { } } +const includeRemote = false // for calls to Index.AvailableShards(localOnly bool) + // Ensure can update and delete available shards. func TestField_AvailableShards(t *testing.T) { idx := test.MustOpenIndex(t) @@ -223,7 +225,7 @@ func TestField_AvailableShards(t *testing.T) { t.Fatal(err) } else if _, err := f.SetBit(tx, 0, ShardWidth*2, nil); err != nil { t.Fatal(err) - } else if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 2}); diff != "" { + } else if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" { t.Fatal(diff) } @@ -231,7 +233,7 @@ func TestField_AvailableShards(t *testing.T) { if err := f.AddRemoteAvailableShards(roaring.NewBitmap(1, 2, 4)); err != nil { t.Fatalf("adding remote shards: %v", err) } - if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 1, 2, 4}); diff != "" { + if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 1, 2, 4}); diff != "" { t.Fatal(diff) } @@ -242,7 +244,7 @@ func TestField_AvailableShards(t *testing.T) { t.Fatalf("removing shard %d: %v", i, err) } } - if diff := cmp.Diff(f.AvailableShards().Slice(), []uint64{0, 2}); diff != "" { + if diff := cmp.Diff(f.AvailableShards(includeRemote).Slice(), []uint64{0, 2}); diff != "" { t.Fatal(diff) } } diff --git a/fragment.go b/fragment.go index cc8423e02..d248f4665 100644 --- a/fragment.go +++ b/fragment.go @@ -173,8 +173,9 @@ type fragment struct { // newFragment returns a new instance of Fragment. func newFragment(holder *Holder, path, index, field, view string, shard uint64, flags byte) *fragment { idx := holder.Index(index) + if idx == nil { - panic(fmt.Sprintf("holder=%#v but got nil idx back from holder!", holder)) + panic(fmt.Sprintf("got nil idx back for '%v' from holder!", index)) } f := &fragment{ path: path, @@ -478,7 +479,7 @@ func (f *fragment) openCache() error { return nil } - tx := f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Fragment: f, Shard: f.shard}) + tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Read in all rows by ID. @@ -996,7 +997,7 @@ func (f *fragment) setValueBase(txOrig Tx, columnID uint64, bitDepth uint, value tx := txOrig if NilInside(tx) { - tx = f.idx.Txf.NewTx(Txo{Write: writable, Index: f.idx, Fragment: f, Shard: f.shard}) + tx = f.idx.holder.txf.NewTx(Txo{Write: writable, Index: f.idx, Fragment: f, Shard: f.shard}) defer func() { if err == nil { panicOn(tx.Commit()) @@ -1996,9 +1997,9 @@ func (f *fragment) Blocks() ([]FragmentBlock, error) { 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)) + panic(fmt.Sprintf("index was nil in fragment.Blocks(): f.index='%v'\n", f.index)) } - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // no Commit below, b/c is read-only. @@ -2083,7 +2084,7 @@ func (f *fragment) blockData(id int) (rowIDs, columnIDs []uint64, err error) { defer f.mu.Unlock() idx := f.holder.Index(f.index) - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Shard: f.shard}) defer tx.Rollback() // readonly, so no Commit() @@ -2785,7 +2786,7 @@ func (f *fragment) WriteTo(w io.Writer) (n int64, err error) { // used in shipping the slices across the network for a resize. func (f *fragment) writeStorageToArchive(tw *tar.Writer) error { - tx := f.idx.Txf.NewTx(Txo{Write: !writable, Index: f.idx, Shard: f.shard}) + tx := f.idx.holder.txf.NewTx(Txo{Write: !writable, Index: f.idx, Shard: f.shard}) defer tx.Rollback() file, sz, err := tx.RoaringBitmapReader(f.index, f.field, f.view, f.shard, f.path) if err != nil { @@ -2859,7 +2860,7 @@ func (f *fragment) ReadFrom(r io.Reader) (n int64, err error) { switch hdr.Name { case "data": idx := f.holder.Index(f.index) - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if err := f.fillFragmentFromArchive(tx, tr); err != nil { return 0, errors.Wrap(err, "reading storage") @@ -3639,7 +3640,7 @@ func (s *fragmentSyncer) syncBlock(id int) error { } idx := f.holder.Index(f.index) - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: f.shard}) defer tx.Rollback() // Merge blocks together. diff --git a/fragment_internal_test.go b/fragment_internal_test.go index 1597b93d1..394a8cabf 100644 --- a/fragment_internal_test.go +++ b/fragment_internal_test.go @@ -81,7 +81,7 @@ func TestFragment_SetBit(t *testing.T) { if err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if n := f.mustRow(tx, 120).Count(); n != 2 { @@ -114,7 +114,7 @@ func TestFragment_ClearBit(t *testing.T) { // 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, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Close and reopen the fragment & verify the data. @@ -199,7 +199,7 @@ func TestFragment_ClearRow(t *testing.T) { t.Fatalf("unexpected count: %d", n) } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Close and reopen the fragment & verify the data. @@ -246,7 +246,7 @@ func TestFragment_SetRow(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Verify data on row. @@ -259,7 +259,7 @@ func TestFragment_SetRow(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Close and reopen the fragment & verify the data. @@ -304,7 +304,7 @@ func TestFragment_SetValue(t *testing.T) { if err := tx.Commit(); err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Read value. @@ -356,7 +356,7 @@ func TestFragment_SetValue(t *testing.T) { if err := tx.Commit(); err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if value, exists, err := f.value(tx, 100, 16); err != nil { @@ -401,7 +401,7 @@ func TestFragment_SetValue(t *testing.T) { if err := tx.Commit(); err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if value, exists, err := f.value(tx, 100, 16); err != nil { @@ -478,7 +478,7 @@ func TestFragment_SetValue(t *testing.T) { if err := tx.Commit(); err != nil { t.Fatal(err) } - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Ensure values are set. @@ -525,7 +525,7 @@ func TestFragment_Sum(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() t.Run("NoFilter", func(t *testing.T) { @@ -549,7 +549,7 @@ func TestFragment_Sum(t *testing.T) { }) panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // verify that clearValue clears values @@ -558,7 +558,7 @@ func TestFragment_Sum(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() t.Run("ClearValue", func(t *testing.T) { @@ -599,7 +599,7 @@ func TestFragment_MinMax(t *testing.T) { panicOn(tx.Commit()) // the new tx is shared by Min/Max below. - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() t.Run("Min", func(t *testing.T) { @@ -1197,7 +1197,7 @@ func TestFragment_Snapshot(t *testing.T) { t.Fatal(err) } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Snapshot bitmap and verify data. @@ -1290,7 +1290,7 @@ func TestFragment_Top_Filter(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Retrieve top rows. @@ -1467,7 +1467,7 @@ func TestFragment_TopN_CacheSize(t *testing.T) { defer f.Clean(t) // Obtain transaction. - tx := index.Txf.NewTx(Txo{Write: writable, Index: index, Fragment: f, Shard: f.shard}) + tx := index.holder.txf.NewTx(Txo{Write: writable, Index: index, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Set bits on various rows. @@ -1546,7 +1546,7 @@ func TestFragment_Blocks(t *testing.T) { } prev = blocks - tx = idx.Txf.NewTx(Txo{Write: true, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: true, Index: idx, Fragment: f, Shard: f.shard}) // Set bit on different row. if _, err := f.setBit(tx, 20, 0); err != nil { t.Fatal(err) @@ -1561,7 +1561,7 @@ func TestFragment_Blocks(t *testing.T) { prev = blocks // Set bit on different column. - tx = idx.Txf.NewTx(Txo{Write: true, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: true, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if _, err := f.setBit(tx, 20, 100); err != nil { t.Fatal(err) @@ -1658,7 +1658,7 @@ func TestFragment_RankCache_Persistence(t *testing.T) { } // Obtain transaction. - tx := index.Txf.NewTx(Txo{Write: writable, Index: index, Fragment: f, Shard: f.shard}) + tx := index.holder.txf.NewTx(Txo{Write: writable, Index: index, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Set bits on the fragment. @@ -1669,7 +1669,7 @@ func TestFragment_RankCache_Persistence(t *testing.T) { } panicOn(tx.Commit()) - tx = index.Txf.NewTx(Txo{Write: !writable, Index: index, Fragment: f, Shard: f.shard}) + tx = index.holder.txf.NewTx(Txo{Write: !writable, Index: index, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Verify correct cache type and size. @@ -1808,7 +1808,7 @@ func BenchmarkFragment_IntersectionCount(b *testing.B) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Snapshot to disk before benchmarking. @@ -2149,7 +2149,7 @@ func TestFragment_ImportSet_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2161,7 +2161,7 @@ func TestFragment_ImportSet_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Clear import. @@ -2171,7 +2171,7 @@ func TestFragment_ImportSet_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2199,12 +2199,12 @@ func TestFragment_ConcurrentImport(t *testing.T) { eg := errgroup.Group{} eg.Go(func() error { - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) defer func() { panicOn(tx.Commit()) }() return f.bulkImportStandard(tx, []uint64{1, 2}, []uint64{1, 2}, &ImportOptions{}) }) eg.Go(func() error { - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) defer func() { panicOn(tx.Commit()) }() return f.bulkImportStandard(tx, []uint64{3, 4}, []uint64{3, 4}, &ImportOptions{}) }) @@ -2428,7 +2428,7 @@ func TestFragment_ImportMutex_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2440,7 +2440,7 @@ func TestFragment_ImportMutex_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Clear import. @@ -2450,7 +2450,7 @@ func TestFragment_ImportMutex_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2678,7 +2678,7 @@ func TestFragment_ImportBool_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2690,7 +2690,7 @@ func TestFragment_ImportBool_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Clear import. @@ -2700,7 +2700,7 @@ func TestFragment_ImportBool_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Check for expected results. @@ -2763,7 +2763,7 @@ func BenchmarkFragment_FullSnapshot(b *testing.B) { i++ } - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if err := f.bulkImport(tx, rows, cols, options); err != nil { @@ -3137,6 +3137,7 @@ func BenchmarkImportIntoLargeFragment(b *testing.B) { fi.Close() h := NewHolder(fi.Name(), nil) + panicOn(h.Open()) idx, err := h.CreateIndex("i", IndexOptions{}) panicOn(err) @@ -3147,7 +3148,7 @@ func BenchmarkImportIntoLargeFragment(b *testing.B) { } // Obtain transaction. - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() copy(rows, rowsOrig) @@ -3195,7 +3196,7 @@ func BenchmarkImportRoaringIntoLargeFragment(b *testing.B) { f := newFragment(th, fi.Name(), "i", "f", viewStandard, 0, 0) defer f.Clean(b) - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() err = f.Open() @@ -3432,6 +3433,7 @@ func mustOpenBSIFragment(tb testing.TB, index, field, view string, shard uint64) func newTestHolder(tb testing.TB) *Holder { path, _ := testhook.TempDirInDir(tb, *TempDir, "holder-dir") h := NewHolder(path, nil) + panicOn(h.Open()) //h.SnapshotQueue = newSnapshotQueue(1, 1, nil) return h } @@ -3446,7 +3448,7 @@ func fragTestMustOpenIndex(index string, holder *Holder, opt IndexOptions) *Inde idx.keys = opt.Keys idx.trackExistence = opt.TrackExistence - if err := idx.Open(false); err != nil { + if err := idx.Open(); err != nil { panic(err) } return idx @@ -3472,10 +3474,10 @@ func mustOpenFragmentFlags(tb testing.TB, index, field, view string, shard uint6 fragPath := fragDir + fmt.Sprintf("%v", shard) f := newFragment(th, fragPath, index, field, view, shard, flags) - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: shard}) testhook.Cleanup(tb, func() { tx.Rollback() - panicOn(idx.Txf.CloseIndex(idx)) + panicOn(idx.holder.txf.CloseIndex(idx)) }) f.CacheType = cacheType @@ -3582,7 +3584,7 @@ func TestFragment_RowsIteration(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() ids, err := f.rows(context.Background(), tx, 0) @@ -4010,7 +4012,7 @@ func TestFragmentRowIterator_WithTxCommit(t *testing.T) { f.mustSetBits(tx, 3, 0) panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() iter, err := f.rowIterator(tx, false) @@ -4058,7 +4060,7 @@ func TestFragmentRowIterator_WithTxCommit(t *testing.T) { f.mustSetBits(tx, 7, 0) panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() iter, err := f.rowIterator(tx, false) @@ -4106,7 +4108,7 @@ func TestFragmentRowIterator_WithTxCommit(t *testing.T) { f.mustSetBits(tx, 3, 0) panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() iter, err := f.rowIterator(tx, true) @@ -4143,7 +4145,7 @@ func TestFragmentRowIterator_WithTxCommit(t *testing.T) { f.mustSetBits(tx, 7, 0) panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() iter, err := f.rowIterator(tx, true) @@ -4166,7 +4168,7 @@ func TestFragmentRowIterator_WithTxCommit(t *testing.T) { t.Errorf("should have wrapped") } if !reflect.DeepEqual(row.Columns(), []uint64{0}) { - t.Fatalf("got wrong columns back on iteration %d - should just be 0 but %v", i, row.Columns()) + t.Fatalf("got wrong columns back on iteration %d - should just be 0 but got %v", i, row.Columns()) } } }) @@ -4392,7 +4394,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { t.Run("<", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4416,7 +4418,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { }) t.Run("<=", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4440,7 +4442,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { }) t.Run(">", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4463,7 +4465,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { } }) t.Run(">=", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4487,7 +4489,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { }) t.Run("Range", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4523,7 +4525,7 @@ func TestFragmentBSIUnsigned(t *testing.T) { }) t.Run("==", func(t *testing.T) { - tx := idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: shard}) defer tx.Rollback() for i := minCheck; i < maxCheck; i++ { @@ -4563,7 +4565,7 @@ func TestFragmentBSIUnsigned_WithTxCommit(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Generate a list of columns. @@ -4726,7 +4728,7 @@ func TestFragmentBSISigned(t *testing.T) { } panicOn(tx.Commit()) - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() // Generate a list of columns. @@ -4926,7 +4928,7 @@ func TestImportClearRestart(t *testing.T) { if err != nil { t.Fatalf("initial small import: %v", err) } - if idx.Txf.TxType() == RoaringTxn { + if idx.holder.txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f.opN != expOpN { t.Errorf("unexpected opN - %d is not %d", f.opN, expOpN) } @@ -4940,13 +4942,13 @@ func TestImportClearRestart(t *testing.T) { panicOn(tx.Commit()) err = f.Open() - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() if err != nil { t.Fatalf("reopening fragment: %v", err) } - if idx.Txf.TxType() == RoaringTxn { + if idx.holder.txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f.opN != expOpN { t.Errorf("unexpected opN after close/open %d is not %d", f.opN, expOpN) } @@ -4970,7 +4972,7 @@ func TestImportClearRestart(t *testing.T) { panicOn(tx.Commit()) // match the f.closeStorage which overlaps the f2 creation. - tx2 := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f2, Shard: f2.shard}) + tx2 := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f2, Shard: f2.shard}) defer tx2.Rollback() err = f.closeStorage() @@ -4983,7 +4985,7 @@ func TestImportClearRestart(t *testing.T) { t.Fatalf("opening new fragment: %v", err) } - if idx.Txf.TxType() == RoaringTxn { + if idx.holder.txf.TxType() == RoaringTxn { if expOpN <= maxOpN && f2.opN != expOpN { t.Errorf("unexpected opN after close/open %d is not %d", f2.opN, expOpN) } @@ -5016,7 +5018,7 @@ func TestImportClearRestart(t *testing.T) { f3.MaxOpN = maxOpN f3.CacheType = f.CacheType - tx3 := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f3, Shard: f3.shard}) + tx3 := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f3, Shard: f3.shard}) defer tx3.Rollback() err = f2.closeStorage() @@ -5072,7 +5074,7 @@ func TestImportValueConcurrent(t *testing.T) { // we will be making a new Tx each time, so we can rollback the default provided one. tx.Rollback() - types := idx.Txf.TxTypes() + types := idx.holder.txf.TxTypes() for _, ty := range types { switch ty { case roaringTxn: @@ -5090,7 +5092,7 @@ func TestImportValueConcurrent(t *testing.T) { for i := 0; i < 4; i++ { i := i eg.Go(func() error { - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) 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) @@ -5138,7 +5140,7 @@ func TestImportMultipleValues(t *testing.T) { // probably too slow, would hit disk alot: //panicOn(tx.Commit()) - //tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard:f.shard, ShardSet:true}) + //tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard:f.shard, ShardSet:true}) //defer tx.Rollback() for i := range test.checkCols { @@ -5232,13 +5234,11 @@ func TestFragmentConcurrentReadWrite(t *testing.T) { f, idx, tx := mustOpenFragment(t, "i", "f", viewStandard, 0, CacheTypeRanked) defer f.Clean(t) tx.Rollback() - // Obtain transaction, but don't start another b/c the - // two goroutines below need the same view. eg := &errgroup.Group{} eg.Go(func() error { - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) for i := uint64(0); i < 1000; i++ { _, err := f.setBit(tx, i%4, i) @@ -5251,7 +5251,7 @@ func TestFragmentConcurrentReadWrite(t *testing.T) { }) // need read-only Tx so as not to block on the writer finishing above. - tx = idx.Txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: !writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() acc := uint64(0) diff --git a/go.mod b/go.mod index 0c24f92df..4bcaa3810 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/cespare/xxhash v1.1.0 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect github.com/davecgh/go-spew v1.1.1 + github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20200709123515-8e896a7af361 + github.com/glycerine/idem v0.0.0-20190127113923-7a8083893311 github.com/glycerine/lmdb-go v1.9.32 github.com/go-ole/go-ole v1.2.4 // indirect github.com/gogo/protobuf v1.2.1 @@ -31,7 +33,6 @@ 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/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.4.0 @@ -45,7 +46,6 @@ require ( golang.org/x/text v0.3.3 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/grpc v1.28.0 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect modernc.org/mathutil v1.0.0 modernc.org/strutil v1.0.0 vitess.io/vitess v3.0.0-rc.3.0.20190602171040-12bfde34629c+incompatible diff --git a/go.sum b/go.sum index 6a9cc69d2..39084cbd8 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= @@ -33,15 +35,25 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 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/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/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/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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +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= @@ -76,6 +88,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/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= @@ -185,6 +199,7 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung 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/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -206,6 +221,7 @@ 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.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= @@ -214,6 +230,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -231,6 +248,7 @@ 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 v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 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= @@ -247,6 +265,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -288,9 +307,11 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/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= diff --git a/gossip/gossip.go b/gossip/gossip.go index 6e7ebb464..d7b6377e7 100644 --- a/gossip/gossip.go +++ b/gossip/gossip.go @@ -330,7 +330,7 @@ func (g *memberSet) LocalState(join bool) []byte { for _, f := range idx.Fields { availableShards := roaring.NewBitmap() if field, _ := g.papi.Field(context.Background(), idx.Name, f.Name); field != nil { - availableShards = field.AvailableShards() + availableShards = field.AvailableShards(false) } fs := &pilosa.FieldStatus{ diff --git a/holdbkg.go b/holdbkg.go new file mode 100644 index 000000000..1a2ef2e6c --- /dev/null +++ b/holdbkg.go @@ -0,0 +1,135 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "github.com/glycerine/idem" +) + +// holderBackgroundGoro avoids the deadlock versus race dilmena +// when creating a new index. The troubles were in having +// the field/view code try to inform the Holder of the indexes +// from a platoon of worker goroutines and tests; see index.go:273 inside +// Index.openFields(). There calling i.holder.addIndex(i) instead +// of locking cleans things up alot. There's no need anymore +// to track which goroutines are holding the Holder's +// mu lock. +type holderBackgroundGoro struct { + h *Holder + halt *idem.Halter + reqIndexCh chan *indexReq + setIdxCh chan *Index + getAllCh chan *indexReq + delIdxCh chan string +} + +// indexReq is used to ask the holderBackgrounGoro +// for index information. +type indexReq struct { + // basic request to map an index name to *Index + index string + idx *Index + + // double duty as a getAll indexes request + getAll bool + all []*Index + + done chan struct{} +} + +func newIndexReq(index string) *indexReq { + return &indexReq{ + index: index, + done: make(chan struct{}), + } +} + +func (b *holderBackgroundGoro) index(index string) *Index { + if b == nil { + // utils_internal_test.go:448 from + // TestCluster_ResizeStates/Multiple_nodes,_with_data + // wants to pass a nil b/nil Holder. sigh. don't panic. + return nil + } + req := newIndexReq(index) + b.reqIndexCh <- req + <-req.done + return req.idx +} + +func (b *holderBackgroundGoro) isClosed() bool { + select { + case <-b.halt.Done.Chan: + return true + default: + return false + } +} + +func (b *holderBackgroundGoro) start() { + go func() { + defer b.halt.Done.Close() + for { + select { + case <-b.halt.ReqStop.Chan: + return + + case r := <-b.reqIndexCh: + r.idx = b.h.indexesOwnedByBkgr[r.index] + close(r.done) + + case idx := <-b.setIdxCh: + b.h.indexesOwnedByBkgr[idx.Name()] = idx + // atomic operation, client doesn't need to wait on done, + // so don't take the time to do another channel, just leave done open. + + case target := <-b.delIdxCh: + delete(b.h.indexesOwnedByBkgr, target) + // atomic operation, client doesn't need to wait on done, + // so don't take the time to do another channel, just leave done open. + + case r := <-b.getAllCh: + r.all = make([]*Index, 0, len(b.h.indexesOwnedByBkgr)) + for _, index := range b.h.indexesOwnedByBkgr { + r.all = append(r.all, index) + } + close(r.done) + } + } + }() +} + +func (h *Holder) stopBkgr() { + h.mu.Lock() + defer h.mu.Unlock() + if h.bkgr != nil { + h.bkgr.halt.ReqStop.Close() + <-h.bkgr.halt.Done.Chan + } +} + +func (h *Holder) newHolderBackgroundGoro() (b *holderBackgroundGoro) { + + b = &holderBackgroundGoro{ + h: h, + halt: idem.NewHalter(), + reqIndexCh: make(chan *indexReq), + setIdxCh: make(chan *Index), + getAllCh: make(chan *indexReq), + delIdxCh: make(chan string), + } + b.start() + return b +} diff --git a/holder.go b/holder.go index 700487683..6848c13ab 100644 --- a/holder.go +++ b/holder.go @@ -60,12 +60,12 @@ func init() { type Holder struct { mu sync.RWMutex + // our configuration + cfg *HolderConfig + // Partition count used by translation. partitionN int - // Indexes by name. - indexes map[string]*Index - // opened channel is closed once Open() completes. opened lockedChan @@ -116,6 +116,32 @@ type Holder struct { Auditor testhook.Auditor txf *TxFactory + + bkgr *holderBackgroundGoro + + // indexesOwnedByBkgr is owned by h.bkgr. Do not touch. + // Only the owning holdbkg.go goroutine should query or + // modify the indexesOwnedByBkgr map. indexesOwnedByBkgr replaces + // the old indexes map which was the source of deadlock vs + // race issues with a dedicated goroutine + // associated with the Holder. + // + // Normally this map would live inside the holderBackgroundGoro + // struct, but tests expect the map to + // persist cross Holder ReOpens(), so it still lives here -- + // since a ReOpen will kill and restart the goroutine and + // lose its state. + // + // Again, do not touch this directly. Use these methods instead: + // + // h.Index() // index name -> *Index + // h.Indexes() // copy of the full list of *Indexes + // h.addIndex() // add one *Index + // h.deleteIndex() // delete one *Index + // + indexesOwnedByBkgr map[string]*Index + + numCtBlueGreenVerified int64 } // HolderOpts holds information about the holder which other things might want @@ -209,6 +235,8 @@ func DefaultHolderConfig() *HolderConfig { } // NewHolder returns a new instance of Holder for the given path. +// It starts the bkgr background goroutine that provides +// exclusive access to the indexesOwnedByBkgr map. func NewHolder(path string, cfg *HolderConfig) *Holder { if cfg == nil { cfg = DefaultHolderConfig() @@ -222,7 +250,7 @@ func NewHolder(path string, cfg *HolderConfig) *Holder { } h := &Holder{ - indexes: make(map[string]*Index), + cfg: cfg, closing: make(chan struct{}), opened: lockedChan{ch: make(chan struct{})}, @@ -245,7 +273,11 @@ func NewHolder(path string, cfg *HolderConfig) *Holder { Auditor: NewAuditor(), path: path, + + indexesOwnedByBkgr: make(map[string]*Index), } + h.bkgr = h.newHolderBackgroundGoro() + txf, err := NewTxFactory(cfg.Txsrc, path, h) panicOn(err) h.txf = txf @@ -535,9 +567,24 @@ func (h *Holder) Inspect(ctx context.Context, req *InspectRequest) (*HolderInfo, // Open initializes the root data directory for the holder. func (h *Holder) Open() error { + h.opening = true defer func() { h.opening = false }() + if h.bkgr == nil || h.bkgr.isClosed() { + h.bkgr = h.newHolderBackgroundGoro() + } + + if h.txf == nil { + txf, err := NewTxFactory(h.cfg.Txsrc, h.path, h) + if err != nil { + return errors.Wrap(err, "Holder.Open NewTxFactory()") + } + h.txf = txf + } + + h.txf.blueGreenOffIfRunningBlueGreen() + // Reset closing in case Holder is being reopened. h.closing = make(chan struct{}) @@ -580,9 +627,7 @@ func (h *Holder) Open() error { continue } // Skip embedded db files too. - if strings.HasSuffix(fi.Name(), "-badgerdb") || - strings.HasSuffix(fi.Name(), "-lmdb") || - strings.HasSuffix(fi.Name(), "-rbfdb") { + if h.txf.IsTxDatabasePath(fi.Name()) { continue } @@ -598,22 +643,19 @@ func (h *Holder) Open() error { if h.isCoordinator() { index.createdAt = timestamp() - err = index.OpenWithTimestamp(false) + err = index.OpenWithTimestamp() } else { - err = index.Open(false) + err = index.Open() } if err != nil { - // FIXME: The holder shouldn't be responsible for closing these, probably. - _ = index.Txf.CloseDB() + _ = h.txf.Close() if err == ErrName { h.Logger.Printf("ERROR opening index: %s, err=%s", index.Name(), err) continue } return fmt.Errorf("open index: name=%s, err=%s", index.Name(), err) } - h.mu.Lock() - h.indexes[index.Name()] = index - h.mu.Unlock() + h.addIndex(index) } // If any fields were opened before their foreign index @@ -631,7 +673,14 @@ func (h *Holder) Open() error { _ = testhook.Opened(h.Auditor, h, nil) + // under blue_green, we must sync blue from green before we turn on checking. + if err := h.txf.green2blue(h); err != nil { + return errors.Wrap(err, "Holder.Open h.txf.UpdateBlueFromGreen(h)") + } + h.txf.blueGreenOnIfRunningBlueGreen() + return nil + } // Activate runs the background tasks relevant to keeping a holder in a stable @@ -676,10 +725,14 @@ func (h *Holder) processForeignIndexFields() error { // Close closes all open fragments. func (h *Holder) Close() error { + defer h.stopBkgr() + if globalUseStatTx { fmt.Printf("%v\n", globalCallStats.report()) } - h.txf.blueGreenReg.Close() + if h.txf.blueGreenReg != nil { + h.txf.blueGreenReg.Close() + } h.Stats.Close() @@ -687,16 +740,17 @@ func (h *Holder) Close() error { close(h.closing) h.wg.Wait() - for _, index := range h.indexes { + for _, index := range h.Indexes() { if err := index.Close(); err != nil { return errors.Wrap(err, "closing index") } - if err := index.Txf.CloseDB(); err != nil { - return errors.Wrap(err, "index.Txf.CloseDB()") - } + } + if err := h.txf.Close(); err != nil { + return errors.Wrap(err, "holder.Txf.Close()") } // Reset opened in case Holder needs to be reopened. + h.txf = nil h.opened.mu.Lock() h.opened.ch = make(chan struct{}) h.opened.mu.Unlock() @@ -713,7 +767,7 @@ func (h *Holder) Close() error { func (h *Holder) NeedsSnapshot() bool { h.mu.RLock() defer h.mu.RUnlock() - for _, idx := range h.indexes { + for _, idx := range h.Indexes() { if idx.NeedsSnapshot() { return true } @@ -727,7 +781,7 @@ func (h *Holder) NeedsSnapshot() bool { func (h *Holder) HasData() (bool, error) { h.mu.RLock() defer h.mu.RUnlock() - if len(h.indexes) > 0 { + if len(h.Indexes()) > 0 { return true, nil } // Open path to read all index directories. @@ -771,7 +825,7 @@ func (h *Holder) hasV1TranslateKeysFile() (bool, error) { func (h *Holder) availableShardsByIndex() map[string]*roaring.Bitmap { m := make(map[string]*roaring.Bitmap) for _, index := range h.Indexes() { - m[index.Name()] = index.AvailableShards() + m[index.Name()] = index.AvailableShards(includeRemote) } return m } @@ -914,27 +968,26 @@ func (h *Holder) HolderPathFromIndexPath(indexPath, indexName string) string { } // Index returns the index by name. -func (h *Holder) Index(name string) *Index { - h.mu.RLock() - defer h.mu.RUnlock() - return h.index(name) -} - -func (h *Holder) index(name string) *Index { - return h.indexes[name] +func (h *Holder) Index(name string) (idx *Index) { + idx = h.bkgr.index(name) + return } // Indexes returns a list of all indexes in the holder. func (h *Holder) Indexes() []*Index { - h.mu.RLock() - a := make([]*Index, 0, len(h.indexes)) - for _, index := range h.indexes { - a = append(a, index) + // utils_internal_test.go:345 func (t *ClusterCluster) Close() error + // wants to close an un-Open()-ed Holder. + // So I guess we won't panic here. + if h.bkgr == nil || h.bkgr.isClosed() { + return nil } - h.mu.RUnlock() + req := newIndexReq("") + req.getAll = true + h.bkgr.getAllCh <- req + <-req.done - sort.Sort(indexSlice(a)) - return a + sort.Sort(indexSlice(req.all)) + return req.all } // CreateIndex creates an index. @@ -944,7 +997,7 @@ func (h *Holder) CreateIndex(name string, opt IndexOptions) (*Index, error) { defer h.mu.Unlock() // Ensure index doesn't already exist. - if h.index(name) != nil { + if h.Index(name) != nil { return nil, newConflictError(ErrIndexExists) } return h.createIndex(name, opt) @@ -957,10 +1010,9 @@ func (h *Holder) CreateIndexIfNotExists(name string, opt IndexOptions) (*Index, defer h.mu.Unlock() // Return index if it exists. - if index := h.index(name); index != nil { + if index := h.Index(name); index != nil { return index, nil } - return h.createIndex(name, opt) } @@ -978,7 +1030,7 @@ func (h *Holder) createIndex(name string, opt IndexOptions) (*Index, error) { index.keys = opt.Keys index.trackExistence = opt.TrackExistence - if err = index.Open(true); err != nil { + if err = index.Open(); err != nil { return nil, errors.Wrap(err, "opening") } if err = index.saveMeta(); err != nil { @@ -986,7 +1038,7 @@ func (h *Holder) createIndex(name string, opt IndexOptions) (*Index, error) { } // Update options. - h.indexes[index.Name()] = index + h.addIndex(index) // Since this is a new index, we need to kick off // its translation sync. @@ -1017,7 +1069,7 @@ func (h *Holder) DeleteIndex(name string) error { defer h.mu.Unlock() // Confirm index exists. - index := h.index(name) + index := h.Index(name) if index == nil { return newNotFoundError(ErrIndexNotFound, name) } @@ -1028,7 +1080,7 @@ func (h *Holder) DeleteIndex(name string) error { } // remove any backing store. - if err := index.Txf.DeleteIndex(name); err != nil { + if err := h.txf.DeleteIndex(name); err != nil { return errors.Wrap(err, "index.Txf.DeleteIndex") } @@ -1038,7 +1090,7 @@ func (h *Holder) DeleteIndex(name string) error { } // Remove reference. - delete(h.indexes, name) + h.deleteIndex(name) // I'm not sure if calling Reset() here is necessary // since closing the index stops its translation @@ -1046,6 +1098,10 @@ func (h *Holder) DeleteIndex(name string) error { return h.translationSyncer.Reset() } +func (h *Holder) deleteIndex(index string) { + h.bkgr.delIdxCh <- index +} + // Field returns the field for an index and name. func (h *Holder) Field(index, name string) *Field { idx := h.Index(index) @@ -1287,7 +1343,7 @@ func (s *holderSyncer) SyncHolder() error { return nil } - itr := s.Holder.Index(di.Name).AvailableShards().Iterator() + itr := s.Holder.Index(di.Name).AvailableShards(includeRemote).Iterator() itr.Seek(0) for shard, eof := itr.Next(); !eof; shard, eof = itr.Next() { // Ignore shards that this host doesn't own. @@ -1736,7 +1792,7 @@ func (c *holderCleaner) CleanHolder() error { } // Get the fragments that node is responsible for (based on hash(index, node)). - containedShards := c.Cluster.containsShards(index.Name(), index.AvailableShards(), c.Node) + containedShards := c.Cluster.containsShards(index.Name(), index.AvailableShards(includeRemote), c.Node) // Get the fragments registered in memory. for _, field := range index.Fields() { @@ -1785,33 +1841,28 @@ func uint64InSlice(i uint64, s []uint64) bool { // Process loops through a holder based on the Check functions in op, calling // the Process functions in op when indicated. func (h *Holder) Process(ctx context.Context, op HolderOperator) (err error) { - var indexNames, fieldNames, viewNames []string + var fieldNames, viewNames []string var fragNums []uint64 - h.mu.Lock() - for indexName := range h.indexes { - indexNames = append(indexNames, indexName) - } - h.mu.Unlock() - for _, indexName := range indexNames { + indexes := h.Indexes() + for _, idx := range indexes { if err = ctx.Err(); err != nil { return err } + if idx == nil { + continue + } + indexName := idx.name process, recurse := op.CheckIndex(indexName) if !process && !recurse { continue } - h.mu.Lock() - index := h.indexes[indexName] - h.mu.Unlock() - if index == nil { - continue - } + if err = ctx.Err(); err != nil { return err } if process { - err = op.ProcessIndex(index) + err = op.ProcessIndex(idx) if err != nil { return err } @@ -1820,22 +1871,22 @@ func (h *Holder) Process(ctx context.Context, op HolderOperator) (err error) { continue } fieldNames = fieldNames[:0] - index.mu.Lock() - for fieldName := range index.fields { + idx.mu.Lock() + for fieldName := range idx.fields { fieldNames = append(fieldNames, fieldName) } - index.mu.Unlock() + idx.mu.Unlock() for _, fieldName := range fieldNames { if err = ctx.Err(); err != nil { return err } - process, recurse := op.CheckField(indexName, fieldName) + process, recurse := op.CheckField(idx.name, fieldName) if !process && !recurse { continue } - index.mu.Lock() - field := index.fields[fieldName] - index.mu.Unlock() + idx.mu.Lock() + field := idx.fields[fieldName] + idx.mu.Unlock() if field == nil { continue } @@ -1913,23 +1964,32 @@ func (h *Holder) Process(ctx context.Context, op HolderOperator) (err error) { // used by Index.openFields(), enabling Tx / Txf by telling // the holder about its own indexes. -func (h *Holder) addIndexFromField(idx *Index) { - h.mu.Lock() - h.indexes[idx.Name()] = idx - h.mu.Unlock() -} - -func (h *Holder) unprotectedAddIndexFromField(idx *Index) { - h.indexes[idx.Name()] = idx +func (h *Holder) addIndex(idx *Index) { + if idx == nil { + panic("cannot pass nil to addIndex") + } + if h == nil { + panic("cannot call addIndex on nil Holder") + } + if h.bkgr == nil || h.bkgr.isClosed() { + // ugh. TestCluster_ResizeStates/Multiple_nodes,_with_data test + // from cluster_internal_test.go:799 + // via utils_internal_test.go:450 + // gets here, with the h.mu already held. + // so we cannot panic and complain or we mess up that test. + // But really, we should not be calling addIndex() on + // Holder that has not be Open()-ed. + h.mu.Lock() + h.bkgr = h.newHolderBackgroundGoro() + h.mu.Unlock() + } + h.bkgr.setIdxCh <- idx } func (h *Holder) DumpAllShards() { h.mu.RLock() defer h.mu.RUnlock() - for index, idx := range h.indexes { - fmt.Printf("dump of index '%v'\n", index) - idx.Txf.dbPerShard.DumpAll() - } + h.txf.dbPerShard.DumpAll() } func (h *Holder) Txf() *TxFactory { @@ -1941,5 +2001,38 @@ func (h *Holder) Txf() *TxFactory { // Begin starts a transaction on the holder. The index and shard // must be specified. func (h *Holder) BeginTx(writable bool, idx *Index, shard uint64) (Tx, error) { - return idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}), nil + return h.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}), nil +} + +func (h *Holder) NumContainersBlueGreenVerified() int { + return int(h.numCtBlueGreenVerified) +} + +func (h *Holder) HasRoaringData() (has bool, err error) { + + idxs := h.Indexes() + for _, idx := range idxs { + paths, err := listFilesUnderDir(idx.path, false, "", true) + if err != nil { + return false, errors.Wrap(err, "HasRoaringData listFilesUnderDir") + } + index := idx.name + + for _, relpath := range paths { + field, view, shard, err := fragmentSpecFromRoaringPath(relpath) + if err != nil { + continue // ignore .meta paths + } + abspath := idx.path + sep + relpath + + hasData, err := roaringFragmentHasData(abspath, index, field, view, shard) + if err != nil { + return false, errors.Wrap(err, "HasRoaringData roaringFragmentHasData") + } + if hasData { + return true, nil + } + } + } + return } diff --git a/holder_internal_test.go b/holder_internal_test.go index c7e9a0bcb..1db02a8b0 100644 --- a/holder_internal_test.go +++ b/holder_internal_test.go @@ -16,12 +16,15 @@ package pilosa import ( "context" + "fmt" "os" "testing" "github.com/pilosa/pilosa/v2/testhook" ) +var _ = fmt.Printf + type testHolderOperator struct { indexSeen, indexProcessed int fieldSeen, fieldProcessed int @@ -79,7 +82,7 @@ func makeHolder(tb testing.TB) (*Holder, string, error) { return nil, "", err } h := NewHolder(path, nil) - return h, path, nil + return h, path, h.Open() } func testSetBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) { @@ -89,21 +92,81 @@ func testSetBit(t *testing.T, h *Holder, index, field string, rowID, columnID ui t.Fatalf("creating index: %v", err) } - shard := columnID / ShardWidth - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) - defer tx.Rollback() - f, err := idx.CreateFieldIfNotExists(field, OptFieldTypeDefault()) if err != nil { t.Fatalf("setting bit: %v", err) } - _, err = f.SetBit(tx, rowID, columnID, nil) + _, err = f.SetBit(nil, rowID, columnID, nil) if err != nil { t.Fatalf("setting bit: %v", err) } - if err := tx.Commit(); err != nil { - t.Fatal(err) +} + +func testMustHaveBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) { + + //shard := columnID / ShardWidth + + // hmm... if its a new holder, meta data isn't there, so ask for it. + idx, err := h.CreateIndexIfNotExists(index, IndexOptions{}) + panicOn(err) + + f := idx.Field(field) + if f == nil { + t.Fatalf("no such field '%v'", field) } + + row, err := f.Row(nil, rowID) + if err != nil { + t.Fatalf("error getting field.Row(rowID=%v): %v", rowID, err) + } + + cols := row.Columns() + if len(cols) == 0 { + t.Fatalf("error getting field.Row().Columns(): empty columns, colID %v bit was not hot", columnID) + } + + for _, c := range cols { + if c == columnID { + return // ok, found it. + } + } + t.Fatalf("error getting field.Row().Columns(): colID %v bit was not hot", columnID) +} + +func testMustNotHaveBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) { + if testHasBit(t, h, index, field, rowID, columnID) { + t.Fatalf("error, expected no bit but this bit was hot: index='%v', field='%v', rowID='%v', columnID='%v'", index, field, rowID, columnID) + } +} + +func testHasBit(t *testing.T, h *Holder, index, field string, rowID, columnID uint64) bool { + + idx := h.Index(index) + if idx == nil { + return false // not even an index by this name. Obviously no hot bits either. + } + + f := idx.Field(field) + if f == nil { + return false + } + + row, err := f.Row(nil, rowID) + if err != nil { + return false + } + + cols := row.Columns() + if len(cols) == 0 { + return false + } + + for _, c := range cols { + if c == columnID { + return true // ok, found it. + } + } + return false } func TestHolderOperatorProcess(t *testing.T) { diff --git a/holder_test.go b/holder_test.go index b79ef71f6..7ccad46f1 100644 --- a/holder_test.go +++ b/holder_test.go @@ -180,7 +180,7 @@ func TestHolder_Open(t *testing.T) { } var shard uint64 - tx := idx.Txf.NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { @@ -214,7 +214,7 @@ func TestHolder_Open(t *testing.T) { } var shard uint64 - tx := idx.Txf.NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) if err != nil { t.Fatal(err) } @@ -247,7 +247,7 @@ func TestHolder_Open(t *testing.T) { t.Fatal(err) } var shard uint64 - tx := idx.Txf.NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.Txf().NewTx(pilosa.Txo{Write: writable, Index: idx, Shard: shard}) defer tx.Rollback() if field, err := idx.CreateField("bar", pilosa.OptFieldTypeDefault()); err != nil { diff --git a/index.go b/index.go index 726dca7c7..f77aaf354 100644 --- a/index.go +++ b/index.go @@ -70,8 +70,6 @@ type Index struct { // Instantiates new translation stores OpenTranslateStore OpenTranslateStoreFunc - - Txf *TxFactory } // NewIndex returns an existing (but possibly empty) instance of @@ -105,19 +103,16 @@ func NewIndex(holder *Holder, path, name string) (*Index, error) { translationSyncer: NopTranslationSyncer, OpenTranslateStore: OpenInMemTranslateStore, - - // the Txf should be shared across all holder. - Txf: holder.txf, } return idx, nil } func (i *Index) NewTx(txo Txo) Tx { - return i.Txf.NewTx(txo) + return i.holder.txf.NewTx(txo) } func (i *Index) NeedsSnapshot() bool { - return i.Txf.NeedsSnapshot() + return i.holder.txf.NeedsSnapshot() } // CreatedAt is an timestamp for a specific version of an index. @@ -174,12 +169,14 @@ func (i *Index) options() IndexOptions { } // Open opens and initializes the index. -func (i *Index) Open(haveHolderLock bool) error { return i.open(false, haveHolderLock) } +func (i *Index) Open() error { + return i.open(false) +} // OpenWithTimestamp opens and initializes the index and set a new CreatedAt timestamp for fields. -func (i *Index) OpenWithTimestamp(haveHolderLock bool) error { return i.open(true, haveHolderLock) } +func (i *Index) OpenWithTimestamp() error { return i.open(true) } -func (i *Index) open(withTimestamp, haveHolderLock bool) (err error) { +func (i *Index) open(withTimestamp bool) (err error) { // Ensure the path exists. i.holder.Logger.Debugf("ensure index path exists: %s", i.path) if err := os.MkdirAll(i.path, 0777); err != nil { @@ -193,7 +190,7 @@ func (i *Index) open(withTimestamp, haveHolderLock bool) (err error) { } i.holder.Logger.Debugf("open fields for index: %s", i.name) - if err := i.openFields(withTimestamp, haveHolderLock); err != nil { + if err := i.openFields(withTimestamp); err != nil { return errors.Wrap(err, "opening fields") } @@ -240,7 +237,7 @@ func (i *Index) open(withTimestamp, haveHolderLock bool) (err error) { var indexQueue = make(chan struct{}, 8) // openFields opens and initializes the fields inside the index. -func (i *Index) openFields(withTimestamp, haveHolderLock bool) error { +func (i *Index) openFields(withTimestamp bool) error { f, err := os.Open(i.path) if err != nil { return errors.Wrap(err, "opening directory") @@ -273,27 +270,8 @@ fileLoop: mu.Lock() - // i.holder needs to know about its index i for the Txf to work. - // - // We face either a deadlock or a race here. - // - // We get a deadlock in TestIndex_CreateField/"BSIFields"/"OK" - // if we call addIndexFromField, because in that test - // we get here while already holding i.holder.mu. - // - // On the other had, we get races on other tests - // such as TestExecutor_Execute_Existence/Row - // if we call unprotectedAddIndexFromField which does - // not lock i.holder.mu. - // - // The resolution was to have the goroutines that are holding - // the lock already tell us. That is the haveHolderLock - // argument. - if haveHolderLock { - i.holder.unprotectedAddIndexFromField(i) - } else { - i.holder.addIndexFromField(i) - } + // goroutine safe + i.holder.addIndex(i) fld, err := i.newField(i.fieldPath(filepath.Base(fi.Name())), filepath.Base(fi.Name())) if withTimestamp { @@ -397,7 +375,7 @@ func (i *Index) Close() error { _ = testhook.Closed(i.holder.Auditor, i, nil) }() - err := i.Txf.CloseIndex(i) + err := i.holder.txf.CloseIndex(i) if err != nil { return errors.Wrap(err, "closing index") } @@ -424,8 +402,12 @@ func (i *Index) Close() error { return nil } +// make it clear what the Index.AvailableShards() calls are trying to obtain. +const includeRemote = false +const localOnly = true + // AvailableShards returns a bitmap of all shards with data in the index. -func (i *Index) AvailableShards() *roaring.Bitmap { +func (i *Index) AvailableShards(localOnly bool) *roaring.Bitmap { if i == nil { return roaring.NewBitmap() } @@ -435,8 +417,8 @@ func (i *Index) AvailableShards() *roaring.Bitmap { b := roaring.NewBitmap() for _, f := range i.fields { - //b.Union(f.AvailableShards()) - b.UnionInPlace(f.AvailableShards()) + //b.Union(f.AvailableShards(localOnly)) + b.UnionInPlace(f.AvailableShards(localOnly)) } i.Stats.Gauge(MetricMaxShard, float64(b.Max()), 1.0) @@ -445,7 +427,7 @@ func (i *Index) AvailableShards() *roaring.Bitmap { // Begin starts a transaction on a shard of the index. func (i *Index) BeginTx(writable bool, shard uint64) (Tx, error) { - return i.Txf.NewTx(Txo{Write: writable, Index: i, Shard: shard}), nil + return i.holder.txf.NewTx(Txo{Write: writable, Index: i, Shard: shard}), nil } // fieldPath returns the path to a field in the index. @@ -621,7 +603,7 @@ func (i *Index) DeleteField(name string) error { return errors.Wrap(err, "closing") } - if err := i.Txf.DeleteFieldFromStore(i.name, name, i.fieldPath(name)); err != nil { + if err := i.holder.txf.DeleteFieldFromStore(i.name, name, i.fieldPath(name)); err != nil { return errors.Wrap(err, "Txf.DeleteFieldFromStore") } @@ -705,7 +687,7 @@ func FormatQualifiedIndexName(index string) string { func (idx *Index) Dump(label string) { //fileline := FileLine(2) fmt.Printf("\nDump: %v\n\n", label) - idx.Txf.dbPerShard.DumpAll() + idx.holder.txf.dbPerShard.DumpAll() } func (idx *Index) SliceOfShards(field, view, viewPath string) (sliceOfShards []uint64, err error) { @@ -843,3 +825,7 @@ func (idx *Index) WriteFragmentChecksums(w io.Writer, showBits, showOps bool) { fmt.Fprintf(w, "empty index '%v'", idx.path) } } + +func (idx *Index) Txf() *TxFactory { + return idx.holder.txf +} diff --git a/index_internal_test.go b/index_internal_test.go index bdcba3989..faed407c5 100644 --- a/index_internal_test.go +++ b/index_internal_test.go @@ -47,7 +47,7 @@ func (i *Index) reopen() error { if err := i.Close(); err != nil { return err } - if err := i.Open(false); err != nil { + if err := i.Open(); err != nil { return err } return nil diff --git a/license.exceptions b/license.exceptions index 1993f71b2..ffb08a26e 100644 --- a/license.exceptions +++ b/license.exceptions @@ -18,3 +18,4 @@ ./gid.go ./cmd/pilosa-keydump/vprint.go ./cmd/pilosa-keydump/keydump.go +./synthload/vprint.go diff --git a/lmdb.go b/lmdb.go index f652b6d7e..dcf2bde68 100644 --- a/lmdb.go +++ b/lmdb.go @@ -69,6 +69,17 @@ type lmdbRegistrar struct { path2db map[string]*LMDBWrapper } +func (r *lmdbRegistrar) Size() int { + r.mu.Lock() + defer r.mu.Unlock() + nmp := len(r.mp) + npa := len(r.path2db) + if nmp != npa { + panic(fmt.Sprintf("nmp=%v, vs npa=%v", nmp, npa)) + } + return nmp +} + var globalLMDBReg *lmdbRegistrar = newLMDBTestRegistrar() var globalNextTxSnLMDB int64 @@ -101,18 +112,19 @@ func (r *lmdbRegistrar) unregister(w *LMDBWrapper) { } func DumpAllLMDB() { + short := true globalLMDBReg.mu.Lock() defer globalLMDBReg.mu.Unlock() for w := range globalLMDBReg.mp { - AlwaysPrintf("this lmdb path='%v' has: \n%v\n", w.path, w.StringifiedLMDBKeys(nil)) + AlwaysPrintf("this lmdb path='%v' has: \n%v\n", w.path, w.StringifiedLMDBKeys(nil, short)) } } // lmdbPath is a helper for determining the full directory // in which the lmdb database will be stored. func lmdbPath(path string) string { - if !strings.HasSuffix(path, "-lmdb") { - return path + "-lmdb" + if !strings.HasSuffix(path, "-lmdb@") { + return path + "-lmdb@" } return path } @@ -154,6 +166,9 @@ func (r *lmdbRegistrar) OpenDBWrapper(path0 string, doAllocZero bool) (DBWrapper //flags := uint(lmdb.NoReadahead | lmdb.NoSubdir) //flags := uint(lmdb.NoSubdir) // no difference without the No.Readahead on ./query. + // NoReadahead should be better for random workloads or those bigger than memory, + // as it avoids loading extra pages which then evict pages you are using. + //flags := uint(lmdb.NoReadahead) flags := uint(0) // unsafe, but get upper bound on performance. @@ -162,7 +177,15 @@ func (r *lmdbRegistrar) OpenDBWrapper(path0 string, doAllocZero bool) (DBWrapper // NoSync = C.MDB_NOSYNC // Don't fsync after commit. // flags = flags | lmdb.WriteMap | lmdb.NoMetaSync | lmdb.NoSync // about the same speed // flags = flags | lmdb.NoMetaSync | lmdb.NoSync // slows things down - //flags = flags | lmdb.WriteMap // seems faster than without: + + // TODO(jea): we got an odd segfault in the roaring/ pkg when we went to read-only mmap + // with the row-cache off; probably means we were trying to write to mmap-ed + // memory. Investigate at some point. reference: 2a77b25d ja/write_map_prevents_segfault + // + // Update: added COW to roaring/roaring.go:3282 func (c *Container) arrayRemove(), + // seems to fix the issue. + // + //flags = flags | lmdb.WriteMap // seems faster than without: or maybe not. not sure. // kRemove N= 710401 avg/op: 7.714µs sd: 27.83µs total: 5.480656859s // kAdd N= 722835 avg/op: 9.096µs sd: 105.787µs total: 6.575497725s @@ -225,6 +248,38 @@ func (r *lmdbRegistrar) OpenDBWrapper(path0 string, doAllocZero bool) (DBWrapper return w, nil } +func (w *LMDBWrapper) Path() string { + return w.path +} + +func (w *LMDBWrapper) HasData() (has bool, err error) { + + tx, err := w.NewTx(!writable, "", Txo{}) + if err != nil { + return false, errors.Wrap(err, "HasData NewTx") + } + defer tx.Rollback() + + bi := NewLMDBIterator(tx.(*LMDBTx), nil) + defer bi.Close() + + for bi.Next() { + return true, nil + } + return false, nil +} + +func (w *LMDBWrapper) CleanupTx(tx Tx) { + // inlined into Rollback and Commit, so this is a no-op, just here to satisfy the interface. +} + +func (tx *LMDBTx) IsDone() (done bool) { + tx.mu.Lock() + done = tx.unlocked + tx.mu.Unlock() + return +} + func (w *LMDBWrapper) OpenListString() (r string) { list := w.listopen() @@ -279,8 +334,6 @@ func (w *LMDBWrapper) DeleteIndex(indexName string) error { var _ Tx = (*LMDBTx)(nil) // LMDBWrapper provides the NewTx() method. -// Execute lmdbJob's via LMDBWrapper.submit(); these must -// be done by the lmdb goroutine worker pool. type LMDBWrapper struct { env *lmdb.Env @@ -310,6 +363,11 @@ type LMDBWrapper struct { openTx map[*LMDBTx]bool } +func (w *LMDBWrapper) SetHolder(h *Holder) { + // don't need it at the moment + //w.h = h +} + // NewTxWRITE lets us see in the callstack dumps where the WRITE tx are. // Can't have more than one active write per database, so the // 2nd one will block until the first finishes. @@ -321,6 +379,9 @@ func (w *LMDBWrapper) NewTxWRITE() *lmdb.Txn { // NewTxREAD lets us see in the callstack dumps where the READ tx are. func (w *LMDBWrapper) NewTxREAD() *lmdb.Txn { + if w.env == nil { + panic("cannot call NewTxREAD() on closed LMDBWrapper() -- open a new Wrapper first") + } lmdbTxn, err := w.env.BeginTxn(nil, lmdb.Readonly) panicOn(err) return lmdbTxn @@ -342,7 +403,8 @@ func (w *LMDBWrapper) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, sn := atomic.AddInt64(&globalNextTxSnLMDB, 1) - //vv("lmdb new tx _sn_ %v; stack \n%v", sn, stack()) + //vv("lmdb new tx _sn_ %v; openTx='%v', stack \n%v", sn, w.OpenListString(), stack()) + //vv("lmdb new (write=%v, shard=%v) tx _sn_ %v; openTx='%v'", write, o.Shard, sn, w.OpenListString()) runtime.LockOSThread() @@ -473,7 +535,7 @@ func (tx *LMDBTx) Type() string { } func (tx *LMDBTx) UseRowCache() bool { - return true + return false } // Pointer gives us a memory address for the underlying transaction for debugging. @@ -502,6 +564,7 @@ func (tx *LMDBTx) Rollback() { if !tx.unlocked { runtime.UnlockOSThread() tx.unlocked = true + tx.o.dbs.Cleanup(tx) } } @@ -527,6 +590,7 @@ func (tx *LMDBTx) Commit() error { if !tx.unlocked { runtime.UnlockOSThread() tx.unlocked = true + tx.o.dbs.Cleanup(tx) } return err } @@ -1244,6 +1308,7 @@ func (tx *LMDBTx) CountRange(index, field, view string, shard uint64, start, end // 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 // @@ -1252,6 +1317,10 @@ func (tx *LMDBTx) CountRange(index, field, view string, shard uint64, start, end // is done to conform to the roaring.OffsetRange() argument convention. // func (tx *LMDBTx) OffsetRange(index, field, view string, shard, offset, start, endx uint64) (other *roaring.Bitmap, err error) { + //vv("top of LMDBTx OffsetRange(index='%v', field='%v', view='%v', shard='%v', offset: %v start: %v, end: %v)", index, field, view, int(shard), int(offset), int(start), int(endx)) + //defer func() { + //vv("returning from LMDBTx OffsetRange(index='%v', field='%v', view='%v', shard='%v', offset: %v start: %v, end: %v) other returning is: '%#v' stack=\n%v", index, field, view, int(shard), int(offset), int(start), int(endx), asInts(other.Slice()), stack()) + //}() // roaring does these three checks in its OffsetRange if lowbits(offset) != 0 { @@ -1469,11 +1538,11 @@ func ToContainer(typ byte, w []byte) (r *roaring.Container) { // StringifiedLMDBKeys returns a string with all the container // keys available in lmdb. -func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx) (r string) { +func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx, short bool) (r string) { if optionalUseThisTx == nil { tx, _ := w.NewTx(!writable, "", Txo{}) defer tx.Rollback() - r = stringifiedLMDBKeysTx(tx.(*LMDBTx)) + r = stringifiedLMDBKeysTx(tx.(*LMDBTx), short) return } @@ -1481,7 +1550,7 @@ func (w *LMDBWrapper) StringifiedLMDBKeys(optionalUseThisTx Tx) (r string) { if !ok { return fmt.Sprintf("", optionalUseThisTx) } - r = stringifiedLMDBKeysTx(btx) + r = stringifiedLMDBKeysTx(btx, short) return } @@ -1507,8 +1576,8 @@ func (tx *LMDBTx) countBitsSet(bkey []byte) (n int) { return } -func (tx *LMDBTx) Dump() { - fmt.Printf("%v\n", stringifiedLMDBKeysTx(tx)) +func (tx *LMDBTx) Dump(short bool) { + fmt.Printf("%v\n", stringifiedLMDBKeysTx(tx, short)) } // stringifiedLMDBKeysTx reports all the lmdb keys and a @@ -1520,7 +1589,7 @@ func (tx *LMDBTx) Dump() { // By convention, we must return the empty string if there // are no keys present. The tests use this to confirm // an empty database. -func stringifiedLMDBKeysTx(tx *LMDBTx) (r string) { +func stringifiedLMDBKeysTx(tx *LMDBTx, short bool) (r string) { r = "allkeys:[\n" it := NewLMDBIterator(tx, nil) @@ -1547,7 +1616,9 @@ func stringifiedLMDBKeysTx(tx *LMDBTx) (r string) { srbm = BitmapAsString(rbm) r += fmt.Sprintf("%v -> %v (%v hot)\n", key, h, tx.countBitsSet(bkey)) - r += " ......." + srbm + "\n" + if !short { + r += " ......." + srbm + "\n" + } } r += "]\n all-in-blake3:" + hash.Blake3sum16([]byte(r)) diff --git a/lmdb_other.go b/lmdb_other.go index d19de8114..3a5c04995 100644 --- a/lmdb_other.go +++ b/lmdb_other.go @@ -63,6 +63,10 @@ func (r *lmdbRegistrar) OpenDBWrapper(path0 string, doAllocZero bool) (DBWrapper panic("lmdb only available on 64-bit arch") } +func (r *lmdbRegistrar) Size() int { + panic("lmdb only available on 64-bit arch") +} + // register each lmdb created under tests, so we // can clean them up. This is called by openLMDBWrapper() while // holding the r.mu.Lock, since it needs to atomically @@ -396,7 +400,11 @@ func (tx *LMDBTx) countBitsSet(bkey []byte) (n int) { panic("lmdb only available on 64-bit arch") } -func (tx *LMDBTx) Dump() { +func (tx *LMDBTx) IsDone() (done bool) { + panic("lmdb only available on 64-bit arch") +} + +func (tx *LMDBTx) Dump(short bool) { panic("lmdb only available on 64-bit arch") } diff --git a/lmdb_test.go b/lmdb_test.go index 8c4a885ae..2997b6c1e 100644 --- a/lmdb_test.go +++ b/lmdb_test.go @@ -94,7 +94,7 @@ func mustOpenEmptyLMDBWrapper(path string) (w *LMDBWrapper, cleaner func()) { w = ww.(*LMDBWrapper) // verify it is empty - allkeys := w.StringifiedLMDBKeys(nil) + allkeys := w.StringifiedLMDBKeys(nil, false) if allkeys != "" { panic(fmt.Sprintf("freshly created database was not empty! had keys:'%v'", allkeys)) } @@ -1013,7 +1013,7 @@ func TestLMDB_ImportRoaringBits(t *testing.T) { if n != 0 { panic(fmt.Sprintf("n = %v not zero so the clearbits didn't happen!", n)) } - allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx)) + allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx), false) // should have no keys if allkeys != "" { @@ -1185,7 +1185,7 @@ func TestLMDB_DeleteIndex(t *testing.T) { exists, err = tx.Contains(index, field, view, shard, v) panicOn(err) if exists { - allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx)) + allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx), false) panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) } } @@ -1240,7 +1240,7 @@ func TestLMDB_DeleteIndex_over100k(t *testing.T) { exists, err = tx.Contains(index, field, view, shard, v<<16) panicOn(err) if exists { - allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx)) + allkeys := stringifiedLMDBKeysTx(tx.(*LMDBTx), false) panic(fmt.Sprintf("after delete of index '%v', bit v=%v was not gone?!?; allkeys='%v'", index, v, allkeys)) } } @@ -1268,3 +1268,33 @@ func TestLMDB_SliceOfShards(t *testing.T) { } } } + +func TestLMDB_HasData(t *testing.T) { + + db, clean := mustOpenEmptyLMDBWrapper("TestLMDB_SliceOfShards") + defer clean() + defer db.Close() + + // HasData should start out false. + hasAnything, err := db.HasData() + if err != nil { + t.Fatal(err) + } + if hasAnything { + t.Fatalf("HasData reported existing data on an empty database") + } + + // check that HasData sees a committed record. + + index, field, view, shard, putme := "i", "f", "v", uint64(123), uint64(42) + LMDBMustSetBitvalue(db, index, field, view, shard, putme) + + // HasData(false) should now report data + hasAnything, err = db.HasData() + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData() reported no data on a database that has bits written to it") + } +} diff --git a/mmap_test.go b/mmap_test.go index 9df7e60e6..52137d043 100644 --- a/mmap_test.go +++ b/mmap_test.go @@ -36,7 +36,7 @@ func forceSnapshotsCheckMapping(t *testing.T) { f.Logger = logger.NewLogfLogger(t) defer f.Clean(t) - tx = idx.Txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) + tx = idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Fragment: f, Shard: f.shard}) defer tx.Rollback() for i := 0; i < f.MaxOpN; i++ { diff --git a/rbf.go b/rbf.go index 6317022e7..33fcd75c3 100644 --- a/rbf.go +++ b/rbf.go @@ -34,7 +34,7 @@ import ( // RbfDBWrapper wraps an *rbf.DB type RbfDBWrapper struct { - Path string + path string db *rbf.DB reg *rbfDBRegistrar muDb sync.Mutex @@ -49,6 +49,34 @@ type RbfDBWrapper struct { doAllocZero bool } +func (w *RbfDBWrapper) Path() string { + return w.path +} + +func (w *RbfDBWrapper) SetHolder(h *Holder) { + // don't need it at the moment + //w.h = h +} + +func (w *RbfDBWrapper) CleanupTx(tx Tx) { + r := tx.(*RBFTx) + r.mu.Lock() + if r.done { + r.mu.Unlock() + return + } + r.done = true + r.mu.Unlock() + + // try not to old r.mu while locking w.muDb + w.muDb.Lock() + + delete(w.openTx, r) + r.o.dbs.Cleanup(tx) // release the read/write lock. + + w.muDb.Unlock() +} + // rbfDBRegistrar also allows opening the same path twice to // result in sharing the same open database handle, and // thus the same transactional guarantees. @@ -60,6 +88,17 @@ type rbfDBRegistrar struct { path2db map[string]*RbfDBWrapper } +func (r *rbfDBRegistrar) Size() int { + r.mu.Lock() + defer r.mu.Unlock() + nmp := len(r.mp) + npa := len(r.path2db) + if nmp != npa { + panic(fmt.Sprintf("nmp=%v, vs npa=%v", nmp, npa)) + } + return nmp +} + var globalRbfDBReg *rbfDBRegistrar = newRbfDBRegistrar() func newRbfDBRegistrar() *rbfDBRegistrar { @@ -77,29 +116,29 @@ func newRbfDBRegistrar() *rbfDBRegistrar { // return the existing instance. func (r *rbfDBRegistrar) unprotectedRegister(w *RbfDBWrapper) { r.mp[w] = true - r.path2db[w.Path] = w + r.path2db[w.path] = w } // unregister removes w from r func (r *rbfDBRegistrar) unregister(w *RbfDBWrapper) { r.mu.Lock() delete(r.mp, w) - delete(r.path2db, w.Path) + delete(r.path2db, w.path) r.mu.Unlock() } // rbfPath is a helper for determining the full directory // in which the RBF database will be stored. func rbfPath(path string) string { - if !strings.HasSuffix(path, "-rbfdb") { - return path + "-rbfdb" + if !strings.HasSuffix(path, "-rbfdb@") { + return path + "-rbfdb@" } return path } // OpenDBWrapper opens the database in the path directoy // without deleting any prior content. Any -// database directory will have the "-rbfdb" suffix. +// database directory will have the "-rbfdb@" suffix. // // OpenDBWrapper will check the registry and make a new instance only // if one does not exist for its path. Otherwise it returns @@ -114,18 +153,21 @@ func (r *rbfDBRegistrar) OpenDBWrapper(path0 string, doAllocZero bool) (DBWrappe // creates the effect of having only one DB open per pilosa node. return w, nil } - db := rbf.NewDB(path) + var db *rbf.DB + if doAllocZero { + db = rbf.NewDBWithAllocZero(path) + } else { + db = rbf.NewDB(path) + } w = &RbfDBWrapper{ reg: r, - Path: path, + path: path, db: db, doAllocZero: doAllocZero, openTx: make(map[*RBFTx]bool), } - r.unprotectedRegister(w) - rbf.DoAllocZero = doAllocZero err := db.Open() if err != nil { @@ -143,6 +185,16 @@ type RBFTx struct { o Txo sn int64 // serial number Db *RbfDBWrapper + + done bool + mu sync.Mutex // protect done as it changes state +} + +func (tx *RBFTx) IsDone() (done bool) { + tx.mu.Lock() + done = tx.done + tx.mu.Unlock() + return } func (tx *RBFTx) DBPath() string { @@ -154,19 +206,18 @@ func (tx *RBFTx) Type() string { } func (tx *RBFTx) Rollback() { - tx.Db.muDb.Lock() - delete(tx.Db.openTx, tx) - tx.Db.muDb.Unlock() - tx.tx.Rollback() + + // must happen after actual rollback + tx.Db.CleanupTx(tx) } -func (tx *RBFTx) Commit() error { - tx.Db.muDb.Lock() - delete(tx.Db.openTx, tx) - tx.Db.muDb.Unlock() +func (tx *RBFTx) Commit() (err error) { + err = tx.tx.Commit() - return tx.tx.Commit() + // must happen after actual commit + tx.Db.CleanupTx(tx) + return } func (tx *RBFTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) { @@ -375,8 +426,8 @@ func (tx *RBFTx) Pointer() string { return fmt.Sprintf("%p", tx) } -func (tx *RBFTx) Dump() { - tx.tx.Dump() +func (tx *RBFTx) Dump(short bool) { + tx.tx.Dump(short) } // Readonly is true if the transaction is not read-and-write, but only doing reads. @@ -416,6 +467,12 @@ func rbfFieldPrefix(index, field string) string { return string(txkey.FieldPrefix(index, field)) } +func (w *RbfDBWrapper) HasData() (has bool, err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + return w.db.HasData(false) // false => any prior attempt at write means we "have data" +} + func (w *RbfDBWrapper) DeleteField(index, field, fieldPath string) error { w.muDb.Lock() defer w.muDb.Unlock() diff --git a/rbf/cursorx.go b/rbf/cursorx.go index bca2c4491..7d2d9332c 100644 --- a/rbf/cursorx.go +++ b/rbf/cursorx.go @@ -26,10 +26,13 @@ import ( // if enableRowCache, then we must not return mmap-ed memory // directly, but only a copy. -const EnableRowCache = true +const EnableRowCache = false -// makes a copy, BUT doesn't do the zero out for now TODO(jea) zero out actually to detect -// access past tx. +// DoAllocZero means we copy mmap read data and +// wipe it afterwards to catch retention of data +// past Tx.Rollback which was a big problem. +// This should be set by NewDBWithAllocZero and never changed +// afterwards in order to avoid a data race. var DoAllocZero bool //probably should just implement the container interface diff --git a/rbf/db.go b/rbf/db.go index c8b4cf9ac..2554dfc08 100644 --- a/rbf/db.go +++ b/rbf/db.go @@ -58,6 +58,14 @@ type DB struct { MaxSize int64 } +// NewDBWithAllocZero sets DoAllocZero true and +// returns a new instance of DB. We set it here +// to avoid a data race afterwards. +func NewDBWithAllocZero(path string) *DB { + DoAllocZero = true + return NewDB(path) +} + // NewDB returns a new instance of DB. func NewDB(path string) *DB { db := &DB{ @@ -544,6 +552,68 @@ func (db *DB) closeWALSegments() (err error) { return err } +// HasData with requireOneHotBit=false returns +// hasAnyRecords true if any record has been stored, +// even if the value for that bitmap record turned out to have +// no bits hot (be all zeroes). +// +// In this case, we are taking the attempted storage +// of any named bitmap into the database as evidence +// that the db is in use, and we return hasAnyRecords true. +// +// Conversely, if requireOneHotBit is true, then a +// database consisting of only a named bitmap with +// an all zeroes (no bits hot) +// will return hasAnyRecords false. We must find at +// least a single hot bit inside the db +// in order to return hasAnyRecords true. +// +// HasData is used by backend migration and blue/green checks. +// +// If there is a disk error we return (false, error), so always +// check the error before deciding if hasAnyRecords is valid. +// +// We will internally create and rollback a read-only +// transaction to answer this query. +func (db *DB) HasData(requireOneHotBit bool) (hasAnyRecords bool, err error) { + + // Read a list of all bitmaps in Tx. + tx, err := db.Begin(false) + if err != nil { + return false, err + } + defer tx.Rollback() + + records, err := tx.RootRecords() + if err != nil { + return false, err + } + // Loop over each bitmap and attempt to move to the first cell. + // If we can move to a cell then we have at least one record. + for _, record := range records { + // Fetch cursor for bitmap. + cur, err := tx.Cursor(record.Name) + if err != nil { + return false, err + } else if cur == nil { + continue // no bitmap + } + if !requireOneHotBit { + return true, nil + } + // INVAR: requireOneHotBit true + + // Check if we can move to the first cell. + if err := cur.First(); err == io.EOF { + continue // no data in bitmap + } else if err != nil { + return false, err + } + return true, nil + } + return false, nil +} + // Size returns the size of the database & WAL, in bytes. func (db *DB) Size() (int64, error) { diff --git a/rbf/db_test.go b/rbf/db_test.go index 667fbf54d..98a70cb24 100644 --- a/rbf/db_test.go +++ b/rbf/db_test.go @@ -191,3 +191,83 @@ func TestDB_BeginWithExclusiveLock(t *testing.T) { } }) } + +func TestDB_HasData(t *testing.T) { + + db := MustOpenDB(t) + defer MustCloseDB(t, db) + + // HasData should start out false. + const requireOneHotBit = true + hasAnything, err := db.HasData(!requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if hasAnything { + t.Fatalf("HasData reported existing data on an empty database") + } + + hasAnything, err = db.HasData(requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if hasAnything { + t.Fatalf("HasData reported existing data on an empty database") + } + + // check that HasData sees a committed record. + + // Create bitmap with no hot bits. + if tx, err := db.Begin(true); err != nil { + t.Fatal(err) + } else if err := tx.CreateBitmap("x"); err != nil { + t.Fatal(err) + } else if err := tx.Commit(); err != nil { + t.Fatal(err) + } + + // HasData(false) should now report seeing the 'x' record, even though + // the value is an empty bitmap, since !requireOneHotBit. + hasAnything, err = db.HasData(!requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData(!requireOneHotBit) reported no data on a database that has 'x' written to it") + } + + // HasData(requireOneHotBit) should report false, since we have no hot bits. + hasAnything, err = db.HasData(requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if hasAnything { + t.Fatalf("HasData(requireOneHotBit) reported data on a database that has 'x' -> empty bitmap") + } + + // hot up a bit. + if tx, err := db.Begin(true); err != nil { + t.Fatal(err) + } else if _, err := tx.Add("x", rand.Uint64()); err != nil { + t.Fatal(err) + } else if err := tx.Commit(); err != nil { + t.Fatal(err) + } + + // Now it shouldn't matter, we should get HasData true either way + // HasData(requireOneHotBit) should report false, since we have no hot bits. + hasAnything, err = db.HasData(requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData should have seen the hot bit") + } + hasAnything, err = db.HasData(!requireOneHotBit) + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData should have seen the hot bit") + } +} diff --git a/rbf/tx.go b/rbf/tx.go index d1d4f1ad4..bf1093c7e 100644 --- a/rbf/tx.go +++ b/rbf/tx.go @@ -1334,10 +1334,10 @@ func (si *emptyContainerIterator) Value() (uint64, *roaring.Container) { panic("emptyContainerIterator never has any Values") } -func (tx *Tx) Dump() { - fmt.Println(tx.DumpString()) +func (tx *Tx) Dump(short bool) { + fmt.Println(tx.DumpString(short)) } -func (tx *Tx) DumpString() (r string) { +func (tx *Tx) DumpString(short bool) (r string) { r = "allkeys:[\n" @@ -1365,7 +1365,7 @@ func (tx *Tx) DumpString() (r string) { ckey := cell.Key ct := toContainer(cell, tx) - s := stringOfCkeyCt(ckey, ct, rr.Name) + s := stringOfCkeyCt(ckey, ct, rr.Name, short) r += s n++ } @@ -1419,7 +1419,7 @@ func bitmapAsString(rbm *roaring.Bitmap) (r string) { return r + ")" } -func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName string) (s string) { +func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName string, short bool) (s string) { by := containerToBytes(ct) hash := hash.Blake3sum16(by) @@ -1433,7 +1433,9 @@ func stringOfCkeyCt(ckey uint64, ct *roaring.Container, rrName string) (s string bkey := pre + fmt.Sprintf("ckey@%020d", ckey) s = fmt.Sprintf("%v -> %v (%v hot)\n", bkey, hash, ct.N()) - s += " ......." + srbm + "\n" + if !short { + s += " ......." + srbm + "\n" + } return } diff --git a/rbf/tx_test.go b/rbf/tx_test.go index b9bc99373..fedd5f47d 100644 --- a/rbf/tx_test.go +++ b/rbf/tx_test.go @@ -483,7 +483,7 @@ func TestTx_Dump(t *testing.T) { } // test that we don't crash, and get *something* back - s := tx.DumpString() + s := tx.DumpString(true) if s == "" { panic("should have had 3 containers!") } diff --git a/roaring/fuzzer.go b/roaring/fuzzer.go index 9312f7865..e76ea397e 100644 --- a/roaring/fuzzer.go +++ b/roaring/fuzzer.go @@ -191,14 +191,14 @@ func FuzzRoaringOps(data []byte) int { expected = make([]uint64, 0) actual = make([]uint64, 0) forEachInSlice(s1, func(v uint64) { expected = append(expected, v) }) - bm1.ForEach(func(v uint64) { actual = append(actual, v) }) + bm1.ForEach(func(v uint64) error { actual = append(actual, v); return nil }) if !reflect.DeepEqual(expected, actual) { panic(fmt.Sprintf("for each:\n expected %v\n got %v", expected, actual)) } expected = make([]uint64, 0) actual = make([]uint64, 0) forEachInSlice(s2, func(v uint64) { expected = append(expected, v) }) - bm2.ForEach(func(v uint64) { actual = append(actual, v) }) + bm2.ForEach(func(v uint64) error { actual = append(actual, v); return nil }) if !reflect.DeepEqual(expected, actual) { panic(fmt.Sprintf("for each:\n expected %v\n got %v", expected, actual)) } @@ -206,14 +206,14 @@ func FuzzRoaringOps(data []byte) int { expected = make([]uint64, 0) actual = make([]uint64, 0) forEachInRangeSlice(s1, start, end, func(v uint64) { expected = append(expected, v) }) - bm1.ForEachRange(start, end, func(v uint64) { actual = append(actual, v) }) + bm1.ForEachRange(start, end, func(v uint64) error { actual = append(actual, v); return nil }) if !reflect.DeepEqual(expected, actual) { panic(fmt.Sprintf("for each in range:\n expected %v\n got %v", expected, actual)) } expected = make([]uint64, 0) actual = make([]uint64, 0) forEachInRangeSlice(s2, start, end, func(v uint64) { expected = append(expected, v) }) - bm2.ForEachRange(start, end, func(v uint64) { actual = append(actual, v) }) + bm2.ForEachRange(start, end, func(v uint64) error { actual = append(actual, v); return nil }) if !reflect.DeepEqual(expected, actual) { panic(fmt.Sprintf("for each in range:\n expected %v\n got %v", expected, actual)) } diff --git a/roaring/roaring.go b/roaring/roaring.go index eabec3b86..52e806f52 100644 --- a/roaring/roaring.go +++ b/roaring/roaring.go @@ -3301,8 +3301,32 @@ func (c *Container) arrayRemove(v uint16) (*Container, bool) { c = c.Thaw() array = c.array() - array = append(array[:i], array[i+1:]...) - c.setArray(array) + const needCopyOnWriteDueToReadOnlyMmap = true + + // TODO(jea) quick benchmarks don't show less performance if needCopyOnWriteDueToReadOnlyMmap + // is true, but we may need more rigorous measurement. + // + // Don't have a COW: + // all benchmarks in roaring/ + // ok github.com/pilosa/pilosa/v2/roaring 217.138s + // + // ok, have a COW: + // all benchmarks in roaring/ + // ok github.com/pilosa/pilosa/v2/roaring 214.295s + + if needCopyOnWriteDueToReadOnlyMmap { + n := len(array) + array2 := make([]uint16, n-1) + copy(array2, array[:i]) + copy(array2[i:], array[i+1:]) + c.setArray(array2) + } else { + // seg fault here with read-only mmap; go 1.14.7 linux. + // example: cap = 7 i = 0 len = 7 + // the append tries to write read-only memory? + array = append(array[:i], array[i+1:]...) + c.setArray(array) + } return c, true } diff --git a/rrtx.go b/rrtx.go index 9ef266c60..597a61ffa 100644 --- a/rrtx.go +++ b/rrtx.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "strconv" + "sync" "sync/atomic" "github.com/pilosa/pilosa/v2/roaring" @@ -35,18 +36,30 @@ type RoaringTx struct { fragment *fragment o Txo sn int64 // serial number + + done bool + mu sync.Mutex // protect done as it changes state + + w *RoaringWrapper +} + +func (tx *RoaringTx) IsDone() (done bool) { + tx.mu.Lock() + done = tx.done + tx.mu.Unlock() + return } func (tx *RoaringTx) Type() string { return RoaringTxn } -func (tx *RoaringTx) Dump() { - fmt.Printf("%v\n", tx.Index.StringifiedRoaringKeys(false, false)) +func (tx *RoaringTx) Dump(short bool) { + fmt.Printf("%v\n", tx.Index.StringifiedRoaringKeys(short, false, tx.o)) } func (tx *RoaringTx) UseRowCache() bool { - return true + return false } func (tx *RoaringTx) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) { @@ -125,11 +138,14 @@ func (tx *RoaringTx) IncrementOpN(index, field, view string, shard uint64, chang frag.incrementOpN(changedN) } -// Rollback is a no-op as Roaring does not support transactions. -func (tx *RoaringTx) Rollback() {} +// Rollback +func (tx *RoaringTx) Rollback() { + tx.w.CleanupTx(tx) +} -// Commit is a no-op as Roaring does not support transactions. +// Commit func (tx *RoaringTx) Commit() error { + tx.w.CleanupTx(tx) return nil } @@ -359,80 +375,6 @@ func (tx *RoaringTx) bitmap(index, field, view string, shard uint64) (*roaring.B return frag.storage, nil } -var globalRoaringReg = &RoaringStore{} - -func (r *RoaringStore) OpenDBWrapper(path string, doAllocZero bool) (DBWrapper, error) { - return r, nil -} - -func (w *RoaringStore) DeleteDBPath(dbs *DBShard) error { - return os.RemoveAll(dbs.Path) -} - -func (r *RoaringStore) Close() error { - return nil -} - -func (w *RoaringStore) OpenListString() (r string) { - return "RoaringStore.OpenListString() not yet implemented" -} - -func (w *RoaringStore) OpenSnList() (sns []int64) { - return nil -} - -type RoaringStore struct{} - -func NewRoaringStore() *RoaringStore { - return &RoaringStore{} -} - -var globalNextTxSnRoaring int64 - -func (db *RoaringStore) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) { - sn := atomic.AddInt64(&globalNextTxSnRoaring, 1) - return &RoaringTx{write: o.Write, Field: o.Field, Index: o.Index, fragment: o.Fragment, o: o, sn: sn}, nil -} - -func (db *RoaringStore) DeleteField(index, field, fieldPath string) error { - - // match txn sn count vs lmdb/etc. - atomic.AddInt64(&globalNextTxSnRoaring, 1) - - // under blue-green badger_roaring, the directory will not be found, b/c badger will have - // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: - // "If the path does not exist, RemoveAll returns nil (no error)" - err := os.RemoveAll(fieldPath) - if err != nil { - return errors.Wrap(err, "removing directory") - } - return nil -} - -// frag should be passed by any RoaringTx user, but for RBF/Badger it can be nil. -// The fragment should be closed before this. -func (db *RoaringStore) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { - - // match txn sn count vs lmdb/etc. - atomic.AddInt64(&globalNextTxSnRoaring, 1) - - fragment, ok := frag.(*fragment) - if !ok { - return fmt.Errorf("RoaringStore.DeleteFragment must get frag of type *fragment, but got '%T'", frag) - } - - // Delete fragment file. - if err := os.Remove(fragment.path); err != nil { - return errors.Wrap(err, "deleting fragment file") - } - - // Delete fragment cache file. - if err := os.Remove(fragment.cachePath()); err != nil { - return errors.Wrap(err, fmt.Sprintf("no cache file to delete for shard %d", fragment.shard)) - } - return nil -} - func (tx *RoaringTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) { file, err := os.Open(fragmentPathForRoaring) // open the fragment file if err != nil { @@ -459,3 +401,193 @@ func (tx *RoaringTx) Options() Txo { func (tx *RoaringTx) Sn() int64 { return tx.sn } + +//////// registrar and wrapper machinery + +// roaringRegistrar mirrors the machinery expected +// for all backends for the roaring files approach. +// +type roaringRegistrar struct { + mu sync.Mutex + mp map[*RoaringWrapper]bool + + path2db map[string]*RoaringWrapper +} + +func (r *roaringRegistrar) Size() int { + r.mu.Lock() + defer r.mu.Unlock() + nmp := len(r.mp) + npa := len(r.path2db) + if nmp != npa { + panic(fmt.Sprintf("nmp=%v, vs npa=%v", nmp, npa)) + } + return nmp +} + +var globalRoaringReg *roaringRegistrar = newRoaringRegistrar() + +func newRoaringRegistrar() *roaringRegistrar { + return &roaringRegistrar{ + mp: make(map[*RoaringWrapper]bool), + path2db: make(map[string]*RoaringWrapper), + } +} + +func (r *roaringRegistrar) unprotectedRegister(w *RoaringWrapper) { + r.mp[w] = true + r.path2db[w.path] = w +} + +// unregister removes w from r +func (r *roaringRegistrar) unregister(w *RoaringWrapper) { + r.mu.Lock() + delete(r.mp, w) + delete(r.path2db, w.path) + r.mu.Unlock() +} + +// openRoaringDB will check the registry and make a new instance only +// if one does not exist for its path0. Otherwise it returns +// the existing instance. +func (r *roaringRegistrar) OpenDBWrapper(path string, doAllocZero bool) (DBWrapper, error) { + + r.mu.Lock() + defer r.mu.Unlock() + w, ok := r.path2db[path] + if ok { + return w, nil + } + // otherwise, make a new roaring and store it in globalRoaringReg + w = &RoaringWrapper{ + reg: r, + path: path, + } + r.unprotectedRegister(w) + + return w, nil +} + +func (w *RoaringWrapper) SetHolder(h *Holder) { + w.h = h +} + +func (w *RoaringWrapper) Path() string { + return w.path +} + +func (w *RoaringWrapper) HasData() (has bool, err error) { + return w.h.HasRoaringData() +} + +func (w *RoaringWrapper) CleanupTx(tx Tx) { + r := tx.(*RoaringTx) + r.mu.Lock() + defer r.mu.Unlock() + if r.done { + return + } + r.done = true + + r.o.dbs.Cleanup(tx) // release the read/write lock. +} + +func (w *RoaringWrapper) OpenListString() (r string) { + return "RoaringWrapper.OpenListString() not yet implemented" +} + +func (w *RoaringWrapper) OpenSnList() (slc []int64) { + return nil +} + +// statically confirm that RoaringTx satisfies the Tx interface. +var _ Tx = (*RoaringTx)(nil) + +// RoaringWrapper provides the NewTx() method. +type RoaringWrapper struct { + muDb sync.Mutex + + path string + + h *Holder + + reg *roaringRegistrar + + // make RoaringWrapper.Close() idempotent, avoiding panic on double Close() + closed bool +} + +var globalNextTxSnRoaring int64 + +func (w *RoaringWrapper) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) { + + sn := atomic.AddInt64(&globalNextTxSnRoaring, 1) + return &RoaringTx{ + write: o.Write, + Field: o.Field, + Index: o.Index, + fragment: o.Fragment, + o: o, + sn: sn, + w: w, + }, nil +} + +// Close shuts down the Roaring database. +func (w *RoaringWrapper) Close() (err error) { + w.muDb.Lock() + defer w.muDb.Unlock() + if !w.closed { + w.reg.unregister(w) + w.closed = true + } + return nil +} + +func (w *RoaringWrapper) IsClosed() (closed bool) { + w.muDb.Lock() + closed = w.closed + w.muDb.Unlock() + return +} + +func (w *RoaringWrapper) DeleteDBPath(dbs *DBShard) (err error) { + return os.RemoveAll(dbs.Path) +} + +func (w *RoaringWrapper) DeleteField(index, field, fieldPath string) error { + + // match txn sn count vs lmdb/etc. + atomic.AddInt64(&globalNextTxSnRoaring, 1) + + // under blue-green badger_roaring, the directory will not be found, b/c badger will have + // already done the os.RemoveAll(). BUT, RemoveAll returns nil error in this case. Docs: + // "If the path does not exist, RemoveAll returns nil (no error)" + err := os.RemoveAll(fieldPath) + if err != nil { + return errors.Wrap(err, "removing directory") + } + return nil +} + +func (w *RoaringWrapper) DeleteFragment(index, field, view string, shard uint64, frag interface{}) error { + + // match txn sn count vs lmdb/etc. + atomic.AddInt64(&globalNextTxSnRoaring, 1) + + fragment, ok := frag.(*fragment) + if !ok { + return fmt.Errorf("RoaringStore.DeleteFragment must get frag of type *fragment, but got '%T'", frag) + } + + // Delete fragment file. + if err := os.Remove(fragment.path); err != nil { + return errors.Wrap(err, "deleting fragment file") + } + + // Delete fragment cache file. + if err := os.Remove(fragment.cachePath()); err != nil { + return errors.Wrap(err, fmt.Sprintf("no cache file to delete for shard %d", fragment.shard)) + } + return nil +} diff --git a/rrtx_internal_test.go b/rrtx_internal_test.go new file mode 100644 index 000000000..3a03ca763 --- /dev/null +++ b/rrtx_internal_test.go @@ -0,0 +1,63 @@ +// Copyright 2020 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "os" + "testing" +) + +func TestRoaring_HasData(t *testing.T) { + + orig := os.Getenv("PILOSA_TXSRC") + defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests! + os.Setenv("PILOSA_TXSRC", "roaring") + + idx := newIndexWithTempPath(t, "i") + defer idx.Close() + + db, err := globalRoaringReg.OpenDBWrapper(idx.path, false) + panicOn(err) + db.SetHolder(idx.holder) + + // HasData should start out false. + hasAnything, err := db.HasData() + panicOn(err) + + if hasAnything { + t.Fatalf("HasData reported existing data on an empty database") + } + + // check that HasData sees a committed record. + + field, shard := "f", uint64(123) + + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + defer tx.Rollback() + + f, err := idx.CreateField(field) + panicOn(err) + _, err = f.SetBit(tx, 1, 1, nil) + panicOn(err) + panicOn(tx.Commit()) + + hasAnything, err = db.HasData() + if err != nil { + t.Fatal(err) + } + if !hasAnything { + t.Fatalf("HasData() reported no data on a database that has 'x' written to it") + } +} diff --git a/server/server.go b/server/server.go index bfd54cead..1b7dea316 100644 --- a/server/server.go +++ b/server/server.go @@ -290,6 +290,9 @@ func (m *Command) SetupServer() error { txty := pilosa.MustTxsrcToTxtype(m.Config.Txsrc) // will panic on unknown Txsrc. os.Setenv("PILOSA_TXSRC", m.Config.Txsrc) m.logger.Printf("using Txsrc '%v'/%v", m.Config.Txsrc, txty) + if len(txty) == 2 { + m.logger.Printf("blue='%v' / green='%v'", txty[0], txty[1]) + } // validateAddrs sets the appropriate values for Bind and Advertise // based on the inputs. It is not responsible for applying defaults, although diff --git a/stattx.go b/stattx.go index 0309faf46..20b30a57c 100644 --- a/stattx.go +++ b/stattx.go @@ -310,8 +310,8 @@ func (c *statTx) ImportRoaringBits(index, field, view string, shard uint64, rit return c.b.ImportRoaringBits(index, field, view, shard, rit, clear, log, rowSize, data) } -func (c *statTx) Dump() { - c.b.Dump() +func (c *statTx) Dump(short bool) { + c.b.Dump(short) } func (c *statTx) Readonly() bool { @@ -433,6 +433,10 @@ func (c *statTx) UseRowCache() bool { return c.b.UseRowCache() } +func (c *statTx) IsDone() (done bool) { + return c.b.IsDone() +} + func (c *statTx) Add(index, field, view string, shard uint64, batched bool, a ...uint64) (changeCount int, err error) { me := kAdd diff --git a/synthload/synthload_test.go b/synthload/synthload_test.go new file mode 100644 index 000000000..ec4c6b1ce --- /dev/null +++ b/synthload/synthload_test.go @@ -0,0 +1,361 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package synthload + +/* // still work in progress, comment out for now, since we + // are getting a hang on go1.13 in CI +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pilosa/pilosa/v2" + "github.com/pilosa/pilosa/v2/server" + "github.com/pilosa/pilosa/v2/test" +) + +func Test_SynthLoad_ImportSchema(t *testing.T) { + c := test.MustRunCluster(t, 1, + []server.CommandOption{ + server.OptCommandServerOptions( + pilosa.OptServerNodeID("node0"), + )}, + ) + defer c.Close() + + m0 := c.GetNode(0) + + tarball := "testindex.tar.gz" + + // get the holder.Path to write to + h := m0.API.Holder() + target := h.Path() + panicOn(h.Close()) + + panicOn(unpackTarball(tarball, target)) + + // reopen + panicOn(h.Open()) + + qs := strings.Split(pql, "\n\n") + //vv("qs = '%#v'", qs) + for _, q := range qs { + req := &pilosa.QueryRequest{ + // Index to execute query against. + Index: "testindex", + + // The query string to parse and execute. + Query: q, + + // // The shards to include in the query execution. + // // If empty, all shards are included. + // Shards []uint64 + + // // Return column attributes, if true. + // ColumnAttrs bool + + // // Do not return row attributes, if true. + // ExcludeRowAttrs bool + + // // Do not return columns, if true. + // ExcludeColumns bool + + // // If true, indicates that query is part of a larger distributed query. + // // If false, this request is on the originating node. + // Remote bool + + // // Should we profile this query? + // Profile bool + + // // Additional data associated with the query, in cases where there's + // // row-style inputs for precomputed values. + // EmbeddedData []*Row + } + + qr, err := m0.API.Query(context.Background(), req) + panicOn(err) + vv("qr = '%#v'", qr) + } +} + +var _ = applySchema + +func applySchema(m0 *test.Command, schemaStr string) { + // don't need schema now that we import the tarball, it has it all. + schema := &pilosa.Schema{} + err := json.NewDecoder(bytes.NewBufferString(schemaStr)).Decode(schema) + panicOn(err) + + ctx := context.Background() + remote := false + err = m0.API.ApplySchema(ctx, schema, remote) + panicOn(err) +} + +func unpackTarball(tarball, target string) error { + + vv("target = '%v'", target) + fd, err := os.Open(tarball) + panicOn(err) + defer fd.Close() + gz, err := gzip.NewReader(fd) + panicOn(err) + defer gz.Close() + + tarReader := tar.NewReader(gz) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } else if err != nil { + panic(err) + } + + path := filepath.Join(target, header.Name) + info := header.FileInfo() + if info.IsDir() { + if err = os.MkdirAll(path, info.Mode()); err != nil { + panicOn(err) + } + continue + } + + file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + panicOn(err) + + _, err = io.Copy(file, tarReader) + panicOn(err) + file.Close() + } + return nil +} + +// mkdir o; bangbang schemator -o o +var pql = ` +Count(All()) + +Rows(field='bools',limit=10) +Count(Row(bools='bool')) +Count(Not(Row(bools='bool'))) +Count(Intersect(Row(bools='bool'))) +Count(Difference(Union(Row(bools='bool')),Intersect(Row(bools='bool')))) +GroupBy(Rows(field='bools'),limit=10) + +Rows(field='bools-exists',limit=10) +Count(Row(bools-exists='bool')) +Count(Not(Row(bools-exists='bool'))) +Count(Intersect(Row(bools-exists='bool'))) +Count(Difference(Union(Row(bools-exists='bool')),Intersect(Row(bools-exists='bool')))) +GroupBy(Rows(field='bools-exists'),limit=10) + +GroupBy(Rows(field='int'),limit=10) +Min(field='int') +Max(field='int') +Sum(field='int') + +Rows(field='mutex',limit=10) +Count(Row(mutex='12')) +Count(Not(Row(mutex='12'))) +Count(Row(mutex='13')) +Count(Not(Row(mutex='13'))) +Count(Row(mutex='0')) +Count(Not(Row(mutex='0'))) +Count(Row(mutex='5')) +Count(Not(Row(mutex='5'))) +Count(Row(mutex='2')) +Count(Not(Row(mutex='2'))) +Count(Row(mutex='14')) +Count(Not(Row(mutex='14'))) +Count(Row(mutex='3')) +Count(Not(Row(mutex='3'))) +Count(Row(mutex='18')) +Count(Not(Row(mutex='18'))) +Count(Row(mutex='7')) +Count(Not(Row(mutex='7'))) +Count(Row(mutex='19')) +Count(Not(Row(mutex='19'))) +Count(Intersect(Row(mutex='12'),Row(mutex='13'),Row(mutex='0'),Row(mutex='5'),Row(mutex='2'),Row(mutex='14'),Row(mutex='3'),Row(mutex='18'),Row(mutex='7'),Row(mutex='19'))) +Count(Difference(Union(Row(mutex='12'),Row(mutex='13'),Row(mutex='0'),Row(mutex='5'),Row(mutex='2'),Row(mutex='14'),Row(mutex='3'),Row(mutex='18'),Row(mutex='7'),Row(mutex='19')),Intersect(Row(mutex='12'),Row(mutex='13'),Row(mutex='0'),Row(mutex='5'),Row(mutex='2'),Row(mutex='14'),Row(mutex='3'),Row(mutex='18'),Row(mutex='7'),Row(mutex='19')))) +GroupBy(Rows(field='mutex'),limit=10) + +Rows(field='set',limit=10) +Count(Row(set='v621')) +Count(Not(Row(set='v621'))) +Count(Row(set='v997')) +Count(Not(Row(set='v997'))) +Count(Row(set='v772')) +Count(Not(Row(set='v772'))) +Count(Row(set='v340')) +Count(Not(Row(set='v340'))) +Count(Row(set='v766')) +Count(Not(Row(set='v766'))) +Count(Row(set='v416')) +Count(Not(Row(set='v416'))) +Count(Row(set='v481')) +Count(Not(Row(set='v481'))) +Count(Row(set='v581')) +Count(Not(Row(set='v581'))) +Count(Row(set='v591')) +Count(Not(Row(set='v591'))) +Count(Row(set='v675')) +Count(Not(Row(set='v675'))) +Count(Intersect(Row(set='v621'),Row(set='v997'),Row(set='v772'),Row(set='v340'),Row(set='v766'),Row(set='v416'),Row(set='v481'),Row(set='v581'),Row(set='v591'),Row(set='v675'))) +Count(Difference(Union(Row(set='v621'),Row(set='v997'),Row(set='v772'),Row(set='v340'),Row(set='v766'),Row(set='v416'),Row(set='v481'),Row(set='v581'),Row(set='v591'),Row(set='v675')),Intersect(Row(set='v621'),Row(set='v997'),Row(set='v772'),Row(set='v340'),Row(set='v766'),Row(set='v416'),Row(set='v481'),Row(set='v581'),Row(set='v591'),Row(set='v675')))) +GroupBy(Rows(field='set'),limit=10) + +Rows(field='string',limit=10) +Count(Row(string='KPFGWOYUGTCF')) +Count(Not(Row(string='KPFGWOYUGTCF'))) +Count(Row(string='VNSHWDTNELDA')) +Count(Not(Row(string='VNSHWDTNELDA'))) +Count(Row(string='LTDOGKKFGZPW')) +Count(Not(Row(string='LTDOGKKFGZPW'))) +Count(Row(string='EETNIIDCDZHB')) +Count(Not(Row(string='EETNIIDCDZHB'))) +Count(Row(string='ATWOMTRASGHP')) +Count(Not(Row(string='ATWOMTRASGHP'))) +Count(Row(string='KOJUBJQVMXZL')) +Count(Not(Row(string='KOJUBJQVMXZL'))) +Count(Row(string='NOXDEYPTGZBH')) +Count(Not(Row(string='NOXDEYPTGZBH'))) +Count(Row(string='DLSRPRADBPKX')) +Count(Not(Row(string='DLSRPRADBPKX'))) +Count(Row(string='WJUEQABAEEUX')) +Count(Not(Row(string='WJUEQABAEEUX'))) +Count(Row(string='PFRGBFDLHHPK')) +Count(Not(Row(string='PFRGBFDLHHPK'))) +Count(Intersect(Row(string='KPFGWOYUGTCF'),Row(string='VNSHWDTNELDA'),Row(string='LTDOGKKFGZPW'),Row(string='EETNIIDCDZHB'),Row(string='ATWOMTRASGHP'),Row(string='KOJUBJQVMXZL'),Row(string='NOXDEYPTGZBH'),Row(string='DLSRPRADBPKX'),Row(string='WJUEQABAEEUX'),Row(string='PFRGBFDLHHPK'))) +Count(Difference(Union(Row(string='KPFGWOYUGTCF'),Row(string='VNSHWDTNELDA'),Row(string='LTDOGKKFGZPW'),Row(string='EETNIIDCDZHB'),Row(string='ATWOMTRASGHP'),Row(string='KOJUBJQVMXZL'),Row(string='NOXDEYPTGZBH'),Row(string='DLSRPRADBPKX'),Row(string='WJUEQABAEEUX'),Row(string='PFRGBFDLHHPK')),Intersect(Row(string='KPFGWOYUGTCF'),Row(string='VNSHWDTNELDA'),Row(string='LTDOGKKFGZPW'),Row(string='EETNIIDCDZHB'),Row(string='ATWOMTRASGHP'),Row(string='KOJUBJQVMXZL'),Row(string='NOXDEYPTGZBH'),Row(string='DLSRPRADBPKX'),Row(string='WJUEQABAEEUX'),Row(string='PFRGBFDLHHPK')))) +GroupBy(Rows(field='string'),limit=10) + +GroupBy(Rows(field='time'),limit=10) +` + +// datagen --source=kitchensink --pilosa.index=testindex --start-from=1 --end-at=1000 --pilosa.batch-size=10000 --pilosa.hosts localhost:10101 +// curl localhost:10101/schema + +var _ = schemaString +var schemaString = ` +{ + "indexes": [ + { + "name": "testindex", + "createdAt": 1599601704641744600, + "options": { + "keys": false, + "trackExistence": true + }, + "fields": [ + { + "name": "bools", + "createdAt": 1599601704647053000, + "options": { + "type": "set", + "cacheType": "ranked", + "cacheSize": 50000, + "keys": true + } + }, + { + "name": "bools-exists", + "createdAt": 1599601704643225900, + "options": { + "type": "set", + "cacheType": "ranked", + "cacheSize": 50000, + "keys": true + } + }, + { + "name": "decimal", + "createdAt": 1599601704642064400, + "options": { + "type": "decimal", + "base": 0, + "scale": 2, + "bitDepth": 63, + "min": -92233720368547760, + "max": 92233720368547760, + "keys": false + } + }, + { + "name": "int", + "createdAt": 1599601704645510100, + "options": { + "type": "int", + "base": 0, + "bitDepth": 63, + "min": -9223372036854776000, + "max": 9223372036854776000, + "keys": false, + "foreignIndex": "" + } + }, + { + "name": "mutex", + "createdAt": 1599601704647187200, + "options": { + "type": "mutex", + "cacheType": "ranked", + "cacheSize": 500, + "keys": true + } + }, + { + "name": "set", + "createdAt": 1599601704643664400, + "options": { + "type": "set", + "cacheType": "lru", + "cacheSize": 1, + "keys": true + } + }, + { + "name": "string", + "createdAt": 1599601704644342000, + "options": { + "type": "set", + "cacheType": "ranked", + "cacheSize": 50000, + "keys": true + } + }, + { + "name": "time", + "createdAt": 1599601704643013400, + "options": { + "type": "time", + "timeQuantum": "YMD", + "keys": true, + "noStandardView": false + } + } + ], + "shardWidth": 1048576 + } + ] +} +` + +*/ diff --git a/synthload/testindex.tar.gz b/synthload/testindex.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..15cdfe2b62f0237bd589572a065f04a2eed4eeb2 GIT binary patch literal 234061 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%0D?`XTKX)2&oqi!H!LL6cyeq<_iyI?S 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 TzT`ilFYV6%t8l?w*@FKDgpuw+ literal 0 HcmV?d00001 diff --git a/synthload/vprint.go b/synthload/vprint.go new file mode 100644 index 000000000..d9f8a03ec --- /dev/null +++ b/synthload/vprint.go @@ -0,0 +1,170 @@ +// home: https://github.com/glycerine/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 synthload + +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 +var _ = 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()) +} + +var _ = stack + +func FileExists(name string) bool { + fi, err := os.Stat(name) + if err != nil { + return false + } + if fi.IsDir() { + return false + } + return true +} + +func DirExists(name string) bool { + fi, err := os.Stat(name) + if err != nil { + return false + } + if fi.IsDir() { + return true + } + return false +} + +func FileSize(name string) (int64, error) { + fi, err := os.Stat(name) + if err != nil { + return -1, err + } + return fi.Size(), nil +} + +// Caller returns the name of the calling function. +func Caller(upStack int) string { + // elide ourself and runtime.Callers + target := upStack + 2 + + pc := make([]uintptr, target+2) + n := runtime.Callers(0, pc) + + f := runtime.Frame{Function: "unknown"} + if n > 0 { + frames := runtime.CallersFrames(pc[:n]) + for i := 0; i <= target; i++ { + contender, more := frames.Next() + if i == target { + f = contender + } + if !more { + break + } + } + } + return f.Function +} diff --git a/test/holder.go b/test/holder.go index ed91d669b..6deb44ce5 100644 --- a/test/holder.go +++ b/test/holder.go @@ -160,7 +160,7 @@ func (h *Holder) SetBitTime(index, field string, rowID, columnID uint64, t *time } shard := columnID / pilosa.ShardWidth - tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) + tx := idx.Index.Txf().NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) defer tx.Rollback() _, err = f.SetBit(tx, rowID, columnID, t) @@ -179,7 +179,7 @@ func (h *Holder) ClearBit(index, field string, rowID, columnID uint64) { } shard := columnID / pilosa.ShardWidth - tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) + tx := idx.Index.Txf().NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) defer tx.Rollback() _, err = f.ClearBit(tx, rowID, columnID) @@ -205,7 +205,7 @@ func (h *Holder) SetValue(index, field string, columnID uint64, value int64) *In panic(err) } shard := columnID / pilosa.ShardWidth - tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) + tx := idx.Index.Txf().NewTx(pilosa.Txo{Write: true, Index: idx.Index, Shard: shard}) defer tx.Rollback() _, err = f.SetValue(tx, columnID, value) @@ -226,7 +226,7 @@ func (h *Holder) Value(index, field string, columnID uint64) (int64, bool) { panic(err) } shard := columnID / pilosa.ShardWidth - tx := idx.Index.Txf.NewTx(pilosa.Txo{Write: false, Index: idx.Index, Shard: shard}) + tx := idx.Index.Txf().NewTx(pilosa.Txo{Write: false, Index: idx.Index, Shard: shard}) defer tx.Rollback() val, exists, err := f.Value(tx, columnID) diff --git a/test/index.go b/test/index.go index b78199263..376ee6d65 100644 --- a/test/index.go +++ b/test/index.go @@ -59,7 +59,7 @@ func (i *Index) Reopen() error { if err := i.Index.Close(); err != nil { return err } - return i.Index.Open(false) + return i.Index.Open() } // CreateField creates a field with the given options. diff --git a/tx.go b/tx.go index 6d2b0642c..8546ac8d3 100644 --- a/tx.go +++ b/tx.go @@ -68,6 +68,11 @@ type Tx interface { // Commit makes the updates in the Tx visible to subsequent transactions. Commit() error + // IsDone must return true if Rollback() or Commit() has already + // been called. Otherwise it must return false. This allows + // DBWrapper.CleanupTx(tx Tx) to be idempotent. + IsDone() bool + // Readonly returns the flag this transaction was created with // during NewTx. If the transaction is writable, it will return false. Readonly() bool @@ -197,7 +202,7 @@ type Tx interface { Group() *TxGroup // Dump is for debugging, what does this Tx see as its database? - Dump() + Dump(short bool) // Options returns the options used to create this Tx. This // can be implementd by embedding Txo, and Txo provides the diff --git a/txfactory.go b/txfactory.go index 6eddbe19e..95c8ccf7d 100644 --- a/txfactory.go +++ b/txfactory.go @@ -37,6 +37,7 @@ const ( RoaringTxn string = "roaring" LmdbTxn string = "lmdb" RBFTxn string = "rbf" + BadgerTxn string = "badger" ) // DefaultTxsrc is set here. pilosa/server/config.go references it @@ -389,6 +390,10 @@ type TxFactory struct { holder *Holder blueGreenReg *blueGreenRegistry + + // allow holder to activate blue-green checking only + // once we have synced both sides at start up time. + blueGreenOff bool } func (f *TxFactory) Types() []txtype { @@ -403,20 +408,41 @@ const ( roaringTxn txtype = 1 // these don't really have any transactions rbfTxn txtype = 2 lmdbTxn txtype = 3 + badgerTxn txtype = 4 ) +// these need to be skipped by the holder.go field scanner that +// calls IsTxDatabasePath +var allTypesWithSuffixes = []txtype{rbfTxn, lmdbTxn, badgerTxn} + +// FileSuffix is used to determine backend directory names. +// We append '@' to be sure we never collide with a field name +// inside the index directory. In the future for different +// versions of the same backend, there might be version +// identifier tacked on too. func (ty txtype) FileSuffix() string { switch ty { case roaringTxn: return "" case rbfTxn: - return "-rbfdb" + return "-rbfdb@" case lmdbTxn: - return "-lmdb" + return "-lmdb@" + case badgerTxn: + return "-badgerdb@" } panic(fmt.Sprintf("unkown txtype %v", int(ty))) } +func (txf *TxFactory) IsTxDatabasePath(path string) bool { + for _, ty := range allTypesWithSuffixes { + if strings.HasSuffix(path, ty.FileSuffix()) { + return true + } + } + return false +} + func (txf *TxFactory) NeedsSnapshot() (b bool) { for _, ty := range txf.types { switch ty { @@ -447,6 +473,8 @@ func MustTxsrcToTxtype(txsrc string) (types []txtype) { types = append(types, rbfTxn) case LmdbTxn: // "lmdb" types = append(types, lmdbTxn) + case BadgerTxn: // "badger" + types = append(types, badgerTxn) default: panic(fmt.Sprintf("unknown txsrc '%v'", s)) } @@ -467,12 +495,15 @@ func NewTxFactory(txsrc string, holderDir string, holder *Holder) (f *TxFactory, types := MustTxsrcToTxtype(txsrc) f = &TxFactory{ - types: types, - typeOfTx: txsrc, - holder: holder, - blueGreenReg: newBlueGreenReg(), + types: types, + typeOfTx: txsrc, + holder: holder, + } + if len(types) == 2 { + f.blueGreenReg = newBlueGreenReg(types) } f.dbPerShard = f.NewDBPerShard(types, holderDir) + return f, err } @@ -489,6 +520,8 @@ type Txo struct { per *DBPerShard Group *TxGroup + + blueGreenOff bool } func (o Txo) String() string { @@ -528,7 +561,7 @@ func (f *TxFactory) CloseIndex(idx *Index) error { return nil } -func (f *TxFactory) CloseDB() (err error) { +func (f *TxFactory) Close() (err error) { if f.dbsClosed { return nil } @@ -684,7 +717,7 @@ func (g *TxGroup) AbortGroup() { } func (f *TxFactory) NewTx(o Txo) (txn Tx) { - f.mu.Lock() // deadlock here + f.mu.Lock() defer f.mu.Unlock() defer func() { @@ -693,6 +726,8 @@ func (f *TxFactory) NewTx(o Txo) (txn Tx) { } }() + o.blueGreenOff = f.blueGreenOff + indexName := "" if o.Index != nil { indexName = o.Index.name @@ -711,8 +746,15 @@ func (f *TxFactory) NewTx(o Txo) (txn Tx) { // per-shard database. Opens a new one if needed. dbs, err := f.dbPerShard.GetDBShard(indexName, o.Shard, o.Index) panicOn(err) + + if dbs.Shard != o.Shard { + panic(fmt.Sprintf("asked for o.Shard=%v but got dbs.Shard=%v", int(o.Shard), int(dbs.Shard))) + } + //vv("got dbs='%p' for o.Index='%v'; shard='%v'; dbs.types='%#v'; dbs.W='%#v'", dbs, o.Index.name, o.Shard, dbs.types, dbs.W) + o.dbs = dbs // our specific database per shard. o.per = f.dbPerShard // for top level debug Dumps + tx, err := dbs.NewTx(o.Write, indexName, o) if err != nil { panic(errors.Wrap(err, "rbfDB.NewRBFTx transaction errored")) @@ -730,6 +772,8 @@ func (ty txtype) String() string { return "rbfTxn" case lmdbTxn: return "lmdbTxn" + case badgerTxn: + return "badgerTxn" } panic(fmt.Sprintf("unhandled ty '%v' in txtype.String()", int(ty))) } @@ -767,7 +811,7 @@ func fragmentSpecFromRoaringPath(path string) (field, view string, shard uint64, // hashOnly means only show the value hash, not the content bits. // showOps means display the ops log. -func (idx *Index) StringifiedRoaringKeys(hashOnly, showOps bool) (r string) { +func (idx *Index) StringifiedRoaringKeys(hashOnly, showOps bool, o Txo) (r string) { paths, err := listFilesUnderDir(idx.path, false, "", true) panicOn(err) index := idx.name @@ -784,9 +828,9 @@ func (idx *Index) StringifiedRoaringKeys(hashOnly, showOps bool) (r string) { s, _, err := stringifiedRawRoaringFragment(abspath, index, field, view, shard, showOps, hashOnly, os.Stdout) panicOn(err) //r += fmt.Sprintf("path:'%v' fragment contains:\n") + s - if s == "" { - s = "" - } + //if s == "" { + //s = "" + //} r += s n++ } @@ -1073,3 +1117,154 @@ func printContainers(w io.Writer, info roaring.BitmapInfo, pC pointerContext) { } tw.Flush() } + +var _ = anyGlobalDBWrappersStillOpen // happy linter + +func anyGlobalDBWrappersStillOpen() bool { + if globalRoaringReg.Size() != 0 { + return true + } + if globalRbfDBReg.Size() != 0 { + return true + } + if globalLMDBReg.Size() != 0 { + return true + } + if globalBadgerReg.Size() != 0 { + return true + } + return false +} + +func (f *TxFactory) blueGreenOnIfRunningBlueGreen() { + if len(f.types) == 2 { + f.blueGreenOff = false + } +} + +func (f *TxFactory) blueGreenOffIfRunningBlueGreen() { + if len(f.types) == 2 { + f.blueGreenOff = true + } +} + +func (f *TxFactory) hasRoaring() bool { + return f.types[0] == roaringTxn || f.types[1] == roaringTxn +} + +var _ = (&TxFactory{}).hasRoaring // happy linter + +func (f *TxFactory) blueHasData() (hasData bool, err error) { + if len(f.types) != 2 { + return false, nil + } + return f.dbPerShard.HasData(0) +} + +// green2blue is called at the very end of Holder.Open(), so +// we know that the holder is ready to go, knowing its holder.Indexes(), fields, +// view, shards, and other metadata if any. +// +// Called by test Test_TxFactory_UpdateBlueFromGreen_OnStartup() in +// txfactory_internal_test.go as well. +// +func (f *TxFactory) green2blue(holder *Holder) (err error) { + + // Holder.Open will always call us, even without blue_green. Which is fine. + // We are just a no-op in that case. + if len(f.types) != 2 { + return nil + } + + blueDest := f.types[0] + greenSrc := f.types[1] + idxs := holder.Indexes() + + verifyInsteadOfCopy := false + + hasData, err := f.blueHasData() + if err != nil { + return errors.Wrap(err, "TxFactory.green2blue DataSize(0)") + } + + if hasData { + verifyInsteadOfCopy = true + } + + for _, idx := range idxs { + + blueShards, err := TypedDBPerShardGetLocalShardsForIndex(blueDest, idx, "") + if err != nil { + return errors.Wrap(err, fmt.Sprintf("GetDBShard(index='%v') error fetching blueShards", idx.name)) + } + greenShards, err := TypedDBPerShardGetLocalShardsForIndex(greenSrc, idx, "") + if err != nil { + return errors.Wrap(err, fmt.Sprintf("GetDBShard(index='%v') error fetching greenShards", idx.name)) + } + + diff := f.shardSliceDiff(blueShards, greenShards) + if diff != "" { + return fmt.Errorf("blue[%v] and green[%v] have different shards for index '%v': %v", blueDest, greenSrc, idx.name, diff) + } + + shards := idx.AvailableShards(localOnly).Slice() + + diff2 := f.shardSliceDiff(blueShards, shards) + if diff2 != "" { + return fmt.Errorf("blue[%v] and meta data (from green[%v]?)have different shards for index '%v': %v", blueDest, greenSrc, idx.name, diff2) + } + + for _, shard := range shards { + + dbs, err := f.dbPerShard.GetDBShard(idx.name, shard, idx) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("GetDBShard(index='%v', shard='%v')", idx.name, int(shard))) + } + + if verifyInsteadOfCopy { + // verify all containers + err = dbs.verifyBlueEqualsGreen(&holder.numCtBlueGreenVerified) + if err != nil { + return errors.Wrap(err, + fmt.Sprintf("dbs.verifyBlueEqualsGreen(blue='%v', "+ + "green='%v') for index='%v', shard='%v'", + blueDest, greenSrc, idx.name, int(shard))) + } + } else { + // the main copy work + err = dbs.populateBlueFromGreen() + if err != nil { + return errors.Wrap(err, + fmt.Sprintf("dbs.copyGreenToBlue(blue='%v', "+ + "green='%v') for index='%v', shard='%v'", + blueDest, greenSrc, idx.name, int(shard))) + } + } + } + } + return nil +} + +func (f *TxFactory) shardSliceDiff(blueShards, greenShards []uint64) (diff string) { + nb := len(blueShards) + ng := len(greenShards) + if nb != ng { + diff = fmt.Sprintf("blueShard[%v] count = %v; greenShard[%v] count = %v; ", f.types[0], nb, f.types[1], ng) + } + b := make(map[uint64]bool) + g := make(map[uint64]bool) + for _, bs := range blueShards { + b[bs] = true + } + for _, gs := range greenShards { + g[gs] = true + } + bmg := mapDiff(b, g) // get blue - green + gmb := mapDiff(g, b) // get green - blue + + if len(bmg) == 0 && len(gmb) == 0 { + return "" + } + diff += fmt.Sprintf("shard diff: blueMinusGreen shards: '%#v'; greenMinusBlue shards: '%#v'", bmg, gmb) + return +} diff --git a/txfactory_internal_test.go b/txfactory_internal_test.go index 4cac2252e..235a009a6 100644 --- a/txfactory_internal_test.go +++ b/txfactory_internal_test.go @@ -15,6 +15,7 @@ package pilosa import ( + "context" "fmt" "os" "testing" @@ -25,10 +26,10 @@ import ( func Test_TxFactory_Qcx_query_context(t *testing.T) { src := os.Getenv("PILOSA_TXSRC") - if src == "rbf" || src == "lmdb" { + if src == "rbf" || src == "lmdb" || src == "badger" { // ok } else { - t.Skip("this test only for lmdb and rbf") + t.Skip("this test only for lmdb and rbf and badger") } shard := uint64(0) @@ -50,7 +51,7 @@ func Test_TxFactory_Qcx_query_context(t *testing.T) { default: } // add to the group txn on the txf. - qcx := idx.Txf.NewQcx() + qcx := idx.holder.txf.NewQcx() tx, finisher := qcx.GetTx(Txo{Write: true, Index: idx, Shard: f.shard}) @@ -95,8 +96,256 @@ func Test_TxFactory_Qcx_query_context(t *testing.T) { // we will crash as the goroutines making Tx will try to use the env // after it is closed. It can take quite a while. // one writer might be blocking the other... so ask for only N-1 at first. - //barrier.BlockUntil(N - 1) - barrier.BlockUntil(N) - //barrier.UnblockReaders() - //time.Sleep(1 * time.Second) + barrier.BlockUntil(N - 1) + //barrier.BlockUntil(N) + barrier.UnblockReaders() + time.Sleep(1 * time.Second) +} + +// test TxFactory.green2blue +// +// blue_green starting with an empty or full blue database +// should copy all of green (if blue is empty); or if blue is ull, +// verify that blue has all the same bits as green. +// +// Benefits: a) we start with known identical state so our testing/comparisons can be valid; +// and b) we have an easy migration mechanism, to go from one storage format to another. +// +func Test_TxFactory_UpdateBlueFromGreen_OnStartup(t *testing.T) { + + orig := os.Getenv("PILOSA_TXSRC") + defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests! + + checked := []string{"lmdb", "roaring", "badger", "rbf"} + + for _, blue := range checked { + for _, green := range checked { + if blue == green { + continue + } + blue_green := blue + "_" + green + + // ============================= + // Begin setup. + // + // Setup happens with green only. + + os.Setenv("PILOSA_TXSRC", green) + h, path, err := makeHolder(t) + if err != nil { + t.Fatalf("creating holder: %v", err) + } + defer os.RemoveAll(path) + + // we will manually h.Close() below + + // Write bits to separate indexes. + testSetBit(t, h, "i0", "f", 100, 200) + testSetBit(t, h, "i1", "f", 100, 200) + testSetBit(t, h, "i1", "f", 100, 12345678) + + testOp := testHolderOperator{} + ctx := context.Background() + err = h.Process(ctx, &testOp) + if err != nil { + t.Fatalf("processing holder: %v", err) + } + expected := testHolderOperator{ + indexSeen: 2, indexProcessed: 2, + fieldSeen: 2, fieldProcessed: 2, + viewSeen: 2, viewProcessed: 2, + fragmentSeen: 3, fragmentProcessed: 3, + } + if testOp != expected { + t.Fatalf("holder processor did not process as expected. expected %#v, got %#v", expected, testOp) + } + + // verify data is there + rowID := uint64(100) + colID := uint64(200) + _, _ = rowID, colID + testMustHaveBit(t, h, "i0", "f", rowID, colID) + testMustHaveBit(t, h, "i1", "f", 100, 200) + testMustHaveBit(t, h, "i1", "f", 100, 12345678) + + h.Close() + + // can we re.Open the same holder h? hopefully without a problem. + panicOn(h.Open()) + + testMustHaveBit(t, h, "i0", "f", rowID, colID) + testMustHaveBit(t, h, "i1", "f", 100, 200) + testMustHaveBit(t, h, "i1", "f", 100, 12345678) + h.Close() + + // successful re-open and then Close again of h. + + // check that we can open a NewHolder on green, on same path, and still see our bits. + // Because the NewHolder is the code that creates and configures TxFactory as blue_green. + h2 := NewHolder(path, nil) + panicOn(h2.Open()) + + testMustHaveBit(t, h2, "i0", "f", rowID, colID) + testMustHaveBit(t, h2, "i1", "f", 100, 200) + testMustHaveBit(t, h2, "i1", "f", 100, 12345678) + h2.Close() + + // verify that blue does not have it. + // open a new holder on path, just looking at blue. + os.Setenv("PILOSA_TXSRC", blue) + + h3 := NewHolder(path, nil) + panicOn(h3.Open()) + + testMustNotHaveBit(t, h3, "i0", "f", rowID, colID) + testMustNotHaveBit(t, h3, "i1", "f", 100, 200) + testMustNotHaveBit(t, h3, "i1", "f", 100, 12345678) + + h3.Close() + + // ============================= + // Setup done. On to actual test. + + // Opening in blue_green mode means that once Holder.Open() + // returns without error, the blue and green databases are + // identical. + // Since blue is empty, the blue database will get synched up + // with the green during Holder.Open(). + + os.Setenv("PILOSA_TXSRC", blue_green) + + // open a holder with path again, now looking at both blue and green. + // The Holder.Open should do the migration from green, populating blue. + h4 := NewHolder(path, nil) + panicOn(h4.Open()) + defer h4.Close() + + testMustHaveBit(t, h4, "i0", "f", rowID, colID) + testMustHaveBit(t, h4, "i1", "f", 100, 200) + testMustHaveBit(t, h4, "i1", "f", 100, 12345678) + } + } +} + +// test the situation where we startup blue_green with existing data and +// go to verify it but blue has more data than green. +// That will also cause query divergence. +func Test_TxFactory_verifyBlueEqualsGreen(t *testing.T) { + + orig := os.Getenv("PILOSA_TXSRC") + defer os.Setenv("PILOSA_TXSRC", orig) // must restore or will mess up other tests! + + checked := []string{"lmdb", "roaring", "badger", "rbf"} + + for _, blue := range checked { + for _, green := range checked { + if blue == green { + continue + } + blue_green := blue + "_" + green + + // ============================= + // Begin setup. + // + // Setup happens with green only. + + os.Setenv("PILOSA_TXSRC", green) + h, path, err := makeHolder(t) + if err != nil { + t.Fatalf("creating holder: %v", err) + } + defer os.RemoveAll(path) + + // we will manually h.Close() below + + // Write bits to separate indexes. + testSetBit(t, h, "i0", "f", 100, 200) + testSetBit(t, h, "i1", "f", 100, 200) + testSetBit(t, h, "i1", "f", 100, 12345678) + + testOp := testHolderOperator{} + ctx := context.Background() + err = h.Process(ctx, &testOp) + if err != nil { + t.Fatalf("processing holder: %v", err) + } + expected := testHolderOperator{ + indexSeen: 2, indexProcessed: 2, + fieldSeen: 2, fieldProcessed: 2, + viewSeen: 2, viewProcessed: 2, + fragmentSeen: 3, fragmentProcessed: 3, + } + if testOp != expected { + t.Fatalf("holder processor did not process as expected. expected %#v, got %#v", expected, testOp) + } + + // verify data is there + rowID := uint64(100) + colID := uint64(200) + _, _ = rowID, colID + testMustHaveBit(t, h, "i0", "f", rowID, colID) + testMustHaveBit(t, h, "i1", "f", 100, 200) + testMustHaveBit(t, h, "i1", "f", 100, 12345678) + + h.Close() + + // verify that blue does not have it. + // open a new holder on path, just looking at blue. + os.Setenv("PILOSA_TXSRC", blue) + + h3 := NewHolder(path, nil) + panicOn(h3.Open()) + + testMustNotHaveBit(t, h3, "i0", "f", rowID, colID) + testMustNotHaveBit(t, h3, "i1", "f", 100, 200) + testMustNotHaveBit(t, h3, "i1", "f", 100, 12345678) + + h3.Close() + + // ============================= + // Setup done. On to actual test. + + // Opening in blue_green mode means that once Holder.Open() + // returns without error, the blue and green databases are + // identical. + // Since blue is empty, the blue database will get synched up + // with the green during Holder.Open(). + + os.Setenv("PILOSA_TXSRC", blue_green) + + // open a holder with path again, now looking at both blue and green. + // The Holder.Open should do the migration from green, populating blue. + h4 := NewHolder(path, nil) + panicOn(h4.Open()) + + testMustHaveBit(t, h4, "i0", "f", rowID, colID) + testMustHaveBit(t, h4, "i1", "f", 100, 200) + testMustHaveBit(t, h4, "i1", "f", 100, 12345678) + h4.Close() + + // now open just blue, and add a bit to a new index, i2. + os.Setenv("PILOSA_TXSRC", blue) + + h5 := NewHolder(path, nil) + panicOn(h5.Open()) + testSetBit(t, h5, "i2", "f", 500, 777) + h5.Close() + + // now open blue_green. should get a verification failure + // due to the extra bit in blue. + os.Setenv("PILOSA_TXSRC", blue_green) + + // BEGIN verficiation that should ERROR out b/c blue has more data. + + // open a holder with path again, now looking at both blue and green. + // The Holder.Open should verify blue against green and notice the extra bit. + h6 := NewHolder(path, nil) + err = h6.Open() + if err == nil { + h6.Close() + t.Fatalf("should have had blue-green verification fail on Holder.Open") + } + h6.Close() + } + } } diff --git a/util.go b/util.go index 0e243c55a..25ed3f826 100644 --- a/util.go +++ b/util.go @@ -20,8 +20,12 @@ import ( "fmt" "io/ioutil" "net" + "os" + "path/filepath" "reflect" "sort" + "strings" + "syscall" "unsafe" "github.com/pilosa/pilosa/v2/roaring" @@ -237,3 +241,119 @@ func fromInterval16(a []roaring.Interval16) []byte { } return (*[8192]byte)(unsafe.Pointer(&a[0]))[: len(a)*4 : len(a)*4] } + +// DiskUse reports the total bytes uses by all files under root +// that match requiredSuffix. requiredSuffix can be empty string. +// Space used by directories is not counted. +func DiskUse(root string, requiredSuffix string) (tot int, err error) { + if !DirExists(root) { + return -1, fmt.Errorf("listFilesUnderDir error: root directory '%v' not found", root) + } + + err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if info == nil { + panic(fmt.Sprintf("info was nil for path = '%v'", path)) + } + if info.IsDir() { + // skip the size of directories themselves, only summing files. + } else { + sz := info.Size() + if requiredSuffix == "" || strings.HasSuffix(path, requiredSuffix) { + tot += int(sz) + } + } + return nil + }) + return +} + +// rootDir must exist. Return the size in bytes of the largest sub-directory +// that has the required suffix. The largestSize is from DiskUse() called +// on the sub-dir. DiskUse only counts file size, nothing for directory inodes. +func SubdirLargestDirWithSuffix(rootDir, requiredDirSuffix string) (exists bool, largestSize int, err error) { + if !DirExists(rootDir) { + return false, -1, fmt.Errorf("SubdirExistsWithSuffix error: root directory '%v' not found", rootDir) + } + + err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if info == nil { + panic(fmt.Sprintf("info was nil for path = '%v'", path)) + } + + if info.IsDir() && strings.HasSuffix(path, requiredDirSuffix) { + exists = true + size, err := DiskUse(path, "") + if err != nil { + // disk error? report it + return err + } + if size > largestSize { + largestSize = size + } + } + return nil + }) + if err != nil { + return exists, -1, err + } + return +} + +// called by Holder.hasRoaringData() +func roaringFragmentHasData(path string, index, field, view string, shard uint64) (hasData bool, err error) { + + var info roaring.BitmapInfo + _ = info + var f *os.File + f, err = os.Open(path) + if err != nil { + return + } + + var fi os.FileInfo + fi, err = f.Stat() + 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 { + err = errors.Wrap(err, "roaringFragmentHasData: munmap failed") + } + err = f.Close() + if err != nil { + err = errors.Wrap(err, "roaringFragmentHasData f.Close() in defer") + } + }() + + // 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 + } + + if info.ContainerCount > 0 { + return true, nil + } + if info.Ops > 0 { + return true, nil + } + + citer, found := rbm.Containers.Iterator(0) + _ = found + + for citer.Next() { + return true, nil + } + + return +} diff --git a/utils_internal_test.go b/utils_internal_test.go index 15d3ac9ee..91f2c0745 100644 --- a/utils_internal_test.go +++ b/utils_internal_test.go @@ -167,9 +167,9 @@ func (t *ClusterCluster) SetBit(index, field string, rowID, colID uint64, x *tim } if err := func() error { - idx := c.holder.indexes[f.index] + idx := c.holder.Index(f.index) shard := colID / ShardWidth - tx := idx.Txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) + tx := idx.holder.txf.NewTx(Txo{Write: writable, Index: idx, Shard: shard}) if tx != nil { defer tx.Rollback() } @@ -488,11 +488,11 @@ func (t *ClusterCluster) FollowResizeInstruction(instr *ResizeInstruction) error // there will be two -badgerdb directories/databases, we need to copy // from src to dest the fragment. This simulates sending the fragment over the network. srcIdx := srcCluster.holder.Index(src.Index) - srctx := srcIdx.Txf.NewTx(Txo{Write: !writable, Index: srcIdx, Fragment: srcFragment, Shard: srcFragment.shard}) + srctx := srcIdx.holder.txf.NewTx(Txo{Write: !writable, Index: srcIdx, Fragment: srcFragment, Shard: srcFragment.shard}) destIdx := destCluster.holder.Index(src.Index) - desttx := destIdx.Txf.NewTx(Txo{Write: writable, Index: destIdx, Fragment: destFragment, Shard: destFragment.shard}) + desttx := destIdx.holder.txf.NewTx(Txo{Write: writable, Index: destIdx, Fragment: destFragment, Shard: destFragment.shard}) citer, _, err := srctx.ContainerIterator(src.Index, src.Field, src.View, src.Shard, 0) panicOn(err) diff --git a/view.go b/view.go index 1a10ac5b6..52c940986 100644 --- a/view.go +++ b/view.go @@ -173,7 +173,6 @@ var workQueue = make(chan struct{}, runtime.NumCPU()*2) // replaces v.openFragments() with Tx generic code. func (v *view) openFragmentsInTx() error { - // we think this is correct for dbpershard, but might be slower. TODO shards, err := DBPerShardGetShardsForIndex(v.idx, v.path) if err != nil { return errors.Wrap(err, "DBPerShardGetShardsForIndex()") @@ -368,15 +367,6 @@ func (v *view) notifyIfNewShard(shard uint64) { func (v *view) newFragment(path string, shard uint64) *fragment { - if v.holder != nil && v.idx != nil { - // A view must have its v.idx *Index registered with its holder. - // Otherwise TestField_AvailableShards crashes, as one example. - hIdx := v.holder.Index(v.idx.name) - if hIdx == nil && v.idx != nil { - v.holder.addIndexFromField(v.idx) - } - } - frag := newFragment(v.holder, path, v.index, v.field, v.name, shard, v.flags()) frag.CacheType = v.cacheType frag.CacheSize = v.cacheSize @@ -402,7 +392,7 @@ func (v *view) deleteFragment(shard uint64) error { idx := f.holder.Index(v.index) f.Close() - if err := idx.Txf.DeleteFragmentFromStore(f.index, f.field, f.view, f.shard, f); err != nil { + if err := idx.holder.txf.DeleteFragmentFromStore(f.index, f.field, f.view, f.shard, f); err != nil { return errors.Wrap(err, "DeleteFragment") } delete(v.fragments, shard) @@ -418,7 +408,7 @@ func (v *view) row(txOrig Tx, rowID uint64) (*Row, error) { tx := txOrig if NilInside(tx) { - tx = v.idx.Txf.NewTx(Txo{Write: !writable, Index: v.idx, Fragment: frag, Shard: frag.shard}) + tx = v.idx.holder.txf.NewTx(Txo{Write: !writable, Index: v.idx, Fragment: frag, Shard: frag.shard}) defer tx.Rollback() } @@ -445,7 +435,7 @@ func (v *view) setBit(txOrig Tx, rowID, columnID uint64) (changed bool, err erro tx := txOrig if NilInside(tx) { - tx = v.idx.Txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) + tx = v.idx.holder.txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) defer func() { if err == nil { panicOn(tx.Commit()) @@ -467,7 +457,7 @@ func (v *view) clearBit(txOrig Tx, rowID, columnID uint64) (changed bool, err er tx := txOrig if NilInside(tx) { - tx = v.idx.Txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) + tx = v.idx.holder.txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) defer func() { if err == nil { panicOn(tx.Commit()) @@ -490,7 +480,7 @@ func (v *view) value(txOrig Tx, columnID uint64, bitDepth uint) (value int64, ex tx := txOrig if NilInside(tx) { - tx = frag.idx.Txf.NewTx(Txo{Write: !writable, Index: frag.idx, Fragment: frag, Shard: frag.shard}) + tx = frag.idx.holder.txf.NewTx(Txo{Write: !writable, Index: frag.idx, Fragment: frag, Shard: frag.shard}) defer tx.Rollback() } @@ -507,7 +497,7 @@ func (v *view) setValue(txOrig Tx, columnID uint64, bitDepth uint, value int64) tx := txOrig if NilInside(tx) { - tx = v.idx.Txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) + tx = v.idx.holder.txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) defer func() { if err == nil { panicOn(tx.Commit()) @@ -530,7 +520,7 @@ func (v *view) clearValue(txOrig Tx, columnID uint64, bitDepth uint, value int64 tx := txOrig if NilInside(tx) { - tx = v.idx.Txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) + tx = v.idx.holder.txf.NewTx(Txo{Write: writable, Index: v.idx, Fragment: frag, Shard: shard}) defer func() { if err == nil { panicOn(tx.Commit()) diff --git a/view_internal_test.go b/view_internal_test.go index 759167458..9076fb788 100644 --- a/view_internal_test.go +++ b/view_internal_test.go @@ -36,6 +36,7 @@ func mustOpenView(tb testing.TB, index, field, name string) *view { h := NewHolder(path, nil) // h needs an *Index so we can call h.Index() and get Index.Txf, in TestView_DeleteFragment + idx, err := h.createIndex(index, IndexOptions{}) testhook.Cleanup(tb, func() { h.Close()