featurebase/dbshard.go

1278 lines
33 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 (
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
rbfcfg "github.com/molecula/featurebase/v2/rbf/cfg"
txkey "github.com/molecula/featurebase/v2/short_txkey"
"github.com/molecula/featurebase/v2/storage"
"github.com/pkg/errors"
. "github.com/molecula/featurebase/v2/vprint" // nolint:staticcheck
)
var _ = sort.Sort
const (
// backendsDir is the default backends directory used to store the
// data for each backend.
backendsDir = "backends"
)
// types to support a database file per shard
type DBHolder struct {
Index map[string]*DBIndex
}
func NewDBHolder() *DBHolder {
return &DBHolder{
Index: make(map[string]*DBIndex),
}
}
type DBIndex struct {
Shard map[uint64]*DBShard
}
type DBWrapper interface {
NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error)
DeleteDBPath(dbs *DBShard) error
Close() error
DeleteFragment(index, field, view string, shard uint64, frag interface{}) error
DeleteField(index, field, fieldPath string) error
OpenListString() string
OpenSnList() (sns []int64)
Path() string
HasData() (has bool, err error)
SetHolder(h *Holder)
//needed for restore
CloseDB() error
OpenDB() error
}
type DBRegistry interface {
OpenDBWrapper(path string, doAllocZero bool, cfg *storage.Config) (DBWrapper, error)
}
type DBShard struct {
HolderPath string
Index string
Shard uint64
Open bool
// With RWMutex, the blue-green Tx can start and commit
// atomically.
mut sync.RWMutex
types []txtype
stypes []string
hasRoaring bool // if either of the types is roaringTxn
W []DBWrapper
ParentDBIndex *DBIndex
idx *Index
per *DBPerShard
useOpenList int
closed bool
isBlueGreen bool
}
func (dbs *DBShard) DeleteFragment(index, field, view string, shard uint64, frag interface{}) (err error) {
for _, w := range dbs.W {
err = w.DeleteFragment(index, field, view, shard, frag)
if err != nil {
return err
}
}
return
}
func (dbs *DBShard) DeleteFieldFromStore(index, field, fieldPath string) (err error) {
for _, w := range dbs.W {
err = w.DeleteField(index, field, fieldPath)
if err != nil {
return err
}
}
return
}
func (dbs *DBShard) Close() (err error) {
for _, w := range dbs.W {
err = w.Close()
if err != nil {
return err
}
}
dbs.closed = true
return
}
// 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.
}
//vv("gid %v top of DBShard %v Cleanup for tx.Sn = %v; dbs=%p; is 2nd: %v; type='%v'; dbs.stypes='%#v'", curGID(), dbs.Shard, tx.Sn(), dbs, tx.Type() == dbs.stypes[1], tx.Type(), dbs.stypes)
if !dbs.hasRoaring {
if dbs.isBlueGreen {
// only release on the 2nd Tx's cleanup
if tx.Type() == dbs.stypes[1] {
if tx.Readonly() {
dbs.mut.RUnlock()
//vv("gid %v released read-lock on shard %v", curGID(), dbs.Shard)
} else {
dbs.mut.Unlock()
//vv("gid %v released write-lock on shard %v", curGID(), dbs.Shard)
}
}
}
}
}
func (dbs *DBShard) NewTx(write bool, initialIndexName string, o Txo) (tx Tx, err error) {
if dbs.isBlueGreen {
// enforce only one writer at a time. The dbs.mut is held until
// the Tx finishes. This makes the two Tx in the blue-green Tx atomic.
if !dbs.hasRoaring {
if write {
//vv("shard %v about to write lock by gid %v; stack =\n%v", dbs.Shard, curGID(), stack())
dbs.mut.Lock()
//vv("shard %v was write locked by gid %v; stack =\n%v", dbs.Shard, curGID(), stack())
} else {
//vv("shard %v about to be read locked by gid %v; stack=\n%v", dbs.Shard, curGID(), stack())
dbs.mut.RLock()
//vv("shard %v was read locked by gid %v; stack=\n%v", dbs.Shard, curGID(), stack())
}
}
}
if o.dbs != dbs {
PanicOn(fmt.Sprintf("TxFactory.NewTx() should have set o.dbs(%p) to equal dbs(%p)", o.dbs, dbs))
}
if o.Shard != dbs.Shard {
PanicOn(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 {
tx, err = w.NewTx(write, initialIndexName, o)
if err != nil {
return nil, err
}
txns = append(txns, tx)
}
if len(txns) == 1 {
return
}
// blue green
tx, err = dbs.per.txf.newBlueGreenTx(txns[0], txns[1], o.Index, o), nil
//vv("dbshard returning blue-green tx sn %v", tx.Sn())
return
}
func (dbs *DBShard) DeleteDBPath() (err error) {
for _, w := range dbs.W {
err = w.DeleteDBPath(dbs)
if err != nil {
return err
}
}
return
}
type flatkey struct {
index string
shard uint64
}
type DBPerShard struct {
Mu sync.Mutex
HolderDir string
dbh *DBHolder
// just flat, not buried within the Node heirarchy.
// Easily see how many we have.
Flatmap map[flatkey]*DBShard
types []txtype
hasRoaring bool
txf *TxFactory
holder *Holder
// which of our types is not-roaring, since
// roaring doesn't keep a list of open Tx sn.
// or default to the 2nd.
useOpenList int
// cache the shards per index to avoid excessive
// directory scans of the index directory. Keep per
// txtype to allow blue-green migrate open to be fast too.
// Keep it up-to-date as we add shards to avoid doing
// a filesystem rescan on new shard creation.
//
// txtype -> index -> *shardSet
index2shards map[txtype]map[string]*shardSet
isBlueGreen bool
StorageConfig *storage.Config
RBFConfig *rbfcfg.Config
}
func newIndex2Shards() (r map[txtype]map[string]*shardSet) {
r = make(map[txtype]map[string]*shardSet)
return
}
type shardSet struct {
shardsMap map[uint64]bool
shardsVer int64 // increment with each change.
// give out readonly to repeated consumers if
// readonlyVer == shardsVer
readonly map[uint64]bool
readonlyVer int64
}
func (a *shardSet) unionInPlace(b *shardSet) {
shards := b.CloneMaybe()
for shard := range shards {
a.add(shard)
}
}
func (a *shardSet) equals(b *shardSet) bool {
if len(a.shardsMap) != len(b.shardsMap) {
return false
}
for shardInA := range a.shardsMap {
_, ok := b.shardsMap[shardInA]
if !ok {
return false
}
}
return true
}
func (a *shardSet) shards() []uint64 {
s := make([]uint64, 0, len(a.shardsMap))
for si := range a.shardsMap {
s = append(s, si)
}
return s
}
func (ss *shardSet) String() (r string) {
r = "["
for k := range ss.shardsMap {
r += fmt.Sprintf("%v, ", k)
}
r += "]"
return
}
func (ss *shardSet) add(shard uint64) {
_, already := ss.shardsMap[shard]
if !already {
ss.shardsMap[shard] = true
ss.shardsVer++
}
}
// CloneMaybe maintains a re-usable readonly version
// ss.shards that can be returned to multiple goroutine
// reads as it will never change. A copy is only made
// once for each change in the shard set.
func (ss *shardSet) CloneMaybe() map[uint64]bool {
if ss.readonlyVer == ss.shardsVer {
return ss.readonly
}
// readonlyVer is out of date.
// readonly needs update. We cannot
// modify the readonly map in place;
// must make a fully new copy here.
ss.readonly = make(map[uint64]bool)
for k, v := range ss.shardsMap {
ss.readonly[k] = v
}
ss.readonlyVer = ss.shardsVer
return ss.readonly
}
func newShardSet() *shardSet {
return &shardSet{
shardsMap: make(map[uint64]bool),
}
}
func newShardSetFromMap(m map[uint64]bool) *shardSet {
return &shardSet{
shardsMap: m,
shardsVer: 1,
}
}
// HasData returns true if the database has at least one key.
// For roaring it returns true if we a fragment 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.
if per.types[which] == roaringTxn {
return per.RoaringHasData() // this needs to be made accurate
}
for _, v := range per.Flatmap {
hasData, err = v.W[which].HasData()
if err != nil {
return
}
if hasData {
return
}
}
return
}
func (per *DBPerShard) RoaringHasData() (bool, error) {
idxs := per.holder.Indexes()
const requireData = true
for _, idx := range idxs {
shards, err := per.TypedDBPerShardGetShardsForIndex(roaringTxn, idx, "", requireData)
if err != nil {
return false, err
}
if len(shards) > 0 {
return true, nil
}
}
return false, nil
}
func (per *DBPerShard) ListOpenString() (r string) {
for _, v := range per.Flatmap {
r += v.HolderPath + " -> " + v.W[per.useOpenList].OpenListString() + "\n"
}
return
}
func (per *DBPerShard) LoadExistingDBs() (err error) {
idxs := per.holder.Indexes()
for _, idx := range idxs {
shardset, err := per.txf.GetShardsForIndex(idx, "", true)
if err != nil {
return err
}
for shard := range shardset {
_, err := per.GetDBShard(idx.name, shard, idx)
if err != nil {
return errors.Wrap(err, "DBPerShard.LoadExistingDBs GetDBShard()")
}
}
}
return
}
func (txf *TxFactory) NewDBPerShard(types []txtype, holderDir string, holder *Holder) (d *DBPerShard) {
if holder.cfg == nil || holder.cfg.RBFConfig == nil || holder.cfg.StorageConfig == nil {
PanicOn("must have holder.cfg.RBFConfig and holder.cfg.StorageConfig set here")
}
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{
types: types,
HolderDir: holderDir,
holder: holder,
dbh: NewDBHolder(),
Flatmap: make(map[flatkey]*DBShard),
txf: txf,
useOpenList: useOpenList,
hasRoaring: hasRoaring,
isBlueGreen: len(types) > 1,
index2shards: newIndex2Shards(),
StorageConfig: holder.cfg.StorageConfig,
RBFConfig: holder.cfg.RBFConfig,
}
return
}
func (per *DBPerShard) DeleteIndex(index string) (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
dbi, ok := per.dbh.Index[index]
if !ok {
// since we lazily make indexes upon use by a Tx now, we won't
// have an index for server/ TestQuerySQLUnary/test-20 to delete.
// Don't freak out. Just return nil.
return nil
}
for _, dbs := range dbi.Shard {
err = dbs.Close()
if err != nil {
return errors.Wrap(err, "DBPerShard.DeleteIndex dbs.Close()")
}
for _, ty := range per.types {
path := dbs.pathForType(ty)
err = os.RemoveAll(path)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("DBPerShard.DeleteIndex os.RemoveAll('%v')", path))
}
delete(per.index2shards[ty], index)
}
}
// allow the index to be created again anew.
delete(per.dbh.Index, index)
return
}
func (per *DBPerShard) DeleteFieldFromStore(index, field, fieldPath string) (err error) {
per.Mu.Lock()
defer func() {
if fieldPath != "" {
_ = os.RemoveAll(fieldPath)
}
per.Mu.Unlock()
}()
dbi, ok := per.dbh.Index[index]
if !ok {
// TestIndex_Existence_Delete in index_internal_test.go
// will call us without having ever created a Tx or DB,
// so we can't complain here.
return nil
}
for _, dbs := range dbi.Shard {
for _, w := range dbs.W {
if e := w.DeleteField(index, field, fieldPath); e != nil && err == nil {
err = errors.Wrap(e, "DeleteFieldFromStore()")
}
}
}
return err
}
func (per *DBPerShard) DeleteFragment(index, field, view string, shard uint64, frag *fragment) error {
idx := per.txf.holder.Index(index)
dbs, err := per.GetDBShard(index, shard, idx)
if err != nil {
return 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()
fmt.Printf("\n============= dumping dbs.W[%v] %v ========\n", i, ty)
tx.Dump(short, dbs.Shard)
switch ty {
case roaringTxn:
case rbfTxn:
case boltTxn:
default:
PanicOn(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() {
per.Mu.Lock()
defer per.Mu.Unlock()
found1 := false
for _, dbi := range per.dbh.Index {
for _, dbs := range dbi.Shard {
if dbs.Open {
found1 = true
dbs.DumpAll()
}
}
}
if !found1 {
AlwaysPrintf("DBPerShard.DumpAll() sees no databases. dir='%v'", per.HolderDir)
}
}
// if you know the shard, you can use this
// pathForType and prefixForType must be kept in sync!
func (dbs *DBShard) pathForType(ty txtype) string {
// top level paths will end in "@@"
// what here for roaring? well, roaringRegistrar.OpenDBWrapper()
// is a no-op anyhow. so doesn't need to be correct atm.
path := dbs.HolderPath + sep + dbs.Index + sep + backendsDir + sep + ty.DirectoryName() + sep + fmt.Sprintf("shard.%04v", dbs.Shard)
if ty == boltTxn {
// special case:
// bolt doesn't use a directory like the others, just a direct path.
path += sep + "bolt.db"
}
return path
}
// if you don't know the shard, you have to use this.
// prefixForType and pathForType must be kept in sync!
func (per *DBPerShard) prefixForType(idx *Index, ty txtype) string {
// top level paths will end in "@@"
return per.HolderDir + sep + idx.name + sep + backendsDir + sep + ty.DirectoryName() + sep
}
var ErrNoData = fmt.Errorf("no data")
// keep our cache of shards up-to-date in memory; after the initial
// directory scan, this is all we should we need. Prevents us from
// doing additional, expensive, directory scans.
//
// Caller must hold per.Mu.Lock() already.
func (per *DBPerShard) updateIndex2ShardCacheWithNewShard(dbs *DBShard) {
for _, ty := range dbs.types {
mapIndex2shardSet, ok := per.index2shards[ty]
if !ok {
mapIndex2shardSet = make(map[string]*shardSet)
per.index2shards[ty] = mapIndex2shardSet
}
// INVAR: mapIndex2shardSet is good, but may be an empty map
shardset, ok := mapIndex2shardSet[dbs.Index]
if !ok {
shardset = newShardSet()
mapIndex2shardSet[dbs.Index] = shardset
}
// INVAR: shardset is present, not nil; a map that can be added to.
shardset.add(dbs.Shard)
}
}
func (per *DBPerShard) GetDBShard(index string, shard uint64, idx *Index) (dbs *DBShard, err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
return per.unprotectedGetDBShard(index, shard, idx)
}
func (per *DBPerShard) unprotectedGetDBShard(index string, shard uint64, idx *Index) (dbs *DBShard, err error) {
dbi, ok := per.dbh.Index[index]
if !ok {
dbi = &DBIndex{
Shard: make(map[uint64]*DBShard),
}
per.dbh.Index[index] = dbi
}
dbs, ok = dbi.Shard[shard]
if dbs != nil && dbs.closed {
if len(per.types) == 1 && per.types[0] == roaringTxn {
// roaring txn are nil/fake anyway. Don't freak out.
} else {
PanicOn(fmt.Sprintf("cannot retain closed dbs across holder ReOpen dbs='%p'; per.types[0]='%v'; len(per.types)=%v", dbs, per.types[0], len(per.types)))
}
}
if !ok {
dbs = &DBShard{
types: per.types,
ParentDBIndex: dbi,
Index: index,
Shard: shard,
HolderPath: per.HolderDir,
idx: idx,
per: per,
useOpenList: per.useOpenList,
hasRoaring: per.hasRoaring,
isBlueGreen: len(per.types) > 1,
}
dbs.stypes = make([]string, len(per.types))
for i, ty := range per.types {
dbs.stypes[i] = ty.String()
}
dbi.Shard[shard] = dbs
per.updateIndex2ShardCacheWithNewShard(dbs)
}
if !dbs.Open {
var registry DBRegistry
for _, ty := range dbs.types {
switch ty {
case roaringTxn:
registry = globalRoaringReg
case rbfTxn:
registry = globalRbfDBReg
registry.(*rbfDBRegistrar).SetRBFConfig(per.RBFConfig)
case boltTxn:
registry = globalBoltReg
default:
PanicOn(fmt.Sprintf("unknown txtyp: '%v'", ty))
}
path := dbs.pathForType(ty)
w, err := registry.OpenDBWrapper(path, DetectMemAccessPastTx, per.StorageConfig)
PanicOn(err)
h := idx.Holder()
w.SetHolder(h)
dbs.Open = true
if w != nil && len(dbs.W) == 0 {
per.Flatmap[flatkey{index: index, shard: shard}] = dbs
}
dbs.W = append(dbs.W, w)
}
}
return
}
func (per *DBPerShard) Del(dbs *DBShard) (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
err = dbs.Close()
if err != nil {
return
}
PanicOn(dbs.DeleteDBPath())
delete(per.Flatmap, flatkey{index: dbs.Index, shard: dbs.Shard})
// delete from the heirarchy
delete(dbs.ParentDBIndex.Shard, dbs.Shard)
return nil
}
func (per *DBPerShard) Close() (err error) {
per.Mu.Lock()
defer per.Mu.Unlock()
for _, dbi := range per.dbh.Index {
for _, dbs := range dbi.Shard {
err = dbs.Close()
PanicOn(err)
}
}
return
}
// DBPerShardGetShardsForIndex returns the shards for idx.
// If requireData, we open the database and see that it has a key, rather
// than assume that the database file presence is enough.
func (f *TxFactory) GetShardsForIndex(idx *Index, roaringViewPath string, requireData bool) (map[uint64]bool, error) {
n := len(f.types)
if n != 1 && n != 2 {
PanicOn(fmt.Sprintf("internal error. only green or blue/green supported. we see types len %v", n))
}
var shards []map[uint64]bool
for _, ty := range f.types {
ss, err := f.dbPerShard.TypedDBPerShardGetShardsForIndex(ty, idx, roaringViewPath, requireData)
if err != nil {
return nil, err
}
shards = append(shards, ss)
}
// Note: we don't actually know when the blue call and when the green call comes
// through here. So if we are deleting a shard, we will see a difference earlier
// in one than the other. TestAPI_ClearFlagForImportAndImportValues for example.
// Hence we cannot do a blue-green check here for matching shards.
// If we are populating blue from green, it does matter that we return green.
return shards[n-1], nil
}
// if roaringViewPath is "" then for ty == roaringTxn we go to disk to discover
// all the view paths under idx for type ty.
// requireData means open the database file and verify that at least one key is set.
// The returned sliceOfShards should not be modified. We will cache it for subsequent
// queries.
//
// when a new DBShard is made, we will update the list of shards then. Thus
// the per.index2shard should always be up to date AFTER the first call here.
//
// Note: we cannot here call GetView2ShardsMapForIndex() because that only ever
// returns the green data and we are used during migration for both blue
// and green.
//
func (per *DBPerShard) TypedDBPerShardGetShardsForIndex(ty txtype, idx *Index, roaringViewPath string, requireData bool) (shardMap map[uint64]bool, err error) {
// use the cache, always
per.Mu.Lock()
defer per.Mu.Unlock()
if ty == roaringTxn && roaringViewPath != "" {
shardMap, err := roaringMapOfShards(roaringViewPath)
if err != nil {
return nil, err
}
return shardMap, nil
}
i2ss, ok := per.index2shards[ty]
if !ok {
// index -> shardSet
i2ss = make(map[string]*shardSet)
per.index2shards[ty] = i2ss
}
// INVAR: i2ss is good, but may be an empty map
ss, ok := i2ss[idx.name]
if ok {
return ss.CloneMaybe(), nil
}
// INVAR: cache miss, and index2shards[ty] exists.
// gotta read shards from disk directory layout.
setOfShards := newShardSet()
per.index2shards[ty][idx.name] = setOfShards
// Upon return, cache the setOfShards value and reuse it next time
if ty == roaringTxn {
// INVAR: roaringViewPath == "", because the other case is
// handled above.
fields := idx.Fields()
for _, field := range fields {
for _, view := range field.views() {
shardMap, err := roaringMapOfShards(view.path)
if err != nil {
return nil,
errors.Wrap(err, fmt.Sprintf(
"TypedDBPerShardGetLocalShardsForIndex roaringTxn view.path='%v'", view.path))
}
for shard := range shardMap {
setOfShards.add(shard)
}
}
}
return setOfShards.CloneMaybe(), nil
}
// INVAR: not-roaring.
path := per.prefixForType(idx, ty)
ignoreEmpty := false
includeRoot := true
dbf, err := listDirUnderDir(path, includeRoot, ignoreEmpty)
PanicOn(err)
for _, nm := range dbf {
base := filepath.Base(nm)
// We're only interested in "shard.*" files, so skip everything else.
const shardPrefix = "shard."
const lenOfShardPrefix = len(shardPrefix)
if !strings.HasPrefix(base, shardPrefix) {
continue
}
// Parse filename into integer.
shard, err := strconv.ParseUint(base[lenOfShardPrefix:], 10, 64)
if err != nil {
PanicOn(err)
continue
}
// exclude those without data?
hasData := false
if requireData {
hasData, err = per.unprotectedTypedIndexShardHasData(ty, idx, shard)
if err != nil {
return nil, err
}
if hasData {
setOfShards.add(shard)
}
} else {
// file presence is enough
setOfShards.add(shard)
}
}
return setOfShards.CloneMaybe(), nil
}
func (per *DBPerShard) unprotectedTypedIndexShardHasData(ty txtype, idx *Index, shard uint64) (hasData bool, err error) {
whichty := 0
if len(per.types) == 2 {
if ty == per.types[1] {
whichty = 1
}
}
if ty != per.types[whichty] {
return
}
// make the dbs if it doesn't get exist
dbs, err := per.unprotectedGetDBShard(idx.name, shard, idx)
if err != nil {
return false, errors.Wrap(err, fmt.Sprintf("DBPerShard.TypedIndexShardHasData() "+
"per.GetDBShard(index='%v', shard='%v', ty='%v')", idx.name, shard, ty.String()))
}
return dbs.W[whichty].HasData()
}
func listDirUnderDir(root string, includeRoot bool, ignoreEmpty bool) (files []string, err error) {
if !dirExists(root) {
return
}
n := len(root) + 1
if includeRoot {
n = 0
}
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if len(path) < n {
// ignore
} else {
if info == nil {
// re-opening an RBF database hit this, racing with a directory rename.
// Don't freak out.
return nil
}
if !info.IsDir() {
// ignore files
} else {
if ignoreEmpty && info.Size() == 0 {
return nil
}
files = append(files, path[n:])
}
}
return nil
})
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_STORAGE_BACKEND=bolt_roaring
// we have bolt as blue, and roaring as green. The contents of
// bolt must be empty or exactly match roaring. If bolt
// starts empty, it will be populated from roaring by
// populateBlueFromGreen().
//
func (dbs *DBShard) populateBlueFromGreen() (err error) {
n := len(dbs.W)
if n != 2 {
PanicOn(fmt.Sprintf("populateBlueFromGreen 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()
ctWriteCount := 0
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 {
// might be an empty fragment. If so, let's not freak out.
if strings.Contains(err.Error(), "fragment not found") {
continue
} else {
writetx.Rollback()
return errors.Wrap(err, "DBShard.populateBlueFromGreen 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()
writetx.Rollback()
return errors.Wrap(err, "DBShard.populateBlueFromGreen writetx.PutContainer")
}
ctWriteCount++
if ctWriteCount%1000 == 1 {
// regularly commiting smaller batches and the first batch as soon as
// possible massively speeds up writing to bolt.
//
// reference: https://github.com/boltdb/bolt/issues/94
//
// benbjohnson commented on Mar 25, 2014
// "Bulk loading more than 1000 items at a time is very slow. This is because nodes
// are not splitting before commit which causes large memmove() operations during insertion."
// runtime.memmove is taking all of the time in our pprof profile, when copying rbf to bolt, so we suspect it is this.
//
err = writetx.Commit()
if err != nil {
citer.Close()
writetx.Rollback()
return errors.Wrap(err, "DBShard.populateBlueFromGreen writetx.Commit")
}
writetx, err = dest.NewTx(writable, dbs.Index, Txo{Write: writable, Index: dbs.idx, Shard: dbs.Shard})
if err != nil {
citer.Close()
writetx.Rollback()
return errors.Wrap(err, "DBShard.populateBlueFromGreen writetx.NewTx inside citer.Next() loop")
}
}
}
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() (err error) {
n := len(dbs.W)
if n != 2 {
PanicOn(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 {
if strings.Contains(err.Error(), "fragment not found") {
continue
} else {
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))
}
}
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
}
type FieldView2Shards struct {
// field -> view -> *shardSet
m map[string]map[string]*shardSet
}
func (vs *FieldView2Shards) getViewsForField(field string) map[string]*shardSet {
return vs.m[field]
}
func (vs *FieldView2Shards) has(field, view string, shard uint64) bool {
vw, ok := vs.m[field]
if !ok {
return false
}
ss, ok := vw[view]
if !ok {
return false
}
shardMap := ss.CloneMaybe()
return shardMap[shard]
}
func (vs *FieldView2Shards) addViewShardSet(fv txkey.FieldView, ss *shardSet) {
f, ok := vs.m[fv.Field]
if !ok {
f = make(map[string]*shardSet)
vs.m[fv.Field] = f
}
// INVAR: f is ready to take ss.
// existing stuff to merge with?
prior, ok := f[fv.View]
if !ok {
f[fv.View] = ss
return
}
// merge ss and prior. No need to put the union back into f[fv.View]
// because prior is a pointer.
prior.unionInPlace(ss)
}
func (a *FieldView2Shards) equals(b *FieldView2Shards) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a.m) != len(b.m) {
return false
}
for field, viewmapA := range a.m {
viewmapB, ok := b.m[field]
if !ok {
return false
}
if len(viewmapB) != len(viewmapA) {
return false
}
for k, va := range viewmapA {
vb, ok := viewmapB[k]
if !ok {
return false
}
if !va.equals(vb) {
return false
}
}
}
return true
}
func NewFieldView2Shards() *FieldView2Shards {
return &FieldView2Shards{
m: make(map[string]map[string]*shardSet), // expected response from GetView2ShardMapForIndex
}
}
func (vs *FieldView2Shards) addShard(fv txkey.FieldView, shard uint64) {
viewmap, ok := vs.m[fv.Field]
if !ok {
viewmap = make(map[string]*shardSet)
vs.m[fv.Field] = viewmap
}
ss, ok := viewmap[fv.View]
if !ok {
ss = newShardSet()
viewmap[fv.View] = ss
}
ss.add(shard)
}
func (vs *FieldView2Shards) String() (r string) {
r = "\n"
for field, viewmap := range vs.m {
for view, shards := range viewmap {
r += fmt.Sprintf("field '%v' view:'%v' shards:%v\n", field, view, shards)
}
}
r += "\n"
return
}
// Note: cannot call this during migration, because
// it only ever returns the green shards if we are in blue-green.
func (per *DBPerShard) GetFieldView2ShardsMapForIndex(idx *Index) (vs *FieldView2Shards, err error) {
// for blue-green, it does matter that we return green, so we can migrate from it.
ty := per.types[0]
if per.isBlueGreen {
ty = per.types[1]
}
switch ty {
case roaringTxn:
return roaringGetFieldView2Shards(idx)
default:
vs = NewFieldView2Shards()
shardMap, err := per.TypedDBPerShardGetShardsForIndex(ty, idx, "", true)
if err != nil {
return nil, err
}
for shard := range shardMap {
dbs, err := per.GetDBShard(idx.name, shard, idx)
if err != nil {
return nil, errors.Wrap(err, "DBPerShard.GetFieldView2ShardsMapForIndex GetDBShard()")
}
fieldviews, err := dbs.AllFieldViews()
if err != nil {
return nil, errors.Wrap(err, "DBPerShard.GetFieldView2ShardsMapForIndex dbs.AllFieldViews()")
}
for _, fv := range fieldviews {
vs.addShard(fv, shard)
}
}
}
return
}
func (dbs *DBShard) AllFieldViews() (fvs []txkey.FieldView, err error) {
tx, err := dbs.NewTx(!writable, dbs.idx.name, Txo{Write: !writable, Shard: dbs.Shard, Index: dbs.idx, dbs: dbs})
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("dbshard.NewTx for index '%v', shard %v", dbs.idx.name, dbs.Shard))
}
defer tx.Rollback()
return tx.GetSortedFieldViewList(dbs.idx, dbs.Shard)
}