mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
508 lines
17 KiB
Go
508 lines
17 KiB
Go
// Copyright 2019 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"
|
|
"math/bits"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/molecula/featurebase/v2/logger"
|
|
"github.com/molecula/featurebase/v2/testhook"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// snapshotQueue is a thing which can handle enqueuing snapshots. A snapshot
|
|
// queue distinguishes between high-priority requests, which get satisfied
|
|
// by the next available worker, and regular requests, which get enqueued
|
|
// if there's space in the queue, and otherwise dropped. There's also a
|
|
// separate background task to scan a holder for fragments which may need
|
|
// snapshots, but which is processed only when the queue is empty, and only
|
|
// slowly. "Await" awaits an existing snapshot if one is already enqueued.
|
|
// "Immediate" tries to do one right away. (If one's already enqueued, this
|
|
// can leave it in the queue, which will ignore anything that shows up with
|
|
// the request flag cleared.)
|
|
//
|
|
// Await, Enqueue, and Immediate should be called only with the fragment lock
|
|
// held.
|
|
//
|
|
// If you create a queue, it should get stopped at some point. The
|
|
// atomicSnapshotQueue implementation used as defaultSnapshotQueue has
|
|
// a Start function which will tell you whether it actually started a
|
|
// queue. This logic exists because in a normal server case, you probably
|
|
// want the queue to be shut down as part of server shutdown, but if you're
|
|
// running cluster tests, you probably want to start and shop the queue as
|
|
// part of the test, not stop it when any server terminates.
|
|
//
|
|
// It's less likely to be desireable to start/stop individual queues,
|
|
// because fragments use the defaultSnapshotQueue anyway. This design
|
|
// needs revisiting.
|
|
type SnapshotQueue interface {
|
|
Immediate(*fragment) error
|
|
Enqueue(*fragment)
|
|
Await(*fragment) error
|
|
ScanHolder(*Holder, chan struct{})
|
|
Stop()
|
|
}
|
|
|
|
// queuelessSnapshotQueue isn't a snapshot queue, but it satisfies the
|
|
// interface.
|
|
type queuelessSnapshotQueue struct{}
|
|
|
|
func (q *queuelessSnapshotQueue) Enqueue(f *fragment) {
|
|
// We don't actually try to enqueue the snapshot; it breaks things
|
|
// if a snapshot gets caused during a transaction.
|
|
}
|
|
|
|
func (q *queuelessSnapshotQueue) Await(f *fragment) error {
|
|
return nil
|
|
}
|
|
|
|
func (q *queuelessSnapshotQueue) Immediate(f *fragment) error {
|
|
return f.snapshot()
|
|
}
|
|
|
|
func (q *queuelessSnapshotQueue) ScanHolder(h *Holder, done chan struct{}) {
|
|
}
|
|
|
|
func (q *queuelessSnapshotQueue) Stop() {
|
|
}
|
|
|
|
var defaultSnapshotQueue = &queuelessSnapshotQueue{}
|
|
|
|
// newSnapshotQueue makes a new snapshot queue, of depth N, with
|
|
// w worker threads.
|
|
func newSnapshotQueue(n int, w int, l logger.Logger) SnapshotQueue {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
sq := &prioritySnapshotQueue{
|
|
normal: make(chan snapshotRequest, n),
|
|
urgent: make(chan snapshotRequest),
|
|
background: make(chan snapshotRequest),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
maxOpN: 10000,
|
|
logger: l,
|
|
}
|
|
if sq.logger == nil {
|
|
sq.logger = logger.NewStandardLogger(os.Stderr)
|
|
}
|
|
_ = testhook.Opened(NewAuditor(), sq, nil)
|
|
sq.spawnWorkers(w)
|
|
return sq
|
|
}
|
|
|
|
type snapshotRequest struct {
|
|
frag *fragment
|
|
when time.Time
|
|
}
|
|
|
|
// prioritySnapshotQueue gives preference to "immediate" requests, and
|
|
// dispreference to "background" requests from ScanHolder. It timestamps
|
|
// requests, so it can discard a request if the most recent snapshot is
|
|
// newer than the request. The snapshotPending flag in the fragment is
|
|
// used to track that a given fragment thinks it has been successfully
|
|
// enqueued. Background requests are not considered enqueued, since
|
|
// they'll never get processed if there's anything else. In normal workloads,
|
|
// immediate/urgent snapshots should be rare, but we'll happily drop
|
|
// most requests on the floor; the scanner should pick them up once things
|
|
// are quiet.
|
|
type prioritySnapshotQueue struct {
|
|
logger logger.Logger
|
|
urgent chan snapshotRequest
|
|
normal chan snapshotRequest
|
|
background chan snapshotRequest
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.RWMutex
|
|
scanWG, workerWG sync.WaitGroup
|
|
maxOpN int
|
|
observedOpN [16]uint32
|
|
stats struct {
|
|
enqueued uint32
|
|
skipped uint32
|
|
}
|
|
stopped bool
|
|
}
|
|
|
|
func (sq *prioritySnapshotQueue) spawnWorkers(w int) {
|
|
sq.mu.Lock()
|
|
defer sq.mu.Unlock()
|
|
if sq.ctx.Err() != nil {
|
|
sq.logger.Infof("prioritySnapshotQueue worker: already done")
|
|
return
|
|
}
|
|
sq.workerWG.Add(w)
|
|
for i := 0; i < w; i++ {
|
|
go sq.worker(sq.ctx, sq.urgent, sq.normal, sq.background)
|
|
}
|
|
}
|
|
|
|
func (sq *prioritySnapshotQueue) worker(ctx context.Context, urgent, normal, background chan snapshotRequest) {
|
|
defer sq.workerWG.Done()
|
|
done := ctx.Done()
|
|
ok := true
|
|
var req snapshotRequest
|
|
for ok {
|
|
req.frag = nil
|
|
select {
|
|
case _, ok = <-done:
|
|
case req, ok = <-urgent:
|
|
default:
|
|
select {
|
|
case _, ok = <-done:
|
|
case req, ok = <-urgent:
|
|
case req, ok = <-normal:
|
|
default:
|
|
select {
|
|
case _, ok = <-done:
|
|
case req, ok = <-urgent:
|
|
case req, ok = <-normal:
|
|
case req, ok = <-background:
|
|
}
|
|
}
|
|
}
|
|
if req.frag != nil {
|
|
sq.process(req)
|
|
}
|
|
}
|
|
}
|
|
|
|
// process actually runs a fragment. it will do this if either the fragment
|
|
// has a pending snapshot, or the force flag is set.
|
|
func (sq *prioritySnapshotQueue) process(req snapshotRequest) {
|
|
f := req.frag
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.snapshotStamp.Before(req.when) {
|
|
f.snapshotErr = f.snapshot()
|
|
if f.snapshotErr != nil {
|
|
fmt.Printf("ERROR: snapshot error: %v\n", f.snapshotErr)
|
|
sq.logger.Errorf("snapshot error: %v", f.snapshotErr)
|
|
}
|
|
f.snapshotPending = false
|
|
f.snapshotCond.Broadcast()
|
|
}
|
|
}
|
|
|
|
// Stop shuts down the snapshot queue. It first marks it as done, causing
|
|
// the background scanner(s), if any, to shut down, then waits for them, then
|
|
// closes and nils the queues. The background scanner has to get stopped
|
|
// because otherwise it might try to write to those closed queues.
|
|
func (sq *prioritySnapshotQueue) Stop() {
|
|
sq.mu.Lock()
|
|
defer sq.mu.Unlock()
|
|
if sq.stopped {
|
|
return
|
|
}
|
|
sq.stopped = true
|
|
sq.cancel()
|
|
// scanners need to be done before we close the other channels.
|
|
sq.scanWG.Wait()
|
|
close(sq.normal)
|
|
sq.normal = nil
|
|
close(sq.urgent)
|
|
sq.urgent = nil
|
|
close(sq.background)
|
|
sq.background = nil
|
|
_ = testhook.Closed(NewAuditor(), sq, nil)
|
|
enqueued := atomic.LoadUint32(&sq.stats.enqueued)
|
|
skipped := atomic.LoadUint32(&sq.stats.skipped)
|
|
if skipped > 0 || enqueued > 1 {
|
|
sq.logger.Infof("snapshot queue: enqueued %d, skipped %d\n", sq.stats.enqueued, sq.stats.skipped)
|
|
}
|
|
}
|
|
|
|
// Enqueue tries to add a fragment to the queue, if the fragment is not already
|
|
// enqueued. You should hold a lock on the fragment when calling this.
|
|
func (sq *prioritySnapshotQueue) Enqueue(f *fragment) {
|
|
if f.snapshotPending {
|
|
return
|
|
}
|
|
sq.observeOpN(uint32(f.opN))
|
|
sq.mu.RLock()
|
|
defer sq.mu.RUnlock()
|
|
if sq.normal == nil {
|
|
sq.logger.Infof("requested snapshot after snapshot queue was closed")
|
|
return
|
|
}
|
|
// we have to set this before enqueing, because it's
|
|
// otherwise possible that we're at the head of the queue,
|
|
// and the recipient gets the fragment before we execute the
|
|
// line after the send.
|
|
f.snapshotPending = true
|
|
// try to enqueue snapshot
|
|
select {
|
|
case sq.normal <- snapshotRequest{frag: f, when: time.Now()}:
|
|
atomic.AddUint32(&sq.stats.enqueued, 1)
|
|
return
|
|
default:
|
|
atomic.AddUint32(&sq.stats.skipped, 1)
|
|
f.snapshotPending = false
|
|
return
|
|
}
|
|
}
|
|
|
|
// Await returns when f is not pending a snapshot. Call with the fragment lock
|
|
// held. Await waits on a condition variable inside f, associated with the
|
|
// fragment's lock, so this does not conflict with the lock being used for
|
|
// snapshots.
|
|
//
|
|
// Note that workers don't stop just because the queue's been stopped; only
|
|
// the background scanner is stopped. So an Await shouldn't block forever
|
|
// even if the queue gets shut down. If you're reading this, possibly that
|
|
// analysis is incorrect.
|
|
func (sq *prioritySnapshotQueue) Await(f *fragment) (err error) {
|
|
for f.snapshotPending {
|
|
f.snapshotCond.Wait()
|
|
}
|
|
err, f.snapshotErr = f.snapshotErr, nil
|
|
return err
|
|
}
|
|
|
|
// Immediate forces an immediate snapshot of the given fragment. Call with
|
|
// the fragment locked. If the queue is already closing, the fragment does
|
|
// not get snapshotted.
|
|
func (sq *prioritySnapshotQueue) Immediate(f *fragment) error {
|
|
sq.mu.RLock()
|
|
// no deferred unlock, because we want to unlock this before calling Await.
|
|
// Not because that needs this lock, but because once we're that far, we
|
|
// *don't* need this lock anymore so someone else should have it.
|
|
if sq.urgent == nil {
|
|
sq.mu.RUnlock()
|
|
sq.logger.Errorf("requested immediate snapshot after snapshot queue was closed")
|
|
return errors.New("requested immediate snapshot after snapshot queue was closed")
|
|
}
|
|
f.snapshotPending = true
|
|
sq.observeOpN(uint32(f.opN))
|
|
req := snapshotRequest{frag: f, when: time.Now()}
|
|
// if the fragment was already in the work queue, it's *possible*
|
|
// that the only available worker just picked it off the queue, and
|
|
// is now waiting on getting the fragment's lock, so it can run
|
|
// a snapshot. So we let go of the lock on the fragment, send the
|
|
// request, then request the fragment lock again, because Await will
|
|
// be sleeping on the condition variable associated with the lock,
|
|
// which means it needs to hold the lock so it can let it go during
|
|
// the wait... No, really, this made sense.
|
|
f.mu.Unlock()
|
|
sq.urgent <- req
|
|
sq.mu.RUnlock()
|
|
f.mu.Lock()
|
|
return sq.Await(f)
|
|
}
|
|
|
|
// ScanHolder spawns a goroutine which iterates through the holder's
|
|
// indexes/fields/views/fragments, looking for fragments which have OpN
|
|
// high enough to justify a snapshot but don't seem to have one pending.
|
|
// It then dumps these in the low priority background queue.
|
|
func (sq *prioritySnapshotQueue) ScanHolder(h *Holder, done chan struct{}) {
|
|
sq.mu.Lock()
|
|
sq.scanWG.Add(1)
|
|
go sq.scanHolderWorker(h, sq.background, done)
|
|
sq.mu.Unlock()
|
|
}
|
|
|
|
// observeOpN reports that a given value of opN was "observed", meaning,
|
|
// we encountered a fragment which had that value. This happens for every
|
|
// enqueue/immediate, including enqueue attempts which fail to actually
|
|
// enter the queue, and it also happens for fragments noticed by the background
|
|
// scan but which don't have high enough opN to trigger a snapshot.
|
|
func (sq *prioritySnapshotQueue) observeOpN(n uint32) {
|
|
// aka "log2(n) + 1", or 0 for n==0
|
|
pow2 := 32 - bits.LeadingZeros32(n)
|
|
// 15 == 16384. Our usual fragment maxOpN is 10k, so most fragments
|
|
// should end up in the 8k-16k bucket, rather than the 16k+ bucket,
|
|
// unless we've got a lot of ingests with large batches going on,
|
|
// in which case the 16k bucket will win.
|
|
if pow2 > 15 {
|
|
pow2 = 15
|
|
}
|
|
// store in inverse order so the lowest slot in the array is the
|
|
// highest cardinality
|
|
atomic.AddUint32(&sq.observedOpN[15-pow2], 1)
|
|
}
|
|
|
|
// computeMaxOpN tries to pick a reasonable new maxOpN for the background
|
|
// scan to use. On a quiet system, we want to gradually lower opN, picking
|
|
// the fragments with the highest opN values first, because those offer the
|
|
// largest benefit. So, whenever we check a fragment in the background, if we
|
|
// *don't* snapshot it, we'll "observe" its OpN value, and then we pick a
|
|
// value which picks up at least 1/4 of them.
|
|
//
|
|
// If there's ingest activity, the Immediate and Enqueue operations will
|
|
// "observe" the OpN of fragments submitted to them. This can drive OpN back
|
|
// up, if those fragments frequently have very high opN values, which reflects
|
|
// the fact that we have enough of that activity that we don't need the
|
|
// background scanner adding more.
|
|
//
|
|
// If we have enough ingest activity that the background scanner never actually
|
|
// gets to submit work, we'll rarely get here, because the background scanner
|
|
// will block until there's no snapshots pending for the normal workload.
|
|
// When we do, we'll probably pick a MaxOpN which is dominated by the ingest
|
|
// workload's opN values. So for instance, if everything coming in from the
|
|
// ingest workload has 10k or more items, because that's the default fragment
|
|
// maxOpN, that will probably set the background snapshot queue value to 8k.
|
|
func (sq *prioritySnapshotQueue) computeMaxOpN() {
|
|
sq.logger.Debugf("observedOpN by power of 2: %d\n", sq.observedOpN[:])
|
|
total := uint32(0)
|
|
for i := range sq.observedOpN {
|
|
total += atomic.LoadUint32(&sq.observedOpN[i])
|
|
}
|
|
target := (total / 4) + 1
|
|
subTotal := uint32(0)
|
|
for i := range sq.observedOpN {
|
|
v := atomic.LoadUint32(&sq.observedOpN[i])
|
|
subTotal += v
|
|
if subTotal >= target {
|
|
prevMaxOpN := sq.maxOpN
|
|
sq.maxOpN = (1 << (15 - uint(i))) / 2
|
|
if sq.maxOpN > 0 {
|
|
sq.maxOpN--
|
|
}
|
|
if prevMaxOpN != sq.maxOpN {
|
|
sq.logger.Infof("background scan: %d/%d fragments considered have opN %d or higher\n",
|
|
subTotal, total, sq.maxOpN)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
// It's conceptually possible that we'll miss a couple of observations
|
|
// here but that's not really important. This is all pretty approximate.
|
|
for i := range sq.observedOpN {
|
|
atomic.StoreUint32(&sq.observedOpN[i], 0)
|
|
}
|
|
}
|
|
|
|
// prioritySnapshotQueueScanner is the data type that implements HolderOperator
|
|
// and represents a single scan of a holder, with a given maxOpN.
|
|
type prioritySnapshotQueueScanner struct {
|
|
HolderFilterAll
|
|
HolderProcessNone
|
|
sq *prioritySnapshotQueue
|
|
holder *Holder
|
|
queue chan snapshotRequest
|
|
ctx context.Context
|
|
maxOpN int
|
|
seen, hits, counter int
|
|
}
|
|
|
|
func (s *prioritySnapshotQueueScanner) ProcessFragment(f *fragment) error {
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
s.seen++
|
|
// we can't defer this reasonably, because otherwise we'll keep
|
|
// the fragment locked forever if we end up trying to send it
|
|
// to the queue, but the workers are busy on other fragments.
|
|
f.mu.Lock()
|
|
open := f.open
|
|
snapshotPending, opN := f.snapshotPending, f.opN
|
|
f.mu.Unlock()
|
|
|
|
// a pending snapshot is one that is either in the normal or
|
|
// immediate queue, or is trying to get into the normal queue
|
|
// and about to fail, but either way, it already got observed
|
|
// there, so we don't need to observe it here. A closed fragment
|
|
// doesn't matter to us -- it should be a transient state that
|
|
// happens during a shutdown, or shouldn't happen, but we don't
|
|
// care about it.
|
|
if snapshotPending || !open {
|
|
return nil
|
|
}
|
|
if opN <= s.maxOpN {
|
|
// observe the value but don't do a snapshot
|
|
s.sq.observeOpN(uint32(opN))
|
|
s.counter++
|
|
if s.counter == 1000 {
|
|
select {
|
|
case <-time.After(1 * time.Second):
|
|
case <-s.ctx.Done():
|
|
return io.EOF
|
|
}
|
|
s.counter = 0
|
|
}
|
|
return nil
|
|
}
|
|
// we don't observe values when we decide to trigger a snapshot,
|
|
// because those values will be changing anyway. we could also
|
|
// observe them as zero, but that's also sort of wrong.
|
|
s.hits++
|
|
select {
|
|
case s.queue <- snapshotRequest{frag: f, when: time.Now()}:
|
|
s.sq.logger.Debugf("found fragment needing snapshot: %s\n", f.path())
|
|
case <-s.ctx.Done():
|
|
return io.EOF
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
func contextMergedWithStructChan(ctx context.Context, ch chan struct{}) (context.Context, context.CancelFunc) {
|
|
canCancel, cancel := context.WithCancel(ctx)
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
cancel()
|
|
case <-ch:
|
|
cancel()
|
|
case <-canCancel.Done():
|
|
// don't need to cancel, but do need to exit this
|
|
// function
|
|
}
|
|
}()
|
|
return canCancel, cancel
|
|
}
|
|
|
|
// scanHolderWorker is a background task that scans a holder looking for
|
|
// fragments which need snapshots taken. It's the cleanup task for snapshots
|
|
// that would have been requested by Enqueue, but the queue was full.
|
|
func (sq *prioritySnapshotQueue) scanHolderWorker(h *Holder, background chan snapshotRequest, done chan struct{}) {
|
|
defer sq.scanWG.Done()
|
|
ctx, cancel := contextMergedWithStructChan(sq.ctx, done)
|
|
defer cancel()
|
|
scanner := &prioritySnapshotQueueScanner{
|
|
sq: sq,
|
|
holder: h,
|
|
queue: background,
|
|
ctx: sq.ctx,
|
|
maxOpN: sq.maxOpN,
|
|
}
|
|
for {
|
|
err := h.Process(ctx, scanner)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if scanner.hits > 0 {
|
|
sq.logger.Infof("background scan: %d/%d fragments needed snapshots\n", scanner.hits, scanner.seen)
|
|
scanner.hits = 0
|
|
} else {
|
|
sq.logger.Debugf("background scan: no fragments needed snapshots, waiting\n")
|
|
// No reason to be active if we're not finding anything.
|
|
select {
|
|
case <-time.After(60 * time.Second):
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
scanner.seen = 0
|
|
sq.computeMaxOpN()
|
|
scanner.maxOpN = sq.maxOpN
|
|
}
|
|
}
|