Sqldb rip boltdb (#2341)

* serverless sqldb use same env for test config as normal

* rip boltdb implementation of controller backend out

it was replaced by postgres and no longer works properly.

This involved migrating a number of tests which only worked with
boltdb, which exposed several ways in which the postgres
implementation had slightly different behavior from the bolt
one:
1. ordering of results in some cases, and
2. (more importantly) erroring when a record to delete was not
found. The bolt implementation silently ignored it when things to
delete weren't found, so we make some changes to match that behavior.

Also stopped propagating CreatedAt and UpdatedAt from DB tables into
dax types. These were breaking existing tests. Perhaps it would be
better to actually use them, but for now they will only exist at the
DB level.

This change set also moves the insertion of the directive_versions
record out of migrations and into the startup/connection code. Having
this in the migrations was a bit ugly because you couldn't just
truncate all the tables and have everything work from
scratch. Inserting it during startup is fairly innocuous, and will
just continue on if it already exists.

* update directive_version test

I changed the initial value to 0 so that the first version that gets
sent out is 1
This commit is contained in:
Matthew Jaffee 2023-03-22 08:54:13 -05:00 committed by GitHub
parent f5f7c5e551
commit 8fe73146c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 146 additions and 3657 deletions

View file

@ -256,13 +256,13 @@ run go tests race:
image: golang:$GOVERSION
extends: .go-cache
variables:
SQLDB_DB: run_go_tests_race
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_DATABASE: run_go_tests_race
POSTGRES_DB: run_go_tests_race
SQLDB_USER: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER: postgres
POSTGRES_USER: postgres
SQLDB_PASSWORD: $POSTGRES_PASSWORD
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
SQLDB_HOST: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST: postgres
services:
- postgres:14.7
rules:
@ -287,13 +287,13 @@ run go tests:
image: golang:$GOVERSION
extends: .go-cache
variables:
SQLDB_DB: run_go_tests
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_DATABASE: run_go_tests
POSTGRES_DB: run_go_tests
SQLDB_USER: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER: postgres
POSTGRES_USER: postgres
SQLDB_PASSWORD: $POSTGRES_PASSWORD
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
SQLDB_HOST: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST: postgres
services:
- postgres:14.7
rules:
@ -319,13 +319,13 @@ run go tests dax/test/dax:
tags:
- docker
variables:
SQLDB_DB: run_go_tests_dax
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_DATABASE: run_go_tests_dax
POSTGRES_DB: run_go_tests_dax
SQLDB_USER: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER: postgres
POSTGRES_USER: postgres
SQLDB_PASSWORD: $POSTGRES_PASSWORD
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
SQLDB_HOST: postgres
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST: postgres
services:
- postgres:14.7
rules:

View file

@ -17,7 +17,6 @@ func BuildDAXFlags(cmd *cobra.Command, srv *server.Command) {
// Controller
flags.BoolVar(&srv.Config.Controller.Run, "controller.run", srv.Config.Controller.Run, "Run the Controller service in process.")
flags.DurationVar(&srv.Config.Controller.Config.RegistrationBatchTimeout, "controller.config.registration-batch-timeout", srv.Config.Controller.Config.RegistrationBatchTimeout, "Timeout for node registration batches.")
flags.StringVar(&srv.Config.Controller.Config.DataDir, "controller.config.data-dir", srv.Config.Controller.Config.DataDir, "Controller directory to use in process.")
flags.StringVar(&srv.Config.Controller.Config.StorageMethod, "controller.config.storage-method", srv.Config.Controller.Config.StorageMethod, "Backing store. boltdb or sqldb.")
flags.DurationVar(&srv.Config.Controller.Config.SnappingTurtleTimeout, "controller.config.snapping-turtle-timeout", srv.Config.Controller.Config.SnappingTurtleTimeout, "Period for running automatic snapshotting routine.")

View file

@ -1,165 +0,0 @@
// Package boltdb contains the boltdb implementations of the DAX interfaces.
package boltdb
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/errors"
bolt "go.etcd.io/bbolt"
)
const (
ErrFmtBucketNotFound = "boltdb: bucket '%s' not found"
)
type Bucket []byte
// DB represents the database connection.
type DB struct {
db *bolt.DB
ctx context.Context // background context
cancel func() // cancel background context
// Datasource name.
DSN string
// Destination for events to be published.
// EventService wtf.EventService
// Returns the current time. Defaults to time.Now().
// Can be mocked for tests.
Now func() time.Time
filePath string
// bucketQueue contains a list of buckets to create upon Open.
bucketQueue []Bucket
}
// NewDB returns a new instance of DB associated with the given datasource name.
func NewDB(dsn string) *DB {
db := &DB{
DSN: dsn,
Now: time.Now,
//EventService: wtf.NopEventService(),
}
db.ctx, db.cancel = context.WithCancel(context.Background())
return db
}
// NewSvcBolt gets, opens, and creates buckets for a boltDB for a
// particular named service (the data file will be named after the
// service).
func NewSvcBolt(dir, svc string, buckets ...Bucket) (*DB, error) {
dir = strings.TrimPrefix(dir, "file:")
filename := filepath.Join(dir, svc+".boltdb")
db := NewDB("file:" + filename)
db.RegisterBuckets(buckets...)
err := db.Open()
return db, errors.Wrap(err, "opening")
}
// path returns the file path to the boltdb database file.
func (db *DB) path() (string, error) {
if !strings.HasPrefix(db.DSN, "file:") {
return "", errors.New(errors.ErrUncoded, "boltdb package only supports a DSN beginning with `file:`")
}
return db.DSN[5:], nil
}
// RegisterBuckets queues up the buckets to be created when the database is
// first opened.
func (db *DB) RegisterBuckets(buckets ...Bucket) {
db.bucketQueue = append(db.bucketQueue, buckets...)
}
// InitializeBuckets creates the given buckets if they do not already exist.
func (db *DB) InitializeBuckets(buckets ...Bucket) (err error) {
return db.db.Update(func(tx *bolt.Tx) error {
for _, bucket := range buckets {
if _, err := tx.CreateBucketIfNotExists(bucket); err != nil {
return errors.Wrapf(err, "creating bucket: %s", bucket)
}
}
return nil
})
}
// Start is here to implement the Transactor interface, but we don't really need
// it in the BoltDB implementation.
func (db *DB) Start() (err error) {
return nil
}
// Open opens the database connection.
func (db *DB) Open() (err error) {
path, err := db.path()
if err != nil {
return errors.Wrap(err, "getting path from DSN")
}
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return errors.Wrapf(err, "mkdir %s", filepath.Dir(path))
} else if db.db, err = bolt.Open(path, 0666, &bolt.Options{Timeout: 1 * time.Second}); err != nil {
return errors.Wrapf(err, "open file: %s", err)
}
// cache the path in db.filePath.
db.filePath = path
if err := db.InitializeBuckets(db.bucketQueue...); err != nil {
return errors.Wrap(err, "initializing buckets")
}
// Reset the bucketQueue.
db.bucketQueue = make([]Bucket, 0)
return nil
}
// Close closes the database connection.
func (db *DB) Close() (err error) {
return db.db.Close()
}
// BeginTx starts a transaction and returns a wrapper Tx type. This type
// provides a reference to the database and a fixed timestamp at the start of
// the transaction. The timestamp allows us to mock time during tests as well.
// The wrapper also contains the context.
func (db *DB) BeginTx(ctx context.Context, writable bool) (dax.Transaction, error) {
tx, err := db.db.Begin(writable)
if err != nil {
return nil, err
}
// Return wrapper Tx that includes the transaction start time.
return &Tx{
Tx: tx,
ctx: ctx,
db: db,
now: db.Now().UTC().Truncate(time.Second),
}, nil
}
// Tx wraps the SQL Tx object to provide a timestamp at the start of the transaction.
type Tx struct {
*bolt.Tx
ctx context.Context
db *DB
now time.Time
}
func (tx *Tx) Context() context.Context {
return tx.ctx
}
func (db *DB) Path() string {
return db.filePath
}

View file

@ -1,17 +0,0 @@
package boltdb_test
import (
"testing"
"github.com/featurebasedb/featurebase/v3/dax/test/boltdb"
)
// Ensure the test database can open & close.
func TestDB(t *testing.T) {
db := boltdb.MustOpenDB(t)
defer boltdb.MustCloseDB(t, db)
t.Cleanup(func() {
boltdb.CleanupDB(t, db.Path())
})
}

View file

