mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
This migrates existing code from the old TranslateKey(s) endpoints to the newer CreateKeys and FindKeys endpoints. The CreateKeys and FindKeys endpoints were created previously as the TranslateKeys endpoint had no way to behave sanely when the looked-up key did not exist (the parallel-arrays representation did not have a good way to represent a missing key). This change also removes the old TranslateKey(s) functions from the translation stores. It leaves a wrapper emulating the TranslateKey(s) endpoints so that old idk still works for now.
2031 lines
57 KiB
Go
2031 lines
57 KiB
Go
// Copyright 2017 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pilosa
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pilosa/pilosa/v2/disco"
|
|
"github.com/pilosa/pilosa/v2/logger"
|
|
"github.com/pilosa/pilosa/v2/roaring"
|
|
"github.com/pilosa/pilosa/v2/topology"
|
|
"github.com/pilosa/pilosa/v2/tracing"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
const (
|
|
resizeJobActionAdd = "ADD"
|
|
resizeJobActionRemove = "REMOVE"
|
|
|
|
defaultConfirmDownRetries = 10
|
|
defaultConfirmDownSleep = 1 * time.Second
|
|
)
|
|
|
|
type ResizeNodeMessage struct {
|
|
NodeID string
|
|
Action string
|
|
}
|
|
|
|
type ResizeNodeProgress struct {
|
|
FromID string
|
|
ToID string
|
|
Done bool
|
|
Error string
|
|
}
|
|
|
|
func (p ResizeNodeProgress) applyJSON(fn func([]byte) error) error {
|
|
data, err := json.Marshal(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return fn(data)
|
|
}
|
|
|
|
type ResizeAbortMessage struct{}
|
|
|
|
// cluster represents a collection of nodes.
|
|
type cluster struct { // nolint: maligned
|
|
noder topology.Noder
|
|
|
|
id string
|
|
Node *topology.Node
|
|
|
|
// Hashing algorithm used to assign partitions to nodes.
|
|
Hasher topology.Hasher
|
|
|
|
// The number of partitions in the cluster.
|
|
partitionN int
|
|
|
|
// The number of replicas a partition has.
|
|
ReplicaN int
|
|
|
|
// Human-readable name of the cluster.
|
|
Name string
|
|
|
|
// Maximum number of Set() or Clear() commands per request.
|
|
maxWritesPerRequest int
|
|
|
|
// Data directory path.
|
|
Path string
|
|
|
|
// Distributed Consensus
|
|
disCo disco.DisCo
|
|
stator disco.Stator
|
|
resizer disco.Resizer
|
|
sharder disco.Sharder
|
|
|
|
// Required for cluster Resize.
|
|
Static bool // Static is primarily used for testing.
|
|
holder *Holder
|
|
broadcaster broadcaster
|
|
|
|
abortAntiEntropyCh chan struct{}
|
|
muAntiEntropy sync.Mutex
|
|
|
|
translationSyncer TranslationSyncer
|
|
|
|
mu sync.RWMutex
|
|
jobs map[int64]*resizeJob
|
|
resizeCancel context.CancelFunc
|
|
|
|
// Close management
|
|
wg sync.WaitGroup
|
|
closing chan struct{}
|
|
|
|
logger logger.Logger
|
|
|
|
InternalClient InternalClient
|
|
|
|
confirmDownRetries int
|
|
confirmDownSleep time.Duration
|
|
}
|
|
|
|
// newCluster returns a new instance of Cluster with defaults.
|
|
func newCluster() *cluster {
|
|
return &cluster{
|
|
Hasher: &topology.Jmphasher{},
|
|
partitionN: topology.DefaultPartitionN,
|
|
ReplicaN: 1,
|
|
|
|
jobs: make(map[int64]*resizeJob),
|
|
closing: make(chan struct{}),
|
|
|
|
translationSyncer: NopTranslationSyncer,
|
|
|
|
InternalClient: newNopInternalClient(),
|
|
|
|
logger: logger.NopLogger,
|
|
|
|
confirmDownRetries: defaultConfirmDownRetries,
|
|
confirmDownSleep: defaultConfirmDownSleep,
|
|
|
|
disCo: disco.NopDisCo,
|
|
noder: topology.NewEmptyLocalNoder(),
|
|
stator: disco.NopStator,
|
|
resizer: disco.NopResizer,
|
|
}
|
|
}
|
|
|
|
// initializeAntiEntropy is called by the anti entropy routine when it starts.
|
|
// If the AE channel is created without a routine reading from it, cluster will
|
|
// block indefinitely when calling abortAntiEntropy().
|
|
func (c *cluster) initializeAntiEntropy() {
|
|
c.mu.Lock()
|
|
c.abortAntiEntropyCh = make(chan struct{})
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// abortAntiEntropyQ checks whether the cluster wants to abort the anti entropy
|
|
// process (so that it can resize). It does not block.
|
|
func (c *cluster) abortAntiEntropyQ() bool {
|
|
select {
|
|
case <-c.abortAntiEntropyCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// abortAntiEntropy blocks until the anti-entropy routine calls abortAntiEntropyQ
|
|
func (c *cluster) abortAntiEntropy() {
|
|
if c.abortAntiEntropyCh != nil {
|
|
c.abortAntiEntropyCh <- struct{}{}
|
|
}
|
|
}
|
|
|
|
func (c *cluster) primaryNode() *topology.Node {
|
|
return c.unprotectedPrimaryNode()
|
|
}
|
|
|
|
// unprotectedPrimaryNode returns the primary node.
|
|
func (c *cluster) unprotectedPrimaryNode() *topology.Node {
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
return snap.PrimaryFieldTranslationNode()
|
|
}
|
|
|
|
func (c *cluster) applySchemaWithNewShards(schema *Schema) error {
|
|
if schema == nil || len(schema.Indexes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := c.holder.applySchema(schema); err != nil {
|
|
return errors.Wrap(err, "applying schema")
|
|
}
|
|
|
|
// Get and set the shards for each field.
|
|
for _, idx := range c.holder.indexes {
|
|
for _, fld := range idx.fields {
|
|
err := fld.loadAvailableShards()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "getting shards for field: %s/%s", idx.name, fld.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addNode adds a node to the Cluster and starts resizing process
|
|
func (c *cluster) addNode(id string) error {
|
|
// If this method is being called on the node which was just added, then the
|
|
// node will be completely empty. That means that it won't have the current
|
|
// schema with which to calculate its resize intructions (in
|
|
// c.resizeNodeOnAdd, which calls c.generateResizeInstructionOnAdd). Because
|
|
// of this, we need to request and apply the current schema from etcd before
|
|
// we can proceed with the resize process.
|
|
if id == c.disCo.ID() {
|
|
schema, err := c.remoteSchema()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.applySchemaWithNewShards(schema); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
eg := &errgroup.Group{}
|
|
for _, n := range c.noder.Nodes() {
|
|
if err := c.sendTo(n, &ResizeNodeMessage{NodeID: id, Action: resizeJobActionAdd}); err != nil {
|
|
return errors.Wrap(err, "broadcasting resize message")
|
|
}
|
|
|
|
nodeID := n.ID
|
|
eg.Go(func() error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
err := c.resizer.Watch(ctx, nodeID, func(data []byte) error {
|
|
var progress ResizeNodeProgress
|
|
if err := json.Unmarshal(data, &progress); err != nil {
|
|
return errors.Wrapf(err, "watching progress node %s", nodeID)
|
|
}
|
|
if progress.Error != "" {
|
|
return errors.Errorf("watching progress node %s: %s", nodeID, progress.Error)
|
|
}
|
|
if progress.Done {
|
|
return io.EOF
|
|
}
|
|
return nil
|
|
})
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Wait for all background resize threads to return. If there were any
|
|
// errors, then we need to delete the node (which we were attempting to add)
|
|
// from the etcd cluster.
|
|
go func() {
|
|
if err := eg.Wait(); err != nil {
|
|
c.logger.Printf("Stop watching all peers: %+v", err)
|
|
|
|
if err := c.disCo.DeleteNode(context.Background(), id); err != nil {
|
|
// resizing failed, so we have to delete the new node.
|
|
c.logger.Printf("Cannot delete the node %s: %+v", id, err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) resizeNodeOnAdd(addNodeID string) error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// set status to RESIZING
|
|
progressFunc, err := c.resizer.Resize(context.Background())
|
|
if err != nil {
|
|
cancel()
|
|
return errors.Wrapf(err, "setting RESIZING state on %s", c.disCo.ID())
|
|
}
|
|
|
|
c.resizeCancel = cancel
|
|
// start async. data balancing
|
|
go func() {
|
|
progress := ResizeNodeProgress{ToID: addNodeID, FromID: c.disCo.ID()}
|
|
defer func() {
|
|
err := progress.applyJSON(progressFunc)
|
|
if err != nil {
|
|
c.logger.Printf("updating resize progress (%s -> %s): %+v", c.disCo.ID(), addNodeID, err)
|
|
}
|
|
if c.resizeCancel != nil {
|
|
c.resizeCancel()
|
|
}
|
|
err = c.resizer.DoneResize()
|
|
if err != nil {
|
|
c.logger.Printf("done resize (%s -> %s): %+v", c.disCo.ID(), addNodeID, err)
|
|
}
|
|
}()
|
|
|
|
instr, err := c.generateResizeInstructionOnAdd(addNodeID)
|
|
if err != nil {
|
|
progress.Error = errors.Wrapf(err, "generating resize instruction (%s -> %s)", c.disCo.ID(), addNodeID).Error()
|
|
c.logger.Printf(progress.Error)
|
|
return
|
|
}
|
|
|
|
if err = c.followResizeInstruction(ctx, instr); err != nil {
|
|
progress.Error = errors.Wrapf(err, "following resize instruction (%s -> %s)", c.disCo.ID(), addNodeID).Error()
|
|
c.logger.Printf(progress.Error)
|
|
return
|
|
}
|
|
progress.Done = true
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) generateResizeInstructionOnAdd(addNodeID string) (*ResizeInstruction, error) {
|
|
fromCluster := newCluster()
|
|
for _, n := range topology.Nodes(c.noder.Nodes()).Clone() {
|
|
if n.ID == addNodeID {
|
|
continue
|
|
}
|
|
fromCluster.noder.AppendNode(n)
|
|
}
|
|
fromCluster.Hasher = c.Hasher
|
|
fromCluster.partitionN = c.partitionN
|
|
fromCluster.ReplicaN = c.ReplicaN
|
|
|
|
// fragmentSourcesByNode is a map of Node.ID to sources of fragment data.
|
|
// It is initialized with all the nodes in toCluster.
|
|
fragmentSourcesByNode := make(map[string][]*ResizeSource)
|
|
for _, n := range c.noder.Nodes() {
|
|
fragmentSourcesByNode[n.ID] = nil
|
|
}
|
|
|
|
indexes := c.holder.Indexes()
|
|
// Add to fragmentSourcesByNode the instructions for each index.
|
|
for _, idx := range indexes {
|
|
fragSources, err := fromCluster.fragSources(c, idx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting sources")
|
|
}
|
|
|
|
for nodeid, sources := range fragSources {
|
|
fragmentSourcesByNode[nodeid] = append(fragmentSourcesByNode[nodeid], sources...)
|
|
}
|
|
}
|
|
|
|
// translationSourcesByNode is a map of Node.ID to sources of partitioned
|
|
// key translation data for indexes.
|
|
// It is initialized with all the nodes in toCluster.
|
|
translationSourcesByNode := make(map[string][]*TranslationResizeSource)
|
|
for _, n := range c.noder.Nodes() {
|
|
translationSourcesByNode[n.ID] = nil
|
|
}
|
|
|
|
if len(indexes) > 0 {
|
|
// Add to translationSourcesByNode the instructions for the cluster.
|
|
translationNodes, err := fromCluster.translationNodes(c)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting translation sources")
|
|
}
|
|
|
|
// Create a list of TranslationResizeSource for each index,
|
|
// using translationNodes as a template.
|
|
translationSources := make(map[string][]*TranslationResizeSource)
|
|
for _, idx := range indexes {
|
|
// Only include indexes with keys.
|
|
if !idx.Keys() {
|
|
continue
|
|
}
|
|
indexName := idx.Name()
|
|
for node, resizeNodes := range translationNodes {
|
|
for i := range resizeNodes {
|
|
translationSources[node] = append(translationSources[node],
|
|
&TranslationResizeSource{
|
|
Node: resizeNodes[i].node,
|
|
Index: indexName,
|
|
PartitionID: resizeNodes[i].partitionID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for nodeid, sources := range translationSources {
|
|
translationSourcesByNode[nodeid] = sources
|
|
}
|
|
}
|
|
|
|
status, err := c.unprotectedStatus()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting cluster status")
|
|
}
|
|
|
|
myid := c.disCo.ID()
|
|
nodeStatus, err := c.nodeStatus()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting node status")
|
|
}
|
|
return &ResizeInstruction{
|
|
Node: c.unprotectedNodeByID(myid),
|
|
Sources: fragmentSourcesByNode[myid],
|
|
TranslationSources: translationSourcesByNode[myid],
|
|
NodeStatus: nodeStatus, // Include the NodeStatus in order to ensure that schema and availableShards are in sync on the receiving node.
|
|
ClusterStatus: status,
|
|
}, nil
|
|
}
|
|
|
|
// removeNode removes a node from the Cluster and starts resizing process.
|
|
func (c *cluster) removeNode(id string) error {
|
|
eg := &errgroup.Group{}
|
|
for _, n := range c.noder.Nodes() {
|
|
// Don't send the resize message to the node being removed.
|
|
if n.ID == id {
|
|
continue
|
|
}
|
|
|
|
if err := c.sendTo(n, &ResizeNodeMessage{NodeID: id, Action: resizeJobActionRemove}); err != nil {
|
|
return errors.Wrap(err, "broadcasting resize message")
|
|
}
|
|
|
|
nodeID := n.ID
|
|
eg.Go(func() error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
err := c.resizer.Watch(ctx, nodeID, func(data []byte) error {
|
|
var progress ResizeNodeProgress
|
|
if err := json.Unmarshal(data, &progress); err != nil {
|
|
return errors.Wrapf(err, "watching progress node %s", nodeID)
|
|
}
|
|
if progress.Error != "" {
|
|
return errors.Errorf("watching progress node %s: %s", nodeID, progress.Error)
|
|
}
|
|
if progress.Done {
|
|
return io.EOF
|
|
}
|
|
return nil
|
|
})
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// monitor all background resize threads
|
|
go func() {
|
|
if err := eg.Wait(); err != nil {
|
|
c.logger.Printf("Stop watching all peers: %+v", err)
|
|
return
|
|
}
|
|
|
|
if err := c.disCo.DeleteNode(context.Background(), id); err != nil {
|
|
// it's ok, we can delete the node
|
|
c.logger.Printf("Cannot delete the node %s: %+v", id, err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) resizeNodeOnRemove(removeNodeID string) error {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// set status to RESIZING
|
|
progressFunc, err := c.resizer.Resize(context.Background())
|
|
if err != nil {
|
|
cancel()
|
|
return errors.Wrapf(err, "setting RESIZING state on %s", c.disCo.ID())
|
|
}
|
|
|
|
c.resizeCancel = cancel
|
|
// start async. data balancing
|
|
go func() {
|
|
progress := ResizeNodeProgress{FromID: removeNodeID, ToID: c.disCo.ID()}
|
|
defer func() {
|
|
err := progress.applyJSON(progressFunc)
|
|
if err != nil {
|
|
c.logger.Printf("updating resize progress (%s <- %s): %+v", c.disCo.ID(), removeNodeID, err)
|
|
}
|
|
if c.resizeCancel != nil {
|
|
c.resizeCancel()
|
|
}
|
|
err = c.resizer.DoneResize()
|
|
if err != nil {
|
|
c.logger.Printf("done resize (%s <- %s): %+v", c.disCo.ID(), removeNodeID, err)
|
|
}
|
|
}()
|
|
|
|
instr, err := c.generateResizeInstructionOnRemove(removeNodeID)
|
|
if err != nil {
|
|
progress.Error = errors.Wrapf(err, "generating resize instruction (%s <- %s)", c.disCo.ID(), removeNodeID).Error()
|
|
c.logger.Printf(progress.Error)
|
|
return
|
|
}
|
|
|
|
if err = c.followResizeInstruction(ctx, instr); err != nil {
|
|
progress.Error = errors.Wrapf(err, "following resize instruction (%s <- %s)", c.disCo.ID(), removeNodeID).Error()
|
|
c.logger.Printf(progress.Error)
|
|
return
|
|
}
|
|
progress.Done = true
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) generateResizeInstructionOnRemove(removeNodeID string) (*ResizeInstruction, error) {
|
|
toCluster := newCluster()
|
|
toCluster.noder.SetNodes(topology.Nodes(c.noder.Nodes()).Clone())
|
|
toCluster.Hasher = c.Hasher
|
|
toCluster.partitionN = c.partitionN
|
|
toCluster.ReplicaN = c.ReplicaN
|
|
toCluster.removeNodeBasicSorted(removeNodeID)
|
|
|
|
// fragmentSourcesByNode is a map of Node.ID to sources of fragment data.
|
|
// It is initialized with all the nodes in toCluster.
|
|
fragmentSourcesByNode := make(map[string][]*ResizeSource)
|
|
for _, n := range toCluster.noder.Nodes() {
|
|
fragmentSourcesByNode[n.ID] = nil
|
|
}
|
|
|
|
indexes := c.holder.Indexes()
|
|
// Add to fragmentSourcesByNode the instructions for each index.
|
|
for _, idx := range indexes {
|
|
fragSources, err := c.fragSources(toCluster, idx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting sources")
|
|
}
|
|
|
|
for nodeid, sources := range fragSources {
|
|
fragmentSourcesByNode[nodeid] = append(fragmentSourcesByNode[nodeid], sources...)
|
|
}
|
|
}
|
|
|
|
// translationSourcesByNode is a map of Node.ID to sources of partitioned
|
|
// key translation data for indexes.
|
|
// It is initialized with all the nodes in toCluster.
|
|
translationSourcesByNode := make(map[string][]*TranslationResizeSource)
|
|
for _, n := range toCluster.noder.Nodes() {
|
|
translationSourcesByNode[n.ID] = nil
|
|
}
|
|
|
|
if len(indexes) > 0 {
|
|
// Add to translationSourcesByNode the instructions for the cluster.
|
|
translationNodes, err := c.translationNodes(toCluster)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting translation sources")
|
|
}
|
|
|
|
// Create a list of TranslationResizeSource for each index,
|
|
// using translationNodes as a template.
|
|
translationSources := make(map[string][]*TranslationResizeSource)
|
|
for _, idx := range indexes {
|
|
// Only include indexes with keys.
|
|
if !idx.Keys() {
|
|
continue
|
|
}
|
|
indexName := idx.Name()
|
|
for node, resizeNodes := range translationNodes {
|
|
for i := range resizeNodes {
|
|
translationSources[node] = append(translationSources[node],
|
|
&TranslationResizeSource{
|
|
Node: resizeNodes[i].node,
|
|
Index: indexName,
|
|
PartitionID: resizeNodes[i].partitionID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
for nodeid, sources := range translationSources {
|
|
translationSourcesByNode[nodeid] = sources
|
|
}
|
|
}
|
|
|
|
status, err := c.unprotectedStatus()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting cluster status")
|
|
}
|
|
|
|
myid := c.disCo.ID()
|
|
nodeStatus, err := c.nodeStatus()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting node status")
|
|
}
|
|
return &ResizeInstruction{
|
|
Node: toCluster.unprotectedNodeByID(myid),
|
|
Sources: fragmentSourcesByNode[myid],
|
|
TranslationSources: translationSourcesByNode[myid],
|
|
NodeStatus: nodeStatus, // Include the NodeStatus in order to ensure that schema and availableShards are in sync on the receiving node.
|
|
ClusterStatus: status,
|
|
}, nil
|
|
}
|
|
|
|
// unprotectedStatus returns the the cluster's status including what nodes it contains, its ID, and current state.
|
|
func (c *cluster) unprotectedStatus() (*ClusterStatus, error) {
|
|
state, err := c.stator.ClusterState(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
indexes, err := c.holder.Schema()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting schema")
|
|
}
|
|
|
|
return &ClusterStatus{
|
|
State: string(state),
|
|
Nodes: c.Nodes(),
|
|
Schema: &Schema{Indexes: indexes},
|
|
}, nil
|
|
}
|
|
|
|
func (c *cluster) remoteSchema() (*Schema, error) {
|
|
for _, n := range c.noder.Nodes() {
|
|
if c.disCo.ID() == n.ID {
|
|
continue
|
|
}
|
|
|
|
ii, err := c.InternalClient.SchemaNode(context.Background(), &n.URI, true)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "getting schema from %s (%v)", n.ID, n.URI)
|
|
}
|
|
|
|
return &Schema{ii}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// nodeIDs returns the list of IDs in the cluster.
|
|
func (c *cluster) nodeIDs() []string {
|
|
return topology.Nodes(c.Nodes()).IDs()
|
|
}
|
|
|
|
func (c *cluster) State() (disco.ClusterState, error) {
|
|
return c.stator.ClusterState(context.Background())
|
|
}
|
|
|
|
func (c *cluster) nodeByID(id string) *topology.Node {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.unprotectedNodeByID(id)
|
|
}
|
|
|
|
// unprotectedNodeByID returns a node reference by ID.
|
|
func (c *cluster) unprotectedNodeByID(id string) *topology.Node {
|
|
for _, n := range c.noder.Nodes() {
|
|
if n.ID == id {
|
|
return n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nodePositionByID returns the position of the node in slice c.Nodes.
|
|
func (c *cluster) nodePositionByID(nodeID string) int {
|
|
for i, n := range c.noder.Nodes() {
|
|
if n.ID == nodeID {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// addNodeBasicSorted adds a node to the cluster, sorted by id. Returns a
|
|
// pointer to the node and true if the node was added or updated. unprotected.
|
|
func (c *cluster) addNodeBasicSorted(node *topology.Node) bool {
|
|
n := c.unprotectedNodeByID(node.ID)
|
|
|
|
if n != nil {
|
|
nn := &topology.Node{
|
|
ID: node.ID,
|
|
URI: node.URI,
|
|
GRPCURI: node.GRPCURI,
|
|
IsPrimary: node.IsPrimary,
|
|
State: node.State,
|
|
}
|
|
if n.State != node.State || n.IsPrimary != node.IsPrimary || n.URI != node.URI {
|
|
*n = *nn
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
c.noder.AppendNode(node)
|
|
return true
|
|
}
|
|
|
|
// Nodes returns a copy of the slice of nodes in the cluster. Safe for
|
|
// concurrent use, result may be modified.
|
|
func (c *cluster) Nodes() []*topology.Node {
|
|
nodes := c.noder.Nodes()
|
|
// duplicate the nodes since we're going to be altering them
|
|
copiedNodes := make([]topology.Node, len(nodes))
|
|
result := make([]*topology.Node, len(nodes))
|
|
|
|
primary := topology.PrimaryNode(nodes, c.Hasher)
|
|
|
|
// Set node states and IsPrimary.
|
|
for i, node := range nodes {
|
|
copiedNodes[i] = *node
|
|
result[i] = &copiedNodes[i]
|
|
if node == primary {
|
|
copiedNodes[i].IsPrimary = true
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// removeNodeBasicSorted removes a node from the cluster, maintaining the sort
|
|
// order. Returns true if the node was removed. unprotected.
|
|
func (c *cluster) removeNodeBasicSorted(nodeID string) bool {
|
|
return c.noder.RemoveNode(nodeID)
|
|
}
|
|
|
|
// frag is a struct of basic fragment information.
|
|
type frag struct {
|
|
field string
|
|
view string
|
|
shard uint64
|
|
}
|
|
|
|
func fragsDiff(a, b []frag) []frag {
|
|
m := make(map[frag]uint64)
|
|
|
|
for _, y := range b {
|
|
m[y]++
|
|
}
|
|
|
|
var ret []frag
|
|
for _, x := range a {
|
|
if m[x] > 0 {
|
|
m[x]--
|
|
continue
|
|
}
|
|
ret = append(ret, x)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
type fragsByHost map[string][]frag
|
|
|
|
type viewsByField map[string][]string
|
|
|
|
func (a viewsByField) addView(field, view string) {
|
|
a[field] = append(a[field], view)
|
|
}
|
|
|
|
func (c *cluster) fragsByHost(idx *Index) fragsByHost {
|
|
// fieldViews is a map of field to slice of views.
|
|
fieldViews := make(viewsByField)
|
|
|
|
for _, field := range idx.Fields() {
|
|
for _, view := range field.views() {
|
|
fieldViews.addView(field.Name(), view.name)
|
|
}
|
|
}
|
|
return c.fragCombos(idx.Name(), idx.AvailableShards(includeRemote), fieldViews)
|
|
}
|
|
|
|
// fragCombos returns a map (by uri) of lists of fragments for a given index
|
|
// by creating every combination of field/view specified in `fieldViews` up
|
|
// for the given set of shards with data.
|
|
func (c *cluster) fragCombos(idx string, availableShards *roaring.Bitmap, fieldViews viewsByField) fragsByHost {
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
t := make(fragsByHost)
|
|
_ = availableShards.ForEach(func(i uint64) error {
|
|
nodes := snap.ShardNodes(idx, i)
|
|
for _, n := range nodes {
|
|
// for each field/view combination:
|
|
for field, views := range fieldViews {
|
|
for _, view := range views {
|
|
t[n.ID] = append(t[n.ID], frag{field, view, i})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return t
|
|
}
|
|
|
|
// diff compares c with another cluster and determines if a node is being
|
|
// added or removed. An error is returned for any case other than where
|
|
// exactly one node is added or removed. unprotected.
|
|
func (c *cluster) diff(other *cluster) (action string, nodeID string, err error) {
|
|
cNodes := c.noder.Nodes()
|
|
otherNodes := other.noder.Nodes()
|
|
lenFrom := len(cNodes)
|
|
lenTo := len(otherNodes)
|
|
// Determine if a node is being added or removed.
|
|
if lenFrom == lenTo {
|
|
return "", "", errors.New("clusters are the same size")
|
|
}
|
|
if lenFrom < lenTo {
|
|
// Adding a node.
|
|
if lenTo-lenFrom > 1 {
|
|
return "", "", errors.New("adding more than one node at a time is not supported")
|
|
}
|
|
action = resizeJobActionAdd
|
|
// Determine the node ID that is being added.
|
|
for _, n := range otherNodes {
|
|
if c.unprotectedNodeByID(n.ID) == nil {
|
|
nodeID = n.ID
|
|
break
|
|
}
|
|
}
|
|
} else if lenFrom > lenTo {
|
|
// Removing a node.
|
|
if lenFrom-lenTo > 1 {
|
|
return "", "", errors.New("removing more than one node at a time is not supported")
|
|
}
|
|
action = resizeJobActionRemove
|
|
// Determine the node ID that is being removed.
|
|
for _, n := range cNodes {
|
|
if other.unprotectedNodeByID(n.ID) == nil {
|
|
nodeID = n.ID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return action, nodeID, nil
|
|
}
|
|
|
|
// fragSources returns a list of ResizeSources - for each node in the `to` cluster -
|
|
// required to move from cluster `c` to cluster `to`. unprotected.
|
|
func (c *cluster) fragSources(to *cluster, idx *Index) (map[string][]*ResizeSource, error) {
|
|
m := make(map[string][]*ResizeSource)
|
|
|
|
// Determine if a node is being added or removed.
|
|
action, diffNodeID, err := c.diff(to)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "diffing")
|
|
}
|
|
|
|
// Initialize the map with all the nodes in `to`.
|
|
for _, n := range to.noder.Nodes() {
|
|
m[n.ID] = nil
|
|
}
|
|
|
|
// If a node is being added, the source can be confined to the
|
|
// primary fragments (i.e. no need to use replicas as source data).
|
|
// In this case, source fragments can be based on a cluster with
|
|
// replica = 1.
|
|
// If a node is being removed, however, then it will most likely
|
|
// require that a replica fragment be the source data.
|
|
srcCluster := c
|
|
if action == resizeJobActionAdd && c.ReplicaN > 1 {
|
|
srcCluster = newCluster()
|
|
srcCluster.noder.SetNodes(topology.Nodes(c.noder.Nodes()).Clone())
|
|
srcCluster.Hasher = c.Hasher
|
|
srcCluster.partitionN = c.partitionN
|
|
srcCluster.ReplicaN = 1
|
|
}
|
|
|
|
// Represents the fragment location for the from/to clusters.
|
|
fFrags := c.fragsByHost(idx)
|
|
tFrags := to.fragsByHost(idx)
|
|
|
|
// srcFrags is the frag map based on a source cluster of replica = 1.
|
|
srcFrags := srcCluster.fragsByHost(idx)
|
|
|
|
// srcNodesByFrag is the inverse representation of srcFrags.
|
|
srcNodesByFrag := make(map[frag]string)
|
|
for nodeID, frags := range srcFrags {
|
|
// If a node is being removed, don't consider it as a source.
|
|
if action == resizeJobActionRemove && nodeID == diffNodeID {
|
|
continue
|
|
}
|
|
for _, frag := range frags {
|
|
srcNodesByFrag[frag] = nodeID
|
|
}
|
|
}
|
|
|
|
// Get the frag diff for each nodeID.
|
|
diffs := make(fragsByHost)
|
|
for nodeID, frags := range tFrags {
|
|
if _, ok := fFrags[nodeID]; ok {
|
|
diffs[nodeID] = fragsDiff(frags, fFrags[nodeID])
|
|
} else {
|
|
diffs[nodeID] = frags
|
|
}
|
|
}
|
|
|
|
// Get the ResizeSource for each diff.
|
|
for nodeID, diff := range diffs {
|
|
m[nodeID] = []*ResizeSource{}
|
|
for _, frag := range diff {
|
|
// If there is no valid source node ID for a fragment,
|
|
// it likely means that the replica factor was not
|
|
// high enough for the remaining nodes to contain
|
|
// the fragment.
|
|
srcNodeID, ok := srcNodesByFrag[frag]
|
|
if !ok {
|
|
return nil, errors.New("not enough data to perform resize (replica factor may need to be increased)")
|
|
}
|
|
|
|
src := &ResizeSource{
|
|
Node: c.unprotectedNodeByID(srcNodeID),
|
|
Index: idx.Name(),
|
|
Field: frag.field,
|
|
View: frag.view,
|
|
Shard: frag.shard,
|
|
}
|
|
|
|
m[nodeID] = append(m[nodeID], src)
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// translationNodes returns a list of translationResizeNodes - for each node
|
|
// in the `to` cluster - required to move from cluster `c` to cluster `to`. unprotected.
|
|
// Because the parition scheme for every index is the same, this is used as a template
|
|
// to create index-specific `TranslationResizeSource`s.
|
|
func (c *cluster) translationNodes(to *cluster) (map[string][]*translationResizeNode, error) {
|
|
m := make(map[string][]*translationResizeNode)
|
|
|
|
// Determine if a node is being added or removed.
|
|
action, diffNodeID, err := c.diff(to)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "diffing")
|
|
}
|
|
|
|
// Initialize the map with all the nodes in `to`.
|
|
for _, n := range to.noder.Nodes() {
|
|
m[n.ID] = nil
|
|
}
|
|
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
fSnap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
toSnap := topology.NewClusterSnapshot(to.noder, c.Hasher, to.ReplicaN)
|
|
|
|
for pid := 0; pid < c.partitionN; pid++ {
|
|
fNodes := fSnap.PartitionNodes(pid)
|
|
tNodes := toSnap.PartitionNodes(pid)
|
|
|
|
// For `to` cluster, we include all nodes containing a
|
|
// replica for the partition. The source for each replica
|
|
// will be the primary in the `from` cluster. For the `from`
|
|
// cluster, we only need the first node, unless that node is
|
|
// being removed, then we use the second node. If no second
|
|
// node exists in that case, then we have to raise an error
|
|
// indicating that not enough replicas exist to support
|
|
// the resize.
|
|
if len(tNodes) > 0 {
|
|
var foundPrimary bool
|
|
for i := range fNodes {
|
|
if action == resizeJobActionRemove && fNodes[i].ID == diffNodeID {
|
|
continue
|
|
}
|
|
// We only need to add the source if the nodes differ;
|
|
// in other words if the primary partition is on the
|
|
// same node, it doesn't need to retrieve it.
|
|
for n := range tNodes {
|
|
if tNodes[n].ID != fNodes[i].ID {
|
|
m[tNodes[n].ID] = append(m[tNodes[n].ID],
|
|
&translationResizeNode{
|
|
node: fNodes[i],
|
|
partitionID: pid,
|
|
})
|
|
}
|
|
}
|
|
foundPrimary = true
|
|
break
|
|
}
|
|
if !foundPrimary {
|
|
return nil, ErrResizeNoReplicas
|
|
}
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// shardDistributionByIndex returns a map of [nodeID][primaryOrReplica][]uint64,
|
|
// where the int slices are lists of shards.
|
|
func (c *cluster) shardDistributionByIndex(indexName string) map[string]map[string][]uint64 {
|
|
dist := make(map[string]map[string][]uint64)
|
|
|
|
for _, node := range c.noder.Nodes() {
|
|
nodeDist := make(map[string][]uint64)
|
|
nodeDist["primary-shards"] = make([]uint64, 0)
|
|
nodeDist["replica-shards"] = make([]uint64, 0)
|
|
dist[node.ID] = nodeDist
|
|
}
|
|
|
|
index := c.holder.Index(indexName)
|
|
available := index.AvailableShards(includeRemote).Slice()
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
for _, shard := range available {
|
|
p := snap.ShardToShardPartition(indexName, shard)
|
|
nodes := snap.PartitionNodes(p)
|
|
dist[nodes[0].ID]["primary-shards"] = append(dist[nodes[0].ID]["primary-shards"], shard)
|
|
for k := 1; k < len(nodes); k++ {
|
|
dist[nodes[k].ID]["replica-shards"] = append(dist[nodes[k].ID]["replica-shards"], shard)
|
|
}
|
|
}
|
|
|
|
return dist
|
|
}
|
|
|
|
func (c *cluster) close() error {
|
|
// Notify goroutines of closing and wait for completion.
|
|
close(c.closing)
|
|
c.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) sendTo(node *topology.Node, m Message) error {
|
|
if err := c.broadcaster.SendTo(node, m); err != nil {
|
|
return errors.Wrap(err, "sending")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) followResizeInstruction(ctx context.Context, instr *ResizeInstruction) error {
|
|
// Make sure the holder has opened.
|
|
c.holder.opened.Recv()
|
|
|
|
span, _ := tracing.StartSpanFromContext(ctx, "Cluster.followResizeInstruction")
|
|
defer span.Finish()
|
|
|
|
// Sync the NodeStatus received in the resize instruction.
|
|
// Sync schema.
|
|
c.logger.Debugf("holder applySchema")
|
|
if err := c.holder.applySchema(instr.NodeStatus.Schema); err != nil {
|
|
return errors.Wrap(err, "applying schema")
|
|
}
|
|
|
|
// Sync available shards.
|
|
for _, is := range instr.NodeStatus.Indexes {
|
|
for _, fs := range is.Fields {
|
|
f := c.holder.Field(is.Name, fs.Name)
|
|
// if we don't know about a field locally, log an error because
|
|
// fields should be created and synced prior to shard creation
|
|
if f == nil {
|
|
c.logger.Errorf("local field not found: %s/%s", is.Name, fs.Name)
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
default:
|
|
err := f.loadAvailableShards()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "getting shards for field: %s/%s", is.Name, f.name)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// Request each source file in ResizeSources.
|
|
for _, src := range instr.Sources {
|
|
srcURI := src.Node.URI
|
|
c.logger.Printf("get shard %d for index %s from host %s", src.Shard, src.Index, srcURI)
|
|
// Retrieve field.
|
|
f := c.holder.Field(src.Index, src.Field)
|
|
if f == nil {
|
|
return newNotFoundError(ErrFieldNotFound, src.Field)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
default:
|
|
// Create view.
|
|
var v *view
|
|
if err := func() (err error) {
|
|
v, err = f.createViewIfNotExists(src.View)
|
|
return err
|
|
}(); err != nil {
|
|
return errors.Wrap(err, "creating view")
|
|
}
|
|
|
|
// Create the local fragment.
|
|
frag, err := v.CreateFragmentIfNotExists(src.Shard)
|
|
if err != nil {
|
|
return errors.Wrap(err, "creating fragment")
|
|
}
|
|
|
|
// Stream shard from remote node.
|
|
c.logger.Printf("retrieve shard %d for index %s from host %s", src.Shard, src.Index, srcURI)
|
|
rd, err := c.InternalClient.RetrieveShardFromURI(ctx, src.Index, src.Field, src.View, src.Shard, srcURI)
|
|
if err != nil {
|
|
// For now it is an acceptable error if the fragment is not found
|
|
// on the remote node. This occurs when a shard has been skipped and
|
|
// therefore doesn't contain data. The primary correctly determined
|
|
// the resize instruction to retrieve the shard, but it doesn't have data.
|
|
// TODO: figure out a way to distinguish from "fragment not found" errors
|
|
// which are true errors and which simply mean the fragment doesn't have data.
|
|
if err == ErrFragmentNotFound {
|
|
continue
|
|
}
|
|
return errors.Wrap(err, "retrieving shard")
|
|
} else if rd == nil {
|
|
return fmt.Errorf("shard %v doesn't exist on host: %s", src.Shard, srcURI)
|
|
}
|
|
|
|
// Write to local field and always close reader.
|
|
if err := func() error {
|
|
defer rd.Close()
|
|
_, err := frag.ReadFrom(rd)
|
|
return err
|
|
}(); err != nil {
|
|
return errors.Wrap(err, "copying remote shard")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Request each translation source file in TranslationResizeSources.
|
|
for _, src := range instr.TranslationSources {
|
|
srcURI := src.Node.URI
|
|
|
|
idx := c.holder.Index(src.Index)
|
|
if idx == nil {
|
|
return newNotFoundError(ErrIndexNotFound, src.Index)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
default:
|
|
// Retrieve partition from remote node.
|
|
c.logger.Printf("retrieve translate partition %d for index %s from host %s", src.PartitionID, src.Index, srcURI)
|
|
rd, err := c.InternalClient.RetrieveTranslatePartitionFromURI(ctx, src.Index, src.PartitionID, srcURI)
|
|
if err != nil {
|
|
return errors.Wrap(err, "retrieving translate partition")
|
|
} else if rd == nil {
|
|
return fmt.Errorf("partition %d doesn't exist on host: %s", src.PartitionID, src.Node.URI)
|
|
}
|
|
|
|
// Write to local store and always close reader.
|
|
if err := func() error {
|
|
defer rd.Close()
|
|
// Get the translate store for this index/partition.
|
|
store := idx.TranslateStore(src.PartitionID)
|
|
_, err = store.ReadFrom(rd)
|
|
return errors.Wrap(err, "reading from reader")
|
|
}(); err != nil {
|
|
return errors.Wrap(err, "copying remote partition")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cluster) resizeAbortAndBroadcast() error {
|
|
if err := c.resizeAbort(); err != nil {
|
|
return err
|
|
}
|
|
return c.broadcaster.SendSync(&ResizeAbortMessage{})
|
|
}
|
|
|
|
func (c *cluster) resizeAbort() error {
|
|
if c.resizeCancel != nil {
|
|
c.resizeCancel()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type resizeJob struct {
|
|
ID int64
|
|
IDs map[string]bool
|
|
Instructions []*ResizeInstruction
|
|
Broadcaster broadcaster
|
|
|
|
action string
|
|
result chan string
|
|
|
|
Logger logger.Logger
|
|
}
|
|
|
|
// newResizeJob returns a new instance of resizeJob.
|
|
func newResizeJob(existingNodes []*topology.Node, node *topology.Node, action string) *resizeJob {
|
|
|
|
// Build a map of uris to track their resize status.
|
|
// The value for a node will be set to true after that node
|
|
// has indicated that it has completed all resize instructions.
|
|
ids := make(map[string]bool)
|
|
|
|
if action == resizeJobActionRemove {
|
|
for _, n := range existingNodes {
|
|
// Exclude the removed node from the map.
|
|
if n.ID == node.ID {
|
|
continue
|
|
}
|
|
ids[n.ID] = false
|
|
}
|
|
} else if action == resizeJobActionAdd {
|
|
for _, n := range existingNodes {
|
|
ids[n.ID] = false
|
|
}
|
|
// Include the added node in the map for tracking.
|
|
ids[node.ID] = false
|
|
}
|
|
|
|
return &resizeJob{
|
|
ID: rand.Int63(),
|
|
IDs: ids,
|
|
action: action,
|
|
result: make(chan string),
|
|
Logger: logger.NopLogger,
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////
|
|
// Cluster implements the Noder interface.
|
|
// This is temporary and should be removed once etcd is fully implemented as
|
|
// noder.
|
|
|
|
// SetNodes implements the Noder interface.
|
|
func (c *cluster) SetNodes(nodes []*topology.Node) {}
|
|
|
|
// AppendNode implements the Noder interface.
|
|
func (c *cluster) AppendNode(node *topology.Node) {}
|
|
|
|
// RemoveNode implements the Noder interface.
|
|
func (c *cluster) RemoveNode(nodeID string) bool {
|
|
return false
|
|
}
|
|
|
|
// SetNodeState implements the Noder interface.
|
|
func (c *cluster) SetNodeState(nodeID string, state string) {}
|
|
|
|
///////////////////////////////////////////
|
|
|
|
func (c *cluster) nodeStatus() (*NodeStatus, error) {
|
|
indexes, err := c.holder.Schema()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting schema")
|
|
}
|
|
ns := &NodeStatus{
|
|
Node: c.Node,
|
|
Schema: &Schema{Indexes: indexes},
|
|
}
|
|
var availableShards *roaring.Bitmap
|
|
for _, idx := range ns.Schema.Indexes {
|
|
is := &IndexStatus{Name: idx.Name, CreatedAt: idx.CreatedAt}
|
|
for _, f := range idx.Fields {
|
|
if field := c.holder.Field(idx.Name, f.Name); field != nil {
|
|
availableShards = field.AvailableShards(includeRemote)
|
|
} else {
|
|
availableShards = roaring.NewBitmap()
|
|
}
|
|
is.Fields = append(is.Fields, &FieldStatus{
|
|
Name: f.Name,
|
|
CreatedAt: f.CreatedAt,
|
|
AvailableShards: availableShards,
|
|
})
|
|
}
|
|
ns.Indexes = append(ns.Indexes, is)
|
|
}
|
|
return ns, nil
|
|
}
|
|
|
|
// unprotectedPreviousNode returns the node listed before the current node in c.Nodes.
|
|
// If there is only one node in the cluster, returns nil.
|
|
// If the current node is the first node in the list, returns the last node.
|
|
func (c *cluster) unprotectedPreviousNode() *topology.Node {
|
|
cNodes := c.noder.Nodes()
|
|
if len(cNodes) <= 1 {
|
|
return nil
|
|
}
|
|
|
|
pos := c.nodePositionByID(c.Node.ID)
|
|
if pos == -1 {
|
|
return nil
|
|
} else if pos == 0 {
|
|
return cNodes[len(cNodes)-1]
|
|
} else {
|
|
return cNodes[pos-1]
|
|
}
|
|
}
|
|
|
|
// PrimaryReplicaNode returns the node listed before the current node in c.Nodes.
|
|
// This is different than "previous node" as the first node always returns nil.
|
|
func (c *cluster) PrimaryReplicaNode() *topology.Node {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.unprotectedPrimaryReplicaNode()
|
|
}
|
|
|
|
func (c *cluster) unprotectedPrimaryReplicaNode() *topology.Node {
|
|
pos := c.nodePositionByID(c.Node.ID)
|
|
if pos <= 0 {
|
|
return nil
|
|
}
|
|
cNodes := c.noder.Nodes()
|
|
return cNodes[pos-1]
|
|
}
|
|
|
|
// TODO: remove this when it is no longer used
|
|
func (c *cluster) translateFieldKeys(ctx context.Context, field *Field, keys []string, writable bool) ([]uint64, error) {
|
|
var trans map[string]uint64
|
|
var err error
|
|
if writable {
|
|
trans, err = c.createFieldKeys(ctx, field, keys...)
|
|
} else {
|
|
trans, err = c.findFieldKeys(ctx, field, keys...)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]uint64, len(keys))
|
|
for i, key := range keys {
|
|
id, ok := trans[key]
|
|
if !ok {
|
|
return nil, ErrTranslatingKeyNotFound
|
|
}
|
|
|
|
ids[i] = id
|
|
}
|
|
|
|
return ids, nil
|
|
}
|
|
|
|
func (c *cluster) findFieldKeys(ctx context.Context, field *Field, keys ...string) (map[string]uint64, error) {
|
|
if idx := field.ForeignIndex(); idx != "" {
|
|
// The field uses foreign index keys.
|
|
// Therefore, the field keys are actually column keys on a different index.
|
|
return c.findIndexKeys(ctx, idx, keys...)
|
|
}
|
|
if !field.Keys() {
|
|
return nil, errors.Errorf("cannot find keys on unkeyed field %q", field.Name())
|
|
}
|
|
|
|
// Attempt to find the keys locally.
|
|
localTranslations, err := field.TranslateStore().FindKeys(keys...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating field(%s/%s) keys(%v) locally", field.Index(), field.Name(), keys)
|
|
}
|
|
|
|
// Check for missing keys.
|
|
var missing []string
|
|
if len(keys) > len(localTranslations) {
|
|
// There are either duplicate keys or missing keys.
|
|
// This should work either way.
|
|
missing = make([]string, 0, len(keys)-len(localTranslations))
|
|
for _, k := range keys {
|
|
_, found := localTranslations[k]
|
|
if !found {
|
|
missing = append(missing, k)
|
|
}
|
|
}
|
|
} else if len(localTranslations) > len(keys) {
|
|
panic(fmt.Sprintf("more translations than keys! translation count=%v, key count=%v", len(localTranslations), len(keys)))
|
|
}
|
|
if len(missing) == 0 {
|
|
// All keys were available locally.
|
|
return localTranslations, nil
|
|
}
|
|
|
|
// It is possible that the missing keys exist, but have not been synced to the local replica.
|
|
primary := c.primaryNode()
|
|
if primary == nil {
|
|
return nil, errors.Errorf("translating field(%s/%s) keys(%v) - cannot find primary node", field.Index(), field.Name(), keys)
|
|
}
|
|
if c.Node.ID == primary.ID {
|
|
// The local copy is the authoritative copy.
|
|
return localTranslations, nil
|
|
}
|
|
|
|
// Forward the missing keys to the primary.
|
|
// The primary has the authoritative copy.
|
|
remoteTranslations, err := c.InternalClient.FindFieldKeysNode(ctx, &primary.URI, field.Index(), field.Name(), missing...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating field(%s/%s) keys(%v) remotely", field.Index(), field.Name(), keys)
|
|
}
|
|
|
|
// Merge the remote translations into the local translations.
|
|
translations := localTranslations
|
|
for key, id := range remoteTranslations {
|
|
translations[key] = id
|
|
}
|
|
|
|
return translations, nil
|
|
}
|
|
|
|
func (c *cluster) createFieldKeys(ctx context.Context, field *Field, keys ...string) (map[string]uint64, error) {
|
|
if idx := field.ForeignIndex(); idx != "" {
|
|
// The field uses foreign index keys.
|
|
// Therefore, the field keys are actually column keys on a different index.
|
|
return c.createIndexKeys(ctx, idx, keys...)
|
|
}
|
|
|
|
if !field.Keys() {
|
|
return nil, errors.Errorf("cannot create keys on unkeyed field %q", field.Name())
|
|
}
|
|
|
|
// The primary is the only node that can create field keys, since it owns the authoritative copy.
|
|
primary := c.primaryNode()
|
|
if primary == nil {
|
|
return nil, errors.Errorf("translating field(%s/%s) keys(%v) - cannot find primary node", field.Index(), field.Name(), keys)
|
|
}
|
|
if c.Node.ID == primary.ID {
|
|
// The local copy is the authoritative copy.
|
|
return field.TranslateStore().CreateKeys(keys...)
|
|
}
|
|
|
|
// Attempt to find the keys locally.
|
|
// They cannot be created locally, but skipping keys that exist can reduce network usage.
|
|
localTranslations, err := field.TranslateStore().FindKeys(keys...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating field(%s/%s) keys(%v) locally", field.Index(), field.Name(), keys)
|
|
}
|
|
|
|
// Check for missing keys.
|
|
var missing []string
|
|
if len(keys) > len(localTranslations) {
|
|
// There are either duplicate keys or missing keys.
|
|
// This should work either way.
|
|
missing = make([]string, 0, len(keys)-len(localTranslations))
|
|
for _, k := range keys {
|
|
_, found := localTranslations[k]
|
|
if !found {
|
|
missing = append(missing, k)
|
|
}
|
|
}
|
|
} else if len(localTranslations) > len(keys) {
|
|
panic(fmt.Sprintf("more translations than keys! translation count=%v, key count=%v", len(localTranslations), len(keys)))
|
|
}
|
|
if len(missing) == 0 {
|
|
// All keys exist locally.
|
|
// There is no need to create anything.
|
|
return localTranslations, nil
|
|
}
|
|
|
|
// Forward the missing keys to the primary to be created.
|
|
remoteTranslations, err := c.InternalClient.CreateFieldKeysNode(ctx, &primary.URI, field.Index(), field.Name(), missing...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating field(%s/%s) keys(%v) remotely", field.Index(), field.Name(), keys)
|
|
}
|
|
|
|
// Merge the remote translations into the local translations.
|
|
translations := localTranslations
|
|
for key, id := range remoteTranslations {
|
|
translations[key] = id
|
|
}
|
|
|
|
return translations, nil
|
|
}
|
|
|
|
func (c *cluster) matchField(ctx context.Context, field *Field, like string) ([]uint64, error) {
|
|
// The primary is the only node that can match field keys, since it is the only node with all of the keys.
|
|
primary := c.primaryNode()
|
|
if primary == nil {
|
|
return nil, errors.Errorf("matching field(%s/%s) like %q - cannot find primary node", field.Index(), field.Name(), like)
|
|
}
|
|
if c.Node.ID == primary.ID {
|
|
// The local copy is the authoritative copy.
|
|
plan := planLike(like)
|
|
return field.TranslateStore().Match(func(key []byte) bool {
|
|
return matchLike(key, plan...)
|
|
})
|
|
}
|
|
|
|
// Forward the request to the primary.
|
|
return c.InternalClient.MatchFieldKeysNode(ctx, &primary.URI, field.Index(), field.Name(), like)
|
|
}
|
|
|
|
func (c *cluster) translateFieldIDs(field *Field, ids map[uint64]struct{}) (map[uint64]string, error) {
|
|
idList := make([]uint64, len(ids))
|
|
{
|
|
i := 0
|
|
for id := range ids {
|
|
idList[i] = id
|
|
i++
|
|
}
|
|
}
|
|
|
|
keyList, err := c.translateFieldListIDs(field, idList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mapped := make(map[uint64]string, len(idList))
|
|
for i, key := range keyList {
|
|
mapped[idList[i]] = key
|
|
}
|
|
return mapped, nil
|
|
}
|
|
|
|
func (c *cluster) translateFieldListIDs(field *Field, ids []uint64) (keys []string, err error) {
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
primary := snap.PrimaryFieldTranslationNode()
|
|
if primary == nil {
|
|
return nil, errors.Errorf("translating field(%s/%s) ids(%v) - cannot find primary node", field.Index(), field.Name(), ids)
|
|
}
|
|
|
|
if c.Node.ID == primary.ID {
|
|
keys, err = field.TranslateStore().TranslateIDs(ids)
|
|
} else {
|
|
keys, err = c.InternalClient.TranslateIDsNode(context.Background(), &primary.URI, field.Index(), field.Name(), ids)
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating field(%s/%s) ids(%v)", field.Index(), field.Name(), ids)
|
|
}
|
|
|
|
return keys, err
|
|
}
|
|
|
|
// TODO: remove this when it is no longer used
|
|
func (c *cluster) translateIndexKey(ctx context.Context, indexName string, key string, writable bool) (uint64, error) {
|
|
keyMap, err := c.translateIndexKeySet(ctx, indexName, map[string]struct{}{key: struct{}{}}, writable)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return keyMap[key], nil
|
|
}
|
|
|
|
// TODO: remove this when it is no longer used
|
|
func (c *cluster) translateIndexKeys(ctx context.Context, indexName string, keys []string, writable bool) ([]uint64, error) {
|
|
var trans map[string]uint64
|
|
var err error
|
|
if writable {
|
|
trans, err = c.createIndexKeys(ctx, indexName, keys...)
|
|
} else {
|
|
trans, err = c.findIndexKeys(ctx, indexName, keys...)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]uint64, len(keys))
|
|
for i, key := range keys {
|
|
id, ok := trans[key]
|
|
if !ok {
|
|
return nil, ErrTranslatingKeyNotFound
|
|
}
|
|
|
|
ids[i] = id
|
|
}
|
|
|
|
return ids, nil
|
|
}
|
|
|
|
// TODO: remove this when it is no longer used
|
|
func (c *cluster) translateIndexKeySet(ctx context.Context, indexName string, keySet map[string]struct{}, writable bool) (map[string]uint64, error) {
|
|
keys := make([]string, 0, len(keySet))
|
|
for key := range keySet {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
if writable {
|
|
return c.createIndexKeys(ctx, indexName, keys...)
|
|
}
|
|
|
|
trans, err := c.findIndexKeys(ctx, indexName, keys...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(trans) != len(keys) {
|
|
return nil, ErrTranslatingKeyNotFound
|
|
}
|
|
|
|
return trans, nil
|
|
}
|
|
|
|
func (c *cluster) findIndexKeys(ctx context.Context, indexName string, keys ...string) (map[string]uint64, error) {
|
|
done := ctx.Done()
|
|
|
|
idx := c.holder.Index(indexName)
|
|
if idx == nil {
|
|
return nil, ErrIndexNotFound
|
|
}
|
|
if !idx.Keys() {
|
|
return nil, errors.Errorf("cannot find keys on unkeyed index %q", indexName)
|
|
}
|
|
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
// Split keys by partition.
|
|
keysByPartition := make(map[int][]string, c.partitionN)
|
|
for _, key := range keys {
|
|
partitionID := snap.KeyToKeyPartition(indexName, key)
|
|
keysByPartition[partitionID] = append(keysByPartition[partitionID], key)
|
|
}
|
|
|
|
// TODO: use local replicas to short-circuit network traffic
|
|
|
|
// Group keys by node.
|
|
keysByNode := make(map[*topology.Node][]string)
|
|
for partitionID, keys := range keysByPartition {
|
|
// Find the primary node for this partition.
|
|
primary := snap.PrimaryPartitionNode(partitionID)
|
|
if primary == nil {
|
|
return nil, errors.Errorf("translating index(%s) keys(%v) on partition(%d) - cannot find primary node", indexName, keys, partitionID)
|
|
}
|
|
|
|
if c.Node.ID == primary.ID {
|
|
// The partition is local.
|
|
continue
|
|
}
|
|
|
|
// Group the partition to be processed remotely.
|
|
keysByNode[primary] = append(keysByNode[primary], keys...)
|
|
|
|
// Delete remote keys from the by-partition map so that it can be used for local translation.
|
|
delete(keysByPartition, partitionID)
|
|
}
|
|
|
|
// Start translating keys remotely.
|
|
// On child calls, there are no remote results since we were only sent the keys that we own.
|
|
remoteResults := make(chan map[string]uint64, len(keysByNode))
|
|
var g errgroup.Group
|
|
defer g.Wait() //nolint:errcheck
|
|
for node, keys := range keysByNode {
|
|
node, keys := node, keys
|
|
|
|
g.Go(func() error {
|
|
translations, err := c.InternalClient.FindIndexKeysNode(ctx, &node.URI, indexName, keys...)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "translating index(%s) keys(%v) on node %s", indexName, keys, node.ID)
|
|
}
|
|
|
|
remoteResults <- translations
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Translate local keys.
|
|
translations := make(map[string]uint64)
|
|
for partitionID, keys := range keysByPartition {
|
|
// Handle cancellation.
|
|
select {
|
|
case <-done:
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Find the keys within the partition.
|
|
t, err := idx.TranslateStore(partitionID).FindKeys(keys...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "translating index(%s) keys(%v) on partition(%d)", idx.Name(), keys, partitionID)
|
|
}
|
|
|
|
// Merge the translations from this partition.
|
|
for key, id := range t {
|
|
translations[key] = id
|
|
}
|
|
}
|
|
|
|
// Wait for remote key sets.
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Merge the translations.
|
|
// All data should have been written to here while we waited.
|
|
// Closing the channel prevents the range from blocking.
|
|
close(remoteResults)
|
|
for t := range remoteResults {
|
|
for key, id := range t {
|
|
translations[key] = id
|
|
}
|
|
}
|
|
return translations, nil
|
|
}
|
|
|
|
func (c *cluster) createIndexKeys(ctx context.Context, indexName string, keys ...string) (map[string]uint64, error) {
|
|
// Check for early cancellation.
|
|
done := ctx.Done()
|
|
select {
|
|
case <-done:
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
idx := c.holder.Index(indexName)
|
|
if idx == nil {
|
|
return nil, ErrIndexNotFound
|
|
}
|
|
|
|
if !idx.keys {
|
|
return nil, errors.Errorf("cannot create keys on unkeyed index %q", indexName)
|
|
}
|
|
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
// Split keys by partition.
|
|
keysByPartition := make(map[int][]string, c.partitionN)
|
|
for _, key := range keys {
|
|
partitionID := snap.KeyToKeyPartition(indexName, key)
|
|
keysByPartition[partitionID] = append(keysByPartition[partitionID], key)
|
|
}
|
|
|
|
// TODO: use local replicas to short-circuit network traffic
|
|
|
|
// Group keys by node.
|
|
// Delete remote keys from the by-partition map so that it can be used for local translation.
|
|
keysByNode := make(map[*topology.Node][]string)
|
|
for partitionID, keys := range keysByPartition {
|
|
// Find the primary node for this partition.
|
|
primary := snap.PrimaryPartitionNode(partitionID)
|
|
if primary == nil {
|
|
return nil, errors.Errorf("translating index(%s) keys(%v) on partition(%d) - cannot find primary node", indexName, keys, partitionID)
|
|
}
|
|
|
|
if c.Node.ID == primary.ID {
|
|
// The partition is local.
|
|
continue
|
|
}
|
|
|
|
// Group the partition to be processed remotely.
|
|
keysByNode[primary] = append(keysByNode[primary], keys...)
|
|
delete(keysByPartition, partitionID)
|
|
}
|
|
|
|
translateResults := make(chan map[string]uint64, len(keysByNode)+len(keysByPartition))
|
|
var g errgroup.Group
|
|
defer g.Wait() //nolint:errcheck
|
|
|
|
// Start translating keys remotely.
|
|
// On child calls, there are no remote results since we were only sent the keys that we own.
|
|
for node, keys := range keysByNode {
|
|
node, keys := node, keys
|
|
|
|
g.Go(func() error {
|
|
translations, err := c.InternalClient.CreateIndexKeysNode(ctx, &node.URI, indexName, keys...)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "translating index(%s) keys(%v) on node %s", indexName, keys, node.ID)
|
|
}
|
|
|
|
translateResults <- translations
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Translate local keys.
|
|
// TODO: make this less horrible (why fsync why?????)
|
|
// This is kinda terrible because each goroutine does an fsync, thus locking up an entire OS thread.
|
|
// AHHHHHHHHHHHHHHHHHH
|
|
for partitionID, keys := range keysByPartition {
|
|
partitionID, keys := partitionID, keys
|
|
|
|
g.Go(func() error {
|
|
// Handle cancellation.
|
|
select {
|
|
case <-done:
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
translations, err := idx.TranslateStore(partitionID).CreateKeys(keys...)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "translating index(%s) keys(%v) on partition(%d)", idx.Name(), keys, partitionID)
|
|
}
|
|
|
|
translateResults <- translations
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Wait for remote key sets.
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Merge the translations.
|
|
// All data should have been written to here while we waited.
|
|
// Closing the channel prevents the range from blocking.
|
|
translations := make(map[string]uint64, len(keys))
|
|
close(translateResults)
|
|
for t := range translateResults {
|
|
for key, id := range t {
|
|
translations[key] = id
|
|
}
|
|
}
|
|
return translations, nil
|
|
}
|
|
|
|
func (c *cluster) translateIndexIDs(ctx context.Context, indexName string, ids []uint64) ([]string, error) {
|
|
idSet := make(map[uint64]struct{})
|
|
for _, id := range ids {
|
|
idSet[id] = struct{}{}
|
|
}
|
|
|
|
idMap, err := c.translateIndexIDSet(ctx, indexName, idSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keys := make([]string, len(ids))
|
|
for i := range ids {
|
|
keys[i] = idMap[ids[i]]
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
func (c *cluster) translateIndexIDSet(ctx context.Context, indexName string, idSet map[uint64]struct{}) (map[uint64]string, error) {
|
|
idMap := make(map[uint64]string, len(idSet))
|
|
|
|
index := c.holder.Index(indexName)
|
|
if index == nil {
|
|
return nil, newNotFoundError(ErrIndexNotFound, indexName)
|
|
}
|
|
|
|
// Create a snapshot of the cluster to use for node/partition calculations.
|
|
snap := topology.NewClusterSnapshot(c.noder, c.Hasher, c.ReplicaN)
|
|
|
|
// Split ids by partition.
|
|
idsByPartition := make(map[int][]uint64, c.partitionN)
|
|
for id := range idSet {
|
|
partitionID := snap.IDToShardPartition(indexName, id)
|
|
idsByPartition[partitionID] = append(idsByPartition[partitionID], id)
|
|
}
|
|
|
|
// Translate ids by partition.
|
|
var g errgroup.Group
|
|
var mu sync.Mutex
|
|
for partitionID := range idsByPartition {
|
|
partitionID := partitionID
|
|
ids := idsByPartition[partitionID]
|
|
|
|
g.Go(func() (err error) {
|
|
var keys []string
|
|
|
|
primary := snap.PrimaryPartitionNode(partitionID)
|
|
if primary == nil {
|
|
return errors.Errorf("translating index(%s) ids(%v) on partition(%d) - cannot find primary node", indexName, ids, partitionID)
|
|
}
|
|
|
|
if c.Node.ID == primary.ID {
|
|
keys, err = index.TranslateStore(partitionID).TranslateIDs(ids)
|
|
} else {
|
|
keys, err = c.InternalClient.TranslateIDsNode(ctx, &primary.URI, indexName, "", ids)
|
|
}
|
|
|
|
if err != nil {
|
|
return errors.Wrapf(err, "translating index(%s) ids(%v) on partition(%d)", indexName, ids, partitionID)
|
|
}
|
|
|
|
mu.Lock()
|
|
for i, id := range ids {
|
|
idMap[id] = keys[i]
|
|
}
|
|
mu.Unlock()
|
|
|
|
return nil
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
return idMap, nil
|
|
}
|
|
|
|
// ClusterStatus describes the status of the cluster including its
|
|
// state and node topology.
|
|
type ClusterStatus struct {
|
|
ClusterID string
|
|
State string
|
|
Nodes []*topology.Node
|
|
Schema *Schema
|
|
}
|
|
|
|
// ResizeInstruction contains the instruction provided to a node
|
|
// during a cluster resize operation.
|
|
type ResizeInstruction struct {
|
|
JobID int64
|
|
Node *topology.Node
|
|
Primary *topology.Node
|
|
Sources []*ResizeSource
|
|
TranslationSources []*TranslationResizeSource
|
|
NodeStatus *NodeStatus
|
|
ClusterStatus *ClusterStatus
|
|
}
|
|
|
|
// ResizeSource is the source of data for a node acting on a
|
|
// ResizeInstruction.
|
|
type ResizeSource struct {
|
|
Node *topology.Node `protobuf:"bytes,1,opt,name=Node" json:"Node,omitempty"`
|
|
Index string `protobuf:"bytes,2,opt,name=Index,proto3" json:"Index,omitempty"`
|
|
Field string `protobuf:"bytes,3,opt,name=Field,proto3" json:"Field,omitempty"`
|
|
View string `protobuf:"bytes,4,opt,name=View,proto3" json:"View,omitempty"`
|
|
Shard uint64 `protobuf:"varint,5,opt,name=Shard,proto3" json:"Shard,omitempty"`
|
|
}
|
|
|
|
// TranslationResizeSource is the source of translation data for
|
|
// a node acting on a ResizeInstruction.
|
|
type TranslationResizeSource struct {
|
|
Node *topology.Node
|
|
Index string
|
|
PartitionID int
|
|
}
|
|
|
|
// translateResizeNode holds the node/partition pairs used
|
|
// to create a TranslationResizeSource for each index.
|
|
type translationResizeNode struct {
|
|
node *topology.Node
|
|
partitionID int
|
|
}
|
|
|
|
// Schema contains information about indexes and their configuration.
|
|
type Schema struct {
|
|
Indexes []*IndexInfo `json:"indexes"`
|
|
}
|
|
|
|
// CreateShardMessage is an internal message indicating shard creation.
|
|
type CreateShardMessage struct {
|
|
Index string
|
|
Field string
|
|
Shard uint64
|
|
}
|
|
|
|
// CreateIndexMessage is an internal message indicating index creation.
|
|
type CreateIndexMessage struct {
|
|
Index string
|
|
CreatedAt int64
|
|
Meta IndexOptions
|
|
}
|
|
|
|
// DeleteIndexMessage is an internal message indicating index deletion.
|
|
type DeleteIndexMessage struct {
|
|
Index string
|
|
}
|
|
|
|
// CreateFieldMessage is an internal message indicating field creation.
|
|
type CreateFieldMessage struct {
|
|
Index string
|
|
Field string
|
|
CreatedAt int64
|
|
Meta *FieldOptions
|
|
}
|
|
|
|
// DeleteFieldMessage is an internal message indicating field deletion.
|
|
type DeleteFieldMessage struct {
|
|
Index string
|
|
Field string
|
|
}
|
|
|
|
// DeleteAvailableShardMessage is an internal message indicating available shard deletion.
|
|
type DeleteAvailableShardMessage struct {
|
|
Index string
|
|
Field string
|
|
ShardID uint64
|
|
}
|
|
|
|
// CreateViewMessage is an internal message indicating view creation.
|
|
type CreateViewMessage struct {
|
|
Index string
|
|
Field string
|
|
View string
|
|
}
|
|
|
|
// DeleteViewMessage is an internal message indicating view deletion.
|
|
type DeleteViewMessage struct {
|
|
Index string
|
|
Field string
|
|
View string
|
|
}
|
|
|
|
// ResizeInstructionComplete is an internal message to the primary indicating
|
|
// that the resize instructions performed on a single node have completed.
|
|
type ResizeInstructionComplete struct {
|
|
JobID int64
|
|
Node *topology.Node
|
|
Error string
|
|
}
|
|
|
|
// NodeStateMessage is an internal message for broadcasting a node's state.
|
|
type NodeStateMessage struct {
|
|
NodeID string `protobuf:"bytes,1,opt,name=NodeID,proto3" json:"NodeID,omitempty"`
|
|
State string `protobuf:"bytes,2,opt,name=State,proto3" json:"State,omitempty"`
|
|
}
|
|
|
|
// NodeStatus is an internal message representing the contents of a node.
|
|
type NodeStatus struct {
|
|
Node *topology.Node
|
|
Indexes []*IndexStatus
|
|
Schema *Schema
|
|
}
|
|
|
|
// IndexStatus is an internal message representing the contents of an index.
|
|
type IndexStatus struct {
|
|
Name string
|
|
CreatedAt int64
|
|
Fields []*FieldStatus
|
|
}
|
|
|
|
// FieldStatus is an internal message representing the contents of a field.
|
|
type FieldStatus struct {
|
|
Name string
|
|
CreatedAt int64
|
|
AvailableShards *roaring.Bitmap
|
|
}
|
|
|
|
// RecalculateCaches is an internal message for recalculating all caches
|
|
// within a holder.
|
|
type RecalculateCaches struct{}
|
|
|
|
// Transaction Actions
|
|
const (
|
|
TRANSACTION_START = "start"
|
|
TRANSACTION_FINISH = "finish"
|
|
TRANSACTION_VALIDATE = "validate"
|
|
)
|
|
|
|
type TransactionMessage struct {
|
|
Transaction *Transaction
|
|
Action string
|
|
}
|