mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
* Introduce ServiceManager and Refactor DAX Integration tests
The ServiceManager provides an interface with which to manage
featurebase (dax) services (mds, queryer, computer). It replaces the
confusing interface implementations in /dax/server/server.go (which
optionally used pointers to in-process objects to satisfy an interface)
with (for now) http implementations. The thought is that even if we're
running all services in-process, we should communicate between services
over http in order to mirror what we would do in a production
environment where the services are running on different nodes.
This batch of commits does quit a lot, most of which is captured here:
- Added `path` support to `dax.Address`. Address is now a string of the form [scheme]://[host]:[port]/[path].
- Added `Holder.directiveApplied` to determine (in tests) if the computer has completed applying the latest directive. This is somewhat temporary until we improve the mds-to-computer logic.
- Removed the "service prefix" code which was prepending client URL paths with the prefix. Instead, the serviceType (mds, queryer, computer[n] is now part of `dax.Address`).
- Removed, from the dax config, the top level `StorageMethod` and `StorageDSN` and now just have `MDS.Config.DataDir`.
- Added `Computer.Config.N` to specify the number of computers to run in-process.
- Moved the `pilosa.MDS` interface to `computer.Registrar`. This is an example of getting the interfaces defined in the right packages.
- Added `SnapshotTable()` method to the mds client (to align with its API).
- Changed `Balancer.AddJob()` to `Balancer.AddJobs()` to support, for example, adding 256 partitions in a single call. Refactored some of the naive Balancer to account for this.
- Added a `Seed` to the top-level config. It's not really useful because of package `crypto/rand`.
- Added an in-memory implementation of the DisCo interface and disabled etcd in a computer service.
- Create sepearte data-dirs for each in-process computer.
- Disabled grpc in dax.
- Modified the sql3 test definition format to support multiple insert steps and separate query results (to align with those steps).
* Changes necessary to get multiple computer instance running in-process
For now the config looks like this:
```
[computer]
run = true
n = 4
```
but we can probably just change that to be something like:
```
[computer]
run = 4
```
*Issues found running multiple "computers" in-process*
- grpc was trying to bind on the same port
- changed GRPCListener from `*net.TCPListener` to `net.Listener`
- created a nopListener and set to that for now (i.e. disabled grpc)
- etcd was starting more than once
- changed dax to use in-memory implementations of the disco interfaces (i.e. stop using etcd)
- IDAllocator (which uses boltdb) was trying to open the `idalloc.db` file more than once
- realized we have to set separate data-dirs for each holder. that fixed it.
* Port dax integration tests to ManagedCommand
* Modify Balancer-related methods like AddJob to AddJobs
There were (and still are) a lot of places where we were adding on job
at a time, even when we had a long list of jobs to add. This resulted in
every job add (for example adding 1 of 256 shards) taking ~40ms, or over
10s to create a keyed table. One reason was because each job add was
making multiple boltdb transactions.
* Port over more dax integration test stuff
* Add DirectiveApplied to signify that snapshot/writes have loaded.
We use this in tests to avoid using sleeps.
This should be considered temporary; we're going to need a more robust
solution for determining when a computer node is ready to serve complete
data.
* Finish porting dax integration tests
* Improve godocs
* Remove docker-based DAX integration tests.
* go mod tidy
* Move test/managed.go to avoid package conflicts
* Modify IDK integration tests to work with ServiceManager changes
This is really just computer -> computer0
And the MDS DataDir config change.
* cleanup found during review
* echo $CI_COMMIT_REF_SLUG in CI
* remove docker image arg, use build instead
(cherry picked from commit 2843f218bc)
984 lines
30 KiB
Go
984 lines
30 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pilosa
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"sync"
|
|
|
|
"github.com/molecula/featurebase/v3/dax"
|
|
"github.com/molecula/featurebase/v3/dax/computer"
|
|
"github.com/molecula/featurebase/v3/disco"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ApplyDirective applies a Directive received, from the Controller, at the
|
|
// /directive endpoint.
|
|
func (api *API) ApplyDirective(ctx context.Context, d *dax.Directive) error {
|
|
// Get the current directive for comparison.
|
|
previousDirective := api.holder.Directive()
|
|
|
|
// Check that incoming version is newer.
|
|
// Note: 0 is an invalid Directive version. This decision was made because
|
|
// previousDirective is not a pointer to a directive, but a concrete
|
|
// Directive. Which means we can't check for nil, and by default it has a
|
|
// version of 0. So in order to ensure the version has increased, we need to
|
|
// require that incoming directive versions are greater than 0.
|
|
if d.Version == 0 {
|
|
return errors.Errorf("directive version cannot be 0")
|
|
} else if previousDirective.Version >= d.Version {
|
|
return errors.Errorf("directive version mismatch, got %d, but already have %d", d.Version, previousDirective.Version)
|
|
}
|
|
|
|
// Handle the operations based on the directive method.
|
|
switch d.Method {
|
|
case dax.DirectiveMethodDiff:
|
|
// pass: normal operation
|
|
|
|
case dax.DirectiveMethodReset:
|
|
// Delete all tables.
|
|
if err := api.deleteAllIndexes(ctx); err != nil {
|
|
return errors.Wrap(err, "deleting all indexes")
|
|
}
|
|
|
|
// Set previousDirective to empty so the diff handles everything as new.
|
|
previousDirective = dax.Directive{}
|
|
|
|
case dax.DirectiveMethodSnapshot:
|
|
// TODO(tlt): this was the existing logic, but we should really diff the
|
|
// directive and ensure that overwriting the value in the cache doesn't
|
|
// have a negative effect.
|
|
api.holder.SetDirective(d)
|
|
return nil
|
|
|
|
default:
|
|
return errors.Errorf("invalid directive method: %s", d.Method)
|
|
}
|
|
|
|
// Cache this directive as the latest applied. There is functionality within
|
|
// the "enactDirective" stage of ApplyDirective which validates against this
|
|
// cached Directive, so it's important that it be set before calling
|
|
// enactDirective(). An example: when loading partition data from the
|
|
// WriteLogger, there are validations to ensure that the partition being
|
|
// loaded is meant to be handled by this node; that validation is done
|
|
// against the cached Directive.
|
|
// TODO(tlt): despite what this comment says, this logic is not sound; we
|
|
// shouldn't be setting the directive until enactiveDirective() succeeds.
|
|
api.holder.SetDirective(d)
|
|
defer api.holder.SetDirectiveApplied(true)
|
|
|
|
return api.enactDirective(ctx, &previousDirective, d)
|
|
}
|
|
|
|
// deleteAllIndexes deletes all indexes handled by this node.
|
|
func (api *API) deleteAllIndexes(ctx context.Context) error {
|
|
indexes, err := api.Schema(ctx, false)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting schema")
|
|
}
|
|
|
|
for i := range indexes {
|
|
if err := api.DeleteIndex(ctx, indexes[i].Name); err != nil {
|
|
return errors.Wrapf(err, "deleting index: %s", indexes[i].Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// directiveJobType allows us to switch on jobType in the directiveWorker in
|
|
// order to use a single worker pool for all job types (as opposed to having a
|
|
// separate worker pool for each job type).
|
|
type directiveJobType interface {
|
|
// We have this method just to prevent *any* struct from implementing this
|
|
// interface automatically. But, interestingly enough, we don't actually
|
|
// have to have this method on the implementation because we embed the
|
|
// interface.
|
|
isJobType() bool
|
|
}
|
|
|
|
type directiveJobTableKeys struct {
|
|
directiveJobType
|
|
idx *Index
|
|
tkey dax.TableKey
|
|
partition dax.VersionedPartition
|
|
}
|
|
|
|
type directiveJobFieldKeys struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
field dax.VersionedField
|
|
}
|
|
|
|
type directiveJobShards struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
shard dax.VersionedShard
|
|
}
|
|
|
|
// directiveWorker is a worker in a worker pool which handles portions of a
|
|
// directive. Multiple instances of directiveWorker run in goroutines in order
|
|
// to load data from snapshotter and writelogger concurrently. Note: unlike the
|
|
// api.ingestWorkerPool, of which one pool is always running, the
|
|
// directiveWorker pool is only running during the life of the
|
|
// api.ApplyDirective call. Technically, this means that multiple
|
|
// directiveWorker pools could be active at the same time, but we should never
|
|
// be running more than once instance of ApplyDirective concurrently.
|
|
func (api *API) directiveWorker(ctx context.Context, jobs <-chan directiveJobType, errs chan<- error) {
|
|
for j := range jobs {
|
|
switch job := j.(type) {
|
|
case directiveJobTableKeys:
|
|
if err := api.loadTableKeys(ctx, job.idx, job.tkey, job.partition); err != nil {
|
|
errs <- errors.Wrapf(err, "loading table keys: %s, %s", job.tkey, job.partition)
|
|
}
|
|
case directiveJobFieldKeys:
|
|
if err := api.loadFieldKeys(ctx, job.tkey, job.field); err != nil {
|
|
errs <- errors.Wrapf(err, "loading field keys: %s, %s", job.tkey, job.field)
|
|
}
|
|
case directiveJobShards:
|
|
if err := api.loadShard(ctx, job.tkey, job.shard); err != nil {
|
|
errs <- errors.Wrapf(err, "loading shard: %s, %s", job.tkey, job.shard)
|
|
}
|
|
default:
|
|
errs <- errors.Errorf("unsupported job type: %T %[1]v", job)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
// continue pulling jobs off the channel
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) enactDirective(ctx context.Context, fromD, toD *dax.Directive) error {
|
|
// enactTables is called before the jobs that run in the worker pool because
|
|
// it probably makes sense to apply the schema before trying to load data
|
|
// concurrently.
|
|
if err := api.enactTables(ctx, fromD, toD); err != nil {
|
|
return errors.Wrap(err, "enactTables")
|
|
}
|
|
|
|
// The following types use a shared pool of workers to run each
|
|
// directiveJobType.
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// open job channel
|
|
jobs := make(chan directiveJobType, api.directiveWorkerPoolSize)
|
|
errs := make(chan error)
|
|
done := make(chan struct{})
|
|
|
|
// Spin up n workers in goroutines that pull jobs from the jobs channel.
|
|
for i := 0; i < api.directiveWorkerPoolSize; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
api.directiveWorker(ctx, jobs, errs)
|
|
defer wg.Done()
|
|
}()
|
|
}
|
|
|
|
// Wait for the WaitGroup counter to reach 0. When it has, indicate that
|
|
// we're done processing all jobs by closing the done channel.
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
// Run through all the "enact" methods. These push jobs onto the jobs
|
|
// channel. Once all the jobs have been queued to the channel, we close the
|
|
// jobs channel. This allows the directiveWorkers to exit out of the
|
|
// function, which will then decrement the WaitGroup counter.
|
|
go func() {
|
|
api.pushJobsTableKeys(ctx, jobs, fromD, toD)
|
|
api.pushJobsFieldKeys(ctx, jobs, fromD, toD)
|
|
api.pushJobsShards(ctx, jobs, fromD, toD)
|
|
close(jobs)
|
|
}()
|
|
|
|
// Keep running until we get an error or until the done channel is closed.
|
|
// Note: the code is written such that only non-nil errors are pushed to the
|
|
// errs channel.
|
|
for {
|
|
select {
|
|
case err := <-errs:
|
|
return err
|
|
case <-done:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) enactTables(ctx context.Context, fromD, toD *dax.Directive) error {
|
|
currentIndexes := api.holder.Indexes()
|
|
|
|
// Make a list of indexes that currently exist (from).
|
|
from := make(dax.TableKeys, 0, len(currentIndexes))
|
|
for _, idx := range currentIndexes {
|
|
qtid, err := dax.QualifiedTableIDFromKey(idx.Name())
|
|
if err != nil {
|
|
return errors.Wrap(err, "converting index name to qualified table id")
|
|
}
|
|
from = append(from, qtid.Key())
|
|
}
|
|
|
|
// TODO sanity check holder against fromD. We're getting existing
|
|
// indexes from holder, but in theory fromD should be
|
|
// identical. If we have an error in our directive-caching logic
|
|
// (it has happened before (just now, in fact!) and we'd be
|
|
// foolish to think it won't happen again), or we have schema
|
|
// mutations that are not going through the directive path, we
|
|
// could potentially catch them here.
|
|
|
|
// Make a list of tables that are in the directive (to) along with a map of
|
|
// tableKey to table (m).
|
|
m := make(map[dax.TableKey]*dax.QualifiedTable, len(toD.Tables))
|
|
to := make(dax.TableKeys, 0, len(toD.Tables))
|
|
for _, t := range toD.Tables {
|
|
m[t.Key()] = t
|
|
to = append(to, t.Key())
|
|
}
|
|
|
|
sc := newSliceComparer(from, to)
|
|
|
|
// Remove all indexes that are no longer part of the directive.
|
|
for _, tkey := range sc.removed() {
|
|
idx := string(tkey)
|
|
if err := api.holder.deleteIndex(idx); err != nil {
|
|
return errors.Wrapf(err, "deleting index: %s", tkey)
|
|
}
|
|
}
|
|
|
|
// Put partitions into a map by table.
|
|
partitionMap := toD.TranslatePartitionsMap()
|
|
|
|
// Add all indexes that weren't previously (but now are) a part of the
|
|
// directive.
|
|
for _, tkey := range sc.added() {
|
|
if qtbl, found := m[tkey]; !found {
|
|
return errors.Errorf("table '%s' was not in map", tkey)
|
|
} else if err := api.createTableAndFields(qtbl, partitionMap[tkey]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check fields on all indexes present in both from and to.
|
|
for _, tkey := range sc.same() {
|
|
if err := api.enactFieldsForTable(ctx, tkey, fromD, toD); err != nil {
|
|
return errors.Wrapf(err, "enacting fields for table: '%s'", tkey)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) enactFieldsForTable(ctx context.Context, tkey dax.TableKey, fromD, toD *dax.Directive) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
fromT, err := fromD.Table(qtid)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting from table")
|
|
}
|
|
toT, err := toD.Table(qtid)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting to table")
|
|
}
|
|
|
|
// Get the index for tkey.
|
|
idx := api.holder.Index(string(tkey))
|
|
if idx == nil {
|
|
return errors.Errorf("index not found: %s", tkey)
|
|
}
|
|
|
|
sc := newSliceComparer(fromT.FieldNames(), toT.FieldNames())
|
|
|
|
// Add fields new to toT.
|
|
for _, fldName := range sc.added() {
|
|
if field, found := toT.Field(fldName); !found {
|
|
return dax.NewErrFieldDoesNotExist(fldName)
|
|
} else if err := createField(idx, field); err != nil {
|
|
return errors.Wrapf(err, "creating field: %s/%s", tkey, fldName)
|
|
}
|
|
}
|
|
|
|
// Remove fields which don't exist in toT.
|
|
for _, fldName := range sc.removed() {
|
|
if err := api.DeleteField(ctx, string(tkey), string(fldName)); err != nil {
|
|
return errors.Wrapf(err, "deleting field: %s/%s", tkey, fldName)
|
|
}
|
|
}
|
|
|
|
// // Update any field options which have changed for existing fields.
|
|
// for _, fldName := range sc.same() {
|
|
// // handle changed field options??
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsTableKeys(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
toPartitionsMap := toD.TranslatePartitionsMap()
|
|
|
|
// Get the diff between from/to directive.partitions.
|
|
partComp := newPartitionsComparer(fromD.TranslatePartitionsMap(), toPartitionsMap)
|
|
|
|
// Loop over the partition map and load from WriteLogger.
|
|
for tkey, partitions := range partComp.added() {
|
|
// Get index in order to find the translate stores (by partition) for
|
|
// the table.
|
|
idx := api.holder.Index(string(tkey))
|
|
if idx == nil {
|
|
log.Printf("index not found in holder: %s", tkey)
|
|
continue
|
|
}
|
|
|
|
// Update the cached version of translate partitions that we keep on the
|
|
// Index.
|
|
idx.SetTranslatePartitions(toPartitionsMap[tkey])
|
|
|
|
for _, partition := range partitions {
|
|
jobs <- directiveJobTableKeys{
|
|
idx: idx,
|
|
tkey: tkey,
|
|
partition: partition,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadTableKeys(ctx context.Context, idx *Index, tkey dax.TableKey, partition dax.VersionedPartition) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if partition.Version > 0 {
|
|
// Load partition snapshot: version - 1
|
|
previousVersion := partition.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadTableKeys(ctx, qtid, partition.Num, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading table keys snapshot")
|
|
}
|
|
defer rc.Close()
|
|
|
|
if err := api.TranslateIndexDB(ctx, string(tkey), int(partition.Num), rc); err != nil {
|
|
return errors.Wrap(err, "restoring table keys")
|
|
|
|
}
|
|
}
|
|
|
|
if err := func() error {
|
|
store := idx.TranslateStore(int(partition.Num))
|
|
|
|
reader := api.writeLogReader.TableKeyReader(ctx, qtid, partition.Num, partition.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, partition: %d: version: %d, err: %s", table, partition.Num, partition.Version, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for msg, err := reader.Read(); err != io.EOF; msg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
for key, id := range msg.StringToID {
|
|
if err := store.ForceSet(id, key); err != nil {
|
|
return errors.Wrapf(err, "forcing set id, key: %d, %s", id, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/partition/version in the holder.
|
|
if err := api.holder.versionStore.AddPartitions(ctx, qtid, partition); err != nil {
|
|
return errors.Wrap(err, "adding partition to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsFieldKeys(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
// Get the diff between from/to directive.fields.
|
|
fieldComp := newFieldsComparer(fromD.TranslateFieldsMap(), toD.TranslateFieldsMap())
|
|
|
|
// Loop over the field map and load from WriteLogger.
|
|
for tkey, fields := range fieldComp.added() {
|
|
for _, field := range fields {
|
|
jobs <- directiveJobFieldKeys{
|
|
tkey: tkey,
|
|
field: field,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadFieldKeys(ctx context.Context, tkey dax.TableKey, field dax.VersionedField) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if field.Version > 0 {
|
|
// Load field snapshot: version - 1
|
|
previousVersion := field.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadFieldKeys(ctx, qtid, field.Name, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading field keys snapshot")
|
|
}
|
|
defer rc.Close()
|
|
|
|
if err := api.TranslateFieldDB(ctx, string(tkey), string(field.Name), rc); err != nil {
|
|
return errors.Wrap(err, "restoring field keys")
|
|
}
|
|
}
|
|
|
|
if err := func() error {
|
|
// Get field in order to find the translate store.
|
|
fld := api.holder.Field(string(tkey), string(field.Name))
|
|
if fld == nil {
|
|
log.Printf("field not found in holder: %s", field.Name)
|
|
return nil
|
|
}
|
|
store := fld.TranslateStore()
|
|
|
|
reader := api.writeLogReader.FieldKeyReader(ctx, qtid, field.Name, field.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, field: %s: version: %d, err: %s", table, field.Name, field.Version, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for msg, err := reader.Read(); err != io.EOF; msg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
for key, id := range msg.StringToID {
|
|
if err := store.ForceSet(id, key); err != nil {
|
|
return errors.Wrapf(err, "forcing set id, key: %d, %s", id, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/field/version in the holder.
|
|
if err := api.holder.versionStore.AddFields(ctx, qtid, field); err != nil {
|
|
return errors.Wrap(err, "adding field to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (api *API) pushJobsShards(ctx context.Context, jobs chan<- directiveJobType, fromD, toD *dax.Directive) {
|
|
// Put shards into a map by table.
|
|
shardMap := toD.ComputeShardsMap()
|
|
|
|
// Get the diff between from/to directive shards.
|
|
shardComp := newShardsComparer(fromD.ComputeShardsMap(), shardMap)
|
|
|
|
// Loop over the shard map and load from WriteLogger.
|
|
for tkey, shards := range shardComp.added() {
|
|
for _, shard := range shards {
|
|
jobs <- directiveJobShards{
|
|
tkey: tkey,
|
|
shard: shard,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) loadShard(ctx context.Context, tkey dax.TableKey, shard dax.VersionedShard) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
partition := disco.ShardToShardPartition(string(tkey), uint64(shard.Num), disco.DefaultPartitionN)
|
|
partitionNum := dax.PartitionNum(partition)
|
|
|
|
// Load the previous snapshot. Version 0 doesn't have a snapshot
|
|
// file; it only has log entries.
|
|
if shard.Version > 0 {
|
|
// Load shard snapshot: version - 1
|
|
previousVersion := shard.Version - 1
|
|
rc, err := api.snapshotReadWriter.ReadShardData(ctx, qtid, partitionNum, shard.Num, previousVersion)
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading shard data snapshot")
|
|
}
|
|
|
|
if err := api.RestoreShard(ctx, string(tkey), uint64(shard.Num), rc); err != nil {
|
|
return errors.Wrap(err, "restoring shard data")
|
|
}
|
|
}
|
|
|
|
// WriteLog reader.
|
|
if err := func() error {
|
|
reader := api.writeLogReader.ShardReader(ctx, qtid, partitionNum, shard.Num, shard.Version)
|
|
if err := reader.Open(); err != nil {
|
|
// TODO: this log can be confusing because on a create
|
|
// table, there is no log file yet, so an error is expected.
|
|
// Instead of swallowing this error, we need to check the
|
|
// error code and handle it differently. This means the
|
|
// writelogger will need to return an error indicating that
|
|
// the log file does not exist, but that that is expected.
|
|
// log.Printf("could not open log file for table: %s, partition: %d: version: %d, shard: %d, err: %s", table, partition, shard.Version, shard.Num, err)
|
|
return nil
|
|
}
|
|
defer reader.Close()
|
|
|
|
for logMsg, err := reader.Read(); err != io.EOF; logMsg, err = reader.Read() {
|
|
if err != nil {
|
|
return errors.Wrap(err, "reading from log reader")
|
|
}
|
|
switch msg := logMsg.(type) {
|
|
case *computer.ImportRoaringMessage:
|
|
req := &ImportRoaringRequest{
|
|
Clear: msg.Clear,
|
|
Action: msg.Action,
|
|
Block: msg.Block,
|
|
Views: msg.Views,
|
|
UpdateExistence: msg.UpdateExistence,
|
|
SuppressLog: true,
|
|
}
|
|
if err := api.ImportRoaring(ctx, msg.Table, msg.Field, msg.Shard, true, req); err != nil {
|
|
return errors.Wrapf(err, "import roaring, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
|
|
case *computer.ImportMessage:
|
|
req := &ImportRequest{
|
|
Index: msg.Table,
|
|
Field: msg.Field,
|
|
Shard: msg.Shard,
|
|
RowIDs: msg.RowIDs,
|
|
ColumnIDs: msg.ColumnIDs,
|
|
RowKeys: msg.RowKeys,
|
|
ColumnKeys: msg.ColumnKeys,
|
|
Timestamps: msg.Timestamps,
|
|
Clear: msg.Clear,
|
|
}
|
|
|
|
qcx := api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
opts := []ImportOption{
|
|
OptImportOptionsClear(msg.Clear),
|
|
OptImportOptionsIgnoreKeyCheck(msg.IgnoreKeyCheck),
|
|
OptImportOptionsPresorted(msg.Presorted),
|
|
OptImportOptionsSuppressLog(true),
|
|
}
|
|
if err := api.Import(ctx, qcx, req, opts...); err != nil {
|
|
return errors.Wrapf(err, "import, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
|
|
case *computer.ImportValueMessage:
|
|
req := &ImportValueRequest{
|
|
Index: msg.Table,
|
|
Field: msg.Field,
|
|
Shard: msg.Shard,
|
|
ColumnIDs: msg.ColumnIDs,
|
|
ColumnKeys: msg.ColumnKeys,
|
|
Values: msg.Values,
|
|
FloatValues: msg.FloatValues,
|
|
TimestampValues: msg.TimestampValues,
|
|
StringValues: msg.StringValues,
|
|
Clear: msg.Clear,
|
|
}
|
|
|
|
qcx := api.Txf().NewQcx()
|
|
defer qcx.Abort()
|
|
|
|
opts := []ImportOption{
|
|
OptImportOptionsClear(msg.Clear),
|
|
OptImportOptionsIgnoreKeyCheck(msg.IgnoreKeyCheck),
|
|
OptImportOptionsPresorted(msg.Presorted),
|
|
OptImportOptionsSuppressLog(true),
|
|
}
|
|
if err := api.ImportValue(ctx, qcx, req, opts...); err != nil {
|
|
return errors.Wrapf(err, "import value, table: %s, field: %s, shard: %d", msg.Table, msg.Field, msg.Shard)
|
|
}
|
|
case *computer.ImportRoaringShardMessage:
|
|
req := &ImportRoaringShardRequest{
|
|
Remote: true,
|
|
Views: make([]RoaringUpdate, len(msg.Views)),
|
|
SuppressLog: true,
|
|
}
|
|
for i, view := range msg.Views {
|
|
req.Views[i] = RoaringUpdate{
|
|
Field: view.Field,
|
|
View: view.View,
|
|
Clear: view.Clear,
|
|
Set: view.Set,
|
|
ClearRecords: view.ClearRecords,
|
|
}
|
|
}
|
|
if err := api.ImportRoaringShard(ctx, msg.Table, msg.Shard, req); err != nil {
|
|
return errors.Wrapf(err, "import roaring shard table: %s, shard: %d", msg.Table, msg.Shard)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the table/shard/version in the holder.
|
|
if err := api.holder.versionStore.AddShards(ctx, qtid, shard); err != nil {
|
|
return errors.Wrap(err, "adding shard to sharder")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
// sliceComparer is used to compare the differences between two slices of comparables.
|
|
type sliceComparer[K comparable] struct {
|
|
from []K
|
|
to []K
|
|
}
|
|
|
|
func newSliceComparer[K comparable](from []K, to []K) *sliceComparer[K] {
|
|
return &sliceComparer[K]{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the items which are present in `to` but not in `from`.
|
|
func (s *sliceComparer[K]) added() []K {
|
|
return thingsAdded(s.from, s.to)
|
|
}
|
|
|
|
// removed returns the items which are present in `from` but not in `to`.
|
|
func (s *sliceComparer[K]) removed() []K {
|
|
return thingsAdded(s.to, s.from)
|
|
}
|
|
|
|
// same returns the items which are in both `to` and `from`.
|
|
func (s *sliceComparer[K]) same() []K {
|
|
var same []K
|
|
for _, fromThing := range s.from {
|
|
for _, toThing := range s.to {
|
|
if fromThing == toThing {
|
|
same = append(same, fromThing)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return same
|
|
}
|
|
|
|
// thingsAdded returns the comparable things which are present in `to` but not
|
|
// in `from`.
|
|
func thingsAdded[K comparable](from []K, to []K) []K {
|
|
var added []K
|
|
for i := range to {
|
|
var found bool
|
|
for j := range from {
|
|
if from[j] == to[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
added = append(added, to[i])
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// partitionsComparer is used to compare the differences between two maps of
|
|
// table:[]partition.
|
|
type partitionsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedPartitions
|
|
to map[dax.TableKey]dax.VersionedPartitions
|
|
}
|
|
|
|
func newPartitionsComparer(from map[dax.TableKey]dax.VersionedPartitions, to map[dax.TableKey]dax.VersionedPartitions) *partitionsComparer {
|
|
return &partitionsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the partitions which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]partition.
|
|
func (p *partitionsComparer) added() map[dax.TableKey]dax.VersionedPartitions {
|
|
return partitionsAdded(p.from, p.to)
|
|
}
|
|
|
|
// removed returns the partitions which are present in `from` but not in `to`.
|
|
// The results remain in the format of a map of table:[]partition.
|
|
func (p *partitionsComparer) removed() map[dax.TableKey]dax.VersionedPartitions {
|
|
return partitionsAdded(p.to, p.from)
|
|
}
|
|
|
|
// partitionsAdded returns the partitions which are present in `to` but not in `from`.
|
|
func partitionsAdded(from map[dax.TableKey]dax.VersionedPartitions, to map[dax.TableKey]dax.VersionedPartitions) map[dax.TableKey]dax.VersionedPartitions {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedPartitions)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedPartitions := dax.VersionedPartitions{}
|
|
for i := range tps {
|
|
var found bool
|
|
for j := range fps {
|
|
if fps[j] == tps[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedPartitions = append(addedPartitions, tps[i])
|
|
}
|
|
}
|
|
|
|
if len(addedPartitions) > 0 {
|
|
added[tt] = addedPartitions
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// fieldsComparer is used to compare the differences between two maps of
|
|
// table:[]fieldVersion.
|
|
type fieldsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedFields
|
|
to map[dax.TableKey]dax.VersionedFields
|
|
}
|
|
|
|
func newFieldsComparer(from map[dax.TableKey]dax.VersionedFields, to map[dax.TableKey]dax.VersionedFields) *fieldsComparer {
|
|
return &fieldsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the fields which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]field.
|
|
func (f *fieldsComparer) added() map[dax.TableKey]dax.VersionedFields {
|
|
return fieldsAdded(f.from, f.to)
|
|
}
|
|
|
|
// removed returns the fields which are present in `from` but not in `to`.
|
|
// The results remain in the format of a map of table:[]field.
|
|
func (f *fieldsComparer) removed() map[dax.TableKey]dax.VersionedFields {
|
|
return fieldsAdded(f.to, f.from)
|
|
}
|
|
|
|
// fieldsAdded returns the fields which are present in `to` but not in `from`.
|
|
func fieldsAdded(from map[dax.TableKey]dax.VersionedFields, to map[dax.TableKey]dax.VersionedFields) map[dax.TableKey]dax.VersionedFields {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedFields)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedFieldVersions := dax.VersionedFields{}
|
|
for i := range tps {
|
|
var found bool
|
|
for j := range fps {
|
|
if fps[j] == tps[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedFieldVersions = append(addedFieldVersions, tps[i])
|
|
}
|
|
}
|
|
|
|
if len(addedFieldVersions) > 0 {
|
|
added[tt] = addedFieldVersions
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// shardsComparer is used to compare the differences between two maps of
|
|
// table:[]shardV.
|
|
type shardsComparer struct {
|
|
from map[dax.TableKey]dax.VersionedShards
|
|
to map[dax.TableKey]dax.VersionedShards
|
|
}
|
|
|
|
func newShardsComparer(from map[dax.TableKey]dax.VersionedShards, to map[dax.TableKey]dax.VersionedShards) *shardsComparer {
|
|
return &shardsComparer{
|
|
from: from,
|
|
to: to,
|
|
}
|
|
}
|
|
|
|
// added returns the shards which are present in `to` but not in `from`. The
|
|
// results remain in the format of a map of table:[]shard.
|
|
func (s *shardsComparer) added() map[dax.TableKey]dax.VersionedShards {
|
|
return shardsAdded(s.from, s.to)
|
|
}
|
|
|
|
// removed returns the shards which are present in `from` but not in `to`. The
|
|
// results remain in the format of a map of table:[]shard.
|
|
func (s *shardsComparer) removed() map[dax.TableKey]dax.VersionedShards {
|
|
return shardsAdded(s.to, s.from)
|
|
}
|
|
|
|
// shardsAdded returns the shards which are present in `to` but not in `from`.
|
|
func shardsAdded(from map[dax.TableKey]dax.VersionedShards, to map[dax.TableKey]dax.VersionedShards) map[dax.TableKey]dax.VersionedShards {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.VersionedShards)
|
|
for tt, tss := range to {
|
|
fss, found := from[tt]
|
|
if !found {
|
|
added[tt] = tss
|
|
continue
|
|
}
|
|
|
|
addedShards := dax.VersionedShards{}
|
|
for i := range tss {
|
|
var found bool
|
|
for j := range fss {
|
|
if fss[j] == tss[i] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
addedShards = append(addedShards, tss[i])
|
|
}
|
|
}
|
|
|
|
if len(addedShards) > 0 {
|
|
added[tt] = addedShards
|
|
}
|
|
}
|
|
return added
|
|
}
|
|
|
|
// createTableAndFields creates the FeatureBase Tables and Fields provided in
|
|
// the dax.Directive format.
|
|
func (api *API) createTableAndFields(tbl *dax.QualifiedTable, partitions dax.VersionedPartitions) error {
|
|
cim := &CreateIndexMessage{
|
|
Index: string(tbl.Key()),
|
|
CreatedAt: 0,
|
|
Meta: IndexOptions{
|
|
Keys: tbl.StringKeys(),
|
|
TrackExistence: true,
|
|
},
|
|
}
|
|
|
|
// Create the index in etcd as the system of record.
|
|
if err := api.holder.persistIndex(context.Background(), cim); err != nil {
|
|
return errors.Wrap(err, "persisting index")
|
|
}
|
|
|
|
idx, err := api.holder.createIndexWithPartitions(cim, partitions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "adding index: %s", tbl.Name)
|
|
}
|
|
|
|
// Add the fields
|
|
for _, fld := range tbl.Fields {
|
|
if fld.IsPrimaryKey() {
|
|
continue
|
|
}
|
|
if err := createField(idx, fld); err != nil {
|
|
return errors.Wrapf(err, "creating field: %s", fld.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createField creates a FeatureBase Field in the provided FeatureBase Index
|
|
// based on the provided field's type.
|
|
//
|
|
// TODO: `time` fields
|
|
func createField(idx *Index, fld *dax.Field) error {
|
|
// Set the cache type and size (or use default) for those fields which
|
|
// require them.
|
|
cacheType := DefaultCacheType
|
|
cacheSize := uint32(DefaultCacheSize)
|
|
if fld.Options.CacheType != "" {
|
|
cacheType = fld.Options.CacheType
|
|
cacheSize = fld.Options.CacheSize
|
|
}
|
|
|
|
opts := []FieldOption{}
|
|
|
|
switch fld.Type {
|
|
case dax.BaseTypeBool:
|
|
opts = append(opts,
|
|
OptFieldTypeBool(),
|
|
)
|
|
case dax.BaseTypeDecimal:
|
|
opts = append(opts,
|
|
OptFieldTypeDecimal(fld.Options.Scale),
|
|
)
|
|
case dax.BaseTypeID:
|
|
opts = append(opts,
|
|
OptFieldTypeMutex(cacheType, cacheSize),
|
|
)
|
|
case dax.BaseTypeIDSet:
|
|
opts = append(opts,
|
|
OptFieldTypeSet(cacheType, cacheSize),
|
|
)
|
|
case dax.BaseTypeInt:
|
|
opts = append(opts,
|
|
OptFieldTypeInt(fld.Options.Min.ToInt64(0), fld.Options.Max.ToInt64(0)),
|
|
)
|
|
case dax.BaseTypeString:
|
|
opts = append(opts,
|
|
OptFieldTypeMutex(cacheType, cacheSize),
|
|
OptFieldKeys(),
|
|
)
|
|
case dax.BaseTypeStringSet:
|
|
opts = append(opts,
|
|
OptFieldTypeSet(cacheType, cacheSize),
|
|
OptFieldKeys(),
|
|
)
|
|
case dax.BaseTypeTimestamp:
|
|
opts = append(opts,
|
|
OptFieldTypeTimestamp(fld.Options.Epoch, fld.Options.TimeUnit),
|
|
)
|
|
default:
|
|
return errors.Errorf("unsupport field type: %s", fld.Type)
|
|
}
|
|
|
|
if _, err := idx.CreateField(string(fld.Name), "", opts...); err != nil {
|
|
return errors.Wrapf(err, "creating field on index: %s", fld.Name)
|
|
}
|
|
return nil
|
|
}
|