mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-05 16:15:56 +00:00
- correct string constants for txtype so that blue-green cleanup correctly detects when 2nd transaction in a pair has Committed and thus the blue-green RWMutex can be relased - test that txtype.String() is consistent with the corresponding string constants. - document in bluegreentx.go the current limitations of blue-green testing: only one github archive import (a single writing client) is supported by blue-green testing. Multiple importers will deadlock eventually on the DBShard.mut RWMutex. We could fix this by ordering the write locks and obtaining them in strictly increasing order (by shard number), but that would require alot of change to the executor and that would introduce more risk for a test-only pathway.
1058 lines
31 KiB
Go
1058 lines
31 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"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/pilosa/pilosa/v2/roaring"
|
|
)
|
|
|
|
// blueGreenTx runs two Tx together and notices differences in their output.
|
|
// By convention, the 'b' Tx is the output that is returned to caller.
|
|
//
|
|
// 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 {
|
|
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 {
|
|
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 {
|
|
panic(fmt.Sprintf("Readonly difference, a=%v, but b =%v", a, b))
|
|
}
|
|
return b
|
|
}
|
|
|
|
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)
|
|
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.short, shard)
|
|
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.short, shard)
|
|
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.short, shard)
|
|
panic(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)
|
|
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.short, shard)
|
|
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.short, shard)
|
|
//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()))
|
|
}
|
|
//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()
|
|
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()))
|
|
}
|
|
//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 panic.
|
|
}
|
|
c.rollbackOrCommitDone = true
|
|
|
|
if c.o.Write {
|
|
c.checkDatabase()
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Rollback() panic '%v' at '%v'", r, stack())
|
|
panic(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() panic '%v' at '%v'", r, stack())
|
|
panic(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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.RoaringBitmap(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.RoaringBitmap(index, field, view, shard)
|
|
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!")
|
|
}
|
|
}
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Container(index, field, view, shard, key)
|
|
b, errB := c.b.Container(index, field, view, shard, key)
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
errA := c.a.PutContainer(index, field, view, shard, key, rc)
|
|
errB := c.b.PutContainer(index, field, view, shard, key, rc)
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(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 {
|
|
panic(fmt.Sprintf("changedA = %v, but changedB = %v", changedA, changedB))
|
|
}
|
|
if len(rowSetA) != len(rowSetB) {
|
|
panic(fmt.Sprintf("rowSetA = %#v, but rowSetB = %#v", rowSetA, rowSetB))
|
|
}
|
|
for k, va := range rowSetA {
|
|
vb, ok := rowSetB[k]
|
|
if !ok {
|
|
panic(fmt.Sprintf("diff on key '%v': present in rowSetA, but not in rowSet B. rowSetA = %#v, but rowSetB = %#v", k, rowSetA, rowSetB))
|
|
}
|
|
if va != vb {
|
|
panic(fmt.Sprintf("diff on key '%v', rowSetA has value '%v', but rowSetB has value '%v'", k, va, vb))
|
|
}
|
|
}
|
|
}
|
|
compareErrors(errA, errB)
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
errA := c.a.RemoveContainer(index, field, view, shard, key)
|
|
errB := c.b.RemoveContainer(index, field, view, shard, key)
|
|
|
|
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, batched bool, a ...uint64) (changeCount int, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
//vv("blueGreenTx) Add(index=%v, field=%v, view=%v, shard=%v", index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Add() panic '%v' for index='%v', field='%v', view='%v', shard='%v' at '%v'", r, index, field, view, shard, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
|
|
// must copy a before calling Add(), since RoaringTx.Add() uses roaring.DirectAddN()
|
|
// which modifies the input array a.
|
|
a2 := make([]uint64, len(a))
|
|
copy(a2, a)
|
|
|
|
ach, errA := c.a.Add(index, field, view, shard, batched, a...)
|
|
_, _ = ach, errA
|
|
|
|
bch, errB := c.b.Add(index, field, view, shard, batched, a2...)
|
|
|
|
if !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)
|
|
}
|
|
|
|
return bch, errB
|
|
}
|
|
|
|
func compareErrors(errA, errB error) {
|
|
switch {
|
|
case errA == nil && errB == nil:
|
|
// OK
|
|
case errA == nil:
|
|
panic(fmt.Sprintf("errA is nil, but errB = %#v", errB))
|
|
case errB == nil:
|
|
panic(fmt.Sprintf("errB is nil, but errA = %#v", errA))
|
|
default:
|
|
ae := errA.Error()
|
|
be := errB.Error()
|
|
if ae != be {
|
|
panic(fmt.Sprintf("errA is '%v', but errB is '%v'", ae, be))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *blueGreenTx) Remove(index, field, view string, shard uint64, a ...uint64) (changeCount int, err error) {
|
|
c.checker.see(index, field, view, shard)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
AlwaysPrintf("see Remove() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
ach, errA := c.a.Remove(index, field, view, shard, a...)
|
|
_, _ = ach, errA
|
|
bch, errB := c.b.Remove(index, field, view, shard, a...)
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
ax, errA := c.a.Contains(index, field, view, shard, key)
|
|
_, _ = ax, errA
|
|
bx, errB := c.b.Contains(index, field, view, shard, key)
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(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 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 {
|
|
panic(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 {
|
|
panic(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() panic '%v' at '%v'", r, stack())
|
|
panic(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() panic '%v' at '%v'", r, stack())
|
|
panic(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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Count(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.Count(index, field, view, shard)
|
|
_, _ = b, errB
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.Max(index, field, view, shard)
|
|
_, _ = a, errA
|
|
b, errB := c.b.Max(index, field, view, shard)
|
|
_, _ = b, errB
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
amin, afound, errA := c.a.Min(index, field, view, shard)
|
|
_, _, _ = amin, afound, errA
|
|
bmin, bfound, errB := c.b.Min(index, field, view, shard)
|
|
_, _, _ = bmin, bfound, errB
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
errA := c.a.UnionInPlace(index, field, view, shard, others...)
|
|
errB := c.b.UnionInPlace(index, field, view, shard, others...)
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(r)
|
|
}
|
|
}()
|
|
a, errA := c.a.CountRange(index, field, view, shard, start, end)
|
|
b, errB := c.b.CountRange(index, field, view, shard, start, end)
|
|
|
|
if !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)
|
|
}
|
|
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, 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)
|
|
|
|
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() panic '%v' at '%v'", r, stack())
|
|
panic(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 {
|
|
panic(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) SliceOfShards(index, field, view, optionalViewPath string) (sliceOfShards []uint64, err error) {
|
|
// doesn't change state, so we don't really need see() call here. And we don't have a single shard for it.
|
|
//c.checker.see(index, field, view, shard) // don't have shard.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
c.Dump(c.short, math.MaxUint64)
|
|
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)
|
|
|
|
if !c.o.blueGreenOff {
|
|
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 !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(c.short, math.MaxUint64)
|
|
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))
|
|
}
|
|
}
|
|
return slcB, errB
|
|
}
|
|
|
|
// 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 {
|
|
panic(fmt.Sprintf("MultiReaderB got ErrUnexpectedEOF: read %v bytes from B, but could only read %v bytes for A", nB, nA))
|
|
}
|
|
if nA != nB {
|
|
panic(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 {
|
|
panic(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
|
|
}
|