mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
Performance of tests on MacOS has been atrocious for a while, and a lot of that is fsync, so we're trying to make that optional. To test all of this, I modified RBF to panic if anything tried to open an RBF database without disabling fsync, and ran the tests that way, and tracked down the various places this could still happen. There's a lot of places in our tree where we were creating test holders which were not getting created with fsync disabled, which results in a surprisingly large number of points at which we end up calling fsync in tests, which makes tests much slower than they need to be. There's also a bunch of places where the flags don't get propagated correctly; for instance, storage.fsync didn't propagate to the RBFConfig. We add an "fsync enabled" flag to OpenTranslateStoreFunc, so we can tell translation stores that we don't need syncing, so the server's config can be passed on appropriately. More of the test code that sets things up is correctly configuring that flag by default. We also change the barely-used bolt storage backend to support this as well. With this done, the only calls to fsync left in a run of `go test -short` in the top-level directory are from the zap logger in etcd, and consumed around 0.03 seconds. The overall impact is that `go test -short` went from "takes enough more than 10 minutes that i don't know how long it takes" to about 2.5 minutes.
589 lines
14 KiB
Go
589 lines
14 KiB
Go
// Copyright 2020 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pilosa
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math/bits"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
// IDAllocKey is an ID allocation key.
|
|
type IDAllocKey struct {
|
|
Index string `json:"index"`
|
|
Key string `json:"key,omitempty"`
|
|
}
|
|
|
|
type IDAllocReserveRequest struct {
|
|
Key IDAllocKey `json:"key"`
|
|
Session [32]byte `json:"session"`
|
|
Offset uint64 `json:"offset"`
|
|
Count uint64 `json:"count"`
|
|
}
|
|
|
|
type IDAllocCommitRequest struct {
|
|
Key IDAllocKey `json:"key"`
|
|
Session [32]byte `json:"session"`
|
|
Count uint64 `json:"count"`
|
|
}
|
|
|
|
func (k IDAllocKey) String() string {
|
|
// This is a pretty printing format.
|
|
// **DO NOT** attempt to parse this.
|
|
return k.Index + ":" + k.Key
|
|
}
|
|
|
|
type idAllocator struct {
|
|
db *bolt.DB
|
|
fsyncEnabled bool
|
|
}
|
|
|
|
type OpenIDAllocatorFunc func(path string, enableFsync bool) (*idAllocator, error) // whyyyyyyyyy
|
|
|
|
func OpenIDAllocator(path string, enableFsync bool) (*idAllocator, error) {
|
|
db, err := bolt.Open(path, 0666, &bolt.Options{Timeout: 1 * time.Second, NoSync: !enableFsync})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &idAllocator{db: db, fsyncEnabled: enableFsync}, nil
|
|
}
|
|
|
|
func (ida *idAllocator) Replace(reader io.Reader) error {
|
|
newFile := ida.db.Path() + ".bak"
|
|
liveFile := ida.db.Path()
|
|
file, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
_, err = io.Copy(file, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file.Close()
|
|
err = ida.db.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Rename(liveFile, liveFile+".sav")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Rename(newFile, liveFile)
|
|
if err != nil {
|
|
err = os.Rename(liveFile+".sav", liveFile)
|
|
return err
|
|
} else {
|
|
_ = os.Remove(liveFile + ".sav")
|
|
}
|
|
db, err := bolt.Open(liveFile, 0666, &bolt.Options{Timeout: 1 * time.Second, NoSync: !ida.fsyncEnabled})
|
|
ida.db = db
|
|
return err
|
|
}
|
|
|
|
func (ida *idAllocator) Close() error {
|
|
if ida == nil || ida.db == nil {
|
|
return nil
|
|
}
|
|
defer func() { ida.db = nil }()
|
|
return ida.db.Close()
|
|
}
|
|
|
|
func (ida *idAllocator) WriteTo(w io.Writer) (int64, error) {
|
|
if ida == nil || ida.db == nil {
|
|
return 0, fmt.Errorf("idAllocator closed")
|
|
}
|
|
|
|
tx, err := ida.db.Begin(false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
return tx.WriteTo(w)
|
|
}
|
|
|
|
// ErrIDOffsetDesync is an error generated when attempting to reserve IDs at a committed offset.
|
|
// This will typically happen when kafka partitions are moved between kafka ingesters - there may be a brief period in which 2 ingesters are processing the same messages at the same time.
|
|
// The ingester can resolve this by ignoring messages under base.
|
|
type ErrIDOffsetDesync struct {
|
|
// Requested is the offset that the client attempted to reserve.
|
|
Requested uint64 `json:"requested"`
|
|
|
|
// Base is the next uncommitted offset for which IDs may be reserved.
|
|
Base uint64 `json:"base"`
|
|
}
|
|
|
|
func (err ErrIDOffsetDesync) Error() string {
|
|
return fmt.Sprintf("attempted to reserve IDs at committed offset %d (base offset: %d)", err.Requested, err.Base)
|
|
}
|
|
|
|
func (ida *idAllocator) reserve(key IDAllocKey, session [32]byte, offset, count uint64) ([]IDRange, error) {
|
|
if session == [32]byte{} {
|
|
return nil, errors.New("detected a broken session key")
|
|
}
|
|
|
|
var ranges []IDRange
|
|
err := ida.db.Update(func(tx *bolt.Tx) error {
|
|
// Find the bucket associated with the key.
|
|
bkt, err := key.findBucket(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Fetch the old reservation.
|
|
var res idReservation
|
|
err = res.decode(bkt.Get([]byte(key.Key + "\x00")))
|
|
if err != nil {
|
|
return errors.Wrap(err, "decoding old reservation")
|
|
}
|
|
res.lock = session
|
|
if offset != ^uint64(0) && offset != res.offset {
|
|
// Offset control is in use and the offset differs.
|
|
if offset < res.offset {
|
|
// This probbably means that 2 clients are running at the same time.
|
|
// This is fine - just tell the client that is behind what had been dealt with.
|
|
return ErrIDOffsetDesync{
|
|
Requested: offset,
|
|
Base: res.offset,
|
|
}
|
|
}
|
|
|
|
// Roll the IDs forward.
|
|
diff := offset - res.offset
|
|
for diff > 0 && len(res.ranges) > 0 {
|
|
if res.ranges[0].width() > diff-1 {
|
|
res.ranges[0].First += diff
|
|
break
|
|
}
|
|
|
|
diff -= res.ranges[0].width() + 1
|
|
res.ranges = res.ranges[1:]
|
|
}
|
|
}
|
|
res.offset = offset
|
|
|
|
// Count already-reserved ranges.
|
|
var preReserved uint64
|
|
for _, r := range res.ranges {
|
|
sum, overflow := bits.Add64(preReserved, r.width(), 1)
|
|
if overflow != 0 {
|
|
return errors.New("impossibly large reservation")
|
|
}
|
|
preReserved = sum
|
|
}
|
|
|
|
switch {
|
|
case preReserved == count:
|
|
// The client probbably failed while ingesting this.
|
|
// Spit out the same ID ranges a second time.
|
|
ranges = res.ranges
|
|
|
|
case preReserved > count:
|
|
// This could happen????
|
|
ranges = make([]IDRange, 0, len(res.ranges))
|
|
for _, r := range res.ranges {
|
|
if count == 0 {
|
|
break
|
|
}
|
|
|
|
if count-1 < r.width() {
|
|
ranges = append(ranges, IDRange{
|
|
First: r.First,
|
|
Last: r.First + count - 1,
|
|
})
|
|
break
|
|
}
|
|
|
|
ranges = append(ranges, r)
|
|
count -= r.width() + 1
|
|
}
|
|
|
|
default:
|
|
// Reserve additional IDs.
|
|
reserved, err := doReserveIDs(bkt, count-preReserved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reserved = append(res.ranges, reserved...)
|
|
res.ranges = reserved
|
|
ranges = reserved
|
|
}
|
|
|
|
// Save the updated reservation.
|
|
encoded, err := res.encode()
|
|
if err != nil {
|
|
return errors.Wrap(err, "encoding updated ID reservation entry")
|
|
}
|
|
err = bkt.Put([]byte(key.Key+"\x00"), encoded)
|
|
if err != nil {
|
|
return errors.Wrap(err, "saving updated ID reservation entry")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "acquiring IDs from key %s in session %v", key, session)
|
|
}
|
|
return ranges, nil
|
|
}
|
|
|
|
func (ida *idAllocator) commit(key IDAllocKey, session [32]byte, count uint64) error {
|
|
if session == [32]byte{} {
|
|
return errors.New("detected a broken session key")
|
|
}
|
|
|
|
err := ida.db.Update(func(tx *bolt.Tx) error {
|
|
// Find the bucket associated with the key.
|
|
bkt, err := key.findBucket(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Fetch the old reservation.
|
|
prev := bkt.Get([]byte(key.Key + "\x00"))
|
|
if prev == nil {
|
|
// There is nothing to commit.
|
|
return errors.New("nothing to commit")
|
|
}
|
|
var res idReservation
|
|
err = res.decode(prev)
|
|
if err != nil {
|
|
return errors.Wrap(err, "decoding old reservation")
|
|
}
|
|
if res.lock != session {
|
|
// This probbably means that there are multiple clients attempting to manipulate the same ID allocator key.
|
|
return errors.New("lock collision")
|
|
}
|
|
|
|
if res.offset != ^uint64(0) {
|
|
// Update the offset.
|
|
if count > ^uint64(1)-res.offset {
|
|
return errors.New("offset overflow")
|
|
}
|
|
res.offset += count
|
|
}
|
|
|
|
// Discard processed ID ranges.
|
|
n := count
|
|
for n > 0 && len(res.ranges) > 0 {
|
|
r := &res.ranges[0]
|
|
w := r.width()
|
|
if n-1 < w {
|
|
r.First += n
|
|
n = 0
|
|
break
|
|
}
|
|
|
|
n -= w + 1
|
|
res.ranges = res.ranges[1:]
|
|
}
|
|
if n > 0 {
|
|
// The client attempted to commit more IDs than they requested.
|
|
return errors.New("overcommitted processed IDs")
|
|
}
|
|
|
|
if res.offset == ^uint64(0) && len(res.ranges) > 0 {
|
|
// Return unused ranges to the central list.
|
|
err = doReleaseIDs(bkt, res.ranges...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "releasing unused IDs")
|
|
}
|
|
res.ranges = nil
|
|
}
|
|
|
|
if res.offset == ^uint64(0) && len(res.ranges) == 0 {
|
|
// Delete the reservation entry, as it no longer contains any useful information.
|
|
err = bkt.Delete([]byte(key.Key + "\x00"))
|
|
if err != nil {
|
|
return errors.Wrap(err, "deleting used ID reservation metadata")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Save the updated reservation.
|
|
encoded, err := res.encode()
|
|
if err != nil {
|
|
return errors.Wrap(err, "encoding updated ID reservation entry")
|
|
}
|
|
err = bkt.Put([]byte(key.Key+"\x00"), encoded)
|
|
if err != nil {
|
|
return errors.Wrap(err, "saving updated ID reservation entry")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return errors.Wrapf(err, "committing %d IDs in allocation session %x with key %s", count, session, key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ida *idAllocator) reset(index string) error {
|
|
err := ida.db.Update(func(tx *bolt.Tx) error {
|
|
if tx.Bucket([]byte(index)) == nil {
|
|
return nil
|
|
}
|
|
|
|
err := tx.DeleteBucket([]byte(index))
|
|
if err != nil {
|
|
return errors.Wrap(err, "deleting bucket")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return errors.Wrapf(err, "resetting ID allocation for index %q", index)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (k IDAllocKey) findBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
|
ibkt, err := tx.CreateBucketIfNotExists([]byte(k.Index))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "creating index bucket")
|
|
}
|
|
return ibkt, nil
|
|
}
|
|
|
|
// IDRange is a reserved ID range.
|
|
type IDRange struct {
|
|
First uint64 `json:"first"`
|
|
Last uint64 `json:"last"`
|
|
}
|
|
|
|
func (r *IDRange) decode(data []byte) error {
|
|
if len(data) != 16 {
|
|
return errors.New("wrong size for ID range")
|
|
}
|
|
first := binary.LittleEndian.Uint64(data[:8])
|
|
last := binary.LittleEndian.Uint64(data[8:16])
|
|
if last < first {
|
|
return errors.New("backwards ID range")
|
|
}
|
|
*r = IDRange{First: first, Last: last}
|
|
return nil
|
|
}
|
|
|
|
// width of the range (count-1).
|
|
func (r IDRange) width() uint64 {
|
|
return r.Last - r.First
|
|
}
|
|
|
|
func decodeRanges(data []byte) ([]IDRange, error) {
|
|
if len(data)%16 != 0 {
|
|
return nil, errors.New("incorrectly sized ranges list")
|
|
}
|
|
|
|
ranges := make([]IDRange, len(data)/16)
|
|
for i := range ranges {
|
|
err := ranges[i].decode(data[16*i:][:16])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "decoding range %d", i)
|
|
}
|
|
}
|
|
|
|
return ranges, nil
|
|
}
|
|
|
|
func encodeRanges(ranges []IDRange) ([]byte, error) {
|
|
data := make([]byte, 16*len(ranges))
|
|
for i, r := range ranges {
|
|
if r.Last < r.First {
|
|
return nil, errors.Errorf("backwards ID range at index %d", i)
|
|
}
|
|
rdat := data[16*i:][:16]
|
|
binary.LittleEndian.PutUint64(rdat[0:8], r.First)
|
|
binary.LittleEndian.PutUint64(rdat[8:16], r.Last)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
type idReservation struct {
|
|
ranges []IDRange
|
|
offset uint64
|
|
lock [32]byte
|
|
}
|
|
|
|
func (r *idReservation) decode(data []byte) error {
|
|
if data == nil {
|
|
*r = idReservation{}
|
|
return nil
|
|
}
|
|
|
|
if len(data) < 8+len(r.lock) {
|
|
return errors.New("invalid ID reservation entry")
|
|
}
|
|
|
|
offset := binary.LittleEndian.Uint64(data[0:8])
|
|
lock := data[8 : 8+len(r.lock)]
|
|
|
|
ranges, err := decodeRanges(data[8+len(r.lock):])
|
|
if err != nil {
|
|
return errors.Wrap(err, "decoding ranges in reservation entry")
|
|
}
|
|
|
|
r.ranges = ranges
|
|
r.offset = offset
|
|
copy(r.lock[:], lock)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r idReservation) encode() ([]byte, error) {
|
|
var odat [8]byte
|
|
binary.LittleEndian.PutUint64(odat[:], r.offset)
|
|
rdat, err := encodeRanges(r.ranges)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(append(odat[:], r.lock[:]...), rdat...), nil
|
|
}
|
|
|
|
func doReserveIDs(bkt *bolt.Bucket, count uint64) ([]IDRange, error) {
|
|
var avail []IDRange
|
|
if prev := bkt.Get([]byte{1}); prev != nil {
|
|
r, err := decodeRanges(prev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
avail = r
|
|
} else {
|
|
avail = []IDRange{{1, ^uint64(0)}}
|
|
}
|
|
reserved, avail, err := reserveIDs(avail, count)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "reserving IDs")
|
|
}
|
|
encoded, err := encodeRanges(avail)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "encoding available ID ranges")
|
|
}
|
|
err = bkt.Put([]byte{1}, encoded)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "saving available ID ranges")
|
|
}
|
|
return reserved, nil
|
|
}
|
|
|
|
func reserveIDs(avail []IDRange, count uint64) (reserved []IDRange, unused []IDRange, err error) {
|
|
// Min-heapify the available ranges by size.
|
|
for i, v := range avail {
|
|
width := v.width()
|
|
for width < avail[(i-1)/2].width() {
|
|
avail[(i-1)/2], avail[i] = v, avail[(i-1)/2]
|
|
i = (i - 1) / 2
|
|
}
|
|
}
|
|
|
|
// Reserve ranges in ascending size order.
|
|
for count > 0 && len(avail) > 0 {
|
|
if count-1 < avail[0].width() {
|
|
// The smallest available range is larger than what is needed.
|
|
reserved = append(reserved, IDRange{
|
|
First: avail[0].First,
|
|
Last: avail[0].First + count - 1,
|
|
})
|
|
avail[0].First += count
|
|
count = 0
|
|
break
|
|
}
|
|
|
|
// Completely reserve the smallest range.
|
|
count -= avail[0].width() + 1
|
|
reserved = append(reserved, avail[0])
|
|
avail[0] = avail[len(avail)-1]
|
|
avail = avail[:len(avail)-1]
|
|
i := 0
|
|
for {
|
|
min := i
|
|
if l := 2*i + 1; l < len(avail) && avail[l].width() < avail[min].width() {
|
|
min = l
|
|
}
|
|
if r := 2*i + 2; r < len(avail) && avail[r].width() < avail[min].width() {
|
|
min = r
|
|
}
|
|
if min == i {
|
|
break
|
|
}
|
|
avail[i], avail[min] = avail[min], avail[i]
|
|
i = min
|
|
}
|
|
}
|
|
if count > 0 {
|
|
return nil, nil, errors.Errorf("insufficient available ID space (missing %d IDs)", count)
|
|
}
|
|
|
|
return reserved, avail, nil
|
|
}
|
|
|
|
func doReleaseIDs(bkt *bolt.Bucket, ranges ...IDRange) error {
|
|
var avail []IDRange
|
|
if prev := bkt.Get([]byte{1}); prev != nil {
|
|
r, err := decodeRanges(prev)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
avail = r
|
|
} else {
|
|
return errors.New("cannot return IDs to the void")
|
|
}
|
|
avail, err := mergeIDs(avail, ranges)
|
|
if err != nil {
|
|
return errors.Wrap(err, "returning IDs")
|
|
}
|
|
encoded, err := encodeRanges(avail)
|
|
if err != nil {
|
|
return errors.Wrap(err, "encoding available ID ranges")
|
|
}
|
|
err = bkt.Put([]byte{1}, encoded)
|
|
if err != nil {
|
|
return errors.Wrap(err, "saving available ID ranges")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mergeIDs(main, returned []IDRange) ([]IDRange, error) {
|
|
if len(main) == 0 && len(returned) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
main = append(main, returned...)
|
|
|
|
sort.Slice(main, func(i, j int) bool { return main[i].First < main[j].First })
|
|
|
|
i := 1
|
|
for _, v := range main[1:] {
|
|
prev := &main[i-1]
|
|
switch {
|
|
case v.First == prev.First || v.First <= prev.Last:
|
|
return nil, errors.Errorf("found unexpected overlap between ranges %v and %v", prev, v)
|
|
case v.First == prev.Last+1:
|
|
prev.Last = v.Last
|
|
default:
|
|
main[i] = v
|
|
i++
|
|
}
|
|
}
|
|
|
|
return main[:i], nil
|
|
}
|