mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-10 07:01:01 +00:00
Data loss was occuring after a cluster restart. The issue was during the unmarshaling of the op log when multiple values had been written to the log. The lines in question were like "changed = changed || b.DirectAdd(v)" in which the DirectAdd would only be executed when changed was initially false, once it was true, it would never be executed again.
2659 lines
71 KiB
Go
2659 lines
71 KiB
Go
// Copyright 2017 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pilosa
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"container/heap"
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/cespare/xxhash"
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/pilosa/pilosa/internal"
|
|
"github.com/pilosa/pilosa/logger"
|
|
"github.com/pilosa/pilosa/pql"
|
|
"github.com/pilosa/pilosa/roaring"
|
|
"github.com/pilosa/pilosa/stats"
|
|
"github.com/pilosa/pilosa/syswrap"
|
|
"github.com/pilosa/pilosa/tracing"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
// ShardWidth is the number of column IDs in a shard. It must be a power of 2 greater than or equal to 16.
|
|
shardWidthExponent = 20
|
|
ShardWidth = 1 << shardWidthExponent
|
|
|
|
// shardVsContainerExponent is the power of 2 of ShardWith minus the power
|
|
// of two of roaring container width (which is 16).
|
|
// 2^shardVsContainerExponent is the number of containers in a shard row.
|
|
//
|
|
// It is represented in this rather awkward way because calculating the row
|
|
// which a given container is in means dividing by the number of rows per
|
|
// container which is performantly expressed as a right shift by this
|
|
// exponent.
|
|
shardVsContainerExponent = shardWidthExponent - 16
|
|
|
|
// width of roaring containers is 2^16
|
|
containerWidth = 1 << 16
|
|
|
|
// snapshotExt is the file extension used for an in-process snapshot.
|
|
snapshotExt = ".snapshotting"
|
|
|
|
// copyExt is the file extension used for the temp file used while copying.
|
|
copyExt = ".copying"
|
|
|
|
// cacheExt is the file extension for persisted cache ids.
|
|
cacheExt = ".cache"
|
|
|
|
// HashBlockSize is the number of rows in a merkle hash block.
|
|
HashBlockSize = 100
|
|
|
|
// defaultFragmentMaxOpN is the default value for Fragment.MaxOpN.
|
|
defaultFragmentMaxOpN = 10000
|
|
|
|
// Row ids used for boolean fields.
|
|
falseRowID = uint64(0)
|
|
trueRowID = uint64(1)
|
|
)
|
|
|
|
// fragment represents the intersection of a field and shard in an index.
|
|
type fragment struct {
|
|
mu sync.RWMutex
|
|
|
|
// Composite identifiers
|
|
index string
|
|
field string
|
|
view string
|
|
shard uint64
|
|
|
|
// File-backed storage
|
|
path string
|
|
file *os.File
|
|
storage *roaring.Bitmap
|
|
storageData []byte
|
|
opN int // number of ops since snapshot
|
|
|
|
// Cache for row counts.
|
|
CacheType string // passed in by field
|
|
cache cache
|
|
CacheSize uint32
|
|
|
|
// Stats reporting.
|
|
maxRowID uint64
|
|
|
|
// Cache containing full rows (not just counts).
|
|
rowCache bitmapCache
|
|
|
|
// Cached checksums for each block.
|
|
checksums map[int][]byte
|
|
|
|
// Number of operations performed before performing a snapshot.
|
|
// This limits the size of fragments on the heap and flushes them to disk
|
|
// so that they can be mmapped and heap utilization can be kept low.
|
|
MaxOpN int
|
|
|
|
// Logger used for out-of-band log entries.
|
|
Logger logger.Logger
|
|
|
|
// Row attribute storage.
|
|
// This is set by the parent field unless overridden for testing.
|
|
RowAttrStore AttrStore
|
|
|
|
// mutexVector is used for mutex field types. It's checked for an
|
|
// existing value (to clear) prior to setting a new value.
|
|
mutexVector vector
|
|
|
|
stats stats.StatsClient
|
|
}
|
|
|
|
// newFragment returns a new instance of Fragment.
|
|
func newFragment(path, index, field, view string, shard uint64) *fragment {
|
|
return &fragment{
|
|
path: path,
|
|
index: index,
|
|
field: field,
|
|
view: view,
|
|
shard: shard,
|
|
CacheType: DefaultCacheType,
|
|
CacheSize: DefaultCacheSize,
|
|
|
|
Logger: logger.NopLogger,
|
|
MaxOpN: defaultFragmentMaxOpN,
|
|
|
|
stats: stats.NopStatsClient,
|
|
}
|
|
}
|
|
|
|
// cachePath returns the path to the fragment's cache data.
|
|
func (f *fragment) cachePath() string { return f.path + cacheExt }
|
|
|
|
// Open opens the underlying storage.
|
|
func (f *fragment) Open() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if err := func() error {
|
|
// Initialize storage in a function so we can close if anything goes wrong.
|
|
if err := f.openStorage(); err != nil {
|
|
return errors.Wrap(err, "opening storage")
|
|
}
|
|
|
|
// Fill cache with rows persisted to disk.
|
|
if err := f.openCache(); err != nil {
|
|
return errors.Wrap(err, "opening cache")
|
|
}
|
|
|
|
// Clear checksums.
|
|
f.checksums = make(map[int][]byte)
|
|
|
|
// Read last bit to determine max row.
|
|
pos := f.storage.Max()
|
|
f.maxRowID = pos / ShardWidth
|
|
f.stats.Gauge("rows", float64(f.maxRowID), 1.0)
|
|
return nil
|
|
}(); err != nil {
|
|
f.close()
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fragment) reopen() (mustClose bool, err error) {
|
|
if f.file == nil {
|
|
// Open the data file to be mmap'd and used as an ops log.
|
|
f.file, mustClose, err = syswrap.OpenFile(f.path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
if err != nil {
|
|
return mustClose, fmt.Errorf("open file: %s", err)
|
|
}
|
|
f.storage.OpWriter = f.file
|
|
}
|
|
return mustClose, nil
|
|
}
|
|
|
|
// openStorage opens the storage bitmap.
|
|
func (f *fragment) openStorage() error {
|
|
// Create a roaring bitmap to serve as storage for the shard.
|
|
if f.storage == nil {
|
|
f.storage = roaring.NewFileBitmap()
|
|
}
|
|
// Open the data file to be mmap'd and used as an ops log.
|
|
file, mustClose, err := syswrap.OpenFile(f.path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
if err != nil {
|
|
return fmt.Errorf("open file: %s", err)
|
|
}
|
|
f.file = file
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
|
|
// Lock the underlying file.
|
|
if err := syscall.Flock(int(f.file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
return fmt.Errorf("flock: %s", err)
|
|
}
|
|
|
|
// If the file is empty then initialize it with an empty bitmap.
|
|
fi, err := f.file.Stat()
|
|
if err != nil {
|
|
return errors.Wrap(err, "statting file before")
|
|
} else if fi.Size() == 0 {
|
|
bi := bufio.NewWriter(f.file)
|
|
if _, err := f.storage.WriteTo(bi); err != nil {
|
|
return fmt.Errorf("init storage file: %s", err)
|
|
}
|
|
bi.Flush()
|
|
_, err = f.file.Stat()
|
|
if err != nil {
|
|
return errors.Wrap(err, "statting file after")
|
|
}
|
|
} else {
|
|
// Mmap the underlying file so it can be zero copied.
|
|
data, err := syswrap.Mmap(int(f.file.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_SHARED)
|
|
if err == syswrap.ErrMaxMapCountReached {
|
|
f.Logger.Debugf("maximum number of maps reached, reading file instead")
|
|
data, err = ioutil.ReadAll(file)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failure file readall")
|
|
}
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "mmap failed")
|
|
} else {
|
|
f.storageData = data
|
|
// Advise the kernel that the mmap is accessed randomly.
|
|
if err := madvise(f.storageData, syscall.MADV_RANDOM); err != nil {
|
|
return fmt.Errorf("madvise: %s", err)
|
|
}
|
|
}
|
|
|
|
if err := f.storage.UnmarshalBinary(data); err != nil {
|
|
return fmt.Errorf("unmarshal storage: file=%s, err=%s", f.file.Name(), err)
|
|
}
|
|
|
|
}
|
|
|
|
f.opN = f.storage.Info().OpN
|
|
|
|
// Attach the file to the bitmap to act as a write-ahead log.
|
|
f.storage.OpWriter = f.file
|
|
f.rowCache = &simpleCache{make(map[uint64]*Row)}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// openCache initializes the cache from row ids persisted to disk.
|
|
func (f *fragment) openCache() error {
|
|
// Determine cache type from field name.
|
|
switch f.CacheType {
|
|
case CacheTypeRanked:
|
|
f.cache = NewRankCache(f.CacheSize)
|
|
case CacheTypeLRU:
|
|
f.cache = newLRUCache(f.CacheSize)
|
|
case CacheTypeNone:
|
|
f.cache = globalNopCache
|
|
return nil
|
|
default:
|
|
return ErrInvalidCacheType
|
|
}
|
|
|
|
// Read cache data from disk.
|
|
path := f.cachePath()
|
|
buf, err := ioutil.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return fmt.Errorf("open cache: %s", err)
|
|
}
|
|
|
|
// Unmarshal cache data.
|
|
var pb internal.Cache
|
|
if err := proto.Unmarshal(buf, &pb); err != nil {
|
|
f.Logger.Printf("error unmarshaling cache data, skipping: path=%s, err=%s", path, err)
|
|
return nil
|
|
}
|
|
|
|
// Read in all rows by ID.
|
|
// This will cause them to be added to the cache.
|
|
for _, id := range pb.IDs {
|
|
n := f.storage.CountRange(id*ShardWidth, (id+1)*ShardWidth)
|
|
f.cache.BulkAdd(id, n)
|
|
}
|
|
f.cache.Invalidate()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close flushes the underlying storage, closes the file and unlocks it.
|
|
func (f *fragment) Close() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.close()
|
|
}
|
|
|
|
func (f *fragment) close() error {
|
|
// Flush cache if closing gracefully.
|
|
if err := f.flushCache(); err != nil {
|
|
f.Logger.Printf("fragment: error flushing cache on close: err=%s, path=%s", err, f.path)
|
|
return errors.Wrap(err, "flushing cache")
|
|
}
|
|
|
|
// Close underlying storage.
|
|
if err := f.closeStorage(); err != nil {
|
|
f.Logger.Printf("fragment: error closing storage: err=%s, path=%s", err, f.path)
|
|
return errors.Wrap(err, "closing storage")
|
|
}
|
|
|
|
// Remove checksums.
|
|
f.checksums = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
// safeClose is unprotected.
|
|
func (f *fragment) safeClose() error {
|
|
// Flush file, unlock & close.
|
|
if f.file != nil {
|
|
if err := f.file.Sync(); err != nil {
|
|
return fmt.Errorf("sync: %s", err)
|
|
}
|
|
if err := syscall.Flock(int(f.file.Fd()), syscall.LOCK_UN); err != nil {
|
|
return fmt.Errorf("unlock: %s", err)
|
|
}
|
|
if err := syswrap.CloseFile(f.file); err != nil {
|
|
return fmt.Errorf("close file: %s", err)
|
|
}
|
|
}
|
|
f.file = nil
|
|
f.storage.OpWriter = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fragment) closeStorage() error {
|
|
// Clear the storage bitmap so it doesn't access the closed mmap.
|
|
|
|
//f.storage = roaring.NewBitmap()
|
|
|
|
// Unmap the file.
|
|
if f.storageData != nil {
|
|
if err := syswrap.Munmap(f.storageData); err != nil {
|
|
return fmt.Errorf("munmap: %s", err)
|
|
}
|
|
f.storageData = nil
|
|
}
|
|
|
|
if err := f.safeClose(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// opN is determined by how many bit set/clear operations are in the storage
|
|
// write log, so once the storage is closed it should be 0. Opening new
|
|
// storage will set opN appropriately.
|
|
f.opN = 0
|
|
|
|
return nil
|
|
}
|
|
|
|
// row returns a row by ID.
|
|
func (f *fragment) row(rowID uint64) *Row {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.unprotectedRow(rowID)
|
|
}
|
|
|
|
// unprotectedRow returns a row from the row cache if available or from storage
|
|
// (updating the cache).
|
|
func (f *fragment) unprotectedRow(rowID uint64) *Row {
|
|
r, ok := f.rowCache.Fetch(rowID)
|
|
if ok && r != nil {
|
|
return r
|
|
}
|
|
|
|
row := f.rowFromStorage(rowID)
|
|
f.rowCache.Add(rowID, row)
|
|
return row
|
|
}
|
|
|
|
// rowFromStorage clones a row data out of fragment storage and returns it as a
|
|
// Row object.
|
|
func (f *fragment) rowFromStorage(rowID uint64) *Row {
|
|
// Only use a subset of the containers.
|
|
// NOTE: The start & end ranges must be divisible by container width.
|
|
data := f.storage.OffsetRange(f.shard*ShardWidth, rowID*ShardWidth, (rowID+1)*ShardWidth)
|
|
|
|
// Reference bitmap subrange in storage. We Clone() data because otherwise
|
|
// row will contain pointers to containers in storage. This causes
|
|
// unexpected results when we cache the row and try to use it later.
|
|
// Basically, since we return the Row and release the fragment lock, the
|
|
// underlying fragment storage could be changed or snapshotted and thrown
|
|
// out at any point.
|
|
row := &Row{
|
|
segments: []rowSegment{{
|
|
data: *data.Clone(),
|
|
shard: f.shard,
|
|
writable: false, // this Row will probably be cached and shared, so it must be read only.
|
|
}},
|
|
}
|
|
row.invalidateCount()
|
|
|
|
return row
|
|
}
|
|
|
|
// setBit sets a bit for a given column & row within the fragment.
|
|
// This updates both the on-disk storage and the in-cache bitmap.
|
|
func (f *fragment) setBit(rowID, columnID uint64) (changed bool, err error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
|
|
// handle mutux field type
|
|
if f.mutexVector != nil {
|
|
if err := f.handleMutex(rowID, columnID); err != nil {
|
|
return changed, errors.Wrap(err, "handling mutex")
|
|
}
|
|
}
|
|
|
|
return f.unprotectedSetBit(rowID, columnID)
|
|
}
|
|
|
|
// handleMutex will clear an existing row and store the new row
|
|
// in the vector.
|
|
func (f *fragment) handleMutex(rowID, columnID uint64) error {
|
|
if existingRowID, found, err := f.mutexVector.Get(columnID); err != nil {
|
|
return errors.Wrap(err, "getting mutex vector data")
|
|
} else if found && existingRowID != rowID {
|
|
if _, err := f.unprotectedClearBit(existingRowID, columnID); err != nil {
|
|
return errors.Wrap(err, "clearing mutex value")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unprotectedSetBit TODO should be replaced by an invocation of importPositions with a single bit to set.
|
|
func (f *fragment) unprotectedSetBit(rowID, columnID uint64) (changed bool, err error) {
|
|
changed = false
|
|
// Determine the position of the bit in the storage.
|
|
pos, err := f.pos(rowID, columnID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "getting bit pos")
|
|
}
|
|
|
|
// Write to storage.
|
|
if changed, err = f.storage.Add(pos); err != nil {
|
|
return false, errors.Wrap(err, "writing")
|
|
}
|
|
|
|
// Don't update the cache if nothing changed.
|
|
if !changed {
|
|
return changed, nil
|
|
}
|
|
|
|
// Invalidate block checksum.
|
|
delete(f.checksums, int(rowID/HashBlockSize))
|
|
|
|
// Increment number of operations until snapshot is required.
|
|
if err := f.incrementOpN(); err != nil {
|
|
return false, errors.Wrap(err, "incrementing")
|
|
}
|
|
|
|
// Get the row from row cache or fragment.storage.
|
|
row := f.unprotectedRow(rowID)
|
|
row.SetBit(columnID)
|
|
|
|
// Update the cache.
|
|
f.cache.Add(rowID, row.Count())
|
|
|
|
f.stats.Count("setBit", 1, 0.001)
|
|
|
|
// Update row count if they have increased.
|
|
if rowID > f.maxRowID {
|
|
f.maxRowID = rowID
|
|
f.stats.Gauge("rows", float64(f.maxRowID), 1.0)
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// clearBit clears a bit for a given column & row within the fragment.
|
|
// This updates both the on-disk storage and the in-cache bitmap.
|
|
func (f *fragment) clearBit(rowID, columnID uint64) (bool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
return f.unprotectedClearBit(rowID, columnID)
|
|
}
|
|
|
|
// unprotectedClearBit TODO should be replaced by an invocation of
|
|
// importPositions with a single bit to clear.
|
|
func (f *fragment) unprotectedClearBit(rowID, columnID uint64) (changed bool, err error) {
|
|
changed = false
|
|
// Determine the position of the bit in the storage.
|
|
pos, err := f.pos(rowID, columnID)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "getting bit pos")
|
|
}
|
|
|
|
// Write to storage.
|
|
if changed, err = f.storage.Remove(pos); err != nil {
|
|
return false, errors.Wrap(err, "writing")
|
|
}
|
|
|
|
// Don't update the cache if nothing changed.
|
|
if !changed {
|
|
return changed, nil
|
|
}
|
|
|
|
// Invalidate block checksum.
|
|
delete(f.checksums, int(rowID/HashBlockSize))
|
|
|
|
// Increment number of operations until snapshot is required.
|
|
if err := f.incrementOpN(); err != nil {
|
|
return false, errors.Wrap(err, "incrementing")
|
|
}
|
|
|
|
// Get the row from cache or fragment.storage.
|
|
row := f.unprotectedRow(rowID)
|
|
row.clearBit(columnID)
|
|
|
|
// Update the cache.
|
|
f.cache.Add(rowID, row.Count())
|
|
|
|
f.stats.Count("clearBit", 1, 1.0)
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// setRow replaces an existing row (specified by rowID) with the given
|
|
// Row. This updates both the on-disk storage and the in-cache bitmap.
|
|
func (f *fragment) setRow(row *Row, rowID uint64) (bool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
return f.unprotectedSetRow(row, rowID)
|
|
}
|
|
|
|
func (f *fragment) unprotectedSetRow(row *Row, rowID uint64) (changed bool, err error) {
|
|
// TODO: In order to return `changed`, we need to first compare
|
|
// the existing row with the given row. Determine if the overhead
|
|
// of this is worth having `changed`.
|
|
// For now we will assume changed is always true.
|
|
changed = true
|
|
|
|
// First container of the row in storage.
|
|
headContainerKey := rowID << shardVsContainerExponent
|
|
|
|
// Remove every existing container in the row.
|
|
for i := uint64(0); i < (1 << shardVsContainerExponent); i++ {
|
|
f.storage.Containers.Remove(headContainerKey + i)
|
|
}
|
|
|
|
// From the given row, get the rowSegment for this shard.
|
|
seg := row.segment(f.shard)
|
|
if seg == nil {
|
|
return changed, nil
|
|
}
|
|
|
|
// Put each container from rowSegment to fragment storage.
|
|
citer, _ := seg.data.Containers.Iterator(f.shard << shardVsContainerExponent)
|
|
for citer.Next() {
|
|
k, c := citer.Value()
|
|
f.storage.Containers.Put(headContainerKey+(k%(1<<shardVsContainerExponent)), c)
|
|
}
|
|
|
|
// Update the row in cache.
|
|
n := f.storage.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth)
|
|
f.cache.BulkAdd(rowID, n)
|
|
|
|
// Snapshot storage.
|
|
if err := f.snapshot(); err != nil {
|
|
return false, errors.Wrap(err, "snapshotting")
|
|
}
|
|
|
|
f.stats.Count("setRow", 1, 1.0)
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// ClearRow clears a row for a given rowID within the fragment.
|
|
// This updates both the on-disk storage and the in-cache bitmap.
|
|
func (f *fragment) clearRow(rowID uint64) (bool, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
return f.unprotectedClearRow(rowID)
|
|
}
|
|
|
|
func (f *fragment) unprotectedClearRow(rowID uint64) (changed bool, err error) {
|
|
changed = false
|
|
|
|
// First container of the row in storage.
|
|
headContainerKey := rowID << shardVsContainerExponent
|
|
|
|
// Remove every container in the row.
|
|
for i := uint64(0); i < (1 << shardVsContainerExponent); i++ {
|
|
k := headContainerKey + i
|
|
// Technically we could bypass the Get() call and only
|
|
// call Remove(), but the Get() gives us the ability
|
|
// to return true if any existing data was removed.
|
|
if cont := f.storage.Containers.Get(k); cont != nil {
|
|
f.storage.Containers.Remove(k)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Clear the row in cache.
|
|
f.cache.Add(rowID, 0)
|
|
|
|
// Snapshot storage.
|
|
if err := f.snapshot(); err != nil {
|
|
return false, errors.Wrap(err, "snapshotting")
|
|
}
|
|
|
|
f.stats.Count("clearRow", 1, 1.0)
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
func (f *fragment) bit(rowID, columnID uint64) (bool, error) {
|
|
pos, err := f.pos(rowID, columnID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return f.storage.Contains(pos), nil
|
|
}
|
|
|
|
// value uses a column of bits to read a multi-bit value.
|
|
func (f *fragment) value(columnID uint64, bitDepth uint) (value uint64, exists bool, err error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// If existence bit is unset then ignore remaining bits.
|
|
if v, err := f.bit(uint64(bitDepth), columnID); err != nil {
|
|
return 0, false, errors.Wrap(err, "getting existence bit")
|
|
} else if !v {
|
|
return 0, false, nil
|
|
}
|
|
|
|
// Compute other bits into a value.
|
|
for i := uint(0); i < bitDepth; i++ {
|
|
if v, err := f.bit(uint64(i), columnID); err != nil {
|
|
return 0, false, errors.Wrapf(err, "getting value bit %d", i)
|
|
} else if v {
|
|
value |= (1 << i)
|
|
}
|
|
}
|
|
|
|
return value, true, nil
|
|
}
|
|
|
|
// clearValue uses a column of bits to clear a multi-bit value.
|
|
func (f *fragment) clearValue(columnID uint64, bitDepth uint, value uint64) (changed bool, err error) {
|
|
return f.setValueBase(columnID, bitDepth, value, true)
|
|
}
|
|
|
|
// setValue uses a column of bits to set a multi-bit value.
|
|
func (f *fragment) setValue(columnID uint64, bitDepth uint, value uint64) (changed bool, err error) {
|
|
return f.setValueBase(columnID, bitDepth, value, false)
|
|
}
|
|
|
|
func (f *fragment) positionsForValue(columnID uint64, bitDepth uint, value uint64, clear bool, toSet, toClear []uint64) ([]uint64, []uint64, error) {
|
|
for i := uint(0); i < bitDepth; i++ {
|
|
bit, err := f.pos(uint64(i), columnID)
|
|
if err != nil {
|
|
return toSet, toClear, errors.Wrap(err, "getting pos")
|
|
}
|
|
if value&(1<<i) != 0 {
|
|
toSet = append(toSet, bit)
|
|
} else {
|
|
toClear = append(toClear, bit)
|
|
}
|
|
}
|
|
|
|
// Mark value as set.
|
|
bit, err := f.pos(uint64(bitDepth), columnID)
|
|
if err != nil {
|
|
return toSet, toClear, errors.Wrap(err, "getting not-null pos")
|
|
}
|
|
if clear {
|
|
toClear = append(toClear, bit)
|
|
} else {
|
|
toSet = append(toSet, bit)
|
|
}
|
|
|
|
return toSet, toClear, nil
|
|
}
|
|
|
|
// TODO get rid of this and use positionsForValue to generate a single write op, and set that with importPositions.
|
|
func (f *fragment) setValueBase(columnID uint64, bitDepth uint, value uint64, clear bool) (changed bool, err error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
|
|
for i := uint(0); i < bitDepth; i++ {
|
|
if value&(1<<i) != 0 {
|
|
if c, err := f.unprotectedSetBit(uint64(i), columnID); err != nil {
|
|
return changed, err
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
} else {
|
|
if c, err := f.unprotectedClearBit(uint64(i), columnID); err != nil {
|
|
return changed, err
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark value as set (or cleared).
|
|
if clear {
|
|
if c, err := f.unprotectedClearBit(uint64(bitDepth), columnID); err != nil {
|
|
return changed, errors.Wrap(err, "clearing not-null")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
} else {
|
|
if c, err := f.unprotectedSetBit(uint64(bitDepth), columnID); err != nil {
|
|
return changed, errors.Wrap(err, "marking not-null")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// importSetValue is a more efficient SetValue just for imports.
|
|
func (f *fragment) importSetValue(columnID uint64, bitDepth uint, value uint64, clear bool) (changed bool, err error) { // nolint: unparam
|
|
for i := uint(0); i < bitDepth; i++ {
|
|
if value&(1<<i) != 0 {
|
|
bit, err := f.pos(uint64(i), columnID)
|
|
if err != nil {
|
|
return changed, errors.Wrap(err, "getting set pos")
|
|
}
|
|
if c, err := f.storage.Add(bit); err != nil {
|
|
return changed, errors.Wrap(err, "adding")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
} else {
|
|
bit, err := f.pos(uint64(i), columnID)
|
|
if err != nil {
|
|
return changed, errors.Wrap(err, "getting clear pos")
|
|
}
|
|
if c, err := f.storage.Remove(bit); err != nil {
|
|
return changed, errors.Wrap(err, "removing")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark value as set.
|
|
p, err := f.pos(uint64(bitDepth), columnID)
|
|
if err != nil {
|
|
return changed, errors.Wrap(err, "getting not-null pos")
|
|
}
|
|
if clear {
|
|
if c, err := f.storage.Remove(p); err != nil {
|
|
return changed, errors.Wrap(err, "removing not-null from storage")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
} else {
|
|
if c, err := f.storage.Add(p); err != nil {
|
|
return changed, errors.Wrap(err, "adding not-null to storage")
|
|
} else if c {
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
return changed, nil
|
|
}
|
|
|
|
// sum returns the sum of a given bsiGroup as well as the number of columns involved.
|
|
// A bitmap can be passed in to optionally filter the computed columns.
|
|
func (f *fragment) sum(filter *Row, bitDepth uint) (sum, count uint64, err error) {
|
|
// Compute count based on the existence row.
|
|
consider := f.row(uint64(bitDepth))
|
|
if filter != nil {
|
|
consider = consider.Intersect(filter)
|
|
}
|
|
count = consider.Count()
|
|
|
|
// Compute the sum based on the bit count of each row multiplied by the
|
|
// place value of each row. For example, 10 bits in the 1's place plus
|
|
// 4 bits in the 2's place plus 3 bits in the 4's place equals a total
|
|
// sum of 30:
|
|
//
|
|
// 10*(2^0) + 4*(2^1) + 3*(2^2) = 30
|
|
//
|
|
var cnt uint64
|
|
for i := uint(0); i < bitDepth; i++ {
|
|
row := f.row(uint64(i))
|
|
cnt = row.intersectionCount(consider)
|
|
sum += (1 << i) * cnt
|
|
}
|
|
|
|
return sum, count, nil
|
|
}
|
|
|
|
// min returns the min of a given bsiGroup as well as the number of columns involved.
|
|
// A bitmap can be passed in to optionally filter the computed columns.
|
|
func (f *fragment) min(filter *Row, bitDepth uint) (min, count uint64, err error) {
|
|
|
|
consider := f.row(uint64(bitDepth))
|
|
if filter != nil {
|
|
consider = consider.Intersect(filter)
|
|
}
|
|
|
|
// If there are no columns to consider, return early.
|
|
if consider.Count() == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
|
|
for i := bitDepth; i > uint(0); i-- {
|
|
ii := i - 1 // allow for uint range: (bitDepth-1) to 0
|
|
row := f.row(uint64(ii))
|
|
|
|
x := consider.Difference(row)
|
|
count = x.Count()
|
|
if count > 0 {
|
|
consider = x
|
|
} else {
|
|
min += (1 << ii)
|
|
if ii == 0 {
|
|
count = consider.Count()
|
|
}
|
|
}
|
|
}
|
|
|
|
return min, count, nil
|
|
}
|
|
|
|
// max returns the max of a given bsiGroup as well as the number of columns involved.
|
|
// A bitmap can be passed in to optionally filter the computed columns.
|
|
func (f *fragment) max(filter *Row, bitDepth uint) (max, count uint64, err error) {
|
|
|
|
consider := f.row(uint64(bitDepth))
|
|
if filter != nil {
|
|
consider = consider.Intersect(filter)
|
|
}
|
|
|
|
// If there are no columns to consider, return early.
|
|
if consider.Count() == 0 {
|
|
return 0, 0, nil
|
|
}
|
|
|
|
for i := bitDepth; i > uint(0); i-- {
|
|
ii := i - 1 // allow for uint range: (bitDepth-1) to 0
|
|
row := f.row(uint64(ii))
|
|
|
|
x := row.Intersect(consider)
|
|
count = x.Count()
|
|
if count > 0 {
|
|
max += (1 << ii)
|
|
consider = x
|
|
} else if ii == 0 {
|
|
count = consider.Count()
|
|
}
|
|
}
|
|
|
|
return max, count, nil
|
|
}
|
|
|
|
// rangeOp returns bitmaps with a bsiGroup value encoding matching the predicate.
|
|
func (f *fragment) rangeOp(op pql.Token, bitDepth uint, predicate uint64) (*Row, error) {
|
|
switch op {
|
|
case pql.EQ:
|
|
return f.rangeEQ(bitDepth, predicate)
|
|
case pql.NEQ:
|
|
return f.rangeNEQ(bitDepth, predicate)
|
|
case pql.LT, pql.LTE:
|
|
return f.rangeLT(bitDepth, predicate, op == pql.LTE)
|
|
case pql.GT, pql.GTE:
|
|
return f.rangeGT(bitDepth, predicate, op == pql.GTE)
|
|
default:
|
|
return nil, ErrInvalidRangeOperation
|
|
}
|
|
}
|
|
|
|
func (f *fragment) rangeEQ(bitDepth uint, predicate uint64) (*Row, error) {
|
|
// Start with set of columns with values set.
|
|
b := f.row(uint64(bitDepth))
|
|
|
|
// Filter any bits that don't match the current bit value.
|
|
for i := int(bitDepth - 1); i >= 0; i-- {
|
|
row := f.row(uint64(i))
|
|
bit := (predicate >> uint(i)) & 1
|
|
|
|
if bit == 1 {
|
|
b = b.Intersect(row)
|
|
} else {
|
|
b = b.Difference(row)
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (f *fragment) rangeNEQ(bitDepth uint, predicate uint64) (*Row, error) {
|
|
// Start with set of columns with values set.
|
|
b := f.row(uint64(bitDepth))
|
|
|
|
// Get the equal bitmap.
|
|
eq, err := f.rangeEQ(bitDepth, predicate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Not-null minus the equal bitmap.
|
|
b = b.Difference(eq)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (f *fragment) rangeLT(bitDepth uint, predicate uint64, allowEquality bool) (*Row, error) {
|
|
keep := NewRow()
|
|
|
|
// Start with set of columns with values set.
|
|
b := f.row(uint64(bitDepth))
|
|
|
|
// Filter any bits that don't match the current bit value.
|
|
leadingZeros := true
|
|
for i := int(bitDepth - 1); i >= 0; i-- {
|
|
row := f.row(uint64(i))
|
|
bit := (predicate >> uint(i)) & 1
|
|
|
|
// Remove any columns with higher bits set.
|
|
if leadingZeros {
|
|
if bit == 0 {
|
|
b = b.Difference(row)
|
|
continue
|
|
} else {
|
|
leadingZeros = false
|
|
}
|
|
}
|
|
|
|
// Handle last bit differently.
|
|
// If bit is zero then return only already kept columns.
|
|
// If bit is one then remove any one columns.
|
|
if i == 0 && !allowEquality {
|
|
if bit == 0 {
|
|
return keep, nil
|
|
}
|
|
return b.Difference(row.Difference(keep)), nil
|
|
}
|
|
|
|
// If bit is zero then remove all set columns not in excluded bitmap.
|
|
if bit == 0 {
|
|
b = b.Difference(row.Difference(keep))
|
|
continue
|
|
}
|
|
|
|
// If bit is set then add columns for set bits to exclude.
|
|
// Don't bother to compute this on the final iteration.
|
|
if i > 0 {
|
|
keep = keep.Union(b.Difference(row))
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (f *fragment) rangeGT(bitDepth uint, predicate uint64, allowEquality bool) (*Row, error) {
|
|
b := f.row(uint64(bitDepth))
|
|
keep := NewRow()
|
|
|
|
// Filter any bits that don't match the current bit value.
|
|
for i := int(bitDepth - 1); i >= 0; i-- {
|
|
row := f.row(uint64(i))
|
|
bit := (predicate >> uint(i)) & 1
|
|
|
|
// Handle last bit differently.
|
|
// If bit is one then return only already kept columns.
|
|
// If bit is zero then remove any unset columns.
|
|
if i == 0 && !allowEquality {
|
|
if bit == 1 {
|
|
return keep, nil
|
|
}
|
|
return b.Difference(b.Difference(row).Difference(keep)), nil
|
|
}
|
|
|
|
// If bit is set then remove all unset columns not already kept.
|
|
if bit == 1 {
|
|
b = b.Difference(b.Difference(row).Difference(keep))
|
|
continue
|
|
}
|
|
|
|
// If bit is unset then add columns with set bit to keep.
|
|
// Don't bother to compute this on the final iteration.
|
|
if i > 0 {
|
|
keep = keep.Union(b.Intersect(row))
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// notNull returns the not-null row (stored at bitDepth).
|
|
func (f *fragment) notNull(bitDepth uint) (*Row, error) {
|
|
return f.row(uint64(bitDepth)), nil
|
|
}
|
|
|
|
// rangeBetween returns bitmaps with a bsiGroup value encoding matching any value between predicateMin and predicateMax.
|
|
func (f *fragment) rangeBetween(bitDepth uint, predicateMin, predicateMax uint64) (*Row, error) {
|
|
b := f.row(uint64(bitDepth))
|
|
keep1 := NewRow() // GTE
|
|
keep2 := NewRow() // LTE
|
|
|
|
// Filter any bits that don't match the current bit value.
|
|
for i := int(bitDepth - 1); i >= 0; i-- {
|
|
row := f.row(uint64(i))
|
|
bit1 := (predicateMin >> uint(i)) & 1
|
|
bit2 := (predicateMax >> uint(i)) & 1
|
|
|
|
// GTE predicateMin
|
|
// If bit is set then remove all unset columns not already kept.
|
|
if bit1 == 1 {
|
|
b = b.Difference(b.Difference(row).Difference(keep1))
|
|
} else {
|
|
// If bit is unset then add columns with set bit to keep.
|
|
// Don't bother to compute this on the final iteration.
|
|
if i > 0 {
|
|
keep1 = keep1.Union(b.Intersect(row))
|
|
}
|
|
}
|
|
|
|
// LTE predicateMin
|
|
// If bit is zero then remove all set bits not in excluded bitmap.
|
|
if bit2 == 0 {
|
|
b = b.Difference(row.Difference(keep2))
|
|
} else {
|
|
// If bit is set then add columns for set bits to exclude.
|
|
// Don't bother to compute this on the final iteration.
|
|
if i > 0 {
|
|
keep2 = keep2.Union(b.Difference(row))
|
|
}
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// pos translates the row ID and column ID into a position in the storage bitmap.
|
|
func (f *fragment) pos(rowID, columnID uint64) (uint64, error) {
|
|
// Return an error if the column ID is out of the range of the fragment's shard.
|
|
minColumnID := f.shard * ShardWidth
|
|
if columnID < minColumnID || columnID >= minColumnID+ShardWidth {
|
|
return 0, errors.Errorf("column:%d out of bounds", columnID)
|
|
}
|
|
return pos(rowID, columnID), nil
|
|
}
|
|
|
|
// forEachBit executes fn for every bit set in the fragment.
|
|
// Errors returned from fn are passed through.
|
|
func (f *fragment) forEachBit(fn func(rowID, columnID uint64) error) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
var err error
|
|
f.storage.ForEach(func(i uint64) {
|
|
// Skip if an error has already occurred.
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Invoke caller's function.
|
|
err = fn(i/ShardWidth, (f.shard*ShardWidth)+(i%ShardWidth))
|
|
})
|
|
return err
|
|
}
|
|
|
|
// top returns the top rows from the fragment.
|
|
// If opt.Src is specified then only rows which intersect src are returned.
|
|
// If opt.FilterValues exist then the row attribute specified by field is matched.
|
|
func (f *fragment) top(opt topOptions) ([]Pair, error) {
|
|
// Retrieve pairs. If no row ids specified then return from cache.
|
|
pairs := f.topBitmapPairs(opt.RowIDs)
|
|
|
|
// If row ids are provided, we don't want to truncate the result set
|
|
if len(opt.RowIDs) > 0 {
|
|
opt.N = 0
|
|
}
|
|
|
|
// Create a fast lookup of filter values.
|
|
var filters map[interface{}]struct{}
|
|
if opt.FilterName != "" && len(opt.FilterValues) > 0 {
|
|
filters = make(map[interface{}]struct{})
|
|
for _, v := range opt.FilterValues {
|
|
filters[v] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Use `tanimotoThreshold > 0` to indicate whether or not we are considering Tanimoto.
|
|
var tanimotoThreshold uint64
|
|
var minTanimoto, maxTanimoto float64
|
|
var srcCount uint64
|
|
if opt.TanimotoThreshold > 0 && opt.Src != nil {
|
|
tanimotoThreshold = opt.TanimotoThreshold
|
|
srcCount = opt.Src.Count()
|
|
minTanimoto = float64(srcCount*tanimotoThreshold) / 100
|
|
maxTanimoto = float64(srcCount*100) / float64(tanimotoThreshold)
|
|
}
|
|
|
|
// Iterate over rankings and add to results until we have enough.
|
|
results := &pairHeap{}
|
|
for _, pair := range pairs {
|
|
rowID, cnt := pair.ID, pair.Count
|
|
|
|
// Ignore empty rows.
|
|
if cnt == 0 {
|
|
continue
|
|
}
|
|
|
|
// Check against either Tanimoto threshold or minimum threshold.
|
|
if tanimotoThreshold > 0 {
|
|
// Ignore counts outside of the Tanimoto min/max values.
|
|
if float64(cnt) <= minTanimoto || float64(cnt) >= maxTanimoto {
|
|
continue
|
|
}
|
|
} else {
|
|
// Ignore counts less than MinThreshold.
|
|
if cnt < opt.MinThreshold {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Apply filter, if set.
|
|
if filters != nil {
|
|
attr, err := f.RowAttrStore.Attrs(rowID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting attrs")
|
|
} else if attr == nil {
|
|
continue
|
|
} else if attrValue := attr[opt.FilterName]; attrValue == nil {
|
|
continue
|
|
} else if _, ok := filters[attrValue]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// The initial n pairs should simply be added to the results.
|
|
if opt.N == 0 || results.Len() < opt.N {
|
|
// Calculate count and append.
|
|
count := cnt
|
|
if opt.Src != nil {
|
|
count = opt.Src.intersectionCount(f.row(rowID))
|
|
}
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
|
|
// Check against either Tanimoto threshold or minimum threshold.
|
|
if tanimotoThreshold > 0 {
|
|
tanimoto := math.Ceil(float64(count*100) / float64(cnt+srcCount-count))
|
|
if tanimoto <= float64(tanimotoThreshold) {
|
|
continue
|
|
}
|
|
} else {
|
|
if count < opt.MinThreshold {
|
|
continue
|
|
}
|
|
}
|
|
|
|
heap.Push(results, Pair{ID: rowID, Count: count})
|
|
|
|
// If we reach the requested number of pairs and we are not computing
|
|
// intersections then simply exit. If we are intersecting then sort
|
|
// and then only keep pairs that are higher than the lowest count.
|
|
if opt.N > 0 && results.Len() == opt.N {
|
|
if opt.Src == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// Retrieve the lowest count we have.
|
|
// If it's too low then don't try finding anymore pairs.
|
|
threshold := results.Pairs[0].Count
|
|
|
|
// If the row doesn't have enough columns set before the intersection
|
|
// then we can assume that any remaining rows also have a count too low.
|
|
if threshold < opt.MinThreshold || cnt < threshold {
|
|
break
|
|
}
|
|
|
|
// Calculate the intersecting column count and skip if it's below our
|
|
// last row in our current result set.
|
|
count := opt.Src.intersectionCount(f.row(rowID))
|
|
if count < threshold {
|
|
continue
|
|
}
|
|
|
|
heap.Push(results, Pair{ID: rowID, Count: count})
|
|
}
|
|
|
|
//Pop first opt.N elements out of heap
|
|
r := make(Pairs, results.Len())
|
|
x := results.Len()
|
|
i := 1
|
|
for results.Len() > 0 {
|
|
r[x-i] = heap.Pop(results).(Pair)
|
|
i++
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (f *fragment) topBitmapPairs(rowIDs []uint64) []bitmapPair {
|
|
// Don't retrieve from storage if CacheTypeNone.
|
|
if f.CacheType == CacheTypeNone {
|
|
return f.cache.Top()
|
|
}
|
|
// If no specific rows are requested, retrieve top rows.
|
|
if len(rowIDs) == 0 {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.cache.Invalidate()
|
|
return f.cache.Top()
|
|
}
|
|
|
|
// Otherwise retrieve specific rows.
|
|
pairs := make([]bitmapPair, 0, len(rowIDs))
|
|
for _, rowID := range rowIDs {
|
|
// Look up cache first, if available.
|
|
if n := f.cache.Get(rowID); n > 0 {
|
|
pairs = append(pairs, bitmapPair{
|
|
ID: rowID,
|
|
Count: n,
|
|
})
|
|
continue
|
|
}
|
|
|
|
row := f.row(rowID)
|
|
if row.Count() > 0 {
|
|
// Otherwise load from storage.
|
|
pairs = append(pairs, bitmapPair{
|
|
ID: rowID,
|
|
Count: row.Count(),
|
|
})
|
|
}
|
|
}
|
|
sort.Sort(bitmapPairs(pairs))
|
|
return pairs
|
|
}
|
|
|
|
// topOptions represents options passed into the Top() function.
|
|
type topOptions struct {
|
|
// Number of rows to return.
|
|
N int
|
|
|
|
// Bitmap to intersect with.
|
|
Src *Row
|
|
|
|
// Specific rows to filter against.
|
|
RowIDs []uint64
|
|
MinThreshold uint64
|
|
|
|
// Filter field name & values.
|
|
FilterName string
|
|
FilterValues []interface{}
|
|
TanimotoThreshold uint64
|
|
}
|
|
|
|
// Checksum returns a checksum for the entire fragment.
|
|
// If two fragments have the same checksum then they have the same data.
|
|
func (f *fragment) Checksum() []byte {
|
|
h := xxhash.New()
|
|
for _, block := range f.Blocks() {
|
|
h.Write(block.Checksum)
|
|
}
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// InvalidateChecksums clears all cached block checksums.
|
|
func (f *fragment) InvalidateChecksums() {
|
|
f.mu.Lock()
|
|
f.checksums = make(map[int][]byte)
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
// Blocks returns info for all blocks containing data.
|
|
func (f *fragment) Blocks() []FragmentBlock {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
var a []FragmentBlock
|
|
|
|
// Initialize the iterator.
|
|
itr := f.storage.Iterator()
|
|
itr.Seek(0)
|
|
|
|
// Initialize block hasher.
|
|
h := newBlockHasher()
|
|
|
|
// Iterate over each value in the fragment.
|
|
v, eof := itr.Next()
|
|
if eof {
|
|
return nil
|
|
}
|
|
blockID := int(v / (HashBlockSize * ShardWidth))
|
|
for {
|
|
// Check for multiple block checksums in a row.
|
|
if n := f.readContiguousChecksums(&a, blockID); n > 0 {
|
|
itr.Seek(uint64(blockID+n) * HashBlockSize * ShardWidth)
|
|
v, eof = itr.Next()
|
|
if eof {
|
|
break
|
|
}
|
|
blockID = int(v / (HashBlockSize * ShardWidth))
|
|
continue
|
|
}
|
|
|
|
// Reset hasher.
|
|
h.blockID = blockID
|
|
h.Reset()
|
|
|
|
// Read all values for the block.
|
|
for ; ; v, eof = itr.Next() {
|
|
// Once we hit the next block, save the value for the next iteration.
|
|
blockID = int(v / (HashBlockSize * ShardWidth))
|
|
if blockID != h.blockID || eof {
|
|
break
|
|
}
|
|
|
|
h.WriteValue(v)
|
|
}
|
|
|
|
// Cache checksum.
|
|
chksum := h.Sum()
|
|
f.checksums[h.blockID] = chksum
|
|
|
|
// Append block.
|
|
a = append(a, FragmentBlock{
|
|
ID: h.blockID,
|
|
Checksum: chksum,
|
|
})
|
|
|
|
// Exit if we're at the end.
|
|
if eof {
|
|
break
|
|
}
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
// readContiguousChecksums appends multiple checksums in a row and returns the count added.
|
|
func (f *fragment) readContiguousChecksums(a *[]FragmentBlock, blockID int) (n int) {
|
|
for i := 0; ; i++ {
|
|
chksum := f.checksums[blockID+i]
|
|
if chksum == nil {
|
|
return i
|
|
}
|
|
|
|
*a = append(*a, FragmentBlock{
|
|
ID: blockID + i,
|
|
Checksum: chksum,
|
|
})
|
|
}
|
|
}
|
|
|
|
// blockData returns bits in a block as row & column ID pairs.
|
|
func (f *fragment) blockData(id int) (rowIDs, columnIDs []uint64) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.storage.ForEachRange(uint64(id)*HashBlockSize*ShardWidth, (uint64(id)+1)*HashBlockSize*ShardWidth, func(i uint64) {
|
|
rowIDs = append(rowIDs, i/ShardWidth)
|
|
columnIDs = append(columnIDs, i%ShardWidth)
|
|
})
|
|
return rowIDs, columnIDs
|
|
}
|
|
|
|
// mergeBlock compares the block's bits and computes a diff with another set of block bits.
|
|
// The state of a bit is determined by consensus from all blocks being considered.
|
|
//
|
|
// For example, if 3 blocks are compared and two have a set bit and one has a
|
|
// cleared bit then the bit is considered cleared. The function returns the
|
|
// diff per incoming block so that all can be in sync.
|
|
func (f *fragment) mergeBlock(id int, data []pairSet) (sets, clears []pairSet, err error) {
|
|
// Ensure that all pair sets are of equal length.
|
|
for i := range data {
|
|
if len(data[i].rowIDs) != len(data[i].columnIDs) {
|
|
return nil, nil, fmt.Errorf("pair set mismatch(idx=%d): %d != %d", i, len(data[i].rowIDs), len(data[i].columnIDs))
|
|
}
|
|
}
|
|
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Track sets and clears for all blocks (including local).
|
|
sets = make([]pairSet, len(data)+1)
|
|
clears = make([]pairSet, len(data)+1)
|
|
|
|
// Limit upper row/column pair.
|
|
maxRowID := uint64(id+1) * HashBlockSize
|
|
maxColumnID := uint64(ShardWidth)
|
|
|
|
// Create buffered iterator for local block.
|
|
itrs := make([]*bufIterator, 1, len(data)+1)
|
|
itrs[0] = newBufIterator(
|
|
newLimitIterator(
|
|
newRoaringIterator(f.storage.Iterator()), maxRowID, maxColumnID,
|
|
),
|
|
)
|
|
|
|
// Append buffered iterators for each incoming block.
|
|
for i := range data {
|
|
var itr iterator = newSliceIterator(data[i].rowIDs, data[i].columnIDs)
|
|
itr = newLimitIterator(itr, maxRowID, maxColumnID)
|
|
itrs = append(itrs, newBufIterator(itr))
|
|
}
|
|
|
|
// Seek to initial pair.
|
|
for _, itr := range itrs {
|
|
itr.Seek(uint64(id)*HashBlockSize, 0)
|
|
}
|
|
|
|
// Determine the number of blocks needed to meet consensus.
|
|
// If there is an even split then a set is used.
|
|
majorityN := (len(itrs) + 1) / 2
|
|
|
|
// Iterate over all values in all iterators to determine differences.
|
|
values := make([]bool, len(itrs))
|
|
for {
|
|
var min struct {
|
|
rowID uint64
|
|
columnID uint64
|
|
}
|
|
|
|
// Find the lowest pair.
|
|
var hasData bool
|
|
for _, itr := range itrs {
|
|
bid, pid, eof := itr.Peek()
|
|
if eof { // no more data
|
|
continue
|
|
} else if !hasData { // first pair
|
|
min.rowID, min.columnID, hasData = bid, pid, true
|
|
} else if bid < min.rowID || (bid == min.rowID && pid < min.columnID) { // lower pair
|
|
min.rowID, min.columnID = bid, pid
|
|
}
|
|
}
|
|
|
|
// If all iterators are EOF then exit.
|
|
if !hasData {
|
|
break
|
|
}
|
|
|
|
// Determine consensus of point.
|
|
var setN int
|
|
for i, itr := range itrs {
|
|
bid, pid, eof := itr.Next()
|
|
|
|
values[i] = !eof && bid == min.rowID && pid == min.columnID
|
|
if values[i] {
|
|
setN++ // set
|
|
} else {
|
|
itr.Unread() // clear
|
|
}
|
|
}
|
|
|
|
// Determine consensus value.
|
|
newValue := setN >= majorityN
|
|
|
|
// Add a diff for any node with a different value.
|
|
for i := range itrs {
|
|
// Value matches, ignore.
|
|
if values[i] == newValue {
|
|
continue
|
|
}
|
|
|
|
// Append to either the set or clear diff.
|
|
if newValue {
|
|
sets[i].rowIDs = append(sets[i].rowIDs, min.rowID)
|
|
sets[i].columnIDs = append(sets[i].columnIDs, min.columnID)
|
|
} else {
|
|
clears[i].rowIDs = append(sets[i].rowIDs, min.rowID)
|
|
clears[i].columnIDs = append(sets[i].columnIDs, min.columnID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set local bits.
|
|
for i := range sets[0].columnIDs {
|
|
if _, err := f.unprotectedSetBit(sets[0].rowIDs[i], (f.shard*ShardWidth)+sets[0].columnIDs[i]); err != nil {
|
|
return nil, nil, errors.Wrap(err, "setting")
|
|
}
|
|
}
|
|
|
|
// Clear local bits.
|
|
for i := range clears[0].columnIDs {
|
|
if _, err := f.unprotectedClearBit(clears[0].rowIDs[i], (f.shard*ShardWidth)+clears[0].columnIDs[i]); err != nil {
|
|
return nil, nil, errors.Wrap(err, "clearing")
|
|
}
|
|
}
|
|
|
|
return sets[1:], clears[1:], nil
|
|
}
|
|
|
|
// bulkImport bulk imports a set of bits and then snapshots the storage.
|
|
// The cache is updated to reflect the new data.
|
|
func (f *fragment) bulkImport(rowIDs, columnIDs []uint64, options *ImportOptions) error {
|
|
// Verify that there are an equal number of row ids and column ids.
|
|
if len(rowIDs) != len(columnIDs) {
|
|
return fmt.Errorf("mismatch of row/column len: %d != %d", len(rowIDs), len(columnIDs))
|
|
}
|
|
|
|
if f.mutexVector != nil && !options.Clear {
|
|
return f.bulkImportMutex(rowIDs, columnIDs)
|
|
}
|
|
return f.bulkImportStandard(rowIDs, columnIDs, options)
|
|
}
|
|
|
|
// bulkImportStandard performs a bulk import on a standard fragment. May mutate
|
|
// its rowIDs and columnIDs arguments.
|
|
func (f *fragment) bulkImportStandard(rowIDs, columnIDs []uint64, options *ImportOptions) (err error) {
|
|
// rowSet maintains the set of rowIDs present in this import. It allows the
|
|
// cache to be updated once per row, instead of once per bit. TODO: consider
|
|
// sorting by rowID/columnID first and avoiding the map allocation here. (we
|
|
// could reuse rowIDs to store the list of unique row IDs)
|
|
rowSet := make(map[uint64]struct{})
|
|
lastRowID := uint64(1 << 63)
|
|
|
|
// replace columnIDs with calculated positions to avoid allocation.
|
|
for i := 0; i < len(columnIDs); i++ {
|
|
rowID, columnID := rowIDs[i], columnIDs[i]
|
|
pos, err := f.pos(rowID, columnID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
columnIDs[i] = pos
|
|
|
|
// Add row to rowSet.
|
|
if rowID != lastRowID {
|
|
lastRowID = rowID
|
|
rowSet[rowID] = struct{}{}
|
|
}
|
|
}
|
|
positions := columnIDs
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if options.Clear {
|
|
err = f.importPositions(nil, positions, rowSet)
|
|
} else {
|
|
err = f.importPositions(positions, nil, rowSet)
|
|
}
|
|
return errors.Wrap(err, "bulkImportStandard")
|
|
}
|
|
|
|
// importPositions takes slices of positions within the fragment to set and
|
|
// clear in storage. One must also pass in the set of unique rows which are
|
|
// affected by the set and clear operations. It is unprotected (f.mu must be
|
|
// locked when calling it). No position should appear in both set and clear.
|
|
//
|
|
// importPositions tries to intelligently decide whether or not to do a full
|
|
// snapshot of the fragment or just do in-memory updates while appending
|
|
// operations to the op log.
|
|
func (f *fragment) importPositions(set, clear []uint64, rowSet map[uint64]struct{}) error {
|
|
smallWrite := false
|
|
if len(set)+len(clear)+f.opN < f.MaxOpN {
|
|
smallWrite = true
|
|
mustClose, err := f.reopen()
|
|
if err != nil {
|
|
return errors.Wrap(err, "reopening")
|
|
}
|
|
if mustClose {
|
|
defer f.safeClose()
|
|
}
|
|
|
|
} else {
|
|
f.storage.OpWriter = nil
|
|
}
|
|
|
|
if len(set) > 0 {
|
|
f.stats.Count("ImportingN", int64(len(set)), 1)
|
|
changedN, err := f.storage.AddN(set...) // TODO benchmark Add/RemoveN behavior with sorted/unsorted positions
|
|
if err != nil {
|
|
return errors.Wrap(err, "adding positions")
|
|
}
|
|
f.stats.Count("ImportedN", int64(changedN), 1)
|
|
f.opN += changedN
|
|
}
|
|
|
|
if len(clear) > 0 {
|
|
f.stats.Count("ClearingN", int64(len(clear)), 1)
|
|
changedN, err := f.storage.RemoveN(clear...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "clearing positions")
|
|
}
|
|
f.stats.Count("ClearedN", int64(changedN), 1)
|
|
f.opN += changedN
|
|
}
|
|
|
|
// Update cache counts for all affected rows.
|
|
for rowID := range rowSet {
|
|
// Invalidate block checksum.
|
|
delete(f.checksums, int(rowID/HashBlockSize))
|
|
|
|
n := f.storage.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth)
|
|
f.cache.BulkAdd(rowID, n)
|
|
|
|
if smallWrite {
|
|
if _, ok := f.rowCache.Fetch(rowID); ok { // we won't update the rowCache if it wasn't already in there.
|
|
f.rowCache.Add(rowID, f.rowFromStorage(rowID))
|
|
}
|
|
}
|
|
}
|
|
|
|
f.cache.Recalculate()
|
|
|
|
if !smallWrite {
|
|
return f.snapshot()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bulkImportMutex performs a bulk import on a fragment while ensuring
|
|
// mutex restrictions. Because the mutex requirements must be checked
|
|
// against storage, this method must acquire a write lock on the fragment
|
|
// during the entire process, and it handles every bit independently.
|
|
func (f *fragment) bulkImportMutex(rowIDs, columnIDs []uint64) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
rowSet := make(map[uint64]struct{})
|
|
// we have to maintain which columns are getting bits set as a map so that
|
|
// we don't end up setting multiple bits in the same column if a column is
|
|
// repeated within the import.
|
|
colSet := make(map[uint64]uint64)
|
|
|
|
// Since each imported bit will at most set one bit and clear one bit, we
|
|
// can reuse the rowIDs and columnIDs slices as the set and clear slice
|
|
// arguments to importPositions. The set positions we'll get from the
|
|
// colSet, but we maintain clearIdx as we loop through row and col ids so
|
|
// that we know how many bits we need to clear and how far through columnIDs
|
|
// we are.
|
|
clearIdx := 0
|
|
for i := range rowIDs {
|
|
rowID, columnID := rowIDs[i], columnIDs[i]
|
|
if existingRowID, found, err := f.mutexVector.Get(columnID); err != nil {
|
|
return errors.Wrap(err, "getting mutex vector data")
|
|
} else if found && existingRowID != rowID {
|
|
// Determine the position of the bit in the storage.
|
|
clearPos, err := f.pos(existingRowID, columnID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
columnIDs[clearIdx] = clearPos
|
|
clearIdx++
|
|
|
|
rowSet[existingRowID] = struct{}{}
|
|
} else if found && existingRowID == rowID {
|
|
continue
|
|
}
|
|
pos, err := f.pos(rowID, columnID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
colSet[columnID] = pos
|
|
rowSet[rowID] = struct{}{}
|
|
}
|
|
|
|
// re-use rowIDs by populating positions to set from colSet.
|
|
i := 0
|
|
for _, pos := range colSet {
|
|
rowIDs[i] = pos
|
|
i++
|
|
}
|
|
toSet := rowIDs[:i]
|
|
toClear := columnIDs[:clearIdx]
|
|
|
|
return errors.Wrap(f.importPositions(toSet, toClear, rowSet), "importing positions")
|
|
}
|
|
|
|
func (f *fragment) importValueSmallWrite(columnIDs, values []uint64, bitDepth uint, clear bool) error {
|
|
// TODO figure out how to avoid re-allocating these each time. Probably
|
|
// possible to store them on the fragment with a capacity based on
|
|
// MaxOpN. For now, we know that the total number of bits to be
|
|
// set+cleared is len(values)*(bitDepth+1), so we make each slice
|
|
// slightly more than half of that to try to avoid reallocation.
|
|
toSet := make([]uint64, 0, len(columnIDs)*int(bitDepth+1)*(5/8))
|
|
toClear := make([]uint64, 0, len(columnIDs)*int(bitDepth+1)*(5/8))
|
|
colSet := make(map[uint64]struct{}, len(columnIDs))
|
|
|
|
if err := func() (err error) {
|
|
for i := len(columnIDs) - 1; i >= 0; i-- {
|
|
columnID, value := columnIDs[i], values[i]
|
|
if _, ok := colSet[columnID]; ok {
|
|
continue
|
|
}
|
|
colSet[columnID] = struct{}{}
|
|
toSet, toClear, err = f.positionsForValue(columnID, bitDepth, value, clear, toSet, toClear)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting positions for value")
|
|
}
|
|
}
|
|
return nil
|
|
}(); err != nil {
|
|
_ = f.closeStorage()
|
|
_ = f.openStorage()
|
|
return err
|
|
}
|
|
rowSet := make(map[uint64]struct{}, bitDepth+1)
|
|
for i := uint(0); i < bitDepth+1; i++ {
|
|
rowSet[uint64(i)] = struct{}{}
|
|
}
|
|
err := f.importPositions(toSet, toClear, rowSet)
|
|
return errors.Wrap(err, "importing positions")
|
|
}
|
|
|
|
// importValue bulk imports a set of range-encoded values.
|
|
func (f *fragment) importValue(columnIDs, values []uint64, bitDepth uint, clear bool) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Verify that there are an equal number of column ids and values.
|
|
if len(columnIDs) != len(values) {
|
|
return fmt.Errorf("mismatch of column/value len: %d != %d", len(columnIDs), len(values))
|
|
}
|
|
|
|
if len(columnIDs)*int(bitDepth+1)+f.opN < f.MaxOpN {
|
|
return errors.Wrap(f.importValueSmallWrite(columnIDs, values, bitDepth, clear), "import small write")
|
|
|
|
}
|
|
|
|
f.storage.OpWriter = nil
|
|
|
|
// Process every value.
|
|
// If an error occurs then reopen the storage.
|
|
if err := func() (err error) {
|
|
for i := range columnIDs {
|
|
columnID, value := columnIDs[i], values[i]
|
|
if _, err := f.importSetValue(columnID, bitDepth, value, clear); err != nil {
|
|
return errors.Wrapf(err, "importSetValue")
|
|
}
|
|
}
|
|
return nil
|
|
}(); err != nil {
|
|
_ = f.closeStorage()
|
|
_ = f.openStorage()
|
|
return err
|
|
}
|
|
|
|
err := f.snapshot()
|
|
return errors.Wrap(err, "snapshotting")
|
|
}
|
|
|
|
// importRoaring imports from the official roaring data format defined at
|
|
// https://github.com/RoaringBitmap/RoaringFormatSpec or from pilosa's version
|
|
// of the roaring format. The cache is updated to reflect the new data.
|
|
func (f *fragment) importRoaring(data []byte, clear bool) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
bm := roaring.NewBTreeBitmap()
|
|
err := bm.UnmarshalBinary(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// get a list of keys in order to update the cache
|
|
iter, _ := bm.Containers.Iterator(0)
|
|
rowSet := make(map[uint64]struct{})
|
|
var lastRow uint64 = math.MaxUint64
|
|
|
|
incomingCnt := uint64(0)
|
|
for iter.Next() {
|
|
key, c := iter.Value()
|
|
incomingCnt += uint64(c.N())
|
|
|
|
// virtual row for the current container
|
|
vRow := key >> shardVsContainerExponent
|
|
|
|
// skip dups
|
|
if vRow == lastRow {
|
|
continue
|
|
}
|
|
rowSet[vRow] = struct{}{}
|
|
lastRow = vRow
|
|
}
|
|
|
|
// take smallPath? TODO - ideally instead of checking f.storage.Any(), the
|
|
// test here would be if the storage size (in bytes) is significantly
|
|
// greater than the size of the incoming bits serialized as append
|
|
// operations. Getting the storage size might be a bit expensive though
|
|
// especially if the fragment isn't mapped.
|
|
if incomingCnt+uint64(f.opN) <= uint64(f.MaxOpN) && f.storage.Any() {
|
|
toSet, toClear := bm.Slice(), []uint64{}
|
|
if clear {
|
|
toSet, toClear = toClear, toSet
|
|
}
|
|
return f.importPositions(toSet, toClear, rowSet)
|
|
}
|
|
|
|
if clear {
|
|
bm = f.storage.Difference(bm)
|
|
} else if f.storage.Containers.Size() >= bm.Containers.Size() {
|
|
f.storage.UnionInPlace(bm)
|
|
bm = f.storage
|
|
} else {
|
|
bm.UnionInPlace(f.storage)
|
|
}
|
|
|
|
for rowID := range rowSet {
|
|
n := bm.CountRange(rowID*ShardWidth, (rowID+1)*ShardWidth)
|
|
f.cache.BulkAdd(rowID, n)
|
|
}
|
|
f.cache.Recalculate()
|
|
|
|
err = unprotectedWriteToFragment(f, bm)
|
|
return err
|
|
}
|
|
|
|
// incrementOpN increase the operation count by one.
|
|
// If the count exceeds the maximum allowed then a snapshot is performed.
|
|
func (f *fragment) incrementOpN() error {
|
|
f.opN++
|
|
if f.opN <= f.MaxOpN {
|
|
return nil
|
|
}
|
|
|
|
if err := f.snapshot(); err != nil {
|
|
return fmt.Errorf("snapshot: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Snapshot writes the storage bitmap to disk and reopens it.
|
|
func (f *fragment) Snapshot() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.snapshot()
|
|
}
|
|
func track(start time.Time, message string, stats stats.StatsClient, logger logger.Logger) {
|
|
elapsed := time.Since(start)
|
|
logger.Printf("%s took %s", message, elapsed)
|
|
stats.Histogram("snapshot", elapsed.Seconds(), 1.0)
|
|
}
|
|
|
|
func (f *fragment) snapshot() error {
|
|
return unprotectedWriteToFragment(f, f.storage)
|
|
}
|
|
|
|
// unprotectedWriteToFragment writes the fragment f with bm as the data. It is unprotected, and
|
|
// f.mu must be locked when calling it.
|
|
func unprotectedWriteToFragment(f *fragment, bm *roaring.Bitmap) error { // nolint: interfacer
|
|
|
|
completeMessage := fmt.Sprintf("fragment: snapshot complete %s/%s/%s/%d", f.index, f.field, f.view, f.shard)
|
|
start := time.Now()
|
|
defer track(start, completeMessage, f.stats, f.Logger)
|
|
|
|
// Create a temporary file to snapshot to.
|
|
snapshotPath := f.path + snapshotExt
|
|
file, err := os.Create(snapshotPath)
|
|
if err != nil {
|
|
return fmt.Errorf("create snapshot file: %s", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Write storage to snapshot.
|
|
bw := bufio.NewWriter(file)
|
|
if _, err := bm.WriteTo(bw); err != nil {
|
|
return fmt.Errorf("snapshot write to: %s", err)
|
|
}
|
|
|
|
if err := bw.Flush(); err != nil {
|
|
return fmt.Errorf("flush: %s", err)
|
|
}
|
|
|
|
// Close current storage.
|
|
if err := f.closeStorage(); err != nil {
|
|
return fmt.Errorf("close storage: %s", err)
|
|
}
|
|
|
|
// Move snapshot to data file location.
|
|
if err := os.Rename(snapshotPath, f.path); err != nil {
|
|
return fmt.Errorf("rename snapshot: %s", err)
|
|
}
|
|
|
|
// Reopen storage.
|
|
if err := f.openStorage(); err != nil {
|
|
return fmt.Errorf("open storage: %s", err)
|
|
}
|
|
|
|
// Reset operation count.
|
|
f.opN = 0
|
|
|
|
return nil
|
|
}
|
|
|
|
// RecalculateCache rebuilds the cache regardless of invalidate time delay.
|
|
func (f *fragment) RecalculateCache() {
|
|
f.mu.Lock()
|
|
f.cache.Recalculate()
|
|
f.mu.Unlock()
|
|
}
|
|
|
|
// FlushCache writes the cache data to disk.
|
|
func (f *fragment) FlushCache() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.flushCache()
|
|
}
|
|
|
|
func (f *fragment) flushCache() error {
|
|
if f.cache == nil {
|
|
return nil
|
|
}
|
|
|
|
if f.CacheType == CacheTypeNone {
|
|
return nil
|
|
}
|
|
|
|
// Retrieve a list of row ids from the cache.
|
|
ids := f.cache.IDs()
|
|
|
|
// Marshal cache data to bytes.
|
|
buf, err := proto.Marshal(&internal.Cache{IDs: ids})
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling")
|
|
}
|
|
|
|
// Write to disk.
|
|
if err := ioutil.WriteFile(f.cachePath(), buf, 0666); err != nil {
|
|
return errors.Wrap(err, "writing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteTo writes the fragment's data to w.
|
|
func (f *fragment) WriteTo(w io.Writer) (n int64, err error) {
|
|
// Force cache flush.
|
|
if err := f.FlushCache(); err != nil {
|
|
return 0, errors.Wrap(err, "flushing cache")
|
|
}
|
|
|
|
// Write out data and cache to a tar archive.
|
|
tw := tar.NewWriter(w)
|
|
if err := f.writeStorageToArchive(tw); err != nil {
|
|
return 0, fmt.Errorf("write storage: %s", err)
|
|
}
|
|
if err := f.writeCacheToArchive(tw); err != nil {
|
|
return 0, fmt.Errorf("write cache: %s", err)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func (f *fragment) writeStorageToArchive(tw *tar.Writer) error {
|
|
// Open separate file descriptor to read from.
|
|
file, err := os.Open(f.path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening file")
|
|
}
|
|
defer file.Close()
|
|
|
|
// Retrieve the current file size under lock so we don't read
|
|
// while an operation is appending to the end.
|
|
var sz int64
|
|
if err := func() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
fi, err := file.Stat()
|
|
if err != nil {
|
|
return errors.Wrap(err, "statting")
|
|
}
|
|
sz = fi.Size()
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write archive header.
|
|
if err := tw.WriteHeader(&tar.Header{
|
|
Name: "data",
|
|
Mode: 0600,
|
|
Size: sz,
|
|
ModTime: time.Now(),
|
|
}); err != nil {
|
|
return errors.Wrap(err, "writing header")
|
|
}
|
|
|
|
// Copy the file up to the last known size.
|
|
// This is done outside the lock because the storage format is append-only.
|
|
if _, err := io.CopyN(tw, file, sz); err != nil {
|
|
return errors.Wrap(err, "copying")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fragment) writeCacheToArchive(tw *tar.Writer) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// Read cache into buffer.
|
|
buf, err := ioutil.ReadFile(f.cachePath())
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "reading cache")
|
|
}
|
|
|
|
// Write archive header.
|
|
if err := tw.WriteHeader(&tar.Header{
|
|
Name: "cache",
|
|
Mode: 0600,
|
|
Size: int64(len(buf)),
|
|
ModTime: time.Now(),
|
|
}); err != nil {
|
|
return errors.Wrap(err, "writing header")
|
|
}
|
|
|
|
// Write data to archive.
|
|
if _, err := tw.Write(buf); err != nil {
|
|
return errors.Wrap(err, "writing")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadFrom reads a data file from r and loads it into the fragment.
|
|
func (f *fragment) ReadFrom(r io.Reader) (n int64, err error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
tr := tar.NewReader(r)
|
|
for {
|
|
// Read next tar header.
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return 0, errors.Wrap(err, "opening")
|
|
}
|
|
|
|
// Process file based on file name.
|
|
switch hdr.Name {
|
|
case "data":
|
|
if err := f.readStorageFromArchive(tr); err != nil {
|
|
return 0, errors.Wrap(err, "reading storage")
|
|
}
|
|
case "cache":
|
|
if err := f.readCacheFromArchive(tr); err != nil {
|
|
return 0, errors.Wrap(err, "reading cache")
|
|
}
|
|
default:
|
|
return 0, fmt.Errorf("invalid fragment archive file: %s", hdr.Name)
|
|
}
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
func (f *fragment) readStorageFromArchive(r io.Reader) error {
|
|
// Create a temporary file to copy into.
|
|
path := f.path + copyExt
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating directory")
|
|
}
|
|
defer file.Close()
|
|
|
|
// Copy reader into temporary path.
|
|
if _, err = io.Copy(file, r); err != nil {
|
|
return errors.Wrap(err, "copying")
|
|
}
|
|
|
|
// Close current storage.
|
|
if err := f.closeStorage(); err != nil {
|
|
return errors.Wrap(err, "closing")
|
|
}
|
|
|
|
// Move snapshot to data file location.
|
|
if err := os.Rename(path, f.path); err != nil {
|
|
return errors.Wrap(err, "renaming")
|
|
}
|
|
|
|
// Reopen storage.
|
|
if err := f.openStorage(); err != nil {
|
|
return errors.Wrap(err, "opening")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fragment) readCacheFromArchive(r io.Reader) error {
|
|
// Slurp data from reader and write to disk.
|
|
buf, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading")
|
|
} else if err := ioutil.WriteFile(f.cachePath(), buf, 0666); err != nil {
|
|
return errors.Wrap(err, "writing")
|
|
}
|
|
|
|
// Re-open cache.
|
|
if err := f.openCache(); err != nil {
|
|
return errors.Wrap(err, "opening")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rowFilter is a function signature for controlling iteration over containers
|
|
// in a fragment. It will be invoked on each container found and returns two
|
|
// booleans. The first is whether the row this container is in should be
|
|
// included or skipped, and the second is whether to stop processing or
|
|
// continue.
|
|
type rowFilter func(rowID, key uint64, c *roaring.Container) (include, done bool)
|
|
|
|
// filterWithLimit returns a filter which will only allow a limited number of
|
|
// rows to be returned. It should be applied last so that it is only called (and
|
|
// therefore only updates its internal state) if the row is being included by
|
|
// every other filter.
|
|
func filterWithLimit(limit uint64) rowFilter {
|
|
return func(rowID, key uint64, c *roaring.Container) (include, done bool) {
|
|
if limit > 0 {
|
|
limit--
|
|
return true, false
|
|
}
|
|
return false, true
|
|
}
|
|
}
|
|
|
|
func filterColumn(col uint64) rowFilter {
|
|
return func(rowID, key uint64, c *roaring.Container) (include, done bool) {
|
|
colID := col % ShardWidth
|
|
colKey := ((rowID * ShardWidth) + colID) >> 16
|
|
colVal := uint16(colID & 0xFFFF) // columnID within the container
|
|
return colKey == key && c.Contains(colVal), false
|
|
}
|
|
}
|
|
|
|
// TODO: this works, but it would be more performant if the fragment could seek
|
|
// to the next row in the rows list rather than asking the filter for each
|
|
// container serially. The container iterator would need to expose a seek
|
|
// method, and the rowFilter would need some way of communicating to
|
|
// fragment.rows what the next rowID to seek to is.
|
|
func filterWithRows(rows []uint64) rowFilter {
|
|
loc := 0
|
|
return func(rowID, key uint64, c *roaring.Container) (include, done bool) {
|
|
if loc >= len(rows) {
|
|
return false, true
|
|
}
|
|
i := sort.Search(len(rows[loc:]), func(i int) bool {
|
|
return rows[loc+i] >= rowID
|
|
})
|
|
loc += i
|
|
if loc >= len(rows) {
|
|
return false, true
|
|
}
|
|
if rows[loc] == rowID {
|
|
if loc == len(rows)-1 {
|
|
done = true
|
|
}
|
|
return true, done
|
|
}
|
|
return false, false
|
|
}
|
|
}
|
|
|
|
// rows returns all rows starting from 'start'. Filters will be applied in
|
|
// order. All filters must return true to include the row. Once a row is
|
|
// included, further containers in that row will be skipped. So, for a row to be
|
|
// included, there must be one container in that row where all filters return
|
|
// true. For a row to be skipped, at least one filter must return false for each
|
|
// container in that row (it need not be the same filter for each). Any filter
|
|
// returning done == true will cause processing to stop after all filters for
|
|
// this container have been processed. The rows accumulated up to this point
|
|
// (including this row if all filters passed) will be returned.
|
|
func (f *fragment) rows(start uint64, filters ...rowFilter) []uint64 {
|
|
startKey := rowToKey(start)
|
|
i, _ := f.storage.Containers.Iterator(startKey)
|
|
rows := make([]uint64, 0)
|
|
var lastRow uint64 = math.MaxUint64
|
|
|
|
// Loop over the existing containers.
|
|
for i.Next() {
|
|
key, c := i.Value()
|
|
|
|
// virtual row for the current container
|
|
vRow := key >> shardVsContainerExponent
|
|
|
|
// skip dups
|
|
if vRow == lastRow {
|
|
continue
|
|
}
|
|
|
|
// apply filters
|
|
addRow, done := true, false
|
|
for _, filter := range filters {
|
|
var d bool
|
|
addRow, d = filter(vRow, key, c)
|
|
done = done || d
|
|
if !addRow {
|
|
break
|
|
}
|
|
}
|
|
if addRow {
|
|
lastRow = vRow
|
|
rows = append(rows, vRow)
|
|
}
|
|
if done {
|
|
return rows
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
type rowIterator struct {
|
|
f *fragment
|
|
rowIDs []uint64
|
|
cur int
|
|
wrap bool
|
|
}
|
|
|
|
func (f *fragment) rowIterator(wrap bool, filters ...rowFilter) *rowIterator {
|
|
return &rowIterator{
|
|
f: f,
|
|
rowIDs: f.rows(0, filters...), // TODO: this may be memory intensive in high cardinality cases
|
|
wrap: wrap,
|
|
}
|
|
}
|
|
|
|
func (ri *rowIterator) Seek(rowID uint64) {
|
|
idx := sort.Search(len(ri.rowIDs), func(i int) bool {
|
|
return ri.rowIDs[i] >= rowID
|
|
})
|
|
ri.cur = idx
|
|
}
|
|
|
|
func (ri *rowIterator) Next() (r *Row, rowID uint64, wrapped bool) {
|
|
if ri.cur >= len(ri.rowIDs) {
|
|
if !ri.wrap || len(ri.rowIDs) == 0 {
|
|
return nil, 0, true
|
|
}
|
|
ri.Seek(0)
|
|
wrapped = true
|
|
}
|
|
rowID = ri.rowIDs[ri.cur]
|
|
r = ri.f.row(rowID)
|
|
ri.cur += 1
|
|
return r, rowID, wrapped
|
|
}
|
|
|
|
// FragmentBlock represents info about a subsection of the rows in a block.
|
|
// This is used for comparing data in remote blocks for active anti-entropy.
|
|
type FragmentBlock struct {
|
|
ID int `json:"id"`
|
|
Checksum []byte `json:"checksum"`
|
|
}
|
|
|
|
type blockHasher struct {
|
|
blockID int
|
|
buf [8]byte
|
|
hash hash.Hash
|
|
}
|
|
|
|
func newBlockHasher() blockHasher {
|
|
return blockHasher{
|
|
blockID: -1,
|
|
hash: xxhash.New(),
|
|
}
|
|
}
|
|
func (h *blockHasher) Reset() {
|
|
h.hash.Reset()
|
|
}
|
|
|
|
func (h *blockHasher) Sum() []byte {
|
|
return h.hash.Sum(nil)[:]
|
|
}
|
|
|
|
func (h *blockHasher) WriteValue(v uint64) {
|
|
binary.BigEndian.PutUint64(h.buf[:], v)
|
|
h.hash.Write(h.buf[:])
|
|
}
|
|
|
|
// fragmentSyncer syncs a local fragment to one on a remote host.
|
|
type fragmentSyncer struct {
|
|
Fragment *fragment
|
|
|
|
Node *Node
|
|
Cluster *cluster
|
|
|
|
Closing <-chan struct{}
|
|
}
|
|
|
|
// isClosing returns true if the closing channel is closed.
|
|
func (s *fragmentSyncer) isClosing() bool {
|
|
select {
|
|
case <-s.Closing:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// syncFragment compares checksums for the local and remote fragments and
|
|
// then merges any blocks which have differences.
|
|
func (s *fragmentSyncer) syncFragment() error {
|
|
span, ctx := tracing.StartSpanFromContext(context.Background(), "FragmentSyncer.syncFragment")
|
|
defer span.Finish()
|
|
|
|
// Determine replica set.
|
|
nodes := s.Cluster.shardNodes(s.Fragment.index, s.Fragment.shard)
|
|
if len(nodes) == 1 {
|
|
return nil
|
|
}
|
|
|
|
// Create a set of blocks.
|
|
blockSets := make([][]FragmentBlock, 0, len(nodes))
|
|
for _, node := range nodes {
|
|
// Read local blocks.
|
|
if node.ID == s.Node.ID {
|
|
b := s.Fragment.Blocks()
|
|
blockSets = append(blockSets, b)
|
|
continue
|
|
}
|
|
|
|
// Retrieve remote blocks.
|
|
blocks, err := s.Cluster.InternalClient.FragmentBlocks(ctx, &node.URI, s.Fragment.index, s.Fragment.field, s.Fragment.view, s.Fragment.shard)
|
|
if err != nil && err != ErrFragmentNotFound {
|
|
return errors.Wrap(err, "getting blocks")
|
|
}
|
|
blockSets = append(blockSets, blocks)
|
|
|
|
// Verify sync is not prematurely closing.
|
|
if s.isClosing() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Iterate over all blocks and find differences.
|
|
checksums := make([][]byte, len(nodes))
|
|
for {
|
|
// Find min block id.
|
|
blockID := -1
|
|
for _, blocks := range blockSets {
|
|
if len(blocks) == 0 {
|
|
continue
|
|
} else if blockID == -1 || blocks[0].ID < blockID {
|
|
blockID = blocks[0].ID
|
|
}
|
|
}
|
|
|
|
// Exit loop if no blocks are left.
|
|
if blockID == -1 {
|
|
break
|
|
}
|
|
|
|
// Read the checksum for the current block.
|
|
for i, blocks := range blockSets {
|
|
// Clear checksum if the next block for the node doesn't match current ID.
|
|
if len(blocks) == 0 || blocks[0].ID != blockID {
|
|
checksums[i] = nil
|
|
continue
|
|
}
|
|
|
|
// Otherwise set checksum and move forward.
|
|
checksums[i] = blocks[0].Checksum
|
|
blockSets[i] = blockSets[i][1:]
|
|
}
|
|
|
|
// Ignore if all the blocks on each node match.
|
|
if byteSlicesEqual(checksums) {
|
|
continue
|
|
}
|
|
// Synchronize block.
|
|
if err := s.syncBlock(blockID); err != nil {
|
|
return fmt.Errorf("sync block: id=%d, err=%s", blockID, err)
|
|
}
|
|
s.Fragment.stats.Count("BlockRepair", 1, 1.0)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncBlock sends and receives all rows for a given block.
|
|
// Returns an error if any remote hosts are unreachable.
|
|
func (s *fragmentSyncer) syncBlock(id int) error {
|
|
span, ctx := tracing.StartSpanFromContext(context.Background(), "FragmentSyncer.syncBlock")
|
|
defer span.Finish()
|
|
|
|
f := s.Fragment
|
|
|
|
// Read pairs from each remote block.
|
|
var uris []*URI
|
|
var pairSets []pairSet
|
|
for _, node := range s.Cluster.shardNodes(f.index, f.shard) {
|
|
if s.Node.ID == node.ID {
|
|
continue
|
|
}
|
|
|
|
// Verify sync is not prematurely closing.
|
|
if s.isClosing() {
|
|
return nil
|
|
}
|
|
|
|
uri := &node.URI
|
|
uris = append(uris, uri)
|
|
|
|
// Only sync the standard block.
|
|
rowIDs, columnIDs, err := s.Cluster.InternalClient.BlockData(ctx, &node.URI, f.index, f.field, f.view, f.shard, id)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting block")
|
|
}
|
|
|
|
pairSets = append(pairSets, pairSet{
|
|
columnIDs: columnIDs,
|
|
rowIDs: rowIDs,
|
|
})
|
|
}
|
|
|
|
// Verify sync is not prematurely closing.
|
|
if s.isClosing() {
|
|
return nil
|
|
}
|
|
|
|
// Merge blocks together.
|
|
sets, clears, err := f.mergeBlock(id, pairSets)
|
|
if err != nil {
|
|
return errors.Wrap(err, "merging")
|
|
}
|
|
|
|
// Write updates to remote blocks.
|
|
for i := 0; i < len(uris); i++ {
|
|
set, clear := sets[i], clears[i]
|
|
|
|
// Handle Sets.
|
|
if len(set.columnIDs) > 0 {
|
|
setData, err := bitsToRoaringData(set)
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting bits to roaring data (set)")
|
|
}
|
|
|
|
setReq := &ImportRoaringRequest{
|
|
Clear: false,
|
|
Views: map[string][]byte{cleanViewName(f.view): setData},
|
|
}
|
|
|
|
if err := s.Cluster.InternalClient.ImportRoaring(ctx, uris[i], f.index, f.field, f.shard, true, setReq); err != nil {
|
|
return errors.Wrap(err, "sending roaring data (set)")
|
|
}
|
|
}
|
|
|
|
// Handle Clears.
|
|
if len(clear.columnIDs) > 0 {
|
|
clearData, err := bitsToRoaringData(clear)
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting bits to roaring data (clear)")
|
|
}
|
|
|
|
clearReq := &ImportRoaringRequest{
|
|
Clear: true,
|
|
Views: map[string][]byte{"": clearData},
|
|
}
|
|
|
|
if err := s.Cluster.InternalClient.ImportRoaring(ctx, uris[i], f.index, f.field, f.shard, true, clearReq); err != nil {
|
|
return errors.Wrap(err, "sending roaring data (clear)")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// cleanViewName converts a viewname into the equivalent
|
|
// string required by the external api. Because views are
|
|
// not exposed externally, the conversion looks like this:
|
|
// "standard" -> ""
|
|
// "standard_YYYYMMDD" -> "YYYYMMDD"
|
|
// "other" -> "other" (there is currently not a use for this)
|
|
func cleanViewName(v string) string {
|
|
viewPrefix := viewStandard + "_"
|
|
if strings.HasPrefix(v, viewPrefix) {
|
|
return v[len(viewPrefix):]
|
|
} else if v == viewStandard {
|
|
return ""
|
|
}
|
|
return v
|
|
}
|
|
|
|
// bitsToRoaringData converts a pairSet into a roaring.Bitmap
|
|
// which represents the data within a single shard.
|
|
func bitsToRoaringData(ps pairSet) ([]byte, error) {
|
|
bmp := roaring.NewBitmap()
|
|
for j := 0; j < len(ps.columnIDs); j++ {
|
|
bmp.DirectAdd(ps.rowIDs[j]*ShardWidth + (ps.columnIDs[j] % ShardWidth))
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
_, err := bmp.WriteTo(&buf)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "writing to buffer")
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func madvise(b []byte, advice int) error { // nolint: unparam
|
|
_, _, err := syscall.Syscall(syscall.SYS_MADVISE, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), uintptr(advice))
|
|
if err != 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pairSet is a list of equal length row and column id lists.
|
|
type pairSet struct {
|
|
rowIDs []uint64
|
|
columnIDs []uint64
|
|
}
|
|
|
|
// byteSlicesEqual returns true if all slices are equal.
|
|
func byteSlicesEqual(a [][]byte) bool {
|
|
if len(a) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, v := range a[1:] {
|
|
if !bytes.Equal(a[0], v) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// pos returns the row position of a row/column pair.
|
|
func pos(rowID, columnID uint64) uint64 {
|
|
return (rowID * ShardWidth) + (columnID % ShardWidth)
|
|
}
|
|
|
|
// vector stores the mapping of colID to rowID.
|
|
// It's used for a mutex field type.
|
|
type vector interface {
|
|
Get(colID uint64) (uint64, bool, error)
|
|
}
|
|
|
|
// rowsVector implements the vector interface by looking
|
|
// at row data as needed.
|
|
type rowsVector struct {
|
|
f *fragment
|
|
}
|
|
|
|
// newRowsVector returns a rowsVector for a given fragment.
|
|
func newRowsVector(f *fragment) *rowsVector {
|
|
return &rowsVector{
|
|
f: f,
|
|
}
|
|
}
|
|
|
|
// Get returns the rowID associated to the given colID.
|
|
// Additionally, it returns true if a value was found,
|
|
// otherwise it returns false.
|
|
func (v *rowsVector) Get(colID uint64) (uint64, bool, error) {
|
|
rows := v.f.rows(0, filterColumn(colID))
|
|
if len(rows) > 1 {
|
|
return 0, false, errors.New("found multiple row values for column")
|
|
} else if len(rows) == 1 {
|
|
return rows[0], true, nil
|
|
}
|
|
return 0, false, nil
|
|
}
|
|
|
|
// rowToKey converts a Pilosa row ID to the key of the container which starts
|
|
// that row in the bitmap which represents this entire fragment. A fragment is
|
|
// all the rows within a shard within a field concatenated together.
|
|
func rowToKey(rowID uint64) (key uint64) {
|
|
return rowID * (ShardWidth / containerWidth)
|
|
}
|
|
|
|
// boolVector implements the vector interface by looking
|
|
// at data in rows 0 and 1.
|
|
type boolVector struct {
|
|
f *fragment
|
|
}
|
|
|
|
// newBoolVector returns a boolVector for a given fragment.
|
|
func newBoolVector(f *fragment) *boolVector {
|
|
return &boolVector{
|
|
f: f,
|
|
}
|
|
}
|
|
|
|
// Get returns the rowID associated to the given colID.
|
|
// Additionally, it returns true if a value was found,
|
|
// otherwise it returns false.
|
|
func (v *boolVector) Get(colID uint64) (uint64, bool, error) {
|
|
rows := v.f.rows(0, filterColumn(colID))
|
|
if len(rows) > 1 {
|
|
return 0, false, errors.New("found multiple row values for column")
|
|
} else if len(rows) == 1 {
|
|
switch rows[0] {
|
|
case falseRowID, trueRowID:
|
|
return rows[0], true, nil
|
|
default:
|
|
return 0, false, errors.New("found non-boolean value")
|
|
}
|
|
}
|
|
return 0, false, nil
|
|
}
|