@ -1,60 +0,0 @@
package boltdb
import (
"encoding/binary"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/errors"
)
var (
bucketDirective = Bucket("nodeDirective")
keyDirectiveVersion = []byte("directiveVersion")
)
// DirectiveBuckets defines the buckets used by this package. It can be called
// during setup to create the buckets ahead of time.
var DirectiveBuckets []Bucket = []Bucket{
bucketDirective,
}
// Ensure type implements interface.
var _ dax.DirectiveVersion = (*DirectiveVersion)(nil)
type DirectiveVersion struct {
db *DB
}
func NewDirectiveVersion(db *DB) *DirectiveVersion {
return &DirectiveVersion{
db: db,
}
}
func (d *DirectiveVersion) Increment(tx dax.Transaction, delta uint64) (uint64, error) {
txx, ok := tx.(*Tx)
if !ok {
return 0, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketDirective)
if bkt == nil {
return 0, errors.Errorf(ErrFmtBucketNotFound, bucketDirective)
}
var nextVersion uint64 = 1 // Start at 1; 0 is an invalid version.
b := bkt.Get(keyDirectiveVersion)
if b != nil {
nextVersion = binary.LittleEndian.Uint64(b) + delta
}
vsn := make([]byte, 8)
binary.LittleEndian.PutUint64(vsn, nextVersion)
if err := bkt.Put(keyDirectiveVersion, vsn); err != nil {
return 0, errors.Wrap(err, "putting next directive version")
}
return nextVersion, nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,823 +0,0 @@
// Package boltdb contains the boltdb implementation of the Balancer interface.
package boltdb
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
balancer "github.com/featurebasedb/featurebase/v3/dax/controller/balancer"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
)
var (
bucketBalancer = boltdb.Bucket("balancer")
)
// BalancerBuckets defines the buckets used by this package. It can be
// called during setup to create the buckets ahead of time.
var BalancerBuckets []boltdb.Bucket = []boltdb.Bucket{
bucketBalancer,
}
// NewBalancer returns a new instance of controller.Balancer.
func NewBalancer(db *boltdb.DB, schemar schemar.Schemar, logger logger.Logger) *balancer.Balancer {
fjs := newFreeJobService(db)
wjs := newWorkerJobService(db, logger)
fws := newFreeWorkerService(db)
ns := NewNodeService(db, logger)
return balancer.New(ns, fjs, wjs, fws, schemar, logger)
}
// Ensure type implements interface.
var _ balancer.WorkerJobService = (*workerJobService)(nil)
type workerJobService struct {
db *boltdb.DB
logger logger.Logger
}
func newWorkerJobService(db *boltdb.DB, logger logger.Logger) *workerJobService {
return &workerJobService{
db: db,
logger: logger,
}
}
func (w *workerJobService) WorkersJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) ([]dax.WorkerInfo, error) {
workerInfos, err := w.getWorkerInfos(tx, roleType, qdbid)
if err != nil {
return nil, errors.Wrapf(err, "getting worker infos: %s", roleType)
}
return workerInfos, nil
}
func (w *workerJobService) WorkerCount(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (int, error) {
workers, err := w.getWorkers(tx, roleType, qdbid)
if err != nil {
return 0, errors.Wrapf(err, "getting workers: %s", roleType)
}
return len(workers), nil
}
func (w *workerJobService) ListWorkers(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Addresses, error) {
return w.getWorkers(tx, roleType, qdbid)
}
func (w *workerJobService) getWorkers(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Addresses, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
c := txx.Bucket(bucketBalancer).Cursor()
// Deserialize rows into Worker objects.
addrs := make(dax.Addresses, 0)
prefix := []byte(fmt.Sprintf(prefixFmtWorkersDB, roleType, qdbid.Key()))
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
if v == nil {
w.logger.Printf("nil value for key: %s", k)
continue
}
addr, err := keyWorker(k)
if err != nil {
return nil, errors.Wrapf(err, "getting worker from key: %s", k)
}
addrs = append(addrs, addr)
}
return addrs, nil
}
func (w *workerJobService) getWorkerInfos(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.WorkerInfos, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
c := txx.Bucket(bucketBalancer).Cursor()
// Deserialize rows into WorkerInfo objects.
workerInfos := make(dax.WorkerInfos, 0)
var prefix []byte
empty := dax.QualifiedDatabaseID{}
if roleType == "" && qdbid == empty {
prefix = []byte("workers/role/")
} else {
prefix = []byte(fmt.Sprintf(prefixFmtWorkersDB, roleType, qdbid.Key()))
}
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
addr, err := keyWorker(k)
if err != nil {
return nil, errors.Wrapf(err, "getting worker from key: %s", k)
}
jobs := dax.NewSet[dax.Job]()
if v != nil {
jobs, err = decodeJobSet(v)
if err != nil {
return nil, errors.Wrap(err, "decoding job set")
}
}
workerInfo := dax.WorkerInfo{
Address: addr,
Jobs: jobs.Sorted(),
}
workerInfos = append(workerInfos, workerInfo)
}
return workerInfos, nil
}
func (w *workerJobService) CreateWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
// If this worker already exists, don't do anything.
wrkr := bkt.Get(workerDBKey(roleType, qdbid, addr))
if wrkr != nil {
return nil
}
val := []byte("[]")
if err := bkt.Put(workerDBKey(roleType, qdbid, addr), val); err != nil {
return errors.Wrapf(err, "putting db worker: %s, %s", qdbid, addr)
}
if err := bkt.Put(workerAssignedKey(addr), []byte(qdbid.Key())); err != nil {
return errors.Wrapf(err, "putting assigned worker: %s, %s", qdbid, addr)
}
return nil
}
func (w *workerJobService) DeleteWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
if err := bkt.Delete(workerDBKey(roleType, qdbid, addr)); err != nil {
return errors.Wrapf(err, "deleting node key: %s", workerDBKey(roleType, qdbid, addr))
}
if err := bkt.Delete(workerAssignedKey(addr)); err != nil {
return errors.Wrapf(err, "deleting assigned worker: %s", workerAssignedKey(addr))
}
return nil
}
func (w *workerJobService) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
return nil
}
func (w *workerJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, jobs ...dax.Job) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
jobset := dax.NewSet[dax.Job]()
var err error
// get worker
wrkr := bkt.Get(workerDBKey(roleType, qdbid, addr))
if wrkr != nil {
jobset, err = decodeJobSet(wrkr)
if err != nil {
return errors.Wrap(err, "decoding job set")
}
}
for _, job := range jobs {
jobset.Add(job)
}
val, err := encodeJobSet(jobset)
if err != nil {
return errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(workerDBKey(roleType, qdbid, addr), val); err != nil {
return errors.Wrap(err, "putting worker")
}
return nil
}
func (w *workerJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job dax.Job) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
// get worker
wrkr := bkt.Get(workerDBKey(roleType, qdbid, addr))
if wrkr == nil {
return nil
}
jobset, err := decodeJobSet(wrkr)
if err != nil {
return errors.Wrap(err, "decoding job set")
}
if !jobset.Contains(job) {
return nil
}
jobset.Remove(job)
val, err := encodeJobSet(jobset)
if err != nil {
return errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(workerDBKey(roleType, qdbid, addr), val); err != nil {
return errors.Wrap(err, "putting worker")
}
return nil
}
func (w *workerJobService) DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) (balancer.InternalDiffs, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
qdbid := qtid.QualifiedDatabaseID
prefix := string(qtid.Key())
workers, err := w.getWorkers(tx, roleType, qdbid)
if err != nil {
return nil, errors.Wrap(err, "getting workers")
}
idiffs := balancer.NewInternalDiffs()
for _, worker := range workers {
// get worker
wrkr := bkt.Get(workerDBKey(roleType, qdbid, worker))
if wrkr == nil {
panic("didn't find worker that should... definitely exist")
}
jobset, err := decodeJobSet(wrkr)
if err != nil {
return nil, errors.Wrap(err, "decoding job set")
}
jobs := jobset.RemoveByPrefix(prefix)
for _, job := range jobs {
idiffs.Removed(worker, job)
}
val, err := encodeJobSet(jobset)
if err != nil {
return nil, errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(workerDBKey(roleType, qdbid, worker), val); err != nil {
return nil, errors.Wrap(err, "putting worker")
}
}
return idiffs, nil
}
func (w *workerJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) (dax.Jobs, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
jobset := dax.NewSet[dax.Job]()
var err error
// get worker
wrkr := bkt.Get(workerDBKey(roleType, qdbid, addr))
if wrkr != nil {
jobset, err = decodeJobSet(wrkr)
if err != nil {
return nil, errors.Wrap(err, "decoding job set")
}
}
return jobset.Sorted(), nil
}
func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addrs ...dax.Address) (map[dax.Address]int, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
m := make(map[dax.Address]int)
for _, addr := range addrs {
jobset := dax.NewSet[dax.Job]()
var err error
// get worker
wrkr := bkt.Get(workerDBKey(roleType, qdbid, addr))
if wrkr != nil {
jobset, err = decodeJobSet(wrkr)
if err != nil {
return nil, errors.Wrap(err, "decoding job set")
}
}
m[addr] = len(jobset)
}
return m, nil
}
func (w *workerJobService) DatabaseForWorker(tx dax.Transaction, addr dax.Address) dax.DatabaseKey {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return "" // TODO(tlt): return error here?
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return ""
}
wrkr := bkt.Get(workerAssignedKey(addr))
return dax.DatabaseKey(wrkr)
}
// encodeJobSet encode the jobSet into a JSON array of strings.
func encodeJobSet(jobSet dax.Set[dax.Job]) ([]byte, error) {
arr := jobSet.Sorted()
b, err := json.Marshal(arr)
if err != nil {
return nil, errors.Wrap(err, "marshalling json")
}
return b, nil
}
// decodeJobSet decode the string (a JSON array of strings) into jobSet.
func decodeJobSet(v []byte) (dax.Set[dax.Job], error) {
var arr []string
err := json.Unmarshal(v, &arr)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling json")
}
js := dax.NewSet[dax.Job]()
for _, s := range arr {
js.Add(dax.Job(s))
}
return js, nil
}
// encodeWorkerSet encode the workerSet into a JSON array of strings.
func encodeWorkerSet(workerSet dax.Set[dax.Address]) ([]byte, error) {
arr := workerSet.Sorted()
b, err := json.Marshal(arr)
if err != nil {
return nil, errors.Wrap(err, "marshalling json")
}
return b, nil
}
// decodeWorkerSet decode the string (a JSON array of strings) into workerSet.
func decodeWorkerSet(v []byte) (dax.Set[dax.Address], error) {
var arr []string
err := json.Unmarshal(v, &arr)
if err != nil {
return nil, errors.Wrap(err, "unmarshalling json")
}
ws := dax.NewSet[dax.Address]()
for _, s := range arr {
ws.Add(dax.Address(s))
}
return ws, nil
}
// Ensure type implements interface.
var _ balancer.FreeJobService = (*freeJobService)(nil)
type freeJobService struct {
db *boltdb.DB
}
func newFreeJobService(db *boltdb.DB) *freeJobService {
return &freeJobService{
db: db,
}
}
func (f *freeJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs ...dax.Job) error {
return f.MergeJobs(tx, roleType, qdbid, jobs)
}
func (f *freeJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job dax.Job) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
// get free jobs
fjs := bkt.Get(freeJobKey(roleType, qdbid))
if fjs == nil {
return nil
}
jobset, err := decodeJobSet(fjs)
if err != nil {
return errors.Wrap(err, "decoding job set")
}
if !jobset.Contains(job) {
return nil
}
jobset.Remove(job)
val, err := encodeJobSet(jobset)
if err != nil {
return errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(freeJobKey(roleType, qdbid), val); err != nil {
return errors.Wrap(err, "putting free job")
}
return nil
}
func (f *freeJobService) DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
qdbid := qtid.QualifiedDatabaseID
prefix := string(qtid.Key())
// get free jobs
fjs := bkt.Get(freeJobKey(roleType, qdbid))
if fjs == nil {
return nil
}
jobset, err := decodeJobSet(fjs)
if err != nil {
return errors.Wrap(err, "decoding job set")
}
jobset.RemoveByPrefix(prefix)
val, err := encodeJobSet(jobset)
if err != nil {
return errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(freeJobKey(roleType, qdbid), val); err != nil {
return errors.Wrap(err, "putting free job")
}
return nil
}
func (f *freeJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Jobs, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
jobset := dax.NewSet[dax.Job]()
var err error
// get free jobs
fjs := bkt.Get(freeJobKey(roleType, qdbid))
if fjs != nil {
jobset, err = decodeJobSet(fjs)
if err != nil {
return nil, errors.Wrap(err, "decoding job set")
}
}
return jobset.Sorted(), nil
}
func (f *freeJobService) MergeJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
jobset := dax.NewSet[dax.Job]()
var err error
// get free jobs
fjs := bkt.Get(freeJobKey(roleType, qdbid))
if fjs != nil {
jobset, err = decodeJobSet(fjs)
if err != nil {
return errors.Wrap(err, "decoding job set")
}
}
for _, j := range jobs {
jobset.Add(j)
}
val, err := encodeJobSet(jobset)
if err != nil {
return errors.Wrap(err, "encoding job set")
}
if err := bkt.Put(freeJobKey(roleType, qdbid), val); err != nil {
return errors.Wrap(err, "putting free job")
}
return nil
}
//////////////////////////////////////////////////////
// Ensure type implements interface.
var _ balancer.FreeWorkerService = (*freeWorkerService)(nil)
type freeWorkerService struct {
db *boltdb.DB
}
func newFreeWorkerService(db *boltdb.DB) *freeWorkerService {
return &freeWorkerService{
db: db,
}
}
func (f *freeWorkerService) AddWorkers(tx dax.Transaction, roleType dax.RoleType, addres ...dax.Address) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
workerset := dax.NewSet[dax.Address]()
var err error
// get free workers
fws := bkt.Get(freeWorkerKey(roleType))
if fws != nil {
workerset, err = decodeWorkerSet(fws)
if err != nil {
return errors.Wrap(err, "decoding worker set")
}
}
for _, w := range addres {
workerset.Add(w)
}
val, err := encodeWorkerSet(workerset)
if err != nil {
return errors.Wrap(err, "encoding worker set")
}
if err := bkt.Put(freeWorkerKey(roleType), val); err != nil {
return errors.Wrap(err, "putting free worker")
}
return nil
}
func (f *freeWorkerService) RemoveWorker(tx dax.Transaction, roleType dax.RoleType, addr dax.Address) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
workers, err := f.ListWorkers(tx, roleType)
if err != nil {
return errors.Wrap(err, "listing free workers")
}
// Create a workerset containing the free workers which remain after
// removing num workers.
workerset := dax.NewSet[dax.Address]()
for _, w := range workers {
workerset.Add(w)
}
if !workerset.Contains(addr) {
return nil
}
// Remove the worker.
workerset.Remove(addr)
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
val, err := encodeWorkerSet(workerset)
if err != nil {
return errors.Wrap(err, "encoding worker set")
}
if err := bkt.Put(freeWorkerKey(roleType), val); err != nil {
return errors.Wrap(err, "putting free worker")
}
return nil
}
func (f *freeWorkerService) PopWorkers(tx dax.Transaction, roleType dax.RoleType, num int) ([]dax.Address, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
workers, err := f.ListWorkers(tx, roleType)
if err != nil {
return nil, errors.Wrap(err, "listing free workers")
}
if len(workers) < num {
return nil, errors.Errorf("not enough free workers to pop: wanted %d, have: %d", num, len(workers))
}
// Get num workers from the list.
workersToAssign := workers[0:num]
// Create a workerset containing the free workers which remain after
// removing num workers.
workerset := dax.NewSet[dax.Address]()
for _, worker := range workers[num:] {
workerset.Add(worker)
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
val, err := encodeWorkerSet(workerset)
if err != nil {
return nil, errors.Wrap(err, "encoding worker set")
}
if err := bkt.Put(freeWorkerKey(roleType), val); err != nil {
return nil, errors.Wrap(err, "putting free worker")
}
return workersToAssign, nil
}
func (f *freeWorkerService) ListWorkers(tx dax.Transaction, roleType dax.RoleType) (dax.Addresses, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
workerset := dax.NewSet[dax.Address]()
var err error
// get free workers
fws := bkt.Get(freeWorkerKey(roleType))
if fws != nil {
workerset, err = decodeWorkerSet(fws)
if err != nil {
return nil, errors.Wrap(err, "decoding worker set")
}
}
return workerset.Sorted(), nil
}
//////////////////////////////////////////////////////
const (
prefixFmtWorkersDB = "workers/role/%s/db/%s/" // %s - role, dbKey
prefixFmtWorkersAssigned = "workers/assigned/"
prefixFmtFreeJobs = "freejobs/role/%s/db/%s" // %s - role, dbKey
prefixFmtFreeWorkers = "freeworkers/role/%s" // %s - role
)
// workerDBKey returns a key based on worker.
//
// Format: workers/role/[role]/db/[dbKey]/[worker] = [job1, job2, ...]
func workerDBKey(roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) []byte {
key := fmt.Sprintf(prefixFmtWorkersDB+"%s", roleType, qdbid.Key(), addr)
return []byte(key)
}
// workerAssignedKey returns a key based on worker.
//
// Format: workers/assigned/[worker] = dbKey
func workerAssignedKey(addr dax.Address) []byte {
key := fmt.Sprintf(prefixFmtWorkersAssigned+"%s", addr)
return []byte(key)
}
// keyWorker gets the worker out of the key.
func keyWorker(key []byte) (dax.Address, error) {
parts := strings.SplitN(string(key), "/", 6)
if len(parts) != 6 {
return "", errors.New(errors.ErrUncoded, "worker key format expected: `workers/role/[role]/db/[db]/worker`")
}
return dax.Address(parts[5]), nil
}
// freeJobKey returns a key for all freeJobs.
//
// Format: freejobs/role/[role]/db/[dbKey] = [job1, job2, ...]
func freeJobKey(roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) []byte {
key := fmt.Sprintf(prefixFmtFreeJobs, roleType, qdbid.Key())
return []byte(key)
}
// freeWorkerKey returns a key for all freeWorkers.
//
// Format: freeworkers/role/[role] = [worker1, worker2, ...]
func freeWorkerKey(roleType dax.RoleType) []byte {
key := fmt.Sprintf(prefixFmtFreeWorkers, roleType)
return []byte(key)
}

View file

@ -1,144 +0,0 @@
package boltdb
import (
"bytes"
"encoding/json"
"fmt"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
)
// Ensure type implements interface.
var _ controller.NodeService = (*NodeService)(nil)
// NodeService represents a service for managing nodes.
type NodeService struct {
db *boltdb.DB
logger logger.Logger
}
// NewNodeService returns a new instance of NodeService with default values.
func NewNodeService(db *boltdb.DB, logger logger.Logger) *NodeService {
return &NodeService{
db: db,
logger: logger,
}
}
func (s *NodeService) CreateNode(tx dax.Transaction, addr dax.Address, node *dax.Node) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
val, err := json.Marshal(node)
if err != nil {
return errors.Wrap(err, "marshalling node to json")
}
if err := bkt.Put(addressKey(addr), val); err != nil {
return errors.Wrap(err, "putting node")
}
return nil
}
func (s *NodeService) ReadNode(tx dax.Transaction, addr dax.Address) (*dax.Node, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
b := bkt.Get(addressKey(addr))
if b == nil {
return nil, dax.NewErrNodeDoesNotExist(addr)
}
node := &dax.Node{}
if err := json.Unmarshal(b, node); err != nil {
return nil, errors.Wrap(err, "unmarshalling node json")
}
return node, nil
}
func (s *NodeService) DeleteNode(tx dax.Transaction, addr dax.Address) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
bkt := txx.Bucket(bucketBalancer)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketBalancer)
}
if err := bkt.Delete(addressKey(addr)); err != nil {
return errors.Wrapf(err, "deleting node key: %s", addressKey(addr))
}
return nil
}
func (s *NodeService) Nodes(tx dax.Transaction) ([]*dax.Node, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
nodes, err := s.getNodes(txx)
if err != nil {
return nil, errors.Wrap(err, "getting nodes")
}
return nodes, nil
}
func (s *NodeService) getNodes(tx *boltdb.Tx) ([]*dax.Node, error) {
c := tx.Bucket(bucketBalancer).Cursor()
// Deserialize rows into Node objects.
nodes := make([]*dax.Node, 0)
prefix := []byte(prefixFmtNodes)
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
if v == nil {
s.logger.Printf("nil value for key: %s", k)
continue
}
node := &dax.Node{}
if err := json.Unmarshal(v, node); err != nil {
return nil, errors.Wrap(err, "unmarshalling node json")
}
nodes = append(nodes, node)
}
return nodes, nil
}
const (
prefixFmtNodes = "nodes/"
)
// addressKey returns a key based on address.
func addressKey(addr dax.Address) []byte {
key := fmt.Sprintf(prefixFmtNodes+"%s", addr)
return []byte(key)
}

