mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* removes unused filesize function * removes ioutil usage * updates ioutil.ReadAll to io.ReadAll * updates ioutil.TempFile to os.CreateTemp * updates ioutil.TempDir to os.MkdirTemp * updates ioutil.ReadAll to os.ReadAll * update ioutil.WriteFile to os.WriteFile * updates ioutil.Discard to io.Discard * updates ioutil.ReadDir to os.ReadDir where applicable * removes unused code in idk * creates type to use for context value keys * replaces assert.Nil with assert.NoError for error checks
461 lines
12 KiB
Go
461 lines
12 KiB
Go
package idk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/bits"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"sync"
|
|
|
|
pilosacore "github.com/featurebasedb/featurebase/v3"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type RangeAllocator interface {
|
|
Get() (*IDRange, error)
|
|
Return(*IDRange) error
|
|
}
|
|
|
|
type IDAllocator interface {
|
|
Next(context.Context, Record) (uint64, error)
|
|
Reserve(context.Context, uint64) error
|
|
Commit(context.Context) error
|
|
}
|
|
|
|
type LocalRangeAllocator struct {
|
|
shardWidth uint64
|
|
next uint64
|
|
returned []*IDRange
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewLocalRangeAllocator(shardWidth uint64) RangeAllocator {
|
|
if shardWidth < 1<<16 || bits.OnesCount64(shardWidth) > 1 {
|
|
panic(fmt.Sprintf("bad shardWidth in NewRangeAllocator: %d", shardWidth))
|
|
}
|
|
return &LocalRangeAllocator{
|
|
shardWidth: shardWidth,
|
|
}
|
|
}
|
|
|
|
// IDRange is inclusive at Start and exclusive at End... like slices.
|
|
type IDRange struct {
|
|
Start uint64
|
|
End uint64
|
|
}
|
|
|
|
type rangeNexter struct {
|
|
a RangeAllocator
|
|
r *IDRange
|
|
}
|
|
|
|
func NewRangeNexter(a RangeAllocator) (IDAllocator, error) {
|
|
r, err := a.Get()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting range")
|
|
}
|
|
return &rangeNexter{
|
|
a: a,
|
|
r: r,
|
|
}, nil
|
|
}
|
|
|
|
func (n *rangeNexter) Next(ctx context.Context, _ Record) (val uint64, err error) {
|
|
if n.r.Start == n.r.End {
|
|
n.r, err = n.a.Get()
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "getting next range")
|
|
}
|
|
}
|
|
if n.r.Start >= n.r.End {
|
|
panic("Start is greater than End")
|
|
}
|
|
val = n.r.Start
|
|
n.r.Start++
|
|
return
|
|
}
|
|
|
|
func (n *rangeNexter) Reserve(ctx context.Context, count uint64) error {
|
|
return nil
|
|
}
|
|
|
|
func (n *rangeNexter) Commit(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (a *LocalRangeAllocator) Get() (*IDRange, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
n := len(a.returned)
|
|
if n > 0 {
|
|
ret := a.returned[n-1]
|
|
a.returned = a.returned[:n-1]
|
|
return ret, nil
|
|
}
|
|
ret := &IDRange{
|
|
Start: a.next,
|
|
End: a.next + a.shardWidth,
|
|
}
|
|
a.next += a.shardWidth
|
|
return ret, nil
|
|
}
|
|
|
|
func (a *LocalRangeAllocator) Return(r *IDRange) error {
|
|
if r.Start == r.End {
|
|
return nil
|
|
}
|
|
if r.Start > r.End {
|
|
return errors.Errorf("attempted to return range with start > end: %v", r)
|
|
}
|
|
a.mu.Lock()
|
|
a.returned = append(a.returned, r)
|
|
a.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
type normalModeNexter struct {
|
|
key pilosacore.IDAllocKey
|
|
session [32]byte
|
|
localIDs []pilosacore.IDRange
|
|
localOffset uint64
|
|
idManager *pilosaIDManager
|
|
}
|
|
|
|
func newNormalModeNexter(idManager *pilosaIDManager, index, key string) IDAllocator {
|
|
nexter := &normalModeNexter{
|
|
key: pilosacore.IDAllocKey{
|
|
Index: index,
|
|
Key: key,
|
|
},
|
|
idManager: idManager,
|
|
}
|
|
return nexter
|
|
}
|
|
|
|
func (n *normalModeNexter) Reserve(ctx context.Context, count uint64) (err error) {
|
|
n.localIDs = nil
|
|
n.localOffset = 0
|
|
_, err = io.ReadFull(rand.Reader, n.session[:])
|
|
if err != nil {
|
|
return errors.Wrap(err, "acquiring session ID")
|
|
}
|
|
reserveReq := pilosacore.IDAllocReserveRequest{
|
|
Key: n.key,
|
|
Session: n.session,
|
|
Offset: ^uint64(0),
|
|
Count: count,
|
|
}
|
|
ids, err := n.idManager.reserve(ctx, reserveReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.localIDs = ids
|
|
return nil
|
|
}
|
|
|
|
func (n *normalModeNexter) Commit(ctx context.Context) error {
|
|
commitRequest := pilosacore.IDAllocCommitRequest{
|
|
Key: n.key,
|
|
Session: n.session,
|
|
Count: n.localOffset,
|
|
}
|
|
err := n.idManager.commit(ctx, commitRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.localIDs = nil
|
|
n.localOffset = 0
|
|
return nil
|
|
}
|
|
|
|
func (n *normalModeNexter) Next(ctx context.Context, _ Record) (uint64, error) {
|
|
if len(n.localIDs) == 0 {
|
|
return 0, errors.New("no reserved IDs")
|
|
}
|
|
|
|
id := n.localIDs[0].First
|
|
if n.localIDs[0].First == n.localIDs[0].Last {
|
|
n.localIDs = n.localIDs[1:]
|
|
} else {
|
|
n.localIDs[0].First++
|
|
}
|
|
n.localOffset++
|
|
return id, nil
|
|
}
|
|
|
|
// idAllocation is a collection of IDs reserved for an offset stream.
|
|
type idAllocation struct {
|
|
// localIDs are the currently-reserved IDs to use.
|
|
// The first ID (if present) maps to offset.
|
|
localIDs []pilosacore.IDRange
|
|
|
|
// offset is the offset of the lowest available mapped record.
|
|
offset uint64
|
|
}
|
|
|
|
// offsetModeNexter reserves IDs when pulling data from a set of offset-streams (e.g. kafka partitions).
|
|
// Currently, a lot of the design of offsetModeNexter assumes
|
|
// it's used with kafka though ideally, it could be used with
|
|
// any other source that's partitioned and offset based.
|
|
type offsetModeNexter struct {
|
|
// idManager is a client to Pilosa's ID allocator.
|
|
idManager *pilosaIDManager
|
|
|
|
// index is the index to reserve IDs on.
|
|
index string
|
|
|
|
// reserveCount is the number of IDs to request from Pilosa at once.
|
|
reserveCount uint64
|
|
|
|
// localStore is the set of currently-reserved IDs.
|
|
// When using kafka, the key is derived from the topic and partition.
|
|
localStore map[string]*idAllocation
|
|
}
|
|
|
|
func newOffsetModeNexter(idManager *pilosaIDManager, index string, reserveCount uint64) IDAllocator {
|
|
nexter := &offsetModeNexter{
|
|
idManager: idManager,
|
|
index: index,
|
|
reserveCount: reserveCount,
|
|
localStore: make(map[string]*idAllocation),
|
|
}
|
|
return nexter
|
|
}
|
|
|
|
func (n *offsetModeNexter) Next(ctx context.Context, r Record) (uint64, error) {
|
|
// Get the stream and offset of the record. In the case of a kafka message
|
|
// the stream key will have the following format: "<topic>:<partition>"
|
|
rec, ok := r.(OffsetStreamRecord)
|
|
if !ok {
|
|
return 0, errors.New("invalid source of records for OffsetMode ID Allocator: should implement OffsetStreamRecord")
|
|
}
|
|
key, offset := rec.StreamOffset()
|
|
|
|
// retrieve allocation based on key
|
|
// for kafka, the key is constructed from the topic + partition
|
|
alloc, ok := n.localStore[key]
|
|
if !ok {
|
|
alloc = &idAllocation{}
|
|
|
|
n.localStore[key] = alloc
|
|
}
|
|
|
|
if offset < alloc.offset {
|
|
// This may happen in a brief period when two ingesters are processing the same partition.
|
|
return 0, ErrIDOffsetDesync{
|
|
Requested: offset,
|
|
Base: alloc.offset,
|
|
}
|
|
}
|
|
|
|
// Skip IDs under the offset.
|
|
if len(alloc.localIDs) != 0 && offset > alloc.offset {
|
|
for len(alloc.localIDs) != 0 && alloc.localIDs[0].Last-alloc.localIDs[0].First < offset-alloc.offset {
|
|
alloc.offset += alloc.localIDs[0].Last - alloc.localIDs[0].First + 1
|
|
alloc.localIDs = alloc.localIDs[1:]
|
|
}
|
|
if len(alloc.localIDs) != 0 {
|
|
skip := offset - alloc.offset
|
|
alloc.offset += skip
|
|
alloc.localIDs[0].First += skip
|
|
}
|
|
}
|
|
|
|
// if we don't have any local IDs to nextify, then
|
|
// get fresh IDs from idManager ie Pilosa
|
|
if len(alloc.localIDs) == 0 {
|
|
// get fresh IDs from Pilosa
|
|
allocKey := pilosacore.IDAllocKey{
|
|
Key: key,
|
|
Index: n.index,
|
|
}
|
|
var session [32]byte
|
|
_, err := io.ReadFull(rand.Reader, session[:])
|
|
if err != nil {
|
|
return 0, errors.Wrap(err, "acquiring session ID")
|
|
}
|
|
reserveReq := pilosacore.IDAllocReserveRequest{
|
|
Key: allocKey,
|
|
Session: session,
|
|
Offset: offset,
|
|
Count: n.reserveCount,
|
|
}
|
|
ids, err := n.idManager.reserve(ctx, reserveReq)
|
|
if err != nil {
|
|
if err, ok := err.(ErrIDOffsetDesync); ok {
|
|
// Skip all messages until the latest offset.
|
|
alloc.offset = err.Base
|
|
}
|
|
|
|
return 0, err
|
|
}
|
|
|
|
alloc.localIDs = ids
|
|
alloc.offset = offset
|
|
}
|
|
|
|
// finally, get the ID
|
|
return alloc.localIDs[0].First, nil
|
|
}
|
|
|
|
// Commits for offset-based allocated IDs are done implicitly when
|
|
// one reserves the next IDs, so there's no need to carry out an
|
|
// explicit commit here (for the time-being).
|
|
func (n *offsetModeNexter) Commit(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (n *offsetModeNexter) Reserve(ctx context.Context, count uint64) (err error) {
|
|
// no op, reservation is done on demand during calls to Next
|
|
return nil
|
|
}
|
|
|
|
// pilosaIDManager is an external ID Manager
|
|
// for autogenerated IDs
|
|
type pilosaIDManager struct {
|
|
hcli *http.Client
|
|
url url.URL
|
|
}
|
|
|
|
// TODO, once pull/1559 is merged into pilosa main branch
|
|
// no need to maintain a duplicate definition of ErrIDOffsetDesync
|
|
// here
|
|
type ErrIDOffsetDesync struct {
|
|
Requested uint64 `json:"requested"`
|
|
// Base is the next lowest uncommitted offset for which
|
|
// IDs may be reserved
|
|
Base uint64 `json:"base"`
|
|
}
|
|
|
|
func (e ErrIDOffsetDesync) Error() string {
|
|
return fmt.Sprintf("attempted to reserve IDs at committed offset %d (base offset: %d)", e.Requested, e.Base)
|
|
}
|
|
|
|
func (ps *pilosaIDManager) reserve(ctx context.Context, reserveReq pilosacore.IDAllocReserveRequest) ([]pilosacore.IDRange, error) {
|
|
data, err := json.Marshal(reserveReq)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "marshaling ID reservation request")
|
|
}
|
|
targ, err := ps.url.Parse("/internal/idalloc/reserve")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "resolving ID reservation target URL")
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targ.String(), bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "preparing ID reservation HTTP request")
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
req.Header.Add("Accept", "application/json")
|
|
|
|
// add authorization token to request
|
|
token, ok := ctx.Value(contextKeyToken).(string)
|
|
if ok && token != "" {
|
|
req.Header.Set("Authorization", token)
|
|
}
|
|
|
|
resp, err := ps.hcli.Do(req)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "dispatching ID reservation HTTP request")
|
|
}
|
|
var body []byte
|
|
if resp.Body != nil {
|
|
defer func() {
|
|
cerr := resp.Body.Close()
|
|
if cerr != nil && err == nil {
|
|
err = errors.Wrap(cerr, "closing ID reservation request body")
|
|
}
|
|
}()
|
|
body, err = io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "reading ID reservation request body")
|
|
}
|
|
}
|
|
if resp.StatusCode/100 != http.StatusOK/100 {
|
|
// first check if OffsetDesync Error
|
|
if resp.StatusCode == http.StatusConflict && resp.Header.Get("Content-Type") == "application/json" {
|
|
var esync ErrIDOffsetDesync
|
|
if err := json.Unmarshal(body, &esync); err == nil {
|
|
return nil, esync
|
|
}
|
|
}
|
|
var err error
|
|
if len(body) > 0 && utf8.Valid(body) {
|
|
err = errors.New(string(body))
|
|
} else {
|
|
err = errors.New(resp.Status)
|
|
}
|
|
err = errors.Wrapf(err, "status %d", resp.StatusCode)
|
|
return nil, errors.Wrap(err, "fetching ID reservation")
|
|
}
|
|
var ids []pilosacore.IDRange
|
|
err = json.Unmarshal(body, &ids)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "decoding ID reservation")
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (ps *pilosaIDManager) commit(ctx context.Context, commitRequest pilosacore.IDAllocCommitRequest) error {
|
|
data, err := json.Marshal(commitRequest)
|
|
if err != nil {
|
|
return errors.Wrap(err, "marshaling ID commit request")
|
|
}
|
|
targ, err := ps.url.Parse("/internal/idalloc/commit")
|
|
if err != nil {
|
|
return errors.Wrap(err, "resolving ID commit target URL")
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targ.String(), bytes.NewReader(data))
|
|
if err != nil {
|
|
return errors.Wrap(err, "preparing ID commit HTTP request")
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
// add authorization token to request
|
|
token, ok := ctx.Value(contextKeyToken).(string)
|
|
if ok && token != "" {
|
|
req.Header.Set("Authorization", token)
|
|
}
|
|
|
|
resp, err := ps.hcli.Do(req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "dispatching ID commit HTTP request")
|
|
}
|
|
var body []byte
|
|
if resp.Body != nil {
|
|
defer func() {
|
|
cerr := resp.Body.Close()
|
|
if cerr != nil && err == nil {
|
|
err = errors.Wrap(cerr, "closing ID reservation request body")
|
|
}
|
|
}()
|
|
body, err = io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading ID reservation request body")
|
|
}
|
|
}
|
|
if resp.StatusCode/100 != http.StatusOK/100 {
|
|
var err error
|
|
if len(body) > 0 && utf8.Valid(body) {
|
|
err = errors.New(string(body))
|
|
} else {
|
|
err = errors.New(resp.Status)
|
|
}
|
|
err = errors.Wrapf(err, "status %d", resp.StatusCode)
|
|
return errors.Wrap(err, "committing ID reservation")
|
|
}
|
|
|
|
return nil
|
|
}
|