mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
1028 lines
30 KiB
Go
1028 lines
30 KiB
Go
// 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"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/molecula/featurebase/v2/roaring"
|
|
txkey "github.com/molecula/featurebase/v2/short_txkey"
|
|
. "github.com/molecula/featurebase/v2/vprint"
|
|
)
|
|
|
|
// blueGreenTx runs two Tx together and notices differences in their output.
|
|
// By convention, the 'b' Tx is the output that is returned to caller.
|
|
//
|
|
// Warning: DATA RACES are expected if RoaringTx is one side of the Tx pair.
|
|
// 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 with RoaringTx
|
|
// on one arm.
|
|
//
|
|
// Note: using the dbshard.go DBShard.mut RWMutex to begin and end
|
|
// both the A and B transactions atomically, we support a single importer and
|
|
// lots of readers running under blue-green transactions. Two writers a.k.a. two
|
|
// github ingests at once will deadlock eventually, but I think that may be asking
|
|
// for more than we want to test under blue-green, as it would require a bunch of
|
|
// test-only internal executor logic that could mess with the production path.
|
|
// So, for now, a limitation on blue green tests is that they be single
|
|
// writer/single importer going at once.
|
|
//
|
|
type blueGreenTx struct {
|
|
a Tx
|
|
b Tx // b's output is returned
|
|
|
|
o Txo
|
|
as string
|
|
bs string
|
|
|
|
types []txtype
|
|
hasRoaring bool
|
|
|
|
// roaring will not create as many Tx (they are
|
|
// psuedo Tx anyway), espcially when deleting
|
|
// files. Return the non-roaring Sn if
|
|
// possible, by referencing useSnA.
|
|
useSnA bool
|
|
|
|
idx *Index
|
|
|
|
checker blueGreenChecker
|
|
mu sync.Mutex
|
|
rollbackOrCommitDone bool
|
|
|
|
txf *TxFactory
|
|
|
|
short bool // short Dump or long
|
|
|
|
FullDump bool // else quieter, don't attemp Dump() if false.
|
|
}
|
|
|
|
// blueGreenRegistry is used to force checking of (read) transactions
|
|
// before writes happen, if roaring is on one of the A/B branches.
|
|
// Because roaring won't have an MVCC view of the world. Writes to
|
|
// 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
|
|
types []txtype
|
|
hasRoaring bool
|
|
}
|
|
|
|
// 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),
|
|
types: types,
|
|
hasRoaring: hasRoaring,
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
if c.useSnA {
|
|
b.m[c.a.Sn()] = c
|
|
} else {
|
|
b.m[c.b.Sn()] = c
|
|
}
|
|
}
|
|
|
|
func (b *blueGreenRegistry) finishedTx(tx *blueGreenTx) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
sn := tx.Sn()
|
|
delete(b.m, sn)
|
|
//vv("blueGreenRegistry deleted _sn_ %v", sn)
|
|
|
|
// Note that a tx.o.dbs.Cleanup(tx) call should not be needed,
|
|
// because the individual tx will call cleanup themselves.
|
|
}
|
|
|
|
func (b *blueGreenRegistry) Close() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if len(b.m) > 0 {
|
|
PanicOn(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 {
|
|
as := a.Type()
|
|
bs := b.Type()
|
|
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
|
|
txf.blueGreenReg.add(c)
|
|
return c
|
|
}
|
|
|
|
var _ Tx = (*blueGreenTx)(nil)
|
|
|
|
func (c *blueGreenTx) Type() string {
|
|
return c.a.Type() + "_" + c.b.Type()
|
|
}
|
|
|
|
var blueGreenTxDumpMut sync.Mutex
|
|
|
|
func (c *blueGreenTx) Dump(short bool, shard uint64) {
|
|
|
|
if !c.FullDump {
|
|
return
|
|
}
|
|
|
|
blueGreenTxDumpMut.Lock()
|
|
defer blueGreenTxDumpMut.Unlock()
|
|
fmt.Printf("%v blueGreenTx.Dump ============== \n", FileLine(2))
|
|
fmt.Printf("A(%v) Dump:\n", c.as)
|
|
c.a.Dump(short, shard)
|
|
fmt.Printf("B(%v) Dump:\n", c.bs)
|
|
c.b.Dump(short, shard)
|
|
|
|
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 {
|
|
PanicOn(fmt.Sprintf("Readonly difference, a=%v, but b =%v", a, b))
|
|
}
|
|
return b
|
|
}
|
|
|
|
// for now we just return B's list, since this is involved in
|
|
// holder Open which can happen before any blue-green is done;
|
|
// in fact this is instrumental in setting up the sync from
|
|
// green to blue.
|
|
func (c *blueGreenTx) GetSortedFieldViewList(idx *Index, shard uint64) (fvB []txkey.FieldView, errB error) {
|
|
fvB, errB = c.b.GetSortedFieldViewList(idx, shard)
|
|
return
|
|
}
|
|
|
|
func (c *blueGreenTx) NewTxIterator(index, field, view string, shard uint64) *roaring.Iterator {
|
|
c.checker.see(index, field, view, shard)
|
|
// can't really do simultaneous iteration on A and B, so punt and
|
|
// just give back B.
|
|
return c.b.NewTxIterator(index, field, view, shard)
|
|
}
|
|
|
|
func (c *blueGreenTx) Pointer() string {
|
|
return fmt.Sprintf("%p", c)
|
|
}
|
|
|
|
func (c *blueGreenTx) IncrementOpN(index, field, view string, shard uint64, changedN int) {
|
|
c.checker.see(index, field, view, shard)
|
|
c.a.IncrementOpN(index, field, view, shard, changedN)
|
|
c.b.IncrementOpN(index, field, view, shard, changedN)
|
|
}
|
|
|
|
// 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)
|
|
bIter, bFound, bErr := c.b.ContainerIterator(index, field, view, shard, 0)
|
|
if aErr == nil || aIter != nil {
|
|
defer aIter.Close()
|
|
}
|
|
if bErr == nil || bIter != nil {
|
|
defer bIter.Close()
|
|
}
|
|
|
|
if aFound != bFound {
|
|
c.Dump(c.short, shard)
|
|
PanicOn(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.short, shard)
|
|
PanicOn(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.short, shard)
|
|
PanicOn(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.short, shard)
|
|
PanicOn(fmt.Sprintf("compareTxState[%v]: B(%v) reported err %v at %v; but A(%v) did not", here, c.bs, bErr, c.as, Stack()))
|
|
}
|
|
}
|
|
for aIter.Next() {
|
|
aKey, aValue := aIter.Value()
|
|
|
|
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.short, shard)
|
|
PanicOn(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.short, shard)
|
|
PanicOn(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.short, shard)
|
|
//vv("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at Stack=%v", here, aKey, err, c.as, c.bs, Stack())
|
|
PanicOn(fmt.Sprintf("compareTxState[%v]: key %v differs: %v; A=%v; B=%v; at Stack=%v", here, aKey, err, c.as, c.bs, Stack()))
|
|
}
|
|
//vv("successfully matched aKey(%v)='%v' and bKey(%v)='%v'", c.as, aKey, c.bs, bKey)
|
|
}
|
|
// 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.short, shard)
|
|
bKey, _ := bIter.Value()
|
|
PanicOn(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()))
|
|
}
|
|
//vv("done without problem. compareTxState here = '%v', _sn_ %v gid=%v", here, c.Sn(), curGID())
|
|
}
|
|
|
|
func (c *blueGreenTx) checkDatabase() {
|
|
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
|
|
// writer (for LMDB/RBF), we should always have the same
|
|
// 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
|
|
// 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.
|
|
return
|
|
}
|
|
|
|
c.checker.mu.Lock()
|
|
defer c.checker.mu.Unlock()
|
|
if c.checker.checkDone {
|
|
return // idemopotent. checkDatabase can be called twice. Only the first does the checks.
|
|
}
|
|
c.checker.checkDone = true
|
|
|
|
for index, fields := range c.checker.seen() {
|
|
for field, views := range fields {
|
|
for view, shards := range views {
|
|
for shard := range shards {
|
|
c.compareTxState(index, field, view, shard)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *blueGreenTx) IsDone() bool {
|
|
return c.b.IsDone()
|
|
}
|
|
|
|
func (c *blueGreenTx) Rollback() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.rollbackOrCommitDone {
|
|
return // avoid using discarded tx for Dump, which will PanicOn.
|
|
}
|
|
c.rollbackOrCommitDone = true
|
|
|
|
if c.o.Write {
|
|
c.checkDatabase()
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Rollback() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
//vv("blueGreenTx.Rollback() about to call (%v) a.Rollback()", c.as)
|
|
c.a.Rollback()
|
|
//vv("blueGreenTx.Rollback() about to call (%v) b.Rollback()", c.bs)
|
|
c.b.Rollback()
|
|
//vv("blueGreenTx.Rollback() done. bgtx p=%p", c)
|
|
|
|
c.txf.blueGreenReg.finishedTx(c)
|
|
}
|
|
|
|
func (c *blueGreenTx) Commit() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.rollbackOrCommitDone {
|
|
return nil
|
|
}
|
|
//vv("blueGreenTx.Commit() called. bgtx p=%p", c)
|
|
c.rollbackOrCommitDone = true
|
|
if c.o.Write {
|
|
if !c.o.blueGreenOff {
|
|
c.checkDatabase()
|
|
}
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Commit() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
errA := c.a.Commit()
|
|
_ = errA
|
|
errB := c.b.Commit()
|
|
|
|
compareErrors(errA, errB)
|
|
c.txf.blueGreenReg.finishedTx(c)
|
|
return errB
|
|
}
|
|
|
|
func (c *blueGreenTx) RoaringBitmap(index, field, view string, shard uint64) (*roaring.Bitmap, error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see RoaringBitmap() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.RoaringBitmap(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.RoaringBitmap(index, field, view, shard)
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
|
|
slcA := a.Slice()
|
|
slcB := b.Slice()
|
|
if !reflect.DeepEqual(slcA, slcB) {
|
|
PanicOn("blueGreenTx.RoaringBitmap() returning different roaring.Bitmaps!")
|
|
}
|
|
}
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) Container(index, field, view string, shard uint64, key uint64) (ct *roaring.Container, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Container() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Container(index, field, view, shard, key)
|
|
b, errB := c.b.Container(index, field, view, shard, key)
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
err = a.BitwiseCompare(b)
|
|
PanicOn(err)
|
|
}
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) PutContainer(index, field, view string, shard uint64, key uint64, rc *roaring.Container) error {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see PutContainer() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
errA := c.a.PutContainer(index, field, view, shard, key, rc)
|
|
errB := c.b.PutContainer(index, field, view, shard, key, rc)
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return errB
|
|
}
|
|
|
|
func (c *blueGreenTx) ImportRoaringBits(index, field, view string, shard uint64, rit roaring.RoaringIterator, clear bool, log bool, rowSize uint64, data []byte) (changed int, rowSet map[uint64]int, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
|
|
// these are the first port of call for debugging, so we leave them in.
|
|
// ================== begin save comments.
|
|
//c.checkDatabase()
|
|
////vv("got past database check at TOP of ImportRoaringBits")
|
|
//c.Dump(c.short, shard)
|
|
////vv("done with top dump; clear=%v", clear)
|
|
// ================== end save comments.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see ImportRoaringBits() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
|
|
// remember where the iterator started, so we can replay it a second time.
|
|
rit2 := rit.Clone()
|
|
PanicOn(err)
|
|
|
|
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 !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 {
|
|
PanicOn(fmt.Sprintf("changedA = %v, but changedB = %v", changedA, changedB))
|
|
}
|
|
if len(rowSetA) != len(rowSetB) {
|
|
PanicOn(fmt.Sprintf("rowSetA = %#v, but rowSetB = %#v", rowSetA, rowSetB))
|
|
}
|
|
for k, va := range rowSetA {
|
|
vb, ok := rowSetB[k]
|
|
if !ok {
|
|
PanicOn(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 {
|
|
PanicOn(fmt.Sprintf("diff on key '%v', rowSetA has value '%v', but rowSetB has value '%v'", k, va, vb))
|
|
}
|
|
}
|
|
}
|
|
compareErrors(errA, errB)
|
|
c.checkDatabase()
|
|
}
|
|
return changedB, rowSetB, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) RemoveContainer(index, field, view string, shard uint64, key uint64) error {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see RemoveContainer() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
errA := c.a.RemoveContainer(index, field, view, shard, key)
|
|
errB := c.b.RemoveContainer(index, field, view, shard, key)
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return errB
|
|
}
|
|
|
|
func (c *blueGreenTx) UseRowCache() bool {
|
|
// avoid cross-talk between our two implementations
|
|
// by never allowing either to use the row cache.
|
|
return false
|
|
}
|
|
|
|
var _ = (&blueGreenTx{}).isIn // happy linter
|
|
|
|
func (c *blueGreenTx) isIn(index, field, view string, shard uint64, ckey uint64) (r []bool) {
|
|
r = make([]bool, 2)
|
|
inA, errA := c.a.Contains(index, field, view, shard, ckey)
|
|
PanicOn(errA)
|
|
inB, errB := c.b.Contains(index, field, view, shard, ckey)
|
|
PanicOn(errB)
|
|
r[0] = inA
|
|
r[1] = inB
|
|
return
|
|
}
|
|
|
|
func (c *blueGreenTx) Add(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
//vv("blueGreenTx) Add(index=%v, field=%v, view=%v, shard=%v", index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Add() PanicOn '%v' for index='%v', field='%v', view='%v', shard='%v' at '%v'", r, index, field, view, shard, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
|
|
// must copy a before calling Add(), since RoaringTx.Add() uses roaring.DirectAddN()
|
|
// which modifies the input array a.
|
|
a2 := make([]uint64, len(a))
|
|
copy(a2, a)
|
|
|
|
ach, errA := c.a.Add(index, field, view, shard, a...)
|
|
_, _ = ach, errA
|
|
|
|
bch, errB := c.b.Add(index, field, view, shard, a2...)
|
|
|
|
if !c.o.blueGreenOff {
|
|
|
|
if ach != bch {
|
|
PanicOn(fmt.Sprintf("Add() difference, ach=%v, but bch=%v; errA='%v'; errB='%v'", ach, bch, errA, errB))
|
|
}
|
|
compareErrors(errA, errB)
|
|
}
|
|
|
|
return bch, errB
|
|
}
|
|
|
|
func compareErrors(errA, errB error) {
|
|
switch {
|
|
case errA == nil && errB == nil:
|
|
// OK
|
|
case errA == nil:
|
|
PanicOn(fmt.Sprintf("errA is nil, but errB = %#v", errB))
|
|
case errB == nil:
|
|
PanicOn(fmt.Sprintf("errB is nil, but errA = %#v", errA))
|
|
default:
|
|
ae := errA.Error()
|
|
be := errB.Error()
|
|
if ae != be {
|
|
PanicOn(fmt.Sprintf("errA is '%v', but errB is '%v'", ae, be))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *blueGreenTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Remove() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
ach, errA := c.a.Remove(index, field, view, shard, a...)
|
|
_, _ = ach, errA
|
|
bch, errB := c.b.Remove(index, field, view, shard, a...)
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return bch, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) Contains(index, field, view string, shard uint64, key uint64) (exists bool, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Contains() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
ax, errA := c.a.Contains(index, field, view, shard, key)
|
|
_, _ = ax, errA
|
|
bx, errB := c.b.Contains(index, field, view, shard, key)
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return bx, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) ContainerIterator(index, field, view string, shard uint64, firstRoaringContainerKey uint64) (citer roaring.ContainerIterator, found bool, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see ContainerIterator() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
|
|
ait, afound, errA := c.a.ContainerIterator(index, field, view, shard, firstRoaringContainerKey)
|
|
_, _, _ = ait, afound, errA
|
|
|
|
bit, bfound, errB := c.b.ContainerIterator(index, field, view, shard, firstRoaringContainerKey)
|
|
|
|
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 {
|
|
ait.Close()
|
|
}
|
|
if bit != nil {
|
|
bit.Close()
|
|
}
|
|
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
|
|
}
|
|
|
|
func (tx *blueGreenTx) GetFieldSizeBytes(index, field string) (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func NewBlueGreenIterator(tx *blueGreenTx, ait, bit roaring.ContainerIterator) *blueGreenIterator {
|
|
return &blueGreenIterator{
|
|
tx: tx,
|
|
as: tx.as,
|
|
bs: tx.bs,
|
|
ait: ait,
|
|
bit: bit,
|
|
}
|
|
}
|
|
|
|
type blueGreenIterator struct {
|
|
tx *blueGreenTx
|
|
as string
|
|
bs string
|
|
|
|
ait roaring.ContainerIterator
|
|
bit roaring.ContainerIterator
|
|
}
|
|
|
|
func (bgi *blueGreenIterator) Next() bool {
|
|
na := bgi.ait.Next()
|
|
nb := bgi.bit.Next()
|
|
if na != nb {
|
|
PanicOn(fmt.Sprintf("na=%v(%v) != nb(%v)=%v", na, bgi.as, bgi.bs, nb))
|
|
}
|
|
return nb
|
|
}
|
|
|
|
func (bgi *blueGreenIterator) Value() (uint64, *roaring.Container) {
|
|
ka, ca := bgi.ait.Value()
|
|
kb, cb := bgi.bit.Value()
|
|
|
|
if !bgi.tx.o.blueGreenOff {
|
|
if ka != kb {
|
|
PanicOn(fmt.Sprintf("ka=%v != kb=%v", ka, kb))
|
|
}
|
|
err := ca.BitwiseCompare(cb)
|
|
PanicOn(err)
|
|
}
|
|
return kb, cb
|
|
}
|
|
func (bgi *blueGreenIterator) Close() {
|
|
bgi.ait.Close()
|
|
bgi.bit.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, 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 {
|
|
AlwaysPrintf("see ForEach() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
return c.b.ForEach(index, field, view, shard, fn)
|
|
|
|
}
|
|
|
|
// ForEachRange cannot change the database, and we also can't control
|
|
// the side effects of the fn() calls. So we only pass through to B, not A.
|
|
// No checker.see() is needed as well, because we are read-only.
|
|
func (c *blueGreenTx) ForEachRange(index, field, view string, shard uint64, start, end uint64, fn func(uint64) error) error {
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see ForEachRange() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
|
|
// calling fn will have side effects; can only call it the right number of times.
|
|
// so can't do this.
|
|
// errA := c.a.ForEachRange(index, field, view, shard, start, end, fn)
|
|
return c.b.ForEachRange(index, field, view, shard, start, end, fn)
|
|
}
|
|
|
|
func (c *blueGreenTx) Count(index, field, view string, shard uint64) (uint64, error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Count() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Count(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.Count(index, field, view, shard)
|
|
_, _ = b, errB
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) Max(index, field, view string, shard uint64) (uint64, error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Max() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Max(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.Max(index, field, view, shard)
|
|
_, _ = b, errB
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) Min(index, field, view string, shard uint64) (uint64, bool, error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Min() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
amin, afound, errA := c.a.Min(index, field, view, shard)
|
|
_, _, _ = amin, afound, errA
|
|
bmin, bfound, errB := c.b.Min(index, field, view, shard)
|
|
_, _, _ = bmin, bfound, errB
|
|
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return bmin, bfound, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) UnionInPlace(index, field, view string, shard uint64, others ...*roaring.Bitmap) error {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see UnionInPlace() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
errA := c.a.UnionInPlace(index, field, view, shard, others...)
|
|
errB := c.b.UnionInPlace(index, field, view, shard, others...)
|
|
if !c.o.blueGreenOff {
|
|
compareErrors(errA, errB)
|
|
}
|
|
return errB
|
|
}
|
|
|
|
func (c *blueGreenTx) CountRange(index, field, view string, shard uint64, start, end uint64) (n uint64, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
c.Dump(c.short, shard)
|
|
AlwaysPrintf("see CountRange() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.CountRange(index, field, view, shard, start, end)
|
|
b, errB := c.b.CountRange(index, field, view, shard, start, end)
|
|
|
|
if !c.o.blueGreenOff {
|
|
if a != b {
|
|
PanicOn(fmt.Sprintf("a(%v) = %v, but b(%v) = %v", c.as, a, c.bs, b))
|
|
}
|
|
|
|
compareErrors(errA, errB)
|
|
}
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) OffsetRange(index, field, view string, shard, offset, start, end uint64) (other *roaring.Bitmap, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see OffsetRange() on _sn_ %v, PanicOn '%v' at '%v'", c.Sn(), r, Stack())
|
|
PanicOn(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)
|
|
|
|
if !c.o.blueGreenOff {
|
|
|
|
err = roaringBitmapDiff(a, b)
|
|
if err != nil {
|
|
c.Dump(false, shard)
|
|
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)
|
|
}
|
|
|
|
return b, errB
|
|
}
|
|
|
|
func (c *blueGreenTx) RoaringBitmapReader(index, field, view string, shard uint64, fragmentPathForRoaring string) (r io.ReadCloser, sz int64, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
c.Dump(c.short, shard)
|
|
AlwaysPrintf("see RoaringBitmapReader() PanicOn '%v' at '%v'", r, Stack())
|
|
PanicOn(r)
|
|
}
|
|
}()
|
|
|
|
rcA, szA, errA := c.a.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring)
|
|
rcB, szB, errB := c.b.RoaringBitmapReader(index, field, view, shard, fragmentPathForRoaring)
|
|
|
|
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 // !c.hasRoaring
|
|
if c.o.blueGreenOff {
|
|
sizeMustMatch = false
|
|
}
|
|
if sizeMustMatch {
|
|
if szA != szB {
|
|
PanicOn(fmt.Sprintf("szA(%v) = %v, but szB(%v) = %v; fragmentPathForRoaring='%v'", c.as, szA, c.bs, szB, fragmentPathForRoaring))
|
|
}
|
|
return &MultiReaderB{a: rcA, b: rcB}, szB, errB
|
|
} else {
|
|
// one db won't get data if we do
|
|
//return &MultiReaderB{a: rcA, b: rcB, allowSizeVariation: true}, szB, errB
|
|
_, _ = szA, errA
|
|
rcA.Close()
|
|
return rcB, szB, errB
|
|
}
|
|
}
|
|
|
|
func (c *blueGreenTx) Group() *TxGroup {
|
|
return c.b.Group()
|
|
}
|
|
|
|
func (c *blueGreenTx) Options() Txo {
|
|
return c.b.Options()
|
|
}
|
|
|
|
// Sn retreives the serial number of the Tx.
|
|
func (c *blueGreenTx) Sn() int64 {
|
|
asn := c.a.Sn()
|
|
bsn := c.b.Sn()
|
|
|
|
if c.useSnA {
|
|
return asn
|
|
}
|
|
return bsn
|
|
}
|
|
|
|
func (c *blueGreenTx) ApplyFilter(index, field, view string, shard uint64, ckey uint64, filter roaring.BitmapFilter) (err error) {
|
|
return GenericApplyFilter(c, index, field, view, shard, ckey, filter)
|
|
}
|
|
|
|
// MultiReaderB is returned by RoaringBitmapReader. It verifies
|
|
// that identical byte streams are read from its two members.
|
|
type MultiReaderB struct {
|
|
a io.ReadCloser
|
|
b io.ReadCloser
|
|
|
|
allowSizeVariation bool
|
|
}
|
|
|
|
// Read implements the standard io.Reader method. It panics
|
|
// if "a" and "b" have even one byte different in their reads.
|
|
func (m *MultiReaderB) Read(p []byte) (nB int, errB error) {
|
|
nB, errB = m.b.Read(p)
|
|
p2 := make([]byte, nB)
|
|
|
|
// read (and discard after comparing for equality) the exact same amount from A.
|
|
|
|
// ReadAtLeast reads from r into buf until it has read at least
|
|
// min bytes. It returns the number of bytes copied and an error
|
|
// if fewer bytes were read. The error is EOF only if no bytes
|
|
// were read. If an EOF happens after reading fewer than min bytes,
|
|
// ReadAtLeast returns ErrUnexpectedEOF. If min is greater than
|
|
// the length of buf, ReadAtLeast returns ErrShortBuffer. On
|
|
// return, n >= min if and only if err == nil. If r returns
|
|
// an error having read at least min bytes, the error is dropped.
|
|
nA, errA := io.ReadAtLeast(m.a, p2, nB)
|
|
|
|
if !m.allowSizeVariation {
|
|
if errA == io.ErrUnexpectedEOF {
|
|
PanicOn(fmt.Sprintf("MultiReaderB got ErrUnexpectedEOF: read %v bytes from B, but could only read %v bytes for A", nB, nA))
|
|
}
|
|
if nA != nB {
|
|
PanicOn(fmt.Sprintf("MultiReaderB read %v bytes from B, but could only read %v bytes for A", nB, nA))
|
|
}
|
|
cmp := bytes.Compare(p[:nB], p2[:nB])
|
|
if cmp != 0 {
|
|
PanicOn(fmt.Sprintf("MultiReaderB reads p and p2 (cmp= %v) differed.", cmp))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (m *MultiReaderB) Close() error {
|
|
m.a.Close()
|
|
return m.b.Close()
|
|
}
|
|
|
|
// blueGreenChecker is used
|
|
type blueGreenChecker struct {
|
|
visited map[string]map[string]map[string]map[uint64]struct{}
|
|
|
|
c *blueGreenTx
|
|
|
|
// lock mu when using visited.
|
|
// otherwise concurrent map writes on TestAPI_Import/RowIDColumnKey
|
|
mu sync.Mutex
|
|
|
|
checkDone bool
|
|
}
|
|
|
|
// see would mark a thing as seen.
|
|
func (b *blueGreenChecker) see(index, field, view string, shard uint64) {
|
|
// keep this next Printf. Useful to see the sequence of Tx operations.
|
|
//fmt.Printf("blueGreenTx.%v on index='%v' shard=%v\n", Caller(1), index, shard)
|
|
|
|
if !b.c.o.Write {
|
|
return
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
if b.visited == nil {
|
|
b.visited = make(map[string]map[string]map[string]map[uint64]struct{})
|
|
}
|
|
var visitedIdx map[string]map[string]map[uint64]struct{}
|
|
var visitedField map[string]map[uint64]struct{}
|
|
var visitedView map[uint64]struct{}
|
|
|
|
if visitedIdx = b.visited[index]; visitedIdx == nil {
|
|
visitedIdx = make(map[string]map[string]map[uint64]struct{})
|
|
b.visited[index] = visitedIdx
|
|
}
|
|
if visitedField = visitedIdx[field]; visitedField == nil {
|
|
visitedField = make(map[string]map[uint64]struct{})
|
|
visitedIdx[field] = visitedField
|
|
}
|
|
if visitedView = visitedField[view]; visitedView == nil {
|
|
visitedView = make(map[uint64]struct{})
|
|
visitedField[view] = visitedView
|
|
}
|
|
visitedView[shard] = struct{}{}
|
|
}
|
|
|
|
// seen reports the things it has seen, exactly once so
|
|
// that Rollback can be called after Commit without repeating
|
|
// the check.
|
|
func (b *blueGreenChecker) seen() map[string]map[string]map[string]map[uint64]struct{} {
|
|
return b.visited
|
|
}
|