mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
There's a lot going on here. First, we were treating "the test is a Condition" as implying BSI, which it doesn't anymore. Second, the behavior of conditions was weird and BSI-specific. Third, we had to propagate these changes and features throughout a bunch of code, including both the core featurebase code and the DAX replacements/copies of it, plus the SQL3 layer. We refactor this so that tests for equality and inequality work for non-BSI fields, so now if you accidentally use `==` in a Row call on a non-BSI field, it still works; that's not specific to BSI fields anymore. We add a TrackExistence flag to fields, and propagate it through things like our protobuf code, etcetera, so that we can successfully create fields. Newly-created fields get this by default, because we add it unconditionally to them, but the paths that are being called with existing fields don't add it. So, when we "create" (really, just load the definition of) a field from something stored in the schema, we don't add TrackExistence to it, but any path to creating a new field should. A time quantum field with NoStandardView will *effectively* lack TrackExistence. For sets, mutexes, and time quantums with a standard view, anything that sets bits will also set a corresponding bit for the record in a new "existence" view. This allows us to distinguish between an empty set and a null, and also allows null checks to be constant-time. When clearing bits, we don't clear existence bits EXCEPT that if you clear a bit in a mutex, *and the bit actually existed*, we clear the existence bit. For sets and time quantums, clearing bits never clears the existence bit. Deleting records clears the existence bit. We also add code to the `batch` subpackage to generate suitable existence field bitmaps and import them. This logic correctly handles empty sets and nils. The `batch` package does not allow specification of anything equivalent to clearing a single bit from an existing record, so we don't have to deal with the mutex complexity in that case, which is good because it would be impossible. This requires a number of other subtle changes, such as allowing new fields to have more than one FieldOption specified for them. We also drop the handful of implementation bits relating to the "fullySorted" internal-use-only import flag, which existed only to support the JSON ingest API, which we've removed. The most dangerous part of this is that the mutex semantics are impossible to implement on top of our existing API, because they require us to know, not how *many* bits we cleared, but which *specific* bits we cleared. I've implemented this as a new Tx method, which is almost certainly going to be tech debt one day; if we some day drop the Import API, we should remove that. The testing for this is only currently covering the Set/Clear behavior of PQL, and the Import API. The batch tests haven't been written yet. Fields that don't have existence tracking enabled refuse to perform null/not-null tests. They should also report themselves as having no null values -- if a record exists, sets in it are considered empty rather than null. The SQL3 support requires a number of subtle modifications to both featurebase and some addon tooling. The essential thing is dropping the unconditional translation of nil slices to non-nil empty slices in translateResult, both in the executor and the orchestrator. We also modify the logic that handles generating results from Extract calls, to ensure that non-null sets get an empty slice created for them even if they never have any values assigned. The expected results for some tests are different now; we expect to get nil slices, rather than 0-length non-nil slices, for fields which were never written for a given record. Most tests were not changed. (In every case, if a test was failing, I actually checked the logic before changing expected results. This required a lot of tracking down of edge cases.) The batch package now rejects as an error attempts to clear single bits from mutex fields, because so far as I can tell it's simply impossible to have a roaring import that specifies the correct semantics there; you can't tell whether to clear an existence bit without access to the currently-set bits, which the batch API doesn't have. We already supported the special case of specifying a clear value of nil for clearing a mutex field; now that is the only allowed value for a mutex field to have in row.Clears. We change the logic for fixing up incoming view names (in two places) to stop assuming that any view in a time field other than "" that does not have viewStandard as a prefix is a partial time quantum name that should have "standard_" prepended to it. This allows us to submit bitmaps for "existence" to time quantum fields and not have them silently transformed into "standard_existence" because that's what we'd do with "202203". We drop the field ClearBits method, which was totally unused. We drop the sliceDifference function, which was used in a previous mutex implementation and hasn't been used in ages, and the test case for it, and the helper function used only by that test case.
971 lines
29 KiB
Go
971 lines
29 KiB
Go
// Copyright 2021 Molecula Corp. All rights reserved.
|
|
package pilosa
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"sync"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/dax/computer"
|
|
"github.com/featurebasedb/featurebase/v3/dax/storage"
|
|
"github.com/featurebasedb/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.PartitionNum
|
|
}
|
|
|
|
type directiveJobFieldKeys struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
field dax.FieldName
|
|
}
|
|
|
|
type directiveJobShards struct {
|
|
directiveJobType
|
|
tkey dax.TableKey
|
|
shard dax.ShardNum
|
|
}
|
|
|
|
// 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.DeleteIndex(ctx, 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)
|
|
|
|
// Remove any partitions which are no longer assigned to this worker.
|
|
// TODO(tlt): currently, this is just removing the file lock on the
|
|
// resource; it's not actually removing the resource from the local
|
|
// computer. We should do that.
|
|
for tkey, partitions := range partComp.removed() {
|
|
qtid := tkey.QualifiedTableID()
|
|
for _, partition := range partitions {
|
|
api.serverlessStorage.RemoveTableKeyResource(qtid, partition)
|
|
}
|
|
}
|
|
|
|
// 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.PartitionNum) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
resource := api.serverlessStorage.GetTableKeyResource(qtid, partition)
|
|
if resource.IsLocked() {
|
|
api.logger().Warnf("skipping loadTableKeys (already held) %s %d", tkey, partition)
|
|
return nil
|
|
}
|
|
|
|
// load latest snapshot
|
|
if rc, err := resource.LoadLatestSnapshot(); err != nil {
|
|
return errors.Wrap(err, "loading table key snapshot")
|
|
} else if rc != nil {
|
|
defer rc.Close()
|
|
if err := api.TranslateIndexDB(ctx, string(tkey), int(partition), rc); err != nil {
|
|
return errors.Wrap(err, "restoring table keys")
|
|
}
|
|
}
|
|
|
|
// define write log loading in a function since we have to do it
|
|
// before and after locking
|
|
loadWriteLog := func() error {
|
|
writelog, err := resource.LoadWriteLog()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting write log reader for table keys")
|
|
}
|
|
if writelog == nil {
|
|
return nil
|
|
}
|
|
reader := storage.NewTableKeyReader(qtid, partition, writelog)
|
|
defer reader.Close()
|
|
store := idx.TranslateStore(int(partition))
|
|
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
|
|
}
|
|
// 1st write log load
|
|
if err := loadWriteLog(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// acquire lock on this partition's keys
|
|
if err := resource.Lock(); err != nil {
|
|
return errors.Wrap(err, "locking table key partition")
|
|
}
|
|
|
|
// reload writelog in case of changes between last load and
|
|
// lock. The resource object takes care of only loading new data.
|
|
return loadWriteLog()
|
|
}
|
|
|
|
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())
|
|
|
|
// Remove any field keys which are no longer assigned to this worker.
|
|
// TODO(tlt): currently, this is just removing the file lock on the
|
|
// resource; it's not actually removing the resource from the local
|
|
// computer. We should do that.
|
|
for tkey, fields := range fieldComp.removed() {
|
|
qtid := tkey.QualifiedTableID()
|
|
for _, field := range fields {
|
|
api.serverlessStorage.RemoveFieldKeyResource(qtid, field)
|
|
}
|
|
}
|
|
|
|
// 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.FieldName) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
resource := api.serverlessStorage.GetFieldKeyResource(qtid, field)
|
|
if resource.IsLocked() {
|
|
api.logger().Warnf("skipping loadFieldKeys (already held) %s %s", tkey, field)
|
|
return nil
|
|
}
|
|
|
|
// load latest snapshot
|
|
if rc, err := resource.LoadLatestSnapshot(); err != nil {
|
|
return errors.Wrap(err, "loading field key snapshot")
|
|
} else if rc != nil {
|
|
defer rc.Close()
|
|
if err := api.TranslateFieldDB(ctx, string(tkey), string(field), rc); err != nil {
|
|
return errors.Wrap(err, "restoring field keys")
|
|
}
|
|
}
|
|
|
|
// define write log loading in a function since we have to do it
|
|
// before and after locking
|
|
loadWriteLog := func() error {
|
|
writelog, err := resource.LoadWriteLog()
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting write log reader for field keys")
|
|
}
|
|
if writelog == nil {
|
|
return nil
|
|
}
|
|
reader := storage.NewFieldKeyReader(qtid, field, writelog)
|
|
defer reader.Close()
|
|
// Get field in order to find the translate store.
|
|
fld := api.holder.Field(string(tkey), string(field))
|
|
if fld == nil {
|
|
log.Printf("field not found in holder: %s", field)
|
|
return nil
|
|
}
|
|
store := fld.TranslateStore()
|
|
|
|
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
|
|
}
|
|
// 1st write log load
|
|
if err := loadWriteLog(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// acquire lock on this partition's keys
|
|
if err := resource.Lock(); err != nil {
|
|
return errors.Wrap(err, "locking field key partition")
|
|
}
|
|
|
|
// reload writelog in case of changes between last load and
|
|
// lock. The resource object takes care of only loading new data.
|
|
return loadWriteLog()
|
|
}
|
|
|
|
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)
|
|
|
|
// Remove any shards which are no longer assigned to this worker.
|
|
// TODO(tlt): currently, this is just removing the file lock on the
|
|
// resource; it's not actually removing the resource from the local
|
|
// computer. We should do that.
|
|
for tkey, shards := range shardComp.removed() {
|
|
qtid := tkey.QualifiedTableID()
|
|
for _, shard := range shards {
|
|
partition := dax.PartitionNum(disco.ShardToShardPartition(string(tkey), uint64(shard), disco.DefaultPartitionN))
|
|
api.serverlessStorage.RemoveShardResource(qtid, partition, shard)
|
|
}
|
|
}
|
|
|
|
// 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.ShardNum) error {
|
|
qtid := tkey.QualifiedTableID()
|
|
|
|
partition := dax.PartitionNum(disco.ShardToShardPartition(string(tkey), uint64(shard), disco.DefaultPartitionN))
|
|
|
|
resource := api.serverlessStorage.GetShardResource(qtid, partition, shard)
|
|
if resource.IsLocked() {
|
|
api.logger().Warnf("skipping loadShard (already held) %s %d", tkey, shard)
|
|
return nil
|
|
}
|
|
|
|
if rc, err := resource.LoadLatestSnapshot(); err != nil {
|
|
return errors.Wrap(err, "reading latest snapshot for shard")
|
|
} else if rc != nil {
|
|
defer rc.Close()
|
|
if err := api.RestoreShard(ctx, string(tkey), uint64(shard), rc); err != nil {
|
|
return errors.Wrap(err, "restoring shard data")
|
|
}
|
|
}
|
|
|
|
// define write log loading in a func because we do it twice.
|
|
loadWriteLog := func() error {
|
|
writelog, err := resource.LoadWriteLog()
|
|
if err != nil {
|
|
return errors.Wrap(err, "")
|
|
}
|
|
if writelog == nil {
|
|
return nil
|
|
}
|
|
|
|
reader := storage.NewShardReader(qtid, partition, shard, writelog)
|
|
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
|
|
}
|
|
// 1st write log load
|
|
if err := loadWriteLog(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// acquire lock on this partition's keys
|
|
if err := resource.Lock(); err != nil {
|
|
return errors.Wrap(err, "locking field key partition")
|
|
}
|
|
|
|
// reload writelog in case of changes between last load and
|
|
// lock. The resource object takes care of only loading new data.
|
|
return loadWriteLog()
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
// 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.PartitionNums
|
|
to map[dax.TableKey]dax.PartitionNums
|
|
}
|
|
|
|
func newPartitionsComparer(from map[dax.TableKey]dax.PartitionNums, to map[dax.TableKey]dax.PartitionNums) *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.PartitionNums {
|
|
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.PartitionNums {
|
|
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.PartitionNums, to map[dax.TableKey]dax.PartitionNums) map[dax.TableKey]dax.PartitionNums {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.PartitionNums)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedPartitions := dax.PartitionNums{}
|
|
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.FieldName
|
|
to map[dax.TableKey][]dax.FieldName
|
|
}
|
|
|
|
func newFieldsComparer(from map[dax.TableKey][]dax.FieldName, to map[dax.TableKey][]dax.FieldName) *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.FieldName {
|
|
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.FieldName {
|
|
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.FieldName, to map[dax.TableKey][]dax.FieldName) map[dax.TableKey][]dax.FieldName {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey][]dax.FieldName)
|
|
for tt, tps := range to {
|
|
fps, found := from[tt]
|
|
if !found {
|
|
added[tt] = tps
|
|
continue
|
|
}
|
|
|
|
addedFieldVersions := []dax.FieldName{}
|
|
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.ShardNums
|
|
to map[dax.TableKey]dax.ShardNums
|
|
}
|
|
|
|
func newShardsComparer(from map[dax.TableKey]dax.ShardNums, to map[dax.TableKey]dax.ShardNums) *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.ShardNums {
|
|
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.ShardNums {
|
|
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.ShardNums, to map[dax.TableKey]dax.ShardNums) map[dax.TableKey]dax.ShardNums {
|
|
if from == nil {
|
|
return to
|
|
}
|
|
|
|
added := make(map[dax.TableKey]dax.ShardNums)
|
|
for tt, tss := range to {
|
|
fss, found := from[tt]
|
|
if !found {
|
|
added[tt] = tss
|
|
continue
|
|
}
|
|
|
|
addedShards := dax.ShardNums{}
|
|
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.PartitionNums) 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.
|
|
func createField(idx *Index, fld *dax.Field) error {
|
|
opts, err := FieldOptionsFromField(fld)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "creating field options from field: %s", fld.Name)
|
|
}
|
|
|
|
if _, err := idx.createNullableField(string(fld.Name), "", opts...); err != nil {
|
|
return errors.Wrapf(err, "creating field on index: %s", fld.Name)
|
|
}
|
|
return nil
|
|
}
|