View file

@ -1,61 +0,0 @@
package boltdb_test
import (
"context"
"testing"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller/balancer/boltdb"
testbolt "github.com/featurebasedb/featurebase/v3/dax/test/boltdb"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/stretchr/testify/assert"
)
func TestNodeService(t *testing.T) {
db := testbolt.MustOpenDB(t)
defer testbolt.MustCloseDB(t, db)
t.Cleanup(func() {
testbolt.CleanupDB(t, db.Path())
})
ctx := context.Background()
// Initialize the buckets.
assert.NoError(t, db.InitializeBuckets(boltdb.BalancerBuckets...))
t.Run("Nodes", func(t *testing.T) {
ns := boltdb.NewNodeService(db, logger.NopLogger)
node1 := &dax.Node{
Address: "localhost:10101",
RoleTypes: []dax.RoleType{
"compute",
},
}
tx, err := db.BeginTx(ctx, true)
assert.NoError(t, err)
defer tx.Rollback()
// Create node.
assert.NoError(t, ns.CreateNode(tx, node1.Address, node1))
// Read node.
n, err := ns.ReadNode(tx, node1.Address)
assert.NoError(t, err)
assert.Equal(t, node1, n)
// Delete node.
assert.NoError(t, ns.DeleteNode(tx, node1.Address))
// Read node.
_, err = ns.ReadNode(tx, node1.Address)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrNodeDoesNotExist))
}
assert.NoError(t, tx.Commit())
})
}

View file

