mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
```bash for file in `cat diffys`; do printf '%s\n%s\n' "// Copyright 2021 Molecula Corp. All rights reserved." "$(cat $file)" >$file; done ```
576 lines
14 KiB
Go
576 lines
14 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
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
|
|
}
|