mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-05 08:10:50 +00:00
* unifying idk and featurebase: first pass * resolved conflict with master for gitignore & dockerignore * deleted binaries that were accidentally pushed to git * combined gitlab jobs for idk & featurebase * run go fmt for idk * updated ssh env variable, and made docker password variable in gitlab env variables * fixed typo assigning variable name * trying to fix docker login error * trying a different solution for docker password * pass registry * fixed docker login * updated paths for idk * exclude idk tests from featurebase test run * fix vendor error * update certificates * grpc needs to be in version 1.38 genproto, which is imported by big query updates the grpc version to 1.47.0 grpc 1.47.0 causes etcd to deadlock when calling etcd.Close() the fix is to have a replace in go.mod to specify a specific grpc version * run go mod tidy * go mod * run go mod tidy * exclude bigquery since it is causing issues and undo grpc replace in go.mod * fix grpc version * fix formatting error * update formatting * attempt to fix formatting * update path for code coverage * update to use current branch binaries, not master * fix for building idk - path updates * udpate path for binaries * update job dependecies * update docker idk tests to use the current branch registry * update stages for jobs * updated job dependencies * not allow idk s3 dump to fail since it is a dependency for integration tests * update dependecy for idk tests * update paths for idk build and code coverage * download featurebase binary from s3 * pass branch name to all setup scripts * change to current branch instead of master * updated sonarcloud * sonarcloud fix and branch name fix * trying to speed up pipeline run time * update stage * branch name fix + sonar cloud * sonarcloud
462 lines
12 KiB
Go
462 lines
12 KiB
Go
package idk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/bits"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"sync"
|
|
|
|
pilosacore "github.com/molecula/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 = ioutil.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 = ioutil.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
|
|
}
|