mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
I'd like to add stat tracking to Roaring, which means it has to be able to import the stats package, which means stats has to be a package rather than part of the pilosa package. If stats stops being in pilosa, it still needs a way to import logger, so logger also has to leave the pilosa package. Then everything using them needs to import them and use package selectors on their names. This doesn't actually add the stats support to roaring, it just makes it so there's a way to import the stats code from something in the roaring package.
496 lines
11 KiB
Go
496 lines
11 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 (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
"github.com/pilosa/pilosa/internal"
|
|
"github.com/pilosa/pilosa/logger"
|
|
"github.com/pilosa/pilosa/roaring"
|
|
"github.com/pilosa/pilosa/stats"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Index represents a container for fields.
|
|
type Index struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
name string
|
|
keys bool // use string keys
|
|
|
|
// Existence tracking.
|
|
trackExistence bool
|
|
existenceFld *Field
|
|
|
|
// Fields by name.
|
|
fields map[string]*Field
|
|
|
|
newAttrStore func(string) AttrStore
|
|
|
|
// Column attribute storage and cache.
|
|
columnAttrs AttrStore
|
|
|
|
broadcaster broadcaster
|
|
Stats stats.StatsClient
|
|
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewIndex returns a new instance of Index.
|
|
func NewIndex(path, name string) (*Index, error) {
|
|
err := validateName(name)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "validating name")
|
|
}
|
|
|
|
return &Index{
|
|
path: path,
|
|
name: name,
|
|
fields: make(map[string]*Field),
|
|
|
|
newAttrStore: newNopAttrStore,
|
|
columnAttrs: nopStore,
|
|
|
|
broadcaster: NopBroadcaster,
|
|
Stats: stats.NopStatsClient,
|
|
logger: logger.NopLogger,
|
|
trackExistence: true,
|
|
}, nil
|
|
}
|
|
|
|
// Name returns name of the index.
|
|
func (i *Index) Name() string { return i.name }
|
|
|
|
// Path returns the path the index was initialized with.
|
|
func (i *Index) Path() string { return i.path }
|
|
|
|
// Keys returns true if the index uses string keys.
|
|
func (i *Index) Keys() bool { return i.keys }
|
|
|
|
// ColumnAttrStore returns the storage for column attributes.
|
|
func (i *Index) ColumnAttrStore() AttrStore { return i.columnAttrs }
|
|
|
|
// Options returns all options for this index.
|
|
func (i *Index) Options() IndexOptions {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
return i.options()
|
|
}
|
|
|
|
func (i *Index) options() IndexOptions {
|
|
return IndexOptions{
|
|
Keys: i.keys,
|
|
TrackExistence: i.trackExistence,
|
|
}
|
|
}
|
|
|
|
// Open opens and initializes the index.
|
|
func (i *Index) Open() error {
|
|
// Ensure the path exists.
|
|
if err := os.MkdirAll(i.path, 0777); err != nil {
|
|
return errors.Wrap(err, "creating directory")
|
|
}
|
|
|
|
// Read meta file.
|
|
if err := i.loadMeta(); err != nil {
|
|
return errors.Wrap(err, "loading meta file")
|
|
}
|
|
|
|
if err := i.openFields(); err != nil {
|
|
return errors.Wrap(err, "opening fields")
|
|
}
|
|
|
|
if i.trackExistence {
|
|
if err := i.openExistenceField(); err != nil {
|
|
return errors.Wrap(err, "opening existence field")
|
|
}
|
|
}
|
|
|
|
if err := i.columnAttrs.Open(); err != nil {
|
|
return errors.Wrap(err, "opening attrstore")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// openFields opens and initializes the fields inside the index.
|
|
func (i *Index) openFields() error {
|
|
f, err := os.Open(i.path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening directory")
|
|
}
|
|
defer f.Close()
|
|
|
|
fis, err := f.Readdir(0)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading directory")
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
if !fi.IsDir() {
|
|
continue
|
|
}
|
|
|
|
fld, err := i.newField(i.fieldPath(filepath.Base(fi.Name())), filepath.Base(fi.Name()))
|
|
if err != nil {
|
|
return ErrName
|
|
}
|
|
if err := fld.Open(); err != nil {
|
|
return fmt.Errorf("open field: name=%s, err=%s", fld.Name(), err)
|
|
}
|
|
i.fields[fld.Name()] = fld
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// openExistenceField gets or creates the existence field and associates it to the index.
|
|
func (i *Index) openExistenceField() error {
|
|
f, err := i.createFieldIfNotExists(existenceFieldName, FieldOptions{CacheType: CacheTypeNone, CacheSize: 0})
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating existence field")
|
|
}
|
|
i.existenceFld = f
|
|
return nil
|
|
}
|
|
|
|
// loadMeta reads meta data for the index, if any.
|
|
func (i *Index) loadMeta() error {
|
|
var pb internal.IndexMeta
|
|
|
|
// Read data from meta file.
|
|
buf, err := ioutil.ReadFile(filepath.Join(i.path, ".meta"))
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "reading")
|
|
} else {
|
|
if err := proto.Unmarshal(buf, &pb); err != nil {
|
|
return errors.Wrap(err, "unmarshalling")
|
|
}
|
|
}
|
|
|
|
// Copy metadata fields.
|
|
i.keys = pb.Keys
|
|
i.trackExistence = pb.TrackExistence
|
|
|
|
return nil
|
|
}
|
|
|
|
// saveMeta writes meta data for the index.
|
|
func (i *Index) saveMeta() error {
|
|
// Marshal metadata.
|
|
buf, err := proto.Marshal(&internal.IndexMeta{
|
|
Keys: i.keys,
|
|
TrackExistence: i.trackExistence,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshalling")
|
|
}
|
|
|
|
// Write to meta file.
|
|
if err := ioutil.WriteFile(filepath.Join(i.path, ".meta"), buf, 0666); err != nil {
|
|
return errors.Wrap(err, "writing")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the index and its fields.
|
|
func (i *Index) Close() error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Close the attribute store.
|
|
i.columnAttrs.Close()
|
|
|
|
// Close all fields.
|
|
for _, f := range i.fields {
|
|
if err := f.Close(); err != nil {
|
|
return errors.Wrap(err, "closing field")
|
|
}
|
|
}
|
|
i.fields = make(map[string]*Field)
|
|
|
|
return nil
|
|
}
|
|
|
|
// AvailableShards returns a bitmap of all shards with data in the index.
|
|
func (i *Index) AvailableShards() *roaring.Bitmap {
|
|
if i == nil {
|
|
return roaring.NewBitmap()
|
|
}
|
|
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
b := roaring.NewBitmap()
|
|
for _, f := range i.fields {
|
|
b = b.Union(f.AvailableShards())
|
|
}
|
|
|
|
i.Stats.Gauge("maxShard", float64(b.Max()), 1.0)
|
|
return b
|
|
}
|
|
|
|
// fieldPath returns the path to a field in the index.
|
|
func (i *Index) fieldPath(name string) string { return filepath.Join(i.path, name) }
|
|
|
|
// Field returns a field in the index by name.
|
|
func (i *Index) Field(name string) *Field {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
return i.field(name)
|
|
}
|
|
|
|
func (i *Index) field(name string) *Field { return i.fields[name] }
|
|
|
|
// Fields returns a list of all fields in the index.
|
|
func (i *Index) Fields() []*Field {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
a := make([]*Field, 0, len(i.fields))
|
|
for _, f := range i.fields {
|
|
a = append(a, f)
|
|
}
|
|
sort.Sort(fieldSlice(a))
|
|
|
|
return a
|
|
}
|
|
|
|
// existenceField returns the internal field used to track column existence.
|
|
func (i *Index) existenceField() *Field {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
return i.existenceFld
|
|
}
|
|
|
|
// recalculateCaches recalculates caches on every field in the index.
|
|
func (i *Index) recalculateCaches() {
|
|
for _, field := range i.Fields() {
|
|
field.recalculateCaches()
|
|
}
|
|
}
|
|
|
|
// CreateField creates a field.
|
|
func (i *Index) CreateField(name string, opts ...FieldOption) (*Field, error) {
|
|
err := validateName(name)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "validating name")
|
|
}
|
|
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Ensure field doesn't already exist.
|
|
if i.fields[name] != nil {
|
|
return nil, newConflictError(ErrFieldExists)
|
|
}
|
|
|
|
// Apply functional options.
|
|
fo := FieldOptions{}
|
|
for _, opt := range opts {
|
|
err := opt(&fo)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "applying option")
|
|
}
|
|
}
|
|
|
|
return i.createField(name, fo)
|
|
}
|
|
|
|
// CreateFieldIfNotExists creates a field with the given options if it doesn't exist.
|
|
func (i *Index) CreateFieldIfNotExists(name string, opts ...FieldOption) (*Field, error) {
|
|
err := validateName(name)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "validating name")
|
|
}
|
|
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Find field in cache first.
|
|
if f := i.fields[name]; f != nil {
|
|
return f, nil
|
|
}
|
|
|
|
// Apply functional options.
|
|
fo := FieldOptions{}
|
|
for _, opt := range opts {
|
|
err := opt(&fo)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "applying option")
|
|
}
|
|
}
|
|
|
|
return i.createField(name, fo)
|
|
}
|
|
|
|
func (i *Index) createFieldIfNotExists(name string, opt FieldOptions) (*Field, error) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Find field in cache first.
|
|
if f := i.fields[name]; f != nil {
|
|
return f, nil
|
|
}
|
|
|
|
return i.createField(name, opt)
|
|
}
|
|
|
|
func (i *Index) createField(name string, opt FieldOptions) (*Field, error) {
|
|
if name == "" {
|
|
return nil, errors.New("field name required")
|
|
} else if opt.CacheType != "" && !isValidCacheType(opt.CacheType) {
|
|
return nil, ErrInvalidCacheType
|
|
}
|
|
|
|
// Initialize field.
|
|
f, err := i.newField(i.fieldPath(name), name)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "initializing")
|
|
}
|
|
|
|
// Open field.
|
|
if err := f.Open(); err != nil {
|
|
return nil, errors.Wrap(err, "opening")
|
|
}
|
|
|
|
// Apply field options.
|
|
if err := f.applyOptions(opt); err != nil {
|
|
f.Close()
|
|
return nil, errors.Wrap(err, "applying options")
|
|
}
|
|
|
|
if err := f.saveMeta(); err != nil {
|
|
f.Close()
|
|
return nil, errors.Wrap(err, "saving meta")
|
|
}
|
|
|
|
// Add to index's field lookup.
|
|
i.fields[name] = f
|
|
|
|
return f, nil
|
|
}
|
|
|
|
func (i *Index) newField(path, name string) (*Field, error) {
|
|
f, err := newField(path, i.name, name, OptFieldTypeDefault())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.logger = i.logger
|
|
f.Stats = i.Stats.WithTags(fmt.Sprintf("field:%s", name))
|
|
f.broadcaster = i.broadcaster
|
|
f.rowAttrStore = i.newAttrStore(filepath.Join(f.path, ".data"))
|
|
return f, nil
|
|
}
|
|
|
|
// DeleteField removes a field from the index.
|
|
func (i *Index) DeleteField(name string) error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Confirm field exists.
|
|
f := i.field(name)
|
|
if f == nil {
|
|
return newNotFoundError(ErrFieldNotFound)
|
|
}
|
|
|
|
// Close field.
|
|
if err := f.Close(); err != nil {
|
|
return errors.Wrap(err, "closing")
|
|
}
|
|
|
|
// Delete field directory.
|
|
if err := os.RemoveAll(i.fieldPath(name)); err != nil {
|
|
return errors.Wrap(err, "removing directory")
|
|
}
|
|
|
|
// If the field being deleted is the existence field,
|
|
// turn off existence tracking on the index.
|
|
if name == existenceFieldName {
|
|
i.trackExistence = false
|
|
i.existenceFld = nil
|
|
|
|
// Update meta data on disk.
|
|
if err := i.saveMeta(); err != nil {
|
|
return errors.Wrap(err, "saving existence meta data")
|
|
}
|
|
}
|
|
|
|
// Remove reference.
|
|
delete(i.fields, name)
|
|
|
|
return nil
|
|
}
|
|
|
|
type indexSlice []*Index
|
|
|
|
func (p indexSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
func (p indexSlice) Len() int { return len(p) }
|
|
func (p indexSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() }
|
|
|
|
// IndexInfo represents schema information for an index.
|
|
type IndexInfo struct {
|
|
Name string `json:"name"`
|
|
Options IndexOptions `json:"options"`
|
|
Fields []*FieldInfo `json:"fields"`
|
|
}
|
|
|
|
type indexInfoSlice []*IndexInfo
|
|
|
|
func (p indexInfoSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
func (p indexInfoSlice) Len() int { return len(p) }
|
|
func (p indexInfoSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
|
|
|
// IndexOptions represents options to set when initializing an index.
|
|
type IndexOptions struct {
|
|
Keys bool `json:"keys"`
|
|
TrackExistence bool `json:"trackExistence"`
|
|
}
|
|
|
|
// hasTime returns true if a contains a non-nil time.
|
|
func hasTime(a []*time.Time) bool {
|
|
for _, t := range a {
|
|
if t != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type importKey struct {
|
|
View string
|
|
Shard uint64
|
|
}
|
|
|
|
type importData struct {
|
|
RowIDs []uint64
|
|
ColumnIDs []uint64
|
|
}
|
|
|
|
type importValueData struct {
|
|
ColumnIDs []uint64
|
|
Values []int64
|
|
}
|