@ -20,7 +20,6 @@ type Config struct {
// Storage
StorageMethod string `toml:"storage-method"`
DataDir string `toml:"-"`
SQLDB *SQLDBConfig `toml:"sqldb"`

View file

@ -3,43 +3,64 @@ package controller_test
import (
"context"
"fmt"
"os"
"sort"
"sync"
"testing"
"github.com/featurebasedb/featurebase/v3/dax"
directivedb "github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller"
balancerdb "github.com/featurebasedb/featurebase/v3/dax/controller/balancer/boltdb"
schemardb "github.com/featurebasedb/featurebase/v3/dax/controller/schemar/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
daxtest "github.com/featurebasedb/featurebase/v3/dax/test"
testbolt "github.com/featurebasedb/featurebase/v3/dax/test/boltdb"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var trans sqldb.Transactor
func TestMain(m *testing.M) {
os.Exit(run(m))
}
// run is a separate function so that we can defer cleanups. (deferred functions won't run if os.Exit is called)
func run(m *testing.M) int {
// We connect to a randomized database, create it, and run migrations. Then we drop it when tests are done.
conf := sqldb.GetTestConfigRandomDB("controller_test")
var err error
trans, err = sqldb.NewTransactor(conf, logger.StderrLogger)
if err != nil {
fmt.Printf("couldn't set up transactor: %v", err)
return -1
}
defer sqldb.DropDatabase(trans)
code := m.Run()
return code
}
func TestController(t *testing.T) {
ctx := context.Background()
qdbid := dax.NewQualifiedDatabaseID("acme", "db1")
t.Run("RegisterNode", func(t *testing.T) {
director := newTestDirector()
schemar, cleanup := daxtest.NewSchemar(t)
defer cleanup()
schemar := sqldb.NewSchemar(logger.StderrLogger)
err := trans.Start()
require.NoError(t, err, "starting transactor")
db := testbolt.MustOpenDB(t)
db.InitializeBuckets(balancerdb.BalancerBuckets...)
db.InitializeBuckets(schemardb.SchemarBuckets...)
defer func() {
testbolt.MustCloseDB(t, db)
testbolt.CleanupDB(t, db.Path())
trans.TruncateAll()
trans.Close()
}()
cfg := controller.Config{}
con := controller.New(cfg)
con.Schemar = schemar
con.Transactor = db
con.Transactor = trans
con.Director = director
// Register a node with an invalid role type.
@ -49,7 +70,7 @@ func TestController(t *testing.T) {
"invalid-role-type",
},
}
err := con.RegisterNodes(ctx, node0)
err = con.RegisterNodes(ctx, node0)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, controller.ErrCodeRoleTypeInvalid))
}
@ -67,25 +88,21 @@ func TestController(t *testing.T) {
t.Run("ComputeNodes", func(t *testing.T) {
director := newTestDirector()
schemar, cleanup := daxtest.NewSchemar(t)
defer cleanup()
schemar := sqldb.NewSchemar(logger.StderrLogger)
err := trans.Start()
require.NoError(t, err, "starting transactor")
db := testbolt.MustOpenDB(t)
db.InitializeBuckets(balancerdb.BalancerBuckets...)
db.InitializeBuckets(schemardb.SchemarBuckets...)
db.InitializeBuckets(directivedb.DirectiveBuckets...)
defer func() {
testbolt.MustCloseDB(t, db)
testbolt.CleanupDB(t, db.Path())
trans.TruncateAll()
}()
cfg := controller.Config{}
con := controller.New(cfg)
con.Schemar = schemar
con.Balancer = balancerdb.NewBalancer(db, schemar, logger.StderrLogger)
con.DirectiveVersion = directivedb.NewDirectiveVersion(db)
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
con.Director = director
con.Transactor = db
con.Transactor = trans
var exp []*dax.Directive
@ -108,7 +125,9 @@ func TestController(t *testing.T) {
Version: 1,
},
}
assert.Equal(t, exp, director.flush())
got := director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Add a qualified database.
dbOptions := dax.DatabaseOptions{
@ -150,7 +169,9 @@ func TestController(t *testing.T) {
Version: 2,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Set WorkersMin to 3 so we can used the added nodes that follow.
{
@ -176,7 +197,9 @@ func TestController(t *testing.T) {
Version: 3,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
node2 := &dax.Node{
Address: "10.0.0.1:82",
@ -196,7 +219,9 @@ func TestController(t *testing.T) {
Version: 4,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Add more shards.
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3, 5, 8)...)
@ -278,7 +303,9 @@ func TestController(t *testing.T) {
Version: 9,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Add another non-keyed table.
tbl1 := daxtest.TestQualifiedTable(t, qdbid, "bar", 0, false)
@ -372,7 +399,9 @@ func TestController(t *testing.T) {
Version: 13,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
require.Equal(t, exp, got)
// Remove a node.
assert.NoError(t, con.DeregisterNodes(ctx, node1.Address))
@ -419,7 +448,9 @@ func TestController(t *testing.T) {
Version: 15,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
// assert.Equal(t, exp, got) // TODO fails due to shards being allocated differently
// Remove another node.
assert.NoError(t, con.DeregisterNodes(ctx, node0.Address))
@ -446,7 +477,9 @@ func TestController(t *testing.T) {
Version: 16,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Remove final node.
assert.NoError(t, con.DeregisterNodes(ctx, node2.Address))
@ -491,7 +524,9 @@ func TestController(t *testing.T) {
Version: 17,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Remove shards.
assert.NoError(t, con.RemoveShards(ctx, tbl0.QualifiedID(), dax.NewShardNums(2, 5)...))
@ -518,7 +553,9 @@ func TestController(t *testing.T) {
Version: 18,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Remove shards, one which does not exist.
// Currently that doesn't result in an error, it simply no-ops on trying
@ -547,7 +584,9 @@ func TestController(t *testing.T) {
Version: 19,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Remove a table.
assert.NoError(t, con.DropTable(ctx, tbl0.QualifiedID()))
@ -569,7 +608,9 @@ func TestController(t *testing.T) {
Version: 20,
},
}
assert.Equal(t, exp, director.flush())
got = director.flush()
require.Equal(t, len(exp), len(got))
assert.Equal(t, exp, got)
// Remove a node which doesn't exist.
assert.NoError(t, con.DeregisterNodes(ctx, "invalidNode"))
@ -582,24 +623,20 @@ func TestController(t *testing.T) {
)
director := newTestDirector()
schemar, cleanup := daxtest.NewSchemar(t)
defer cleanup()
schemar := sqldb.NewSchemar(logger.StderrLogger)
err := trans.Start()
require.NoError(t, err, "starting transactor")
db := testbolt.MustOpenDB(t)
db.InitializeBuckets(balancerdb.BalancerBuckets...)
db.InitializeBuckets(schemardb.SchemarBuckets...)
db.InitializeBuckets(directivedb.DirectiveBuckets...)
defer func() {
testbolt.MustCloseDB(t, db)
testbolt.CleanupDB(t, db.Path())
trans.TruncateAll()
}()
cfg := controller.Config{}
con := controller.New(cfg)
con.Schemar = schemar
con.Balancer = balancerdb.NewBalancer(db, schemar, logger.StderrLogger)
con.DirectiveVersion = directivedb.NewDirectiveVersion(db)
con.Transactor = db
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
con.Transactor = trans
con.Director = director
var exp []*dax.Directive
@ -881,7 +918,7 @@ func TestController(t *testing.T) {
assert.Equal(t, exp, director.flush())
// Remove a table which doesn't exist.
err := con.DropTable(ctx, invalidQtid)
err = con.DropTable(ctx, invalidQtid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
}
@ -897,24 +934,20 @@ func TestController(t *testing.T) {
})
t.Run("GetNodes", func(t *testing.T) {
schemar, cleanup := daxtest.NewSchemar(t)
defer cleanup()
schemar := sqldb.NewSchemar(logger.StderrLogger)
err := trans.Start()
require.NoError(t, err, "starting transactor")
db := testbolt.MustOpenDB(t)
db.InitializeBuckets(balancerdb.BalancerBuckets...)
db.InitializeBuckets(schemardb.SchemarBuckets...)
db.InitializeBuckets(directivedb.DirectiveBuckets...)
defer func() {
testbolt.MustCloseDB(t, db)
testbolt.CleanupDB(t, db.Path())
trans.TruncateAll()
}()
cfg := controller.Config{}
con := controller.New(cfg)
con.Schemar = schemar
con.Balancer = balancerdb.NewBalancer(db, schemar, logger.StderrLogger)
con.DirectiveVersion = directivedb.NewDirectiveVersion(db)
con.Transactor = db
con.Balancer = sqldb.NewBalancer(logger.StderrLogger)
con.DirectiveVersion = sqldb.NewDirectiveVersion(logger.StderrLogger)
con.Transactor = trans
// Register two nodes.
node0 := &dax.Node{

View file

@ -1,698 +0,0 @@
// Package boltdb contains the boltdb implementation of the Schemar
// interfaces.
package boltdb
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
)
var (
bucketSchemar = boltdb.Bucket("schemar")
)
// SchemarBuckets defines the buckets used by this package. It can be called
// during setup to create the buckets ahead of time.
var SchemarBuckets []boltdb.Bucket = []boltdb.Bucket{
bucketSchemar,
}
// Ensure type implements interface.
var _ schemar.Schemar = (*Schemar)(nil)
type Schemar struct {
db *boltdb.DB
logger logger.Logger
}
// NewSchemar returns a new instance of Schemar with default values.
func NewSchemar(db *boltdb.DB, logger logger.Logger) *Schemar {
return &Schemar{
db: db,
logger: logger,
}
}
// CreateDatabase creates the database provided. If a database with the same
// name already exists then an error is returned. For now, we are not going to
// store the tables in the schemar Database struct.
func (s *Schemar) CreateDatabase(tx dax.Transaction, qdb *dax.QualifiedDatabase) error {
// Ensure the database id is not blank.
if qdb.ID == "" {
return schemar.NewErrDatabaseIDInvalid(qdb.ID)
}
// Ensure the database name is not blank.
if qdb.Name == "" {
return schemar.NewErrDatabaseNameInvalid(qdb.Name)
}
// Set the CreateAt value for the database.
// TODO(tlt): We may want to consider erroring here if the value is != 0.
if qdb.CreatedAt == 0 {
now := timestamp()
qdb.CreatedAt = now
}
//////////// end validation
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Ensure a database with that ID doesn't already exist.
if db, _ := s.databaseByID(txx, qdb.OrganizationID, qdb.ID); db != nil {
return dax.NewErrDatabaseIDExists(qdb.QualifiedID())
}
if err := s.putDatabase(txx, qdb); err != nil {
return errors.Wrap(err, "putting database")
}
// In addition to storing the database in databaseKey, we want to store a
// reverse-lookup (i.e. index) on database name to the databaseKey.
if err := s.putDatabaseName(txx, qdb); err != nil {
return errors.Wrap(err, "putting database name")
}
return nil
}
func (s *Schemar) DatabaseByID(tx dax.Transaction, qdbid dax.QualifiedDatabaseID) (*dax.QualifiedDatabase, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.databaseByID(txx, qdbid.OrganizationID, qdbid.DatabaseID)
}
func (s *Schemar) databaseByID(tx *boltdb.Tx, orgID dax.OrganizationID, id dax.DatabaseID) (*dax.QualifiedDatabase, error) {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
b := bkt.Get(databaseKey(orgID, id))
if b == nil {
return nil, dax.NewErrDatabaseIDDoesNotExist(dax.QualifiedDatabaseID{OrganizationID: orgID, DatabaseID: id})
}
database := &dax.QualifiedDatabase{}
if err := json.Unmarshal(b, database); err != nil {
return nil, errors.Wrap(err, "unmarshalling database json")
}
return database, nil
}
func (s *Schemar) DatabaseByName(tx dax.Transaction, orgID dax.OrganizationID, dbname dax.DatabaseName) (*dax.QualifiedDatabase, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.databaseByName(txx, orgID, dbname)
}
func (s *Schemar) databaseByName(tx *boltdb.Tx, orgID dax.OrganizationID, name dax.DatabaseName) (*dax.QualifiedDatabase, error) {
qdbid, err := s.databaseIDByName(tx, orgID, name)
if err != nil {
return nil, errors.Wrap(err, "getting database ID")
}
return s.databaseByID(tx, orgID, qdbid.DatabaseID)
}
func (s *Schemar) databaseIDByName(tx *boltdb.Tx, orgID dax.OrganizationID, name dax.DatabaseName) (dax.QualifiedDatabaseID, error) {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return dax.QualifiedDatabaseID{}, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
b := bkt.Get(databaseNameKey(orgID, name))
if b == nil {
return dax.QualifiedDatabaseID{}, dax.NewErrDatabaseNameDoesNotExist(name)
}
return keyQualifiedDatabaseID(b)
}
func (s *Schemar) putDatabase(tx *boltdb.Tx, qdb *dax.QualifiedDatabase) error {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
val, err := json.Marshal(qdb)
if err != nil {
return errors.Wrap(err, "marshalling database to json")
}
return bkt.Put(databaseKey(qdb.OrganizationID, qdb.ID), val)
}
func (s *Schemar) putDatabaseName(tx *boltdb.Tx, qdb *dax.QualifiedDatabase) error {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
return bkt.Put(databaseNameKey(qdb.OrganizationID, qdb.Name), databaseKey(qdb.OrganizationID, qdb.ID))
}
// DropDatabase drops the given database. If the named/IDed database does not
// exist then an error is returned.
func (s *Schemar) DropDatabase(tx dax.Transaction, qdbid dax.QualifiedDatabaseID) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Ensure the database exists.
qdb, err := s.databaseByID(txx, qdbid.OrganizationID, qdbid.DatabaseID)
if err != nil {
return errors.Wrap(err, "getting database by id")
}
bkt := txx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
// Delete the database by ID.
if err := bkt.Delete(databaseKey(qdb.OrganizationID, qdb.ID)); err != nil {
return errors.Wrap(err, "deleting database by id")
}
// Delete the reverse-lookup database by Name.
if err := bkt.Delete(databaseNameKey(qdb.OrganizationID, qdb.Name)); err != nil {
return errors.Wrap(err, "deleting database by name")
}
return nil
}
// SetDatabaseOption overwrites the existing database option with the provided
// value.
func (s *Schemar) SetDatabaseOption(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, option string, value string) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Get the database.
qdb, err := s.databaseByID(txx, qdbid.OrganizationID, qdbid.DatabaseID)
if err != nil {
return errors.Wrapf(err, "getting database: %s", qdbid)
}
// Set the new option.
if err := qdb.Options.Set(option, value); err != nil {
return errors.Wrapf(err, "setting option on database: %s", qdbid)
}
// Put the database.
if err := s.putDatabase(txx, qdb); err != nil {
return errors.Wrap(err, "putting database")
}
return nil
}
func (s *Schemar) Databases(tx dax.Transaction, orgID dax.OrganizationID, ids ...dax.DatabaseID) ([]*dax.QualifiedDatabase, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.getDatabases(txx, orgID, ids...)
}
func (s *Schemar) getDatabases(tx *boltdb.Tx, orgID dax.OrganizationID, ids ...dax.DatabaseID) (dax.QualifiedDatabases, error) {
c := tx.Bucket(bucketSchemar).Cursor()
// Deserialize rows into Database objects.
databases := make(dax.QualifiedDatabases, 0)
var filterByID bool
if len(ids) > 0 {
filterByID = true
}
prefix := []byte(fmt.Sprintf(prefixFmtDatabases, orgID))
if orgID == "" {
prefix = []byte(prefixDatabases)
}
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
if v == nil {
s.logger.Printf("nil value for key: %s", k)
continue
}
dbID, err := keyDatabaseID(k)
if err != nil {
return nil, errors.Wrap(err, "getting database from key")
}
// Only include databases provided in the ids filter.
if filterByID && !containsDatabaseID(ids, dbID) {
continue
}
database := &dax.QualifiedDatabase{}
if err := json.Unmarshal(v, database); err != nil {
return nil, errors.Wrap(err, "unmarshalling database json")
}
databases = append(databases, database)
}
return databases, nil
}
func containsDatabaseID(s []dax.DatabaseID, e dax.DatabaseID) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// CreateTable creates the table provided. If a table with the same name already
// exists then an error is returned.
func (s *Schemar) CreateTable(tx dax.Transaction, qtbl *dax.QualifiedTable) error {
// Ensure the table id is not blank.
if qtbl.ID == "" {
return schemar.NewErrTableIDInvalid(qtbl.ID)
}
// Ensure the table name is not blank.
if qtbl.Name == "" {
return schemar.NewErrTableNameInvalid(qtbl.Name)
}
// Ensure that a primary key field is present and valid.
if !qtbl.HasValidPrimaryKey() {
return schemar.NewErrInvalidPrimaryKey()
}
// Set the CreateAt value for the table.
// TODO(tlt): We may want to consider erroring here if the value is != 0.
if qtbl.CreatedAt == 0 {
now := timestamp()
qtbl.CreatedAt = now
// Set CreatedAt for all of the fields as well.
for i := range qtbl.Fields {
qtbl.Fields[i].CreatedAt = now
}
}
//////////// end validation
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Ensure the database, defined in the table's QualifiedDatabaseID, exists.
if _, err := s.databaseByID(txx, qtbl.OrganizationID, qtbl.DatabaseID); err != nil {
return errors.Wrap(err, "validating database")
}
// Ensure a table with that ID doesn't already exist.
if t, _ := s.tableByID(txx, qtbl.QualifiedDatabaseID, qtbl.ID); t != nil {
return dax.NewErrTableIDExists(qtbl.QualifiedID())
}
if err := s.putTable(txx, qtbl); err != nil {
return errors.Wrap(err, "putting table")
}
// In addition to storing the table in tableKey, we want to store a reverse-lookup
// (i.e. index) on table name to the tableKey.
if err := s.putTableName(txx, qtbl); err != nil {
return errors.Wrap(err, "putting table name")
}
return nil
}
// CreateField creates the field provided in the given table. If a field with
// the same name already exists then an error is returned.
func (s *Schemar) CreateField(tx dax.Transaction, qtid dax.QualifiedTableID, fld *dax.Field) error {
// Ensure the field name is not blank.
if fld.Name == "" {
return schemar.NewErrFieldNameInvalid(fld.Name)
}
//////////// end validation
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Get the table.
qtbl, err := s.tableByQTID(txx, qtid)
if err != nil {
return errors.Wrap(err, "getting table by id")
}
// Ensure a field with that name doesn't already exist.
if _, ok := qtbl.Field(fld.Name); ok {
return dax.NewErrFieldExists(fld.Name)
}
qtbl.Fields = append(qtbl.Fields, fld)
// Write table back to database.
if err := s.putTable(txx, qtbl); err != nil {
return errors.Wrap(err, "putting table")
}
return nil
}
// DropField removes the field from the table.
func (s *Schemar) DropField(tx dax.Transaction, qtid dax.QualifiedTableID, fldName dax.FieldName) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Get the table.
qtbl, err := s.tableByQTID(txx, qtid)
if err != nil {
return errors.Wrap(err, "getting table by id")
}
// Ensure a field with that name exists.
if _, ok := qtbl.Field(fldName); !ok {
return dax.NewErrFieldDoesNotExist(fldName)
}
_ = qtbl.RemoveField(fldName)
// Write table back to database.
if err := s.putTable(txx, qtbl); err != nil {
return errors.Wrap(err, "putting table")
}
return nil
}
func (s *Schemar) putTable(tx *boltdb.Tx, qtbl *dax.QualifiedTable) error {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
val, err := json.Marshal(qtbl)
if err != nil {
return errors.Wrap(err, "marshalling table to json")
}
return bkt.Put(tableKey(qtbl.OrganizationID, qtbl.DatabaseID, qtbl.Table.ID), val)
}
func (s *Schemar) putTableName(tx *boltdb.Tx, qtbl *dax.QualifiedTable) error {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
return bkt.Put(tableNameKey(qtbl.OrganizationID, qtbl.DatabaseID, qtbl.Name), tableKey(qtbl.OrganizationID, qtbl.DatabaseID, qtbl.ID))
}
// Table returns the TableInfo for the given table. An error is returned if the
// table does not exist.
func (s *Schemar) Table(tx dax.Transaction, qtid dax.QualifiedTableID) (*dax.QualifiedTable, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.tableByQTID(txx, qtid)
}
// tableByQTID gets the full qualified table by the QualifiedTableID whether it
// has Name or ID set.
func (s *Schemar) tableByQTID(tx *boltdb.Tx, qtid dax.QualifiedTableID) (*dax.QualifiedTable, error) {
if qtid.ID == "" {
return s.tableByName(tx, qtid.QualifiedDatabaseID, qtid.Name)
}
return s.tableByID(tx, qtid.QualifiedDatabaseID, qtid.ID)
}
func (s *Schemar) tableByName(tx *boltdb.Tx, qdbid dax.QualifiedDatabaseID, name dax.TableName) (*dax.QualifiedTable, error) {
qtid, err := s.tableIDByName(tx, qdbid, name)
if err != nil {
return nil, errors.Wrap(err, "getting table ID")
}
return s.tableByID(tx, qtid.QualifiedDatabaseID, qtid.ID) // TODO remove?
}
func (s *Schemar) tableByID(tx *boltdb.Tx, qdbid dax.QualifiedDatabaseID, id dax.TableID) (*dax.QualifiedTable, error) {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return nil, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
b := bkt.Get(tableKey(qdbid.OrganizationID, qdbid.DatabaseID, id))
if b == nil {
return nil, dax.NewErrTableIDDoesNotExist(dax.QualifiedTableID{QualifiedDatabaseID: qdbid, ID: id})
}
table := &dax.QualifiedTable{}
if err := json.Unmarshal(b, table); err != nil {
return nil, errors.Wrap(err, "unmarshalling table json")
}
return table, nil
}
func (s *Schemar) tableIDByName(tx *boltdb.Tx, qdbid dax.QualifiedDatabaseID, name dax.TableName) (dax.QualifiedTableID, error) {
bkt := tx.Bucket(bucketSchemar)
if bkt == nil {
return dax.QualifiedTableID{}, errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
b := bkt.Get(tableNameKey(qdbid.OrganizationID, qdbid.DatabaseID, name))
if b == nil {
return dax.QualifiedTableID{}, dax.NewErrTableNameDoesNotExist(name)
}
return keyQualifiedTableID(b)
}
// Tables returns a list of Table for all existing tables. If one or more table
// IDs is provided, then only those will be included in the output.
func (s *Schemar) Tables(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, ids ...dax.TableID) ([]*dax.QualifiedTable, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return nil, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.getTables(txx, qdbid, ids...)
}
func (s *Schemar) getTables(tx *boltdb.Tx, qdbid dax.QualifiedDatabaseID, ids ...dax.TableID) (dax.QualifiedTables, error) {
c := tx.Bucket(bucketSchemar).Cursor()
// Deserialize rows into Table objects.
tables := make(dax.QualifiedTables, 0)
var filterByID bool
if len(ids) > 0 {
filterByID = true
}
prefix := []byte(fmt.Sprintf(prefixFmtTables, qdbid.OrganizationID, qdbid.DatabaseID))
if qdbid.OrganizationID == "" && qdbid.DatabaseID == "" {
prefix = []byte(prefixTables)
} else if qdbid.DatabaseID == "" {
prefix = []byte(fmt.Sprintf(prefixFmtTablesOrg, qdbid.OrganizationID))
}
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
if v == nil {
s.logger.Printf("nil value for key: %s", k)
continue
}
tblID, err := keyTableID(k)
if err != nil {
return nil, errors.Wrap(err, "getting table from key")
}
// Only include tables provided in the ids filter.
if filterByID && !containsTableID(ids, tblID) {
continue
}
table := &dax.QualifiedTable{}
if err := json.Unmarshal(v, table); err != nil {
return nil, errors.Wrap(err, "unmarshalling table json")
}
tables = append(tables, table)
}
return tables, nil
}
func containsTableID(s []dax.TableID, e dax.TableID) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// DropTable drops the given table. If the named/IDed table does not exist
// then an error is returned.
func (s *Schemar) DropTable(tx dax.Transaction, qtid dax.QualifiedTableID) error {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.NewErrInvalidTransaction("*boltdb.Tx")
}
// Ensure the table exists.
qtbl, err := s.tableByQTID(txx, qtid)
if err != nil {
return errors.Wrap(err, "getting table by id")
}
bkt := txx.Bucket(bucketSchemar)
if bkt == nil {
return errors.Errorf(boltdb.ErrFmtBucketNotFound, bucketSchemar)
}
// Delete the table by ID.
if err := bkt.Delete(tableKey(qtbl.OrganizationID, qtbl.DatabaseID, qtbl.ID)); err != nil {
return errors.Wrap(err, "deleting table by id")
}
// Delete the reverse-lookup table by Name.
if err := bkt.Delete(tableNameKey(qtbl.OrganizationID, qtbl.DatabaseID, qtbl.Name)); err != nil {
return errors.Wrap(err, "deleting table by name")
}
return nil
}
func (s *Schemar) TableID(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, name dax.TableName) (dax.QualifiedTableID, error) {
txx, ok := tx.(*boltdb.Tx)
if !ok {
return dax.QualifiedTableID{}, dax.NewErrInvalidTransaction("*boltdb.Tx")
}
return s.tableIDByName(txx, qdbid, name)
}
const (
prefixTables = "tables/"
prefixFmtTablesOrg = prefixTables + "%s/" // org-id
prefixFmtTables = prefixFmtTablesOrg + "%s/" // db-id
prefixFmtTableNames = "tablenames/%s/%s/" // org-id, db-id
prefixDatabases = "databases/"
prefixFmtDatabases = prefixDatabases + "%s/" // org-id
prefixFmtDatabase = prefixFmtDatabases + "%s" // db-id
prefixFmtDatabaseNames = "databasenames/%s/" // org-id
)
// databaseKey returns a key based on a qualified database ID.
func databaseKey(orgID dax.OrganizationID, dbID dax.DatabaseID) []byte {
key := fmt.Sprintf(prefixFmtDatabase, orgID, dbID)
return []byte(key)
}
// databaseNameKey returns a key based on a qualified database name.
func databaseNameKey(orgID dax.OrganizationID, name dax.DatabaseName) []byte {
key := fmt.Sprintf(prefixFmtDatabaseNames+"%s", orgID, name)
return []byte(key)
}
// keyDatabaseID gets the DatabaseID out of the key.
func keyDatabaseID(key []byte) (dax.DatabaseID, error) {
parts := strings.Split(string(key), "/")
if len(parts) != 3 {
return "", errors.New(errors.ErrUncoded, "database key format expected: `databases/orgID/dbID`")
}
return dax.DatabaseID(parts[2]), nil
}
// tableKey returns a key based on a qualified table ID.
func tableKey(orgID dax.OrganizationID, dbID dax.DatabaseID, tblID dax.TableID) []byte {
key := fmt.Sprintf(prefixFmtTables+"%s", orgID, dbID, tblID)
return []byte(key)
}
// tableNameKey returns a key based on a qualified table name.
func tableNameKey(orgID dax.OrganizationID, dbID dax.DatabaseID, name dax.TableName) []byte {
key := fmt.Sprintf(prefixFmtTableNames+"%s", orgID, dbID, name)
return []byte(key)
}
// keyTableID gets the TableID out of the key.
func keyTableID(key []byte) (dax.TableID, error) {
parts := strings.Split(string(key), "/")
if len(parts) != 4 {
return "", errors.New(errors.ErrUncoded, "table key format expected: `tables/orgID/dbID/tblID`")
}
return dax.TableID(parts[3]), nil
}
// keyQualifedTableID gets the QualifiedTableID out of the key.
func keyQualifiedTableID(key []byte) (dax.QualifiedTableID, error) {
parts := strings.Split(string(key), "/")
if len(parts) != 4 {
return dax.QualifiedTableID{}, errors.New(errors.ErrUncoded, "table key format expected: `tables/orgID/dbID/tblID`")
}
return dax.NewQualifiedTableID(
dax.NewQualifiedDatabaseID(
dax.OrganizationID(parts[1]),
dax.DatabaseID(parts[2]),
),
dax.TableID(parts[3]),
), nil
}
// keyQualifedDatabaseID gets the QualifiedDatabaseID out of the key.
func keyQualifiedDatabaseID(key []byte) (dax.QualifiedDatabaseID, error) {
parts := strings.Split(string(key), "/")
if len(parts) != 3 {
return dax.QualifiedDatabaseID{}, errors.New(errors.ErrUncoded, "table key format expected: `databases/orgID/dbID`")
}
return dax.NewQualifiedDatabaseID(
dax.OrganizationID(parts[1]),
dax.DatabaseID(parts[2]),
), nil
}
func timestamp() int64 {
return time.Now().Unix()
}

View file

@ -1,227 +0,0 @@
package boltdb_test
import (
"context"
"testing"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar/boltdb"
daxtest "github.com/featurebasedb/featurebase/v3/dax/test"
testbolt "github.com/featurebasedb/featurebase/v3/dax/test/boltdb"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/stretchr/testify/assert"
)
func TestSchemar(t *testing.T) {
orgID := dax.OrganizationID("acme")
dbID := dax.DatabaseID("db1")
dbName := dax.DatabaseName("dbname1")
invalidTableID := dax.TableID("invalidID")
tableName := dax.TableName("foo")
tableName0 := dax.TableName("foo")
tableName1 := dax.TableName("bar")
tableID0 := "2"
tableID1 := "1"
partitionN := 12
ctx := context.Background()
qdbid := dax.NewQualifiedDatabaseID(orgID, dbID)
qdb := &dax.QualifiedDatabase{
OrganizationID: orgID,
Database: dax.Database{
ID: dbID,
Name: dbName,
},
}
t.Run("NewSchemar", func(t *testing.T) {
db := testbolt.MustOpenDB(t)
defer testbolt.MustCloseDB(t, db)
t.Cleanup(func() {
testbolt.CleanupDB(t, db.Path())
})
// Initialize the buckets.
assert.NoError(t, db.InitializeBuckets(boltdb.SchemarBuckets...))
s := boltdb.NewSchemar(db, logger.NopLogger)
tx, err := db.BeginTx(ctx, true)
assert.NoError(t, err)
defer tx.Rollback()
// Create database.
assert.NoError(t, s.CreateDatabase(tx, qdb))
// Add new table.
tbl := dax.NewTable(tableName)
tbl.CreateID()
tbl.Fields = []*dax.Field{
{
Name: dax.PrimaryKeyFieldName,
Type: dax.BaseTypeString,
},
{
Name: "intField",
Type: dax.BaseTypeInt,
},
}
qtbl := dax.NewQualifiedTable(qdbid, tbl)
assert.NoError(t, s.CreateTable(tx, qtbl))
// Try adding the table again.
err = s.CreateTable(tx, qtbl)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDExists))
}
qtid := qtbl.QualifiedID()
// Get the table.
{
tbl, err := s.Table(tx, qtid)
assert.NoError(t, err)
assert.Equal(t, tableName, tbl.Name)
}
// Drop the table.
assert.NoError(t, s.DropTable(tx, qtid))
// Make sure the reverse-lookup (table by name) was dropped as well.
{
_, err := s.TableID(tx, qdbid, tableName)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableNameDoesNotExist))
}
}
// Try adding the table (i.e. the same table name) again.
assert.NoError(t, s.CreateTable(tx, qtbl))
// Drop the table again.
assert.NoError(t, s.DropTable(tx, qtid))
// Drop invalid table.
{
iqtid := dax.NewQualifiedTableID(qdbid, invalidTableID)
err := s.DropTable(tx, iqtid)
if assert.Error(t, err) {
assert.True(t, errors.Is(err, dax.ErrTableIDDoesNotExist))
}
}
})
t.Run("GetTables", func(t *testing.T) {
db := testbolt.MustOpenDB(t)
defer testbolt.MustCloseDB(t, db)
t.Cleanup(func() {
testbolt.CleanupDB(t, db.Path())
})
// Initialize the buckets.
assert.NoError(t, db.InitializeBuckets(boltdb.SchemarBuckets...))
s := boltdb.NewSchemar(db, logger.NopLogger)
tx, err := db.BeginTx(ctx, true)
assert.NoError(t, err)
defer tx.Rollback()
// Create database.
assert.NoError(t, s.CreateDatabase(tx, qdb))
exp := []*dax.QualifiedTable{}
tables, err := s.Tables(tx, qdbid)
assert.NoError(t, err)
assert.Equal(t, exp, tables)
qtbl0 := daxtest.TestQualifiedTableWithID(t, qdbid, tableID0, tableName0, partitionN, false)
qtbl1 := daxtest.TestQualifiedTableWithID(t, qdbid, tableID1, tableName1, partitionN, false)
// Add a couple of tables.
assert.NoError(t, s.CreateTable(tx, qtbl0))
assert.NoError(t, s.CreateTable(tx, qtbl1))
exp = []*dax.QualifiedTable{
qtbl1,
qtbl0,
}
// All tables.
tables, err = s.Tables(tx, qdbid)
assert.NoError(t, err)
assert.Equal(t, exp, tables)
// With a valid filter.
tables, err = s.Tables(tx, qdbid, qtbl0.ID)
assert.NoError(t, err)
assert.Equal(t, exp[1:], tables)
// With an invalid filter.
tables, err = s.Tables(tx, qdbid, invalidTableID)
assert.NoError(t, err)
assert.Equal(t, exp[0:0], tables)
// With both valid and invalid filters.
tables, err = s.Tables(tx, qdbid, qtbl0.ID, invalidTableID)
assert.NoError(t, err)
assert.Equal(t, exp[1:], tables)
// With all valid filters.
tables, err = s.Tables(tx, qdbid, qtbl0.ID, qtbl1.ID)
assert.NoError(t, err)
assert.Equal(t, exp, tables)
})
t.Run("GetTablesAll", func(t *testing.T) {
db := testbolt.MustOpenDB(t)
defer testbolt.MustCloseDB(t, db)
t.Cleanup(func() {
testbolt.CleanupDB(t, db.Path())
})
// Initialize the buckets.
assert.NoError(t, db.InitializeBuckets(boltdb.SchemarBuckets...))
s := boltdb.NewSchemar(db, logger.NopLogger)
tx, err := db.BeginTx(ctx, true)
assert.NoError(t, err)
defer tx.Rollback()
qtbl0 := daxtest.TestQualifiedTableWithID(t, qdbid, tableID0, tableName0, partitionN, false)
orgID2 := dax.OrganizationID("acme2")
qdbid2 := dax.NewQualifiedDatabaseID(orgID2, dbID)
tableID2 := "3"
qtbl2 := daxtest.TestQualifiedTableWithID(t, qdbid2, tableID2, dax.TableName("two"), partitionN, false)
// Create databases.
assert.NoError(t, s.CreateDatabase(tx, qdb))
qdb2 := &dax.QualifiedDatabase{
OrganizationID: orgID2,
Database: dax.Database{
ID: dbID,
Name: dbName,
},
}
assert.NoError(t, s.CreateDatabase(tx, qdb2))
assert.NoError(t, s.CreateTable(tx, qtbl0))
assert.NoError(t, s.CreateTable(tx, qtbl2))
exp := []*dax.QualifiedTable{qtbl0, qtbl2}
tables, err := s.Tables(tx, dax.QualifiedDatabaseID{})
assert.NoError(t, err)
assert.Equal(t, exp, tables)
tables, err = s.Tables(tx, dax.QualifiedDatabaseID{OrganizationID: orgID2})
assert.NoError(t, err)
assert.Equal(t, []*dax.QualifiedTable{qtbl2}, tables)
})
}

View file

@ -5,11 +5,8 @@ import (
"os"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller"
balancerboltdb "github.com/featurebasedb/featurebase/v3/dax/controller/balancer/boltdb"
controllerhttp "github.com/featurebasedb/featurebase/v3/dax/controller/http"
schemarboltdb "github.com/featurebasedb/featurebase/v3/dax/controller/schemar/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
@ -33,16 +30,6 @@ func New(uri *fbnet.URI, cfg controller.Config) *controllerService {
logr = cfg.Logger.WithPrefix("Controller: ")
}
if cfg.DataDir == "" {
dir, err := os.MkdirTemp("", "controller_*")
if err != nil {
logr.Printf("Making temp dir for Controller storage: %v", err)
os.Exit(1)
}
cfg.DataDir = dir
logr.Warnf("no DataDir given (like '/path/to/directory'); using temp dir at '%s'", cfg.DataDir)
}
controller := controller.New(cfg)
controllerSvc := &controllerService{
uri: uri,
@ -52,23 +39,6 @@ func New(uri *fbnet.URI, cfg controller.Config) *controllerService {
// Storage methods.
switch cfg.StorageMethod {
case "boltdb":
buckets := append(schemarboltdb.SchemarBuckets, balancerboltdb.BalancerBuckets...)
controllerDB, err := boltdb.NewSvcBolt(cfg.DataDir, "controller", buckets...)
if err != nil {
logr.Printf(errors.Wrap(err, "creating controller bolt").Error())
os.Exit(1)
}
// Directive version.
if err := controllerDB.InitializeBuckets(boltdb.DirectiveBuckets...); err != nil {
logr.Panicf("initializing directive buckets: %v", err)
}
controller.Schemar = schemarboltdb.NewSchemar(controllerDB, logr)
controller.Balancer = balancerboltdb.NewBalancer(controllerDB, controller.Schemar, logr)
directiveVersion := boltdb.NewDirectiveVersion(controllerDB)
controller.DirectiveVersion = directiveVersion
controller.Transactor = controllerDB
case "sqldb":
controller.Schemar = sqldb.NewSchemar(logr)
controller.Balancer = sqldb.NewBalancer(logr)
@ -81,7 +51,7 @@ func New(uri *fbnet.URI, cfg controller.Config) *controllerService {
}
controller.Transactor = transactor
default:
logr.Printf("storagemethod %s not supported, try 'boltdb' or 'sqldb'", cfg.StorageMethod)
logr.Printf("storagemethod %s not supported, only 'sqldb' is currently accepted.", cfg.StorageMethod)
os.Exit(1)
}

View file

@ -6,12 +6,12 @@ import (
)
// NewBalancer returns a new instance of controller.Balancer.
func NewBalancer(logger logger.Logger) *balancer.Balancer {
schemar := NewSchemar(logger)
fjs := NewFreeJobService(logger)
wjs := NewWorkerJobService(logger)
fws := NewFreeWorkerService(logger)
ns := NewNodeService(logger)
func NewBalancer(log logger.Logger) *balancer.Balancer {
schemar := NewSchemar(log)
fjs := NewFreeJobService(log)
wjs := NewWorkerJobService(log)
fws := NewFreeWorkerService(log)
ns := NewNodeService(log)
return balancer.New(ns, fjs, wjs, fws, schemar, logger)
return balancer.New(ns, fjs, wjs, fws, schemar, log)
}

View file

@ -82,9 +82,12 @@ func (n *nodeService) DeleteNode(tx dax.Transaction, addr dax.Address) error {
node := &models.Node{}
err := dt.C.Eager().Where("address = ?", addr).First(node)
if err != nil {
return errors.Wrap(err, "getting node")
if isNoRowsError(err) {
return nil
} else if err != nil {
return errors.Wrap(err, "finding node")
}
err = dt.C.Destroy(node)
return errors.Wrap(err, "destroying node")
}

View file

@ -293,10 +293,9 @@ func toField(col models.Column) *dax.Field {
panic(err)
}
return &dax.Field{
Name: col.Name,
Type: col.Type,
Options: opts,
CreatedAt: col.CreatedAt.Unix(),
Name: col.Name,
Type: col.Type,
Options: opts,
}
}
@ -317,8 +316,6 @@ func toQualifiedTable(mtbl *models.Table) *dax.QualifiedTable {
PartitionN: mtbl.PartitionN,
Description: mtbl.Description,
Owner: mtbl.Owner,
CreatedAt: mtbl.CreatedAt.Unix(),
UpdatedAt: mtbl.UpdatedAt.Unix(),
UpdatedBy: mtbl.UpdatedBy,
},
}
@ -425,7 +422,7 @@ func (s *Schemar) Tables(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, tabl
query = query.Where("id in (?)", ifaceIDs)
}
tables := []*models.Table{}
err := query.Eager().Order("created_at asc").All(&tables)
err := query.Eager().Order("name asc").All(&tables)
if err != nil {
return nil, errors.Wrap(err, "querying all tables")
}

View file

@ -20,11 +20,11 @@ func EnvOr(envName, defaultVal string) string {
func GetTestConfig() *controller.SQLDBConfig {
return &controller.SQLDBConfig{
Dialect: "postgres",
Database: EnvOr("SQLDB_DB", "dax_test"),
Host: EnvOr("SQLDB_HOST", "127.0.0.1"),
Port: EnvOr("SQLDB_PORT", "5432"),
User: EnvOr("SQLDB_USER", "postgres"),
Password: EnvOr("SQLDB_PASSWORD", "testpass"),
Database: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_DATABASE", "dax_test"),
Host: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST", "127.0.0.1"),
Port: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PORT", "5432"),
User: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER", "postgres"),
Password: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD", "testpass"),
}
}
@ -33,9 +33,9 @@ func GetTestConfigRandomDB(dbprefix string) *controller.SQLDBConfig {
return &controller.SQLDBConfig{
Dialect: "postgres",
Database: fmt.Sprintf("%s_%d", dbprefix, rnd.Int()),
Host: EnvOr("SQLDB_HOST", "127.0.0.1"),
Port: EnvOr("SQLDB_PORT", "5432"),
User: EnvOr("SQLDB_USER", "postgres"),
Password: EnvOr("SQLDB_PASSWORD", "testpass"),
Host: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST", "127.0.0.1"),
Port: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PORT", "5432"),
User: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER", "postgres"),
Password: EnvOr("FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD", "testpass"),
}
}

View file

@ -2,6 +2,7 @@ package sqldb
import (
"context"
"strings"
"database/sql"
@ -66,6 +67,11 @@ func (t Transactor) Start() error {
return errors.Wrap(err, "migrating DB")
}
err := t.RawQuery("INSERT INTO directive_versions (id, version, created_at, updated_at) VALUES (1, 0, '1970-01-01T00:00', '1970-01-01T00:00')").Exec()
if err != nil && !strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
return errors.Wrap(err, "unexpected error (re)inserting directive_version record")
}
return nil
}

View file

@ -112,9 +112,12 @@ func (w *workerJobService) DeleteWorker(tx dax.Transaction, roleType dax.RoleTyp
worker := &models.Worker{}
err := dt.C.Where("address = ? and role = ? and database_id = ?", addr, roleType, qdbid.DatabaseID).First(worker)
if err != nil {
if isNoRowsError(err) {
return nil
} else if err != nil {
return errors.Wrap(err, "getting worker")
}
err = dt.C.Destroy(worker)
return errors.Wrap(err, "deleting worker")
}

View file

@ -28,5 +28,5 @@ func TestDirectiveVersion(t *testing.T) {
n, err := dvSvc.Increment(tx, 1)
require.NoError(t, err)
require.Equal(t, uint64(2), n)
require.Equal(t, uint64(1), n)
}

View file

@ -1,64 +0,0 @@
version: '3'
services:
controller:
build:
context: ../.quick
dockerfile: ../Dockerfile-dax-quick
environment:
FEATUREBASE_BIND: 0.0.0.0:8080
FEATUREBASE_VERBOSE: "true"
FEATUREBASE_CONTROLLER_RUN: "true"
FEATUREBASE_CONFIG_DATA_DIR: file:/dax-data/controller
ports:
- "8081:8080"
queryer:
build:
context: ../.quick
dockerfile: ../Dockerfile-dax-quick
environment:
FEATUREBASE_BIND: 0.0.0.0:8080
FEATUREBASE_VERBOSE: "true"
FEATUREBASE_QUERYER_RUN: "true"
FEATUREBASE_QUERYER_CONFIG_CONTROLLER_ADDRESS: "controller:8080/controller"
depends_on:
- controller
ports:
- "8080:8080"
computer:
build:
context: ../.quick
dockerfile: ../Dockerfile-dax-quick
environment:
FEATUREBASE_COMPUTER_RUN: "true"
FEATUREBASE_COMPUTER_CONFIG_CONTROLLER_ADDRESS: "controller:8080/controller"
FEATUREBASE_COMPUTER_CONFIG_DATA_DIR: /dax-data/computer
FEATUREBASE_COMPUTER_CONFIG_VERBOSE: true
FEATUREBASE_BIND: 0.0.0.0:8080
FEATUREBASE_VERBOSE: "true"
FEATUREBASE_STORAGE_METHOD: boltdb
FEATUREBASE_COMPUTER_CONFIG_WRITELOGGER_DIR: "/dax-data/writelogger"
FEATUREBASE_COMPUTER_CONFIG_SNAPSHOTTER_DIR: "/dax-data/snapshotter"
volumes:
- "./dax-data/writelogger:/dax-data/writelogger"
- "./dax-data/snapshotter:/dax-data/snapshotter"
depends_on:
- controller
deploy:
replicas: 1
datagen:
build:
context: ..
dockerfile: Dockerfile-datagen
profiles: [ "datagen" ]
environment:
GEN_CUSTOM_CONFIG: "/testdata/keys_ids.yaml"
GEN_FEATUREBASE_ORG_ID: "testorg"
GEN_FEATUREBASE_DB_ID: "testdb"
GEN_USE_SHARD_TRANSACTIONAL_ENDPOINT: "true"
GEN_SOURCE: "custom"
GEN_TARGET: "serverless"
GEN_CONTROLLER_ADDRESS: "controller:8080/controller"

View file

@ -81,4 +81,3 @@ create_table("directive_versions") {
t.Timestamps()
}
sql("INSERT INTO directive_versions (id, version, created_at, updated_at) VALUES (1, 1, '1970-01-01T00:00', '1970-01-01T00:00');")

View file

@ -19,7 +19,7 @@ type Database struct {
Description string `json:"description" db:"description"`
Owner string `json:"owner" db:"owner"`
UpdatedBy string `json:"updated_by" db:"updated_by"`
Tables Tables `json:"tables" has_many:"tables"`
Tables Tables `json:"tables" has_many:"tables" order_by:"name asc"`
Organization *Organization `json:"organization" belongs_to:"organization"`
OrganizationID string `json:"organization_id" db:"organization_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`

View file

@ -15,7 +15,7 @@ import (
type Node struct {
ID uuid.UUID `json:"id" db:"id"`
Address dax.Address `json:"address" db:"address"`
NodeRoles NodeRoles `json:"node_roles" has_many:"node_roles"`
NodeRoles NodeRoles `json:"node_roles" has_many:"node_roles" order_by:"created_at asc"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}

View file

@ -200,7 +200,6 @@ func NewManagedCommand(tb fbtest.DirCleaner, opts ...server.CommandOption) *Mana
mc.svcmgr = svcmgr
mc.Config.Bind = "http://localhost:0"
mc.Config.Controller.Config.DataDir = path + "/controller"
mc.Config.Computer.Config.DataDir = path
mc.Config.Computer.Config.WriteloggerDir = path + "/wl"
mc.Config.Controller.Config.WriteloggerDir = path + "/wl"

View file

@ -1,70 +0,0 @@
package snapshotter_test
import (
"fmt"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSnapshotter(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "testWritelogger-*")
assert.NoError(t, err)
// Remove the temp directory.
defer func() {
os.RemoveAll(tmpDir)
}()
// t.Run("Basic", func(t *testing.T) {
// type payload struct {
// Foo string `json:"foo"`
// Bar int `json:"bar"`
// }
// cfg := core.Config{
// DataDir: tmpDir,
// }
// wl := core.NewSnapshotter(cfg)
// table := "tbl"
// partition := 1
// version := 0
// key := "keys"
// msg1 := payload{
// Foo: "message 1",
// Bar: 88,
// }
// // Write the message.
// msg, err := json.Marshal(msg1)
// assert.NoError(t, err)
// err = wl.AppendMessage(bucket(table, partition), key, version, msg)
// assert.NoError(t, err)
// // Read the message.
// reader, closer, err := wl.LogReader(bucket(table, partition), key, version)
// assert.NoError(t, err)
// defer closer.Close()
// buf, err := ioutil.ReadAll(reader)
// assert.NoError(t, err)
// var out payload
// err = json.Unmarshal(buf, &out)
// assert.NoError(t, err)
// assert.Equal(t, msg1.Foo, out.Foo)
// assert.Equal(t, msg1.Bar, out.Bar)
// })
}
func bucket(table string, partition int) string {
return path.Join(table, fmt.Sprintf("%d", partition))
}

View file

@ -380,8 +380,6 @@ type Table struct {
Description string `json:"description,omitempty"`
Owner string `json:"owner,omitempty"`
CreatedAt int64 `json:"createdAt,omitempty"`
UpdatedAt int64 `json:"updatedAt,omitempty"`
UpdatedBy string `json:"updatedBy,omitempty"`
}

View file

@ -1,50 +0,0 @@
package boltdb
import (
"os"
"testing"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/stretchr/testify/assert"
)
func MustGetDB(tb testing.TB) *boltdb.DB {
tb.Helper()
f, err := os.CreateTemp("", "dax-boltdb")
assert.NoError(tb, err)
dsn := "file:" + f.Name()
db := boltdb.NewDB(dsn)
return db
}
// MustOpenDB returns a new, open DB. Fatal on error.
func MustOpenDB(tb testing.TB) *boltdb.DB {
db := MustGetDB(tb)
if err := db.Open(); err != nil {
tb.Fatal(err)
}
return db
}
// MustCloseDB closes the DB. Fatal on error.
func MustCloseDB(tb testing.TB, db *boltdb.DB) {
tb.Helper()
if err := db.Close(); err != nil {
tb.Fatal(err)
}
}
func CleanupDB(tb testing.TB, path string) {
tb.Helper()
if path == "" {
return
}
if err := os.Remove(path); err != nil {
tb.Fatal(err)
}
}

View file

@ -1,29 +0,0 @@
package test
import (
"os"
"testing"
"github.com/featurebasedb/featurebase/v3/dax/boltdb"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
schemarbolt "github.com/featurebasedb/featurebase/v3/dax/controller/schemar/boltdb"
testbolt "github.com/featurebasedb/featurebase/v3/dax/test/boltdb"
"github.com/featurebasedb/featurebase/v3/logger"
)
func NewSchemar(t *testing.T) (schemar schemar.Schemar, cleanup func()) {
td, err := os.MkdirTemp("", "schemartest_*")
if err != nil {
t.Fatalf(": %v", err)
}
db, err := boltdb.NewSvcBolt(td, "schemar", schemarbolt.SchemarBuckets...)
if err != nil {
t.Fatalf("opening boltdb: %v", err)
}
s := schemarbolt.NewSchemar(db, logger.StderrLogger)
return s, func() {
testbolt.MustCloseDB(t, db)
testbolt.CleanupDB(t, db.Path())
}
}

View file

@ -147,10 +147,6 @@ services:
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_USER: "postgres"
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_PASSWORD: "password"
FEATUREBASE_CONTROLLER_CONFIG_SQLDB_HOST: "postgres"
SQLDB_DB: "postgres"
SQLDB_USER: "postgres"
SQLDB_PASSWORD: "password"
SQLDB_HOST: "postgres"
FEATUREBASE_COMPUTER_RUN: "true"
FEATUREBASE_COMPUTER_CONFIG_DATA_DIR: /dax-data/computer
FEATUREBASE_COMPUTER_CONFIG_WRITELOGGER_DIR: /dax-data/wl

View file

@ -162,8 +162,6 @@ func IndexInfoToTable(ii *IndexInfo) *dax.Table {
Description: ii.Options.Description,
Owner: ii.Owner,
CreatedAt: ii.CreatedAt,
UpdatedAt: ii.UpdatedAt,
UpdatedBy: ii.LastUpdateUser,
}
@ -178,9 +176,8 @@ func IndexInfoToTable(ii *IndexInfo) *dax.Table {
idType = dax.BaseTypeString
}
tbl.Fields = append(tbl.Fields, &dax.Field{
Name: "_id",
Type: idType,
CreatedAt: ii.CreatedAt,
Name: "_id",
Type: idType,
})
// Populate the rest of the fields.
@ -268,8 +265,6 @@ func FieldInfoToField(fi *FieldInfo) *dax.Field {
TTL: fo.TTL,
ForeignIndex: foreignIndex,
},
CreatedAt: fi.CreatedAt,
}
}
@ -308,8 +303,6 @@ func TableToIndexInfo(tbl *dax.Table) *IndexInfo {
ii := &IndexInfo{
Name: string(tbl.Name),
Owner: tbl.Owner,
CreatedAt: tbl.CreatedAt,
UpdatedAt: tbl.UpdatedAt,
LastUpdateUser: tbl.UpdatedBy,
Options: IndexOptions{
Keys: tbl.StringKeys(),
@ -359,8 +352,7 @@ func FieldToFieldInfo(fld *dax.Field) *FieldInfo {
}
return &FieldInfo{
Name: string(fld.Name),
CreatedAt: fld.CreatedAt,
Name: string(fld.Name),
Options: FieldOptions{
Type: fieldToFieldType(fld),
Base: base,