mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-05 08:10:50 +00:00
The snapshot queue needs a bit more subtlety. In some cases, we really do want to do a snapshot right now -- these shouldn't have to wait for possibly a hundred or more other snapshots to complete. In other cases, we don't really care that much whether we do a snapshot, and just dropping it is probably fine. To accommodate this, we distinguish between "urgent" and "normal" snapshots, and between "Immediate" (does an urgent snapshot, waits for it) and "Enqueue" (might enqueue a snapshot but *also might not* if we're already busy). There's a corresponding "Await" to wait for a snapshot, if one is pending, but not if one isn't. We also have a background scan that checks the holder. It will scan pretty actively when it's finding fragments that need snapshots (no enqueued snapshot, opN > MaxOpN). It pauses for a second after every hundred fragments that didn't need snapshots, and for a minute after each holder scan that didn't find any. So, if you don't need snapshots, it does basically nothing, if you do, it'll be moderately aggressive about submitting tasks -- but it always waits if there's *any* requested snapshots in the queues. Updates since initial draft: Check results from Await more consistently, and in one case, use Immediate instead and then check its error. Fix a race condition. The race condition comes about if: 1. You have a limited enough worker pool that this can happen. (In testing we tend to have a worker pool of 1.) 2. A fragment is in the normal, non-urgent, queue already. 3. An immediate request comes in for that fragment. This always happens *with the fragment lock held*. 4. A worker thread grabs that fragment from the queue. 5. The worker thread now waits on the lock. Meanwhile, the immediate request blocks on sending the fragment to the urgent queue. 6. The worker can't read the urgent queue, and the immediate request can't send it, so the immediate request can't proceed. What's supposed to happen is that the immediate request sends the thing, and gets into Await(), which sleeps on a condition variable using the lock, which is to say, releases the lock. The obvious resolution is to let go of the lock, send the message, and then reclaim the lock. But then we have the possibility that the message sent ends up with a timestamp right after a snapshot that happened *after* the Immediate request was started. Oops. So we create the request, then let go of the lock, then send the request, then reclaim the lock and go into the Await state. All is well. This is on top of more general use of wait groups, etcetera, to allow us to ensure that any holder scans terminate *before* we close the channels they might otherwise be trying to write to. So, shutdown process is now: * grab lock on queue (workers and scanners don't use the lock) * mark snapshotqueue done * wait for holder scans to complete/exit * close and nil out all the channels * release lock Anything trying to submit to this needs to hold the lock, unless it's a holder scan, so either it got the lock before we did and already submitted the thing, or it will get the lock after this and not find a channel to write to; it's just the holder scanner that has an ongoing thing that might have started a write to the channel *without* a lock held, because it's expected that it might have to wait minutes or hours before the write will complete because it's a background task. Also, rework the background holder scan to grab lists of indexes/fields/views/fragments, then scan the grabbed/copied lists, rather than iterating over maps, allowing us to grab the lock when we're about to access a thing and let it go when done. There might be a simpler/cleaner way to do this but opinions on how safe it is are very mixed, so in the mean time, I'm making the range behavior not depend at all on there being no writes to the various tiers of holder/index/view/fragment during the background scans.
1190 lines
31 KiB
Go
1190 lines
31 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 (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/pilosa/pilosa/v2/logger"
|
|
"github.com/pilosa/pilosa/v2/roaring"
|
|
"github.com/pilosa/pilosa/v2/stats"
|
|
"github.com/pilosa/pilosa/v2/tracing"
|
|
"github.com/pkg/errors"
|
|
uuid "github.com/satori/go.uuid"
|
|
)
|
|
|
|
const (
|
|
// defaultCacheFlushInterval is the default value for Fragment.CacheFlushInterval.
|
|
defaultCacheFlushInterval = 1 * time.Minute
|
|
|
|
// fileLimit is the maximum open file limit (ulimit -n) to automatically set.
|
|
fileLimit = 262144 // (512^2)
|
|
|
|
// existenceFieldName is the name of the internal field used to store existence values.
|
|
existenceFieldName = "_exists"
|
|
)
|
|
|
|
// Holder represents a container for indexes.
|
|
type Holder struct {
|
|
mu sync.RWMutex
|
|
|
|
// Indexes by name.
|
|
indexes map[string]*Index
|
|
|
|
// opened channel is closed once Open() completes.
|
|
opened lockedChan
|
|
|
|
broadcaster broadcaster
|
|
|
|
NewAttrStore func(string) AttrStore
|
|
|
|
// Close management
|
|
wg sync.WaitGroup
|
|
closing chan struct{}
|
|
|
|
// Stats
|
|
Stats stats.StatsClient
|
|
|
|
// Data directory path.
|
|
Path string
|
|
|
|
// The interval at which the cached row ids are persisted to disk.
|
|
cacheFlushInterval time.Duration
|
|
|
|
Logger logger.Logger
|
|
|
|
snapshotQueue snapshotQueue
|
|
|
|
// Manages replication from the primary node.
|
|
primaryTranslateNode *Node
|
|
translateStoreReplicator *holderTranslateStoreReplicator
|
|
|
|
// Instantiates new translation stores for indexes & fields.
|
|
OpenTranslateStore OpenTranslateStoreFunc // local store
|
|
OpenTranslateReader OpenTranslateReaderFunc // replication
|
|
}
|
|
|
|
// lockedChan looks a little ridiculous admittedly, but exists for good reason.
|
|
// The channel within is used (for example) to signal to other goroutines when
|
|
// the Holder has finished opening (via closing the channel). However, it is
|
|
// possible for the holder to be closed and then reopened, but a channel which
|
|
// is closed cannot be re-opened. We must create a new channel - this creates a
|
|
// data race with any goroutine which might be accessing the channel. To ensure
|
|
// that there is no data race on the value of the channel itself, we wrap any
|
|
// operation on it with an RWMutex so that we can guarantee that nothing is
|
|
// trying to listen on it when it gets swapped.
|
|
type lockedChan struct {
|
|
ch chan struct{}
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (lc *lockedChan) Close() {
|
|
lc.mu.RLock()
|
|
close(lc.ch)
|
|
lc.mu.RUnlock()
|
|
}
|
|
|
|
func (lc *lockedChan) Recv() {
|
|
lc.mu.RLock()
|
|
<-lc.ch
|
|
lc.mu.RUnlock()
|
|
}
|
|
|
|
// NewHolder returns a new instance of Holder.
|
|
func NewHolder() *Holder {
|
|
return &Holder{
|
|
indexes: make(map[string]*Index),
|
|
closing: make(chan struct{}),
|
|
|
|
opened: lockedChan{ch: make(chan struct{})},
|
|
|
|
broadcaster: NopBroadcaster,
|
|
Stats: stats.NopStatsClient,
|
|
|
|
NewAttrStore: newNopAttrStore,
|
|
|
|
cacheFlushInterval: defaultCacheFlushInterval,
|
|
|
|
Logger: logger.NopLogger,
|
|
|
|
OpenTranslateStore: OpenInMemTranslateStore,
|
|
}
|
|
}
|
|
|
|
// Open initializes the root data directory for the holder.
|
|
func (h *Holder) Open() error {
|
|
// Reset closing in case Holder is being reopened.
|
|
h.closing = make(chan struct{})
|
|
|
|
h.setFileLimit()
|
|
|
|
h.Logger.Printf("open holder path: %s", h.Path)
|
|
if err := os.MkdirAll(h.Path, 0777); err != nil {
|
|
return errors.Wrap(err, "creating directory")
|
|
}
|
|
|
|
// Verify that we are not trying to open with v1 translation data.
|
|
if ok, err := h.hasV1TranslateKeysFile(); err != nil {
|
|
return errors.Wrap(err, "verify v1 translation file")
|
|
} else if !ok {
|
|
return ErrCannotOpenV1TranslateFile
|
|
}
|
|
|
|
// Open path to read all index directories.
|
|
f, err := os.Open(h.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")
|
|
}
|
|
|
|
// Run snapshots asynchronously. The snapshotQueue will have a background
|
|
// task associated with it which flushes it and waits until this channel
|
|
// is closed, so we should always close this channel when done.
|
|
h.snapshotQueue = newSnapshotQueue(10, 2, h.Logger)
|
|
|
|
for _, fi := range fis {
|
|
// Skip files or hidden directories.
|
|
if !fi.IsDir() || strings.HasPrefix(fi.Name(), ".") {
|
|
continue
|
|
}
|
|
|
|
h.Logger.Printf("opening index: %s", filepath.Base(fi.Name()))
|
|
|
|
index, err := h.newIndex(h.IndexPath(filepath.Base(fi.Name())), filepath.Base(fi.Name()))
|
|
if errors.Cause(err) == ErrName {
|
|
h.Logger.Printf("ERROR opening index: %s, err=%s", fi.Name(), err)
|
|
continue
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "opening index")
|
|
}
|
|
|
|
if err := index.Open(); err != nil {
|
|
if err == ErrName {
|
|
h.Logger.Printf("ERROR opening index: %s, err=%s", index.Name(), err)
|
|
continue
|
|
}
|
|
return fmt.Errorf("open index: name=%s, err=%s", index.Name(), err)
|
|
}
|
|
h.mu.Lock()
|
|
h.indexes[index.Name()] = index
|
|
h.mu.Unlock()
|
|
}
|
|
h.Logger.Printf("open holder: complete")
|
|
|
|
// Periodically flush cache.
|
|
h.wg.Add(1)
|
|
go func() { defer h.wg.Done(); h.monitorCacheFlush() }()
|
|
|
|
h.Stats.Open()
|
|
h.snapshotQueue.ScanHolder(h)
|
|
|
|
h.opened.Close()
|
|
return nil
|
|
}
|
|
|
|
// Close closes all open fragments.
|
|
func (h *Holder) Close() error {
|
|
h.Stats.Close()
|
|
|
|
// Notify goroutines of closing and wait for completion.
|
|
close(h.closing)
|
|
h.wg.Wait()
|
|
|
|
for _, index := range h.indexes {
|
|
if err := index.Close(); err != nil {
|
|
return errors.Wrap(err, "closing index")
|
|
}
|
|
}
|
|
if h.snapshotQueue != nil {
|
|
h.snapshotQueue.Stop()
|
|
h.snapshotQueue = nil
|
|
}
|
|
|
|
// Reset opened in case Holder needs to be reopened.
|
|
h.opened.mu.Lock()
|
|
h.opened.ch = make(chan struct{})
|
|
h.opened.mu.Unlock()
|
|
|
|
h.mu.Lock()
|
|
if h.translateStoreReplicator != nil {
|
|
h.translateStoreReplicator.Close()
|
|
h.translateStoreReplicator = nil
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasData returns true if Holder contains at least one index.
|
|
// This is used to determine if the rebalancing of data is necessary
|
|
// when a node joins the cluster.
|
|
func (h *Holder) HasData() (bool, error) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
if len(h.indexes) > 0 {
|
|
return true, nil
|
|
}
|
|
// Open path to read all index directories.
|
|
if _, err := os.Stat(h.Path); os.IsNotExist(err) {
|
|
return false, nil
|
|
} else if err != nil {
|
|
return false, errors.Wrap(err, "statting data dir")
|
|
}
|
|
|
|
f, err := os.Open(h.Path)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "opening data dir")
|
|
}
|
|
defer f.Close()
|
|
|
|
fis, err := f.Readdir(0)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, "reading data dir")
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
if !fi.IsDir() {
|
|
continue
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// hasV1TranslateKeysFile returns true if a v1 translation data file exists on disk.
|
|
func (h *Holder) hasV1TranslateKeysFile() (bool, error) {
|
|
if _, err := os.Stat(filepath.Join(h.Path, ".keys")); os.IsNotExist(err) {
|
|
return true, nil
|
|
} else if err != nil {
|
|
return false, err
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// availableShardsByIndex returns a bitmap of all shards by indexes.
|
|
func (h *Holder) availableShardsByIndex() map[string]*roaring.Bitmap {
|
|
m := make(map[string]*roaring.Bitmap)
|
|
for _, index := range h.Indexes() {
|
|
m[index.Name()] = index.AvailableShards()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Schema returns schema information for all indexes, fields, and views.
|
|
func (h *Holder) Schema() []*IndexInfo {
|
|
var a []*IndexInfo
|
|
for _, index := range h.Indexes() {
|
|
di := &IndexInfo{Name: index.Name()}
|
|
for _, field := range index.Fields() {
|
|
fi := &FieldInfo{Name: field.Name(), Options: field.Options()}
|
|
for _, view := range field.views() {
|
|
fi.Views = append(fi.Views, &ViewInfo{Name: view.name})
|
|
}
|
|
sort.Sort(viewInfoSlice(fi.Views))
|
|
di.Fields = append(di.Fields, fi)
|
|
}
|
|
sort.Sort(fieldInfoSlice(di.Fields))
|
|
a = append(a, di)
|
|
}
|
|
sort.Sort(indexInfoSlice(a))
|
|
return a
|
|
}
|
|
|
|
// limitedSchema returns schema information for all indexes and fields.
|
|
func (h *Holder) limitedSchema() []*IndexInfo {
|
|
var a []*IndexInfo
|
|
for _, index := range h.Indexes() {
|
|
di := &IndexInfo{
|
|
Name: index.Name(),
|
|
Options: index.Options(),
|
|
ShardWidth: ShardWidth,
|
|
}
|
|
for _, field := range index.Fields() {
|
|
if strings.HasPrefix(field.name, "_") {
|
|
continue
|
|
}
|
|
fi := &FieldInfo{Name: field.Name(), Options: field.Options()}
|
|
di.Fields = append(di.Fields, fi)
|
|
}
|
|
sort.Sort(fieldInfoSlice(di.Fields))
|
|
a = append(a, di)
|
|
}
|
|
sort.Sort(indexInfoSlice(a))
|
|
return a
|
|
}
|
|
|
|
// applySchema applies an internal Schema to Holder.
|
|
func (h *Holder) applySchema(schema *Schema) error {
|
|
// Create indexes that don't exist.
|
|
for _, index := range schema.Indexes {
|
|
idx, err := h.CreateIndexIfNotExists(index.Name, index.Options)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating index")
|
|
}
|
|
// Create fields that don't exist.
|
|
for _, f := range index.Fields {
|
|
field, err := idx.createFieldIfNotExists(f.Name, f.Options)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating field")
|
|
}
|
|
// Create views that don't exist.
|
|
for _, v := range f.Views {
|
|
_, err := field.createViewIfNotExists(v.Name)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating view")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IndexPath returns the path where a given index is stored.
|
|
func (h *Holder) IndexPath(name string) string { return filepath.Join(h.Path, name) }
|
|
|
|
// Index returns the index by name.
|
|
func (h *Holder) Index(name string) *Index {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.index(name)
|
|
}
|
|
|
|
func (h *Holder) index(name string) *Index { return h.indexes[name] }
|
|
|
|
// Indexes returns a list of all indexes in the holder.
|
|
func (h *Holder) Indexes() []*Index {
|
|
h.mu.RLock()
|
|
a := make([]*Index, 0, len(h.indexes))
|
|
for _, index := range h.indexes {
|
|
a = append(a, index)
|
|
}
|
|
h.mu.RUnlock()
|
|
|
|
sort.Sort(indexSlice(a))
|
|
return a
|
|
}
|
|
|
|
// CreateIndex creates an index.
|
|
// An error is returned if the index already exists.
|
|
func (h *Holder) CreateIndex(name string, opt IndexOptions) (*Index, error) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Ensure index doesn't already exist.
|
|
if h.indexes[name] != nil {
|
|
return nil, newConflictError(ErrIndexExists)
|
|
}
|
|
return h.createIndex(name, opt)
|
|
}
|
|
|
|
// CreateIndexIfNotExists returns an index by name.
|
|
// The index is created if it does not already exist.
|
|
func (h *Holder) CreateIndexIfNotExists(name string, opt IndexOptions) (*Index, error) {
|
|
h.mu.RLock()
|
|
|
|
// Find index in cache first.
|
|
if index := h.indexes[name]; index != nil {
|
|
h.mu.RUnlock()
|
|
return index, nil
|
|
}
|
|
|
|
h.mu.RUnlock()
|
|
|
|
index, err := h.CreateIndex(name, opt)
|
|
if _, ok := err.(ConflictError); err != nil && !ok {
|
|
return nil, err
|
|
}
|
|
return index, nil
|
|
}
|
|
|
|
func (h *Holder) createIndex(name string, opt IndexOptions) (*Index, error) {
|
|
if name == "" {
|
|
return nil, errors.New("index name required")
|
|
}
|
|
|
|
// Return index if it exists.
|
|
if index := h.index(name); index != nil {
|
|
return index, nil
|
|
}
|
|
|
|
// Otherwise create a new index.
|
|
index, err := h.newIndex(h.IndexPath(name), name)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "creating")
|
|
}
|
|
|
|
index.keys = opt.Keys
|
|
index.trackExistence = opt.TrackExistence
|
|
|
|
if err := index.Open(); err != nil {
|
|
return nil, errors.Wrap(err, "opening")
|
|
} else if err := index.saveMeta(); err != nil {
|
|
return nil, errors.Wrap(err, "meta")
|
|
}
|
|
|
|
// Update options.
|
|
h.indexes[index.Name()] = index
|
|
|
|
// Restart replication.
|
|
go h.refreshTranslateStoreReplicator()
|
|
|
|
return index, nil
|
|
}
|
|
|
|
func (h *Holder) newIndex(path, name string) (*Index, error) {
|
|
index, err := NewIndex(path, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index.logger = h.Logger
|
|
index.Stats = h.Stats.WithTags(fmt.Sprintf("index:%s", index.Name()))
|
|
index.broadcaster = h.broadcaster
|
|
index.newAttrStore = h.NewAttrStore
|
|
index.columnAttrs = h.NewAttrStore(filepath.Join(index.path, ".data"))
|
|
index.snapshotQueue = h.snapshotQueue
|
|
index.holder = h
|
|
index.OpenTranslateStore = h.OpenTranslateStore
|
|
return index, nil
|
|
}
|
|
|
|
// DeleteIndex removes an index from the holder.
|
|
func (h *Holder) DeleteIndex(name string) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Confirm index exists.
|
|
index := h.index(name)
|
|
if index == nil {
|
|
return newNotFoundError(ErrIndexNotFound)
|
|
}
|
|
|
|
// Close index.
|
|
if err := index.Close(); err != nil {
|
|
return errors.Wrap(err, "closing")
|
|
}
|
|
|
|
// Delete index directory.
|
|
if err := os.RemoveAll(h.IndexPath(name)); err != nil {
|
|
return errors.Wrap(err, "removing directory")
|
|
}
|
|
|
|
// Remove reference.
|
|
delete(h.indexes, name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Field returns the field for an index and name.
|
|
func (h *Holder) Field(index, name string) *Field {
|
|
idx := h.Index(index)
|
|
if idx == nil {
|
|
return nil
|
|
}
|
|
return idx.Field(name)
|
|
}
|
|
|
|
// view returns the view for an index, field, and name.
|
|
func (h *Holder) view(index, field, name string) *view {
|
|
f := h.Field(index, field)
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
return f.view(name)
|
|
}
|
|
|
|
// fragment returns the fragment for an index, field & shard.
|
|
func (h *Holder) fragment(index, field, view string, shard uint64) *fragment {
|
|
v := h.view(index, field, view)
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return v.Fragment(shard)
|
|
}
|
|
|
|
// monitorCacheFlush periodically flushes all fragment caches sequentially.
|
|
// This is run in a goroutine.
|
|
func (h *Holder) monitorCacheFlush() {
|
|
ticker := time.NewTicker(h.cacheFlushInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-h.closing:
|
|
return
|
|
case <-ticker.C:
|
|
h.flushCaches()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Holder) flushCaches() {
|
|
for _, index := range h.Indexes() {
|
|
for _, field := range index.Fields() {
|
|
for _, view := range field.views() {
|
|
for _, fragment := range view.allFragments() {
|
|
select {
|
|
case <-h.closing:
|
|
return
|
|
default:
|
|
}
|
|
|
|
if err := fragment.FlushCache(); err != nil {
|
|
h.Logger.Printf("ERROR flushing cache: err=%s, path=%s", err, fragment.cachePath())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// recalculateCaches recalculates caches on every index in the holder. This is
|
|
// probably not practical to call in real-world workloads, but makes writing
|
|
// integration tests much eaiser, since one doesn't have to wait 10 seconds
|
|
// after setting bits to get expected response.
|
|
func (h *Holder) recalculateCaches() {
|
|
for _, index := range h.Indexes() {
|
|
index.recalculateCaches()
|
|
}
|
|
}
|
|
|
|
// setFileLimit attempts to set the open file limit to the FileLimit constant defined above.
|
|
func (h *Holder) setFileLimit() {
|
|
oldLimit := &syscall.Rlimit{}
|
|
newLimit := &syscall.Rlimit{}
|
|
|
|
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, oldLimit); err != nil {
|
|
h.Logger.Printf("ERROR checking open file limit: %s", err)
|
|
return
|
|
}
|
|
// If the soft limit is lower than the FileLimit constant, we will try to change it.
|
|
if oldLimit.Cur < fileLimit {
|
|
newLimit.Cur = fileLimit
|
|
// If the hard limit is not high enough, we will try to change it too.
|
|
if oldLimit.Max < fileLimit {
|
|
newLimit.Max = fileLimit
|
|
} else {
|
|
newLimit.Max = oldLimit.Max
|
|
}
|
|
|
|
// Try to set the limit
|
|
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, newLimit); err != nil {
|
|
// If we just tried to change the hard limit and failed, we probably don't have permission. Let's try again without setting the hard limit.
|
|
if newLimit.Max > oldLimit.Max {
|
|
newLimit.Max = oldLimit.Max
|
|
// Obviously the hard limit cannot be higher than the soft limit.
|
|
if newLimit.Cur >= newLimit.Max {
|
|
newLimit.Cur = newLimit.Max
|
|
}
|
|
// Try setting again with lowered Max (hard limit)
|
|
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, newLimit); err != nil {
|
|
h.Logger.Printf("ERROR setting open file limit: %s", err)
|
|
}
|
|
// If we weren't trying to change the hard limit, let the user know something is wrong.
|
|
} else {
|
|
h.Logger.Printf("ERROR setting open file limit: %s", err)
|
|
}
|
|
}
|
|
|
|
// Check the limit after setting it. OS may not obey Setrlimit call.
|
|
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, oldLimit); err != nil {
|
|
h.Logger.Printf("ERROR checking open file limit: %s", err)
|
|
} else {
|
|
if oldLimit.Cur < fileLimit {
|
|
h.Logger.Printf("WARNING: Tried to set open file limit to %d, but it is %d. You may consider running \"sudo ulimit -n %d\" before starting Pilosa to avoid \"too many open files\" error. See https://www.pilosa.com/docs/latest/administration/#open-file-limits for more information.", fileLimit, oldLimit.Cur, fileLimit)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Holder) loadNodeID() (string, error) {
|
|
idPath := path.Join(h.Path, ".id")
|
|
h.Logger.Printf("load NodeID: %s", idPath)
|
|
if err := os.MkdirAll(h.Path, 0777); err != nil {
|
|
return "", errors.Wrap(err, "creating directory")
|
|
}
|
|
|
|
nodeIDBytes, err := ioutil.ReadFile(idPath)
|
|
if err == nil {
|
|
return strings.TrimSpace(string(nodeIDBytes)), nil
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return "", errors.Wrap(err, "reading file")
|
|
}
|
|
nodeID := uuid.NewV4().String()
|
|
err = ioutil.WriteFile(idPath, []byte(nodeID), 0600)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "writing file")
|
|
}
|
|
return nodeID, nil
|
|
}
|
|
|
|
// Log startup time and version to $DATA_DIR/.startup.log
|
|
func (h *Holder) logStartup() error {
|
|
time, err := time.Now().MarshalText()
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating timestamp")
|
|
}
|
|
logLine := fmt.Sprintf("%s\t%s\n", time, Version)
|
|
|
|
f, err := os.OpenFile(h.Path+"/.startup.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return errors.Wrap(err, "opening startup log")
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
if _, err = f.WriteString(logLine); err != nil {
|
|
return errors.Wrap(err, "writing startup log")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TranslateStore returns store for the given index or field.
|
|
func (h *Holder) TranslateStore(index, field string) (TranslateStore, error) {
|
|
if field == "" {
|
|
idx := h.Index(index)
|
|
if idx == nil {
|
|
return nil, ErrIndexNotFound
|
|
}
|
|
return idx.TranslateStore(), nil
|
|
}
|
|
|
|
f := h.Field(index, field)
|
|
if f == nil {
|
|
return nil, ErrFieldNotFound
|
|
}
|
|
return f.TranslateStore(), nil
|
|
}
|
|
|
|
// TranslateOffsetMap returns a map of offsets for all indexes & fields.
|
|
func (h *Holder) TranslateOffsetMap() (TranslateOffsetMap, error) {
|
|
m := make(TranslateOffsetMap)
|
|
for _, idx := range h.Indexes() {
|
|
id, err := idx.TranslateStore().MaxID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.SetIndexOffset(idx.Name(), id+1)
|
|
|
|
for _, field := range idx.Fields() {
|
|
id, err := field.TranslateStore().MaxID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.SetFieldOffset(idx.Name(), field.Name(), id+1)
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (h *Holder) setTranslateStoreReadOnly(v bool) {
|
|
for _, idx := range h.Indexes() {
|
|
idx.TranslateStore().SetReadOnly(v)
|
|
for _, field := range idx.Fields() {
|
|
field.TranslateStore().SetReadOnly(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Holder) setPrimaryTranslateStore(node *Node) error {
|
|
if node != nil && h.OpenTranslateReader == nil {
|
|
return nil
|
|
}
|
|
h.mu.Lock()
|
|
h.primaryTranslateNode = node.Clone()
|
|
h.mu.Unlock()
|
|
|
|
go h.refreshTranslateStoreReplicator()
|
|
return nil
|
|
}
|
|
|
|
func (h *Holder) refreshTranslateStoreReplicator() {
|
|
h.mu.RLock()
|
|
node := h.primaryTranslateNode
|
|
h.mu.RUnlock()
|
|
|
|
var nodeURL string
|
|
if node != nil {
|
|
u := node.URI.URL()
|
|
nodeURL = u.String()
|
|
}
|
|
|
|
// Stop existing replication, if running.
|
|
h.mu.Lock()
|
|
if h.translateStoreReplicator != nil {
|
|
h.translateStoreReplicator.Close()
|
|
h.translateStoreReplicator = nil
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
// Set all stores read only mode based on if we have a primary.
|
|
h.setTranslateStoreReadOnly(node != nil)
|
|
|
|
// Start replication monitor, if needed.
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if nodeURL != "" {
|
|
h.translateStoreReplicator = newHolderTranslateStoreReplicator(h, nodeURL)
|
|
h.translateStoreReplicator.logger = h.Logger
|
|
if err := h.translateStoreReplicator.Open(); err != nil {
|
|
h.Logger.Printf("cannot open translate store replicator: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TranslateEntryReader returns a reader that merges all index & field reader
|
|
// that are specified in the offsets map.
|
|
func (h *Holder) TranslateEntryReader(ctx context.Context, offsets TranslateOffsetMap) (_ TranslateEntryReader, err error) {
|
|
// Ensure all readers are cleaned up if any error.
|
|
var a []TranslateEntryReader
|
|
defer func() {
|
|
if err != nil {
|
|
for i := range a {
|
|
a[i].Close()
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Fetch all readers.
|
|
for indexName, m := range offsets {
|
|
for fieldName, offset := range m {
|
|
var store TranslateStore
|
|
|
|
idx := h.Index(indexName)
|
|
if idx == nil {
|
|
return nil, ErrIndexNotFound
|
|
}
|
|
|
|
// Fetch from index or field store.
|
|
if fieldName == "" {
|
|
store = idx.TranslateStore()
|
|
} else {
|
|
f := idx.Field(fieldName)
|
|
if f == nil {
|
|
return nil, ErrFieldNotFound
|
|
}
|
|
store = f.TranslateStore()
|
|
}
|
|
|
|
// Generate reader and append to multireader.
|
|
r, err := store.EntryReader(ctx, uint64(offset))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "translate reader")
|
|
}
|
|
a = append(a, r)
|
|
}
|
|
}
|
|
|
|
return NewMultiTranslateEntryReader(ctx, a), nil
|
|
}
|
|
|
|
// holderTranslateStoreReplicator manages the replication of translation store
|
|
// data from a primary store to the local replica. Continually tries to
|
|
// reconnect on disconnect.
|
|
type holderTranslateStoreReplicator struct {
|
|
ctx context.Context
|
|
cancel func()
|
|
wg sync.WaitGroup
|
|
|
|
holder *Holder
|
|
nodeURL string
|
|
|
|
logger logger.Logger
|
|
}
|
|
|
|
func newHolderTranslateStoreReplicator(h *Holder, nodeURL string) *holderTranslateStoreReplicator {
|
|
r := &holderTranslateStoreReplicator{
|
|
holder: h,
|
|
nodeURL: nodeURL,
|
|
logger: logger.NopLogger,
|
|
}
|
|
r.ctx, r.cancel = context.WithCancel(context.Background())
|
|
return r
|
|
}
|
|
|
|
// Open starts the background monitoring goroutine.
|
|
func (r *holderTranslateStoreReplicator) Open() error {
|
|
r.wg.Add(1)
|
|
go func() { defer r.wg.Done(); r.monitor() }()
|
|
return nil
|
|
}
|
|
|
|
// Close stops the replicator.
|
|
func (r *holderTranslateStoreReplicator) Close() error {
|
|
r.cancel()
|
|
return nil
|
|
}
|
|
|
|
// monitor runs in a background goroutine and continually tries to connect and
|
|
// stream translate changes from the primary store.
|
|
func (r *holderTranslateStoreReplicator) monitor() {
|
|
for {
|
|
select {
|
|
case <-r.ctx.Done():
|
|
return
|
|
default:
|
|
if err := r.replicate(); err != nil {
|
|
r.logger.Printf("cannot replicate: nodeURL=%s err=%s", r.nodeURL, err)
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *holderTranslateStoreReplicator) replicate() error {
|
|
// Determine the offsets of every index & field store.
|
|
offsets, err := r.holder.TranslateOffsetMap()
|
|
if err != nil {
|
|
return err
|
|
} else if len(offsets) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Begin streaming from remote primary.
|
|
rd, err := r.holder.OpenTranslateReader(r.ctx, r.nodeURL, offsets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rd.Close()
|
|
|
|
for {
|
|
var entry TranslateEntry
|
|
if err := rd.ReadEntry(&entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Find appropriate store.
|
|
var store TranslateStore
|
|
if entry.Field == "" {
|
|
idx := r.holder.Index(entry.Index)
|
|
if idx == nil {
|
|
return ErrIndexNotFound
|
|
}
|
|
store = idx.TranslateStore()
|
|
} else {
|
|
f := r.holder.Field(entry.Index, entry.Field)
|
|
if f == nil {
|
|
return ErrFieldNotFound
|
|
}
|
|
store = f.TranslateStore()
|
|
}
|
|
|
|
// Apply replication to store.
|
|
if err := store.ForceSet(entry.ID, entry.Key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// holderSyncer is an active anti-entropy tool that compares the local holder
|
|
// with a remote holder based on block checksums and resolves differences.
|
|
type holderSyncer struct {
|
|
mu sync.Mutex
|
|
|
|
Holder *Holder
|
|
|
|
Node *Node
|
|
Cluster *cluster
|
|
|
|
// Stats
|
|
Stats stats.StatsClient
|
|
|
|
// Signals that the sync should stop.
|
|
Closing <-chan struct{}
|
|
}
|
|
|
|
// IsClosing returns true if the syncer has been asked to close.
|
|
func (s *holderSyncer) IsClosing() bool {
|
|
if s.Cluster.abortAntiEntropyQ() {
|
|
return true
|
|
}
|
|
select {
|
|
case <-s.Closing:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// SyncHolder compares the holder on host with the local holder and resolves differences.
|
|
func (s *holderSyncer) SyncHolder() error {
|
|
s.mu.Lock() // only allow one instance of SyncHolder to be running at a time
|
|
defer s.mu.Unlock()
|
|
ti := time.Now()
|
|
// Iterate over schema in sorted order.
|
|
for _, di := range s.Holder.Schema() {
|
|
// Verify syncer has not closed.
|
|
if s.IsClosing() {
|
|
return nil
|
|
}
|
|
|
|
// Sync index column attributes.
|
|
if err := s.syncIndex(di.Name); err != nil {
|
|
return fmt.Errorf("index sync error: index=%s, err=%s", di.Name, err)
|
|
}
|
|
|
|
tf := time.Now()
|
|
for _, fi := range di.Fields {
|
|
// Verify syncer has not closed.
|
|
if s.IsClosing() {
|
|
return nil
|
|
}
|
|
|
|
// Sync field row attributes.
|
|
if err := s.syncField(di.Name, fi.Name); err != nil {
|
|
return fmt.Errorf("field sync error: index=%s, field=%s, err=%s", di.Name, fi.Name, err)
|
|
}
|
|
|
|
for _, vi := range fi.Views {
|
|
// Verify syncer has not closed.
|
|
if s.IsClosing() {
|
|
return nil
|
|
}
|
|
|
|
itr := s.Holder.Index(di.Name).AvailableShards().Iterator()
|
|
itr.Seek(0)
|
|
for shard, eof := itr.Next(); !eof; shard, eof = itr.Next() {
|
|
// Ignore shards that this host doesn't own.
|
|
if !s.Cluster.ownsShard(s.Node.ID, di.Name, shard) {
|
|
continue
|
|
}
|
|
|
|
// Verify syncer has not closed.
|
|
if s.IsClosing() {
|
|
return nil
|
|
}
|
|
|
|
// Sync fragment if own it.
|
|
if err := s.syncFragment(di.Name, fi.Name, vi.Name, shard); err != nil {
|
|
return fmt.Errorf("fragment sync error: index=%s, field=%s, view=%s, shard=%d, err=%s", di.Name, fi.Name, vi.Name, shard, err)
|
|
}
|
|
}
|
|
}
|
|
s.Stats.Histogram("syncField", float64(time.Since(tf)), 1.0)
|
|
tf = time.Now() // reset tf
|
|
}
|
|
s.Stats.Histogram("syncIndex", float64(time.Since(ti)), 1.0)
|
|
ti = time.Now() // reset ti
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncIndex synchronizes index attributes with the rest of the cluster.
|
|
func (s *holderSyncer) syncIndex(index string) error {
|
|
span, ctx := tracing.StartSpanFromContext(context.Background(), "HolderSyncer.syncIndex")
|
|
defer span.Finish()
|
|
|
|
// Retrieve index reference.
|
|
idx := s.Holder.Index(index)
|
|
if idx == nil {
|
|
return nil
|
|
}
|
|
indexTag := fmt.Sprintf("index:%s", index)
|
|
|
|
// Read block checksums.
|
|
blks, err := idx.ColumnAttrStore().Blocks()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting blocks")
|
|
}
|
|
s.Stats.CountWithCustomTags("ColumnAttrStoreBlocks", int64(len(blks)), 1.0, []string{indexTag})
|
|
|
|
// Sync with every other host.
|
|
for _, node := range Nodes(s.Cluster.nodes).FilterID(s.Node.ID) {
|
|
// Retrieve attributes from differing blocks.
|
|
// Skip update and recomputation if no attributes have changed.
|
|
m, err := s.Cluster.InternalClient.ColumnAttrDiff(ctx, &node.URI, index, blks)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting differing blocks")
|
|
} else if len(m) == 0 {
|
|
continue
|
|
}
|
|
s.Stats.CountWithCustomTags("ColumnAttrDiff", int64(len(m)), 1.0, []string{indexTag, node.ID})
|
|
|
|
// Update local copy.
|
|
if err := idx.ColumnAttrStore().SetBulkAttrs(m); err != nil {
|
|
return errors.Wrap(err, "setting attrs")
|
|
}
|
|
|
|
// Recompute blocks.
|
|
blks, err = idx.ColumnAttrStore().Blocks()
|
|
if err != nil {
|
|
return errors.Wrap(err, "recomputing blocks")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncField synchronizes field attributes with the rest of the cluster.
|
|
func (s *holderSyncer) syncField(index, name string) error {
|
|
span, ctx := tracing.StartSpanFromContext(context.Background(), "HolderSyncer.syncField")
|
|
defer span.Finish()
|
|
|
|
// Retrieve field reference.
|
|
f := s.Holder.Field(index, name)
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
indexTag := fmt.Sprintf("index:%s", index)
|
|
fieldTag := fmt.Sprintf("field:%s", name)
|
|
|
|
// Read block checksums.
|
|
blks, err := f.RowAttrStore().Blocks()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting blocks")
|
|
}
|
|
s.Stats.CountWithCustomTags("RowAttrStoreBlocks", int64(len(blks)), 1.0, []string{indexTag, fieldTag})
|
|
|
|
// Sync with every other host.
|
|
for _, node := range Nodes(s.Cluster.nodes).FilterID(s.Node.ID) {
|
|
// Retrieve attributes from differing blocks.
|
|
// Skip update and recomputation if no attributes have changed.
|
|
m, err := s.Cluster.InternalClient.RowAttrDiff(ctx, &node.URI, index, name, blks)
|
|
if err == ErrFieldNotFound {
|
|
continue // field not created remotely yet, skip
|
|
} else if err != nil {
|
|
return errors.Wrap(err, "getting differing blocks")
|
|
} else if len(m) == 0 {
|
|
continue
|
|
}
|
|
s.Stats.CountWithCustomTags("RowAttrDiff", int64(len(m)), 1.0, []string{indexTag, fieldTag, node.ID})
|
|
|
|
// Update local copy.
|
|
if err := f.RowAttrStore().SetBulkAttrs(m); err != nil {
|
|
return errors.Wrap(err, "setting attrs")
|
|
}
|
|
|
|
// Recompute blocks.
|
|
blks, err = f.RowAttrStore().Blocks()
|
|
if err != nil {
|
|
return errors.Wrap(err, "recomputing blocks")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// syncFragment synchronizes a fragment with the rest of the cluster.
|
|
func (s *holderSyncer) syncFragment(index, field, view string, shard uint64) error {
|
|
// Retrieve local field.
|
|
f := s.Holder.Field(index, field)
|
|
if f == nil {
|
|
return ErrFieldNotFound
|
|
}
|
|
|
|
// Ensure view exists locally.
|
|
v, err := f.createViewIfNotExists(view)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating view")
|
|
}
|
|
|
|
// Ensure fragment exists locally.
|
|
frag, err := v.CreateFragmentIfNotExists(shard)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating fragment")
|
|
}
|
|
|
|
// Sync fragments together.
|
|
fs := fragmentSyncer{
|
|
Fragment: frag,
|
|
Node: s.Node,
|
|
Cluster: s.Cluster,
|
|
Closing: s.Closing,
|
|
}
|
|
if err := fs.syncFragment(); err != nil {
|
|
return errors.Wrap(err, "syncing fragment")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// holderCleaner removes fragments and data files that are no longer used.
|
|
type holderCleaner struct {
|
|
Node *Node
|
|
|
|
Holder *Holder
|
|
Cluster *cluster
|
|
|
|
// Signals that the sync should stop.
|
|
Closing <-chan struct{}
|
|
}
|
|
|
|
// IsClosing returns true if the cleaner has been marked to close.
|
|
func (c *holderCleaner) IsClosing() bool {
|
|
select {
|
|
case <-c.Closing:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CleanHolder compares the holder with the cluster state and removes
|
|
// any unnecessary fragments and files.
|
|
func (c *holderCleaner) CleanHolder() error {
|
|
for _, index := range c.Holder.Indexes() {
|
|
// Verify cleaner has not closed.
|
|
if c.IsClosing() {
|
|
return nil
|
|
}
|
|
|
|
// Get the fragments that node is responsible for (based on hash(index, node)).
|
|
containedShards := c.Cluster.containsShards(index.Name(), index.AvailableShards(), c.Node)
|
|
|
|
// Get the fragments registered in memory.
|
|
for _, field := range index.Fields() {
|
|
for _, view := range field.views() {
|
|
for _, fragment := range view.allFragments() {
|
|
fragShard := fragment.shard
|
|
// Ignore fragments that should be present.
|
|
if uint64InSlice(fragShard, containedShards) {
|
|
continue
|
|
}
|
|
// Delete fragment.
|
|
if err := view.deleteFragment(fragShard); err != nil {
|
|
return errors.Wrap(err, "deleting fragment")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func uint64InSlice(i uint64, s []uint64) bool {
|
|
for _, o := range s {
|
|
if i == o {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|