mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
1278 lines
33 KiB
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)
|
|
}
|