Merge pull request #1362 from travisturner/disco-node-id

use etcd for node.ID
This commit is contained in:
Travis Turner 2021-01-28 15:26:48 -06:00 committed by GitHub
commit 92ee314891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 652 additions and 495 deletions

26
api.go
View file

@ -497,7 +497,10 @@ func (api *API) ImportRoaring(ctx context.Context, indexName, fieldName string,
qcx := api.Txf().NewQcx()
defer qcx.Abort()
nodes := api.cluster.shardNodes(indexName, shard)
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
nodes := snap.ShardNodes(indexName, shard)
errCh := make(chan error, len(nodes))
for _, node := range nodes {
node := node
@ -619,8 +622,11 @@ func (api *API) ExportCSV(ctx context.Context, indexName string, fieldName strin
return errors.Wrap(err, "validating api method")
}
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
// Validate that this handler owns the shard.
if !api.cluster.ownsShard(api.Node().ID, indexName, shard) {
if !snap.OwnsShard(api.Node().ID, indexName, shard) {
api.server.logger.Printf("node %s does not own shard %d of index %s", api.Node().ID, shard, indexName)
return ErrClusterDoesNotOwnShard
}
@ -668,7 +674,7 @@ func (api *API) ExportCSV(ctx context.Context, indexName string, fieldName strin
}
if index.Keys() {
if store := index.TranslateStore(api.cluster.idPartition(indexName, columnID)); store == nil {
if store := index.TranslateStore(snap.IDToShardPartition(indexName, columnID)); store == nil {
return errors.Wrap(err, "partition does not exist")
} else if colStr, err = store.TranslateID(columnID); err != nil {
return errors.Wrap(err, "translating column")
@ -702,7 +708,10 @@ func (api *API) ShardNodes(ctx context.Context, indexName string, shard uint64)
return nil, errors.Wrap(err, "validating api method")
}
return api.cluster.shardNodes(indexName, shard), nil
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
return snap.ShardNodes(indexName, shard), nil
}
// FragmentBlockData is an endpoint for internal usage. It is not guaranteed to
@ -1683,8 +1692,10 @@ func (api *API) LongQueryTime() time.Duration {
}
func (api *API) validateShardOwnership(indexName string, shard uint64) error {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
// Validate that this handler owns the shard.
if !api.cluster.ownsShard(api.Node().ID, indexName, shard) {
if !snap.OwnsShard(api.Node().ID, indexName, shard) {
api.server.logger.Printf("node %s does not own shard %d of index %s", api.Node().ID, shard, indexName)
return ErrClusterDoesNotOwnShard
}
@ -2003,7 +2014,10 @@ func (api *API) CreateFieldKeys(ctx context.Context, index, field string, keys .
// PrimaryReplicaNodeURL returns the URL of the cluster's primary replica.
func (api *API) PrimaryReplicaNodeURL() url.URL {
node := api.cluster.PrimaryReplicaNode()
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
node := snap.PrimaryReplicaNode(api.Node().ID)
if node == nil {
return url.URL{}
}

View file

@ -299,6 +299,7 @@ func TestAPI_ImportValue(t *testing.T) {
)
defer c.Close()
coord := c.GetCoordinator()
m0 := c.GetNode(0)
m1 := c.GetNode(1)
@ -307,11 +308,11 @@ func TestAPI_ImportValue(t *testing.T) {
index := "valck"
field := "f"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{Keys: true})
_, err := coord.API.CreateIndex(ctx, index, pilosa.IndexOptions{Keys: true})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateField(ctx, index, field, pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
_, err = coord.API.CreateField(ctx, index, field, pilosa.OptFieldTypeInt(math.MinInt64, math.MaxInt64))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -334,8 +335,8 @@ func TestAPI_ImportValue(t *testing.T) {
Values: values,
}
qcx := m0.API.Txf().NewQcx()
if err := m0.API.ImportValue(ctx, qcx, req); err != nil {
qcx := coord.API.Txf().NewQcx()
if err := coord.API.ImportValue(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
@ -376,7 +377,7 @@ func TestAPI_ImportValue(t *testing.T) {
t.Fatalf("creating field: %v", err)
}
// Generate some keyed records.
// Generate some records.
values := []float64{}
colIDs := []uint64{}
for i := 0; i < 10; i++ {
@ -384,8 +385,8 @@ func TestAPI_ImportValue(t *testing.T) {
colIDs = append(colIDs, uint64(i))
}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
// Import data with keys to node1 and verify that it gets translated and
// forwarded to the owner of shard 0 (node0; because of offsetModHasher)
req := &pilosa.ImportValueRequest{
Index: index,
Field: field,
@ -431,16 +432,16 @@ func TestAPI_ImportValue(t *testing.T) {
fgnIndex := "fgnvalstr"
_, err := m0.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
_, err := coord.API.CreateIndex(ctx, index, pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = m0.API.CreateIndex(ctx, fgnIndex, pilosa.IndexOptions{Keys: true})
_, err = coord.API.CreateIndex(ctx, fgnIndex, pilosa.IndexOptions{Keys: true})
if err != nil {
t.Fatalf("creating foreign index: %v", err)
}
_, err = m0.API.CreateField(ctx, index, field,
_, err = coord.API.CreateField(ctx, index, field,
pilosa.OptFieldTypeInt(0, math.MaxInt64),
pilosa.OptFieldForeignIndex(fgnIndex),
)
@ -457,8 +458,9 @@ func TestAPI_ImportValue(t *testing.T) {
colIDs = append(colIDs, uint64(i))
}
// Import data with keys to the coordinator (node0) and verify that it gets
// translated and forwarded to the owner of shard 0 (node1; because of offsetModHasher)
// Import data with keys to the node0 and verify that it gets translated
// and forwarded to the owner of shard 0 (node1; because of
// offsetModHasher)
req := &pilosa.ImportValueRequest{
Index: index,
Field: field,
@ -473,8 +475,8 @@ func TestAPI_ImportValue(t *testing.T) {
pql := fmt.Sprintf(`Row(%s=="strval-110")`, field)
// Query node0.
if res, err := m0.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil {
// Query node1.
if res, err := m1.API.Query(ctx, &pilosa.QueryRequest{Index: index, Query: pql}); err != nil {
t.Fatal(err)
} else if ids := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(ids, []uint64{1}) {
t.Fatalf("unexpected columns: observerd %+v; expected '%+v'", ids, []uint64{1})

View file

@ -74,7 +74,8 @@ type nodeAction struct {
// cluster represents a collection of nodes.
type cluster struct { // nolint: maligned
noder topology.Noder
noder topology.Noder
unprotectedNoder topology.Noder
id string
Node *topology.Node
@ -161,10 +162,41 @@ func newCluster() *cluster {
confirmDownRetries: defaultConfirmDownRetries,
confirmDownSleep: defaultConfirmDownSleep,
}
c.noder = c // TODO: this is temporary until etcd fully implements noder
// TODO: these are temporary until etcd fully implements noder
c.noder = c
c.unprotectedNoder = &unprotectedCluster{
c: c,
}
return c
}
// unprotectedCluster is a temporary struct used in cases of NewClusterSnapshot
// which are inside of a c.mu.Lock(). These cases can't use the normal c.noder
// (which is also temporary), because c.Nodes() aquires c.mu.Lock() as well.
type unprotectedCluster struct {
c *cluster
}
// Nodes returns a copy of the slice of nodes in the cluster.
func (uc *unprotectedCluster) Nodes() []*topology.Node {
ret := make([]*topology.Node, len(uc.c.nodes))
copy(ret, uc.c.nodes)
return ret
}
// SetNodes implements the Noder interface.
func (uc *unprotectedCluster) SetNodes(nodes []*topology.Node) {}
// AppendNode implements the Noder interface.
func (uc *unprotectedCluster) AppendNode(node *topology.Node) {}
// RemoveNode implements the Noder interface.
func (uc *unprotectedCluster) RemoveNode(nodeID string) bool {
return false
}
// 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().
@ -192,16 +224,6 @@ func (c *cluster) abortAntiEntropy() {
}
}
// node gets the Node for the ID associated with this instance of cluster.
func (c *cluster) node() *topology.Node {
for _, n := range c.Nodes() {
if n.ID == c.disCo.ID() {
return n
}
}
return nil
}
func (c *cluster) coordinatorNode() *topology.Node {
c.mu.RLock()
defer c.mu.RUnlock()
@ -676,9 +698,12 @@ func (c *cluster) fragsByHost(idx *Index) fragsByHost {
// 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.unprotectedNoder, c.Hasher, c.ReplicaN)
t := make(fragsByHost)
_ = availableShards.ForEach(func(i uint64) error {
nodes := c.shardNodes(idx, i)
nodes := snap.ShardNodes(idx, i)
for _, n := range nodes {
// for each field/view combination:
for field, views := range fieldViews {
@ -838,9 +863,13 @@ func (c *cluster) translationNodes(to *cluster) (map[string][]*translationResize
m[n.ID] = nil
}
// Create a snapshot of the cluster to use for node/partition calculations.
fSnap := topology.NewClusterSnapshot(c.unprotectedNoder, c.Hasher, c.ReplicaN)
toSnap := topology.NewClusterSnapshot(to.unprotectedNoder, c.Hasher, to.ReplicaN)
for pid := 0; pid < c.partitionN; pid++ {
fNodes := c.partitionNodes(pid)
tNodes := to.partitionNodes(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
@ -898,9 +927,12 @@ func (c *cluster) shardDistributionByIndex(indexName string) map[string]map[stri
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 := c.shardToShardPartition(indexName, shard)
nodes := c.partitionNodes(p)
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)
@ -941,11 +973,6 @@ func keyToKeyPartition(index, key string, partitionN int) int {
return int(h.Sum64() % uint64(partitionN))
}
// idPartition returns the partition that an id belongs to.
func (c *cluster) idPartition(index string, id uint64) int {
return shardToShardPartition(index, id/ShardWidth, c.partitionN)
}
// ShardNodes returns a list of nodes that own a fragment. Safe for concurrent use.
func (c *cluster) ShardNodes(index string, shard uint64) []*topology.Node {
c.mu.RLock()
@ -970,13 +997,6 @@ func (c *cluster) keyNodes(index, key string) []*topology.Node {
return c.partitionNodes(c.Topology.KeyPartition(index, key))
}
// ownsShard returns true if a host owns a fragment.
func (c *cluster) ownsShard(nodeID string, index string, shard uint64) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return topology.Nodes(c.shardNodes(index, shard)).ContainsID(nodeID)
}
// partitionNodes returns a list of nodes that own a partition. unprotected.
func (c *cluster) partitionNodes(partitionID int) []*topology.Node {
// Default replica count to between one and the number of nodes.
@ -1148,6 +1168,7 @@ func (c *cluster) setup() error {
return nil
}
// open is only used in internal tests.
func (c *cluster) open() error {
err := c.setup()
if err != nil {
@ -1475,10 +1496,14 @@ func (c *cluster) unprotectedGenerateResizeJobByAction(nodeAction nodeAction) (*
j.IDs[node.ID] = true
continue
}
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(c.unprotectedNoder, c.Hasher, c.ReplicaN)
instr := &ResizeInstruction{
JobID: j.ID,
Node: toCluster.unprotectedNodeByID(node.ID),
Coordinator: c.unprotectedCoordinatorNode(),
Coordinator: snap.PrimaryFieldTranslationNode(),
Sources: fragmentSourcesByNode[node.ID],
TranslationSources: translationSourcesByNode[node.ID],
NodeStatus: c.nodeStatus(), // Include the NodeStatus in order to ensure that schema and availableShards are in sync on the receiving node.
@ -1499,7 +1524,9 @@ func (c *cluster) completeCurrentJob(state string) error {
}
func (c *cluster) unprotectedCompleteCurrentJob(state string) error {
if !c.unprotectedIsCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(c.unprotectedNoder, c.Hasher, c.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(c.Node.ID) {
return ErrNodeNotCoordinator
}
if c.currentJob == nil {
@ -1661,7 +1688,6 @@ func (c *cluster) followResizeInstruction(instr *ResizeInstruction) error {
}
func (c *cluster) markResizeInstructionComplete(complete *ResizeInstructionComplete) error {
j := c.job(complete.JobID)
// Abort the job if an error exists in the complete object.
@ -2423,36 +2449,24 @@ func (c *cluster) unprotectedPrimaryReplicaNode() *topology.Node {
return c.nodes[pos-1]
}
// setStatic is unprotected, but only called before the cluster has been started
// (and therefore not concurrently).
func (c *cluster) setStatic(hosts []string) error {
c.Static = true
c.Coordinator = c.Node.ID
for _, address := range hosts {
uri, err := pnet.NewURIFromAddress(address)
if err != nil {
return errors.Wrap(err, "getting URI")
}
c.nodes = append(c.nodes, &topology.Node{URI: *uri})
}
return nil
}
// translateFieldKeys is basically a wrapper around
// field.TranslateStore().TranslateKey(key), but in
// the case where the local node is not coordinator, then this method will forward the translation
// request to the coordinator.
func (c *cluster) translateFieldKeys(ctx context.Context, field *Field, keys []string, writable bool) (ids []uint64, err error) {
coordinator := c.coordinatorNode()
if coordinator == nil {
// 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) keys(%v) - cannot find coordinator node", field.Index(), field.Name(), keys)
}
if c.Node.ID == coordinator.ID {
if c.Node.ID == primary.ID {
ids, err = field.TranslateStore().TranslateKeys(keys, writable)
} else {
// If it's writable, then forward the request to the coordinator.
ids, err = c.InternalClient.TranslateKeysNode(ctx, &coordinator.URI, field.Index(), field.Name(), keys, writable)
ids, err = c.InternalClient.TranslateKeysNode(ctx, &primary.URI, field.Index(), field.Name(), keys, writable)
}
if err != nil {
@ -2470,7 +2484,7 @@ func (c *cluster) findFieldKeys(ctx context.Context, field *Field, keys ...strin
}
if !field.Keys() {
return nil, errors.Wrap(ErrTranslatingKeyNotFound, "field is not keyed")
return nil, errors.Wrap(ErrTranslatingKeyNotFound, "field is not keyed 1")
}
// Attempt to find the keys locally.
@ -2533,7 +2547,7 @@ func (c *cluster) createFieldKeys(ctx context.Context, field *Field, keys ...str
}
if !field.Keys() {
return nil, errors.Wrap(ErrTranslatingKeyNotFound, "field is not keyed")
return nil, errors.Wrap(ErrTranslatingKeyNotFound, "field is not keyed 2")
}
// The coordinator is the only node that can create field keys, since it owns the authoritative copy.
@ -2612,15 +2626,18 @@ func (c *cluster) translateFieldIDs(field *Field, ids map[uint64]struct{}) (map[
}
func (c *cluster) translateFieldListIDs(field *Field, ids []uint64) (keys []string, err error) {
coordinator := c.coordinatorNode()
if coordinator == nil {
// 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 coordinator node", field.Index(), field.Name(), ids)
}
if c.Node.ID == coordinator.ID {
if c.Node.ID == primary.ID {
keys, err = field.TranslateStore().TranslateIDs(ids)
} else {
keys, err = c.InternalClient.TranslateIDsNode(context.Background(), &coordinator.URI, field.Index(), field.Name(), ids)
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)
@ -2693,10 +2710,13 @@ func (c *cluster) translateIndexKeySet(ctx context.Context, indexName string, ke
return nil, ErrIndexNotFound
}
// 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 keySet {
partitionID := c.Topology.KeyPartition(indexName, key)
partitionID := snap.KeyToKeyPartition(indexName, key)
keysByPartition[partitionID] = append(keysByPartition[partitionID], key)
}
@ -2710,7 +2730,7 @@ func (c *cluster) translateIndexKeySet(ctx context.Context, indexName string, ke
g.Go(func() (err error) {
var ids []uint64
primary := c.primaryPartitionNode(partitionID)
primary := snap.PrimaryPartitionNode(partitionID)
if primary == nil {
return errors.Errorf("translating index(%s) keys(%v) on partition(%d) - cannot find primary node", indexName, keys, partitionID)
}
@ -2973,10 +2993,13 @@ func (c *cluster) translateIndexIDSet(ctx context.Context, indexName string, idS
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 := c.idPartition(indexName, id)
partitionID := snap.IDToShardPartition(indexName, id)
idsByPartition[partitionID] = append(idsByPartition[partitionID], id)
}
@ -2990,7 +3013,7 @@ func (c *cluster) translateIndexIDSet(ctx context.Context, indexName string, idS
g.Go(func() (err error) {
var keys []string
primary := c.primaryPartitionNode(partitionID)
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)
}

View file

@ -892,6 +892,13 @@ func TestCluster_ResizeStates(t *testing.T) {
t.Fatal(err)
}
// Close TestCluster with defer.
defer func() {
if err := tc.Close(); err != nil {
t.Fatal(err)
}
}()
// Add Bit Data to node0.
if err := tc.CreateField("i", "f", OptFieldTypeDefault()); err != nil {
t.Fatalf("creating field: %v", err)
@ -962,11 +969,6 @@ func TestCluster_ResizeStates(t *testing.T) {
} else if !bytes.Equal(chksum, node0Checksum) {
t.Fatalf("expected standard view checksum to match: %x - %x", chksum, node0Checksum)
}
// Close TestCluster.
if err := tc.Close(); err != nil {
t.Fatal(err)
}
})
}

View file

@ -32,7 +32,7 @@ import (
)
func Test_Repair(t *testing.T) {
t.Skip("I don't quite understand what this test is doing and will need help adjusting it to pass again.")
// a) setup 1 primary + 3 replicas of disagree-ing cluster dirs.
nNodes := 4
@ -402,6 +402,9 @@ func check(dirs []string, cfg *FsckConfig, targetIndex string, targetPartition i
return firstChecksum, nil
}
// These are here to satisfy the linter in CI while the test is being skipped.
var _ = getFwdRev
var _ = check
var _ = getChecksums
func getChecksums(dirs []string, cfg *FsckConfig, targetPartition int) (chksum []string) {

View file

@ -139,11 +139,6 @@ func parseOptions(opt Options) *embed.Config {
copy(lcs, opt.LClientSocket)
cfg.LClientSocket = lcs
cfg.Logger = "zap"
cfg.ZapLoggerBuilder = func(*embed.Config) error {
return nil
}
if opt.InitCluster != "" {
cfg.InitialCluster = opt.InitCluster
cfg.ClusterState = embed.ClusterStateFlagNew

View file

@ -151,7 +151,6 @@ func (e *executor) Close() error {
// Execute executes a PQL query.
func (e *executor) Execute(ctx context.Context, index string, q *pql.Query, shards []uint64, opt *execOptions) (QueryResponse, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "Executor.Execute")
span.LogKV("pql", q.String())
defer span.Finish()
@ -1513,7 +1512,18 @@ func executeDistinctShardSet(ctx context.Context, qcx *Qcx, idx *Index, fieldNam
fragData, _, err := tx.ContainerIterator(index, fieldName, "standard", shard, 0)
switch errors.Cause(err) {
case ViewNotFound, FragmentNotFound:
return nil, nil
// It may seem reasonable to return `nil` here in the case where the
// fragment for this shard does not exist. The problem with doing that
// is that if this operation is being performed on a remote node, then
// this result is going to get serialized as a QueryResponse and sent
// back to the original, non-remote node. When this happens, the
// encodeRow/decodeRow logic replaces `nil` with an empty `Row`. An
// empty Row will cause problems during the union step of the reduce
// phase if it is the "left" side of the union, because then the
// resulting Row after the union will have blank Index and Field values.
// Here, we ensure that we send a non-nil Row with valid Index and Field
// values so that the union step doesn't cause problems.
return &Row{Index: index, Field: fieldName}, nil
case nil:
default:
return nil, errors.Wrap(err, "getting fragment data")
@ -4712,8 +4722,11 @@ func (e *executor) executeClearBitField(ctx context.Context, qcx *Qcx, index str
shard := colID / ShardWidth
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(e.Cluster.noder, e.Cluster.Hasher, e.Cluster.ReplicaN)
ret := false
for _, node := range e.Cluster.shardNodes(index, shard) {
for _, node := range snap.ShardNodes(index, shard) {
// Update locally if host matches.
if node.ID == e.Node.ID {
@ -5070,7 +5083,10 @@ func (e *executor) executeSetBitField(ctx context.Context, qcx *Qcx, index strin
shard := colID / ShardWidth
ret := false
for _, node := range e.Cluster.shardNodes(index, shard) {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(e.Cluster.noder, e.Cluster.Hasher, e.Cluster.ReplicaN)
for _, node := range snap.ShardNodes(index, shard) {
// Update locally if host matches.
if node.ID == e.Node.ID {
@ -5113,7 +5129,10 @@ func (e *executor) executeSetValueField(ctx context.Context, qcx *Qcx, index str
shard := colID / ShardWidth
ret := false
for _, node := range e.Cluster.shardNodes(index, shard) {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(e.Cluster.noder, e.Cluster.Hasher, e.Cluster.ReplicaN)
for _, node := range snap.ShardNodes(index, shard) {
// Update locally if host matches.
if node.ID == e.Node.ID {
@ -5157,10 +5176,12 @@ func (e *executor) executeClearValueField(ctx context.Context, qcx *Qcx, index s
shard := colID / ShardWidth
ret := false
for _, node := range e.Cluster.shardNodes(index, shard) {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(e.Cluster.noder, e.Cluster.Hasher, e.Cluster.ReplicaN)
for _, node := range snap.ShardNodes(index, shard) {
// Update locally if host matches.
if node.ID == e.Node.ID {
idx := e.Holder.Index(index)
tx, finisher, err := qcx.GetTx(Txo{Write: writable, Index: idx, Shard: shard})
if err != nil {
@ -5441,9 +5462,15 @@ func (e *executor) remoteExec(ctx context.Context, node *topology.Node, index st
func (e *executor) shardsByNode(nodes []*topology.Node, index string, shards []uint64) (map[*topology.Node][]uint64, error) {
m := make(map[*topology.Node][]uint64)
// Create a snapshot of the cluster to use for node/partition calculations.
// We use e.Cluster.Nodes() here instead of e.Cluster.noder because we need
// the node states in order to ensure that we don't include an unavailable
// node in the map of nodes to which we distribute the query.
snap := topology.NewClusterSnapshot(topology.NewLocalNoder(e.Cluster.Nodes()), e.Cluster.Hasher, e.Cluster.ReplicaN)
loop:
for _, shard := range shards {
for _, node := range e.Cluster.ShardNodes(index, shard) {
for _, node := range snap.ShardNodes(index, shard) {
if topology.Nodes(nodes).Contains(node) {
m[node] = append(m[node], shard)
continue loop

View file

@ -2957,11 +2957,11 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
hldr0 := c.GetHolder(0)
hldr1 := c.GetHolder(1)
_, err := c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
_, err := c.GetCoordinator().API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "i", "f", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -2994,7 +2994,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("remote with timestamp", func(t *testing.T) {
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "z", pilosa.OptFieldTypeTime("Y"))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "i", "z", pilosa.OptFieldTypeTime("Y"))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3009,7 +3009,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("remote topn", func(t *testing.T) {
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "fn", pilosa.OptFieldTypeSet(pilosa.CacheTypeRanked, 100))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "i", "fn", pilosa.OptFieldTypeSet(pilosa.CacheTypeRanked, 100))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3056,7 +3056,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("remote groupBy", func(t *testing.T) {
if res, err := c.GetNode(1).API.Query(context.Background(), &pilosa.QueryRequest{
if res, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{
Index: "i",
Query: `GroupBy(Rows(f))`,
}); err != nil {
@ -3072,7 +3072,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("remote groupBy on ints", func(t *testing.T) {
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "fint", pilosa.OptFieldTypeInt(-1000, 1000))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "i", "fint", pilosa.OptFieldTypeInt(-1000, 1000))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3114,7 +3114,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("groupBy on ints with offset regression", func(t *testing.T) {
_, err = c.GetNode(0).API.CreateField(context.Background(), "i", "hint", pilosa.OptFieldTypeInt(1, 1000))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "i", "hint", pilosa.OptFieldTypeInt(1, 1000))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3145,12 +3145,12 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("Row on ints with ASSIGN condition", func(t *testing.T) {
_, err := c.GetNode(0).API.CreateIndex(context.Background(), "intidx", pilosa.IndexOptions{})
_, err := c.GetCoordinator().API.CreateIndex(context.Background(), "intidx", pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "intidx", "gint", pilosa.OptFieldTypeInt(-1000, 1000))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "intidx", "gint", pilosa.OptFieldTypeInt(-1000, 1000))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3180,12 +3180,12 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("Row on decimals with ASSIGN condition", func(t *testing.T) {
_, err := c.GetNode(0).API.CreateIndex(context.Background(), "decidx", pilosa.IndexOptions{})
_, err := c.GetCoordinator().API.CreateIndex(context.Background(), "decidx", pilosa.IndexOptions{})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "decidx", "fdec", pilosa.OptFieldTypeDecimal(0))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "decidx", "fdec", pilosa.OptFieldTypeDecimal(0))
if err != nil {
t.Fatalf("creating field: %v", err)
}
@ -3214,19 +3214,19 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
})
t.Run("Row on foreign key with ASSIGN condition", func(t *testing.T) {
_, err := c.GetNode(0).API.CreateIndex(context.Background(), "parent", pilosa.IndexOptions{Keys: true})
_, err := c.GetCoordinator().API.CreateIndex(context.Background(), "parent", pilosa.IndexOptions{Keys: true})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "parent", "general", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
_, err = c.GetCoordinator().API.CreateField(context.Background(), "parent", "general", pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, pilosa.DefaultCacheSize))
if err != nil {
t.Fatalf("creating field: %v", err)
}
_, err = c.GetNode(0).API.CreateIndex(context.Background(), "child", pilosa.IndexOptions{Keys: false})
_, err = c.GetCoordinator().API.CreateIndex(context.Background(), "child", pilosa.IndexOptions{Keys: false})
if err != nil {
t.Fatalf("creating index: %v", err)
}
_, err = c.GetNode(0).API.CreateField(context.Background(), "child", "parentid",
_, err = c.GetCoordinator().API.CreateField(context.Background(), "child", "parentid",
pilosa.OptFieldForeignIndex("parent"),
pilosa.OptFieldTypeInt(-9223372036854775808, 9223372036854775807),
)
@ -3264,7 +3264,7 @@ func TestExecutor_Execute_Remote_Row(t *testing.T) {
func TestExecutor_Execute_ErrMaxWritesPerRequest(t *testing.T) {
c := test.MustNewCluster(t, 1)
defer c.Close()
c.GetNode(0).Config.MaxWritesPerRequest = 3
c.GetIdleNode(0).Config.MaxWritesPerRequest = 3
err := c.Start()
if err != nil {
t.Fatal(err)
@ -4494,7 +4494,7 @@ func TestExecutor_Execute_SetRow(t *testing.T) {
func benchmarkExistence(nn bool, b *testing.B) {
c := test.MustNewCluster(b, 1)
var err error
c.GetNode(0).Config.DataDir, err = testhook.TempDirInDir(b, *TempDir, "benchmarkExistence")
c.GetIdleNode(0).Config.DataDir, err = testhook.TempDirInDir(b, *TempDir, "benchmarkExistence")
if err != nil {
b.Fatalf("getting temp dir: %v", err)
}
@ -5934,7 +5934,7 @@ func TestExecutor_Execute_GroupBy(t *testing.T) {
func BenchmarkGroupBy(b *testing.B) {
c := test.MustNewCluster(b, 1)
var err error
c.GetNode(0).Config.DataDir, err = testhook.TempDirInDir(b, *TempDir, "benchmarkGroupBy-")
c.GetIdleNode(0).Config.DataDir, err = testhook.TempDirInDir(b, *TempDir, "benchmarkGroupBy-")
if err != nil {
b.Fatalf("getting temp dir: %v", err)
}

View file

@ -106,7 +106,8 @@ func (g *memberSet) Open() (err error) {
// Close attempts to gracefully leave the cluster, and finally calls shutdown
// after (at most) a timeout period.
func (g *memberSet) Close() error {
g.eventReceiver.Close()
defer g.eventReceiver.Close()
leaveErr := g.memberlist.Leave(5 * time.Second)
shutdownErr := g.memberlist.Shutdown()
if leaveErr != nil || shutdownErr != nil {

View file

@ -1343,6 +1343,10 @@ func (s *holderSyncer) SyncHolder() error {
s.mu.Lock() // only allow one instance of SyncHolder to be running at a time
defer s.mu.Unlock()
ti := time.Now()
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(s.Cluster.noder, s.Cluster.Hasher, s.Cluster.ReplicaN)
// Iterate over schema in sorted order.
for _, di := range s.Holder.Schema() {
// Verify syncer has not closed.
@ -1377,7 +1381,7 @@ func (s *holderSyncer) SyncHolder() error {
itr.Seek(0)
for shard, eof := itr.Next(); !eof; shard, eof = itr.Next() {
// Ignore shards that this host doesn't own.
if !s.Cluster.ownsShard(s.Node.ID, di.Name, shard) {
if !snap.OwnsShard(s.Node.ID, di.Name, shard) {
continue
}
@ -1539,16 +1543,19 @@ func (s *holderSyncer) resetTranslationSync() error {
return errors.Wrap(err, "stop translation sync")
}
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(s.Cluster.noder, s.Cluster.Hasher, s.Cluster.ReplicaN)
// Set read-only flag for all translation stores.
s.setTranslateReadOnlyFlags()
s.setTranslateReadOnlyFlags(snap)
// Connect to each node that has a primary for which we are a replica.
if err := s.initializeIndexTranslateReplication(); err != nil {
if err := s.initializeIndexTranslateReplication(snap); err != nil {
return errors.Wrap(err, "initialize index translate replication")
}
// Connect to coordinator to stream field data.
if err := s.initializeFieldTranslateReplication(); err != nil {
if err := s.initializeFieldTranslateReplication(snap); err != nil {
return errors.Wrap(err, "initialize field translate replication")
}
return nil
@ -1619,9 +1626,9 @@ func (s *holderSyncer) stopTranslationSync() error {
// setTranslateReadOnlyFlags updates all translation stores to enable or disable
// writing new translation keys. Index stores are writable if the node owns the
// partition. Field stores are writable if the node is the coordinator.
func (s *holderSyncer) setTranslateReadOnlyFlags() {
func (s *holderSyncer) setTranslateReadOnlyFlags(snap *topology.ClusterSnapshot) {
s.Cluster.mu.RLock()
isCoordinator := s.Cluster.unprotectedIsCoordinator()
isPrimaryFieldTranslator := snap.IsPrimaryFieldTranslationNode(s.Cluster.Node.ID)
for _, index := range s.Holder.Indexes() {
// There is a race condition here:
@ -1642,8 +1649,8 @@ func (s *holderSyncer) setTranslateReadOnlyFlags() {
//
// Update: there was another path down to Index.Close(), so
// we shrink to lock to be inside index.TranslateStore() now.
for partitionID := 0; partitionID < s.Cluster.partitionN; partitionID++ {
primary := s.Cluster.unprotectedPrimaryPartitionNode(partitionID)
for partitionID := 0; partitionID < snap.PartitionN; partitionID++ {
primary := snap.PrimaryPartitionNode(partitionID)
isPrimary := primary != nil && s.Node.ID == primary.ID
if ts := index.TranslateStore(partitionID); ts != nil {
@ -1652,7 +1659,7 @@ func (s *holderSyncer) setTranslateReadOnlyFlags() {
}
for _, field := range index.Fields() {
field.TranslateStore().SetReadOnly(!isCoordinator)
field.TranslateStore().SetReadOnly(!isPrimaryFieldTranslator)
}
}
s.Cluster.mu.RUnlock()
@ -1660,8 +1667,8 @@ func (s *holderSyncer) setTranslateReadOnlyFlags() {
// initializeIndexTranslateReplication connects to each node that is the
// primary for a partition that we are a replica of.
func (s *holderSyncer) initializeIndexTranslateReplication() error {
for _, node := range s.Cluster.Nodes() {
func (s *holderSyncer) initializeIndexTranslateReplication(snap *topology.ClusterSnapshot) error {
for _, node := range snap.Nodes {
// Skip local node.
if node.ID == s.Node.ID {
continue
@ -1673,8 +1680,8 @@ func (s *holderSyncer) initializeIndexTranslateReplication() error {
if !index.Keys() {
continue
}
for partitionID := 0; partitionID < s.Cluster.partitionN; partitionID++ {
partitionNodes := s.Cluster.partitionNodes(partitionID)
for partitionID := 0; partitionID < snap.PartitionN; partitionID++ {
partitionNodes := snap.PartitionNodes(partitionID)
isPrimary := partitionNodes[0].ID == node.ID // remote is primary?
isReplica := topology.Nodes(partitionNodes[1:]).ContainsID(s.Node.ID) // local is replica?
if !isPrimary || !isReplica {
@ -1713,9 +1720,9 @@ func (s *holderSyncer) initializeIndexTranslateReplication() error {
}
// initializeFieldTranslateReplication connects the coordinator to stream field data.
func (s *holderSyncer) initializeFieldTranslateReplication() error {
func (s *holderSyncer) initializeFieldTranslateReplication(snap *topology.ClusterSnapshot) error {
// Skip if coordinator.
if s.Cluster.isCoordinator() {
if !snap.IsPrimaryFieldTranslationNode(s.Cluster.Node.ID) {
return nil
}
@ -1737,9 +1744,9 @@ func (s *holderSyncer) initializeFieldTranslateReplication() error {
return nil
}
// Connect to coordinator and begin streaming.
coordinator := s.Cluster.coordinatorNode()
rd, err := s.Holder.OpenTranslateReader(context.Background(), coordinator.URI.String(), m)
// Connect to primary and begin streaming.
primary := snap.PrimaryFieldTranslationNode()
rd, err := s.Holder.OpenTranslateReader(context.Background(), primary.URI.String(), m)
if err != nil {
return err
}
@ -1754,6 +1761,9 @@ func (s *holderSyncer) initializeFieldTranslateReplication() error {
}
func (s *holderSyncer) readIndexTranslateReader(rd TranslateEntryReader) {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(s.Cluster.noder, s.Cluster.Hasher, s.Cluster.ReplicaN)
for {
var entry TranslateEntry
if err := rd.ReadEntry(&entry); err != nil {
@ -1769,7 +1779,7 @@ func (s *holderSyncer) readIndexTranslateReader(rd TranslateEntryReader) {
}
// Apply replication to store.
store := idx.TranslateStore(s.Cluster.Topology.KeyPartition(entry.Index, entry.Key))
store := idx.TranslateStore(snap.KeyToKeyPartition(entry.Index, entry.Key))
if err := store.ForceSet(entry.ID, entry.Key); err != nil {
s.Holder.Logger.Printf("cannot force set index translation data: %d=%q", entry.ID, entry.Key)
return
@ -1825,6 +1835,9 @@ func (c *holderCleaner) IsClosing() bool {
// CleanHolder compares the holder with the cluster state and removes
// any unnecessary fragments and files.
func (c *holderCleaner) CleanHolder() error {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(c.Cluster.unprotectedNoder, c.Cluster.Hasher, c.Cluster.ReplicaN)
for _, index := range c.Holder.Indexes() {
// Verify cleaner has not closed.
if c.IsClosing() {
@ -1832,7 +1845,7 @@ func (c *holderCleaner) CleanHolder() error {
}
// Get the fragments that node is responsible for (based on hash(index, node)).
containedShards := c.Cluster.containsShards(index.Name(), index.AvailableShards(includeRemote), c.Node)
containedShards := snap.ContainsShards(index.Name(), index.AvailableShards(includeRemote), c.Node)
// Get the fragments registered in memory.
for _, field := range index.Fields() {

View file

@ -432,10 +432,10 @@ func TestHolder_DeleteIndex(t *testing.T) {
// Ensure holder can sync with a remote holder.
func TestHolderSyncer_SyncHolder(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 2
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 2
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
@ -544,12 +544,12 @@ func TestHolderSyncer_SyncHolder(t *testing.T) {
// the row boundaries of the block.
func TestHolderSyncer_BlockIteratorLimits(t *testing.T) {
c := test.MustNewCluster(t, 3)
c.GetNode(0).Config.Cluster.ReplicaN = 3
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 3
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetNode(2).Config.Cluster.ReplicaN = 3
c.GetNode(2).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 3
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 3
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(2).Config.Cluster.ReplicaN = 3
c.GetIdleNode(2).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
@ -601,10 +601,12 @@ func TestHolderSyncer_BlockIteratorLimits(t *testing.T) {
// Ensure holder correctly handles clears during block sync.
func TestHolderSyncer_Clears(t *testing.T) {
c := test.MustNewCluster(t, 3)
c.GetNode(0).Config.Cluster.ReplicaN = 3
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 3
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 3
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 3
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(2).Config.Cluster.ReplicaN = 3
c.GetIdleNode(2).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
@ -650,10 +652,10 @@ func TestHolderSyncer_Clears(t *testing.T) {
// Ensure holder can sync time quantum views with a remote holder.
func TestHolderSyncer_TimeQuantum(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 2
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 2
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
@ -703,10 +705,10 @@ func TestHolderSyncer_TimeQuantum(t *testing.T) {
func TestHolderSyncer_IntField(t *testing.T) {
t.Run("BasicSync", func(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 2
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 2
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)
@ -714,7 +716,6 @@ func TestHolderSyncer_IntField(t *testing.T) {
defer c.Close()
var idx0 *pilosa.Index
_ = idx0
idx0, err = c.GetNode(0).API.CreateIndex(context.Background(), "i", pilosa.IndexOptions{})
_ = idx0
if err != nil {
@ -761,10 +762,10 @@ func TestHolderSyncer_IntField(t *testing.T) {
t.Run("MultiShard", func(t *testing.T) {
c := test.MustNewCluster(t, 2)
c.GetNode(0).Config.Cluster.ReplicaN = 2
c.GetNode(0).Config.AntiEntropy.Interval = 0
c.GetNode(1).Config.Cluster.ReplicaN = 2
c.GetNode(1).Config.AntiEntropy.Interval = 0
c.GetIdleNode(0).Config.Cluster.ReplicaN = 2
c.GetIdleNode(0).Config.AntiEntropy.Interval = 0
c.GetIdleNode(1).Config.Cluster.ReplicaN = 2
c.GetIdleNode(1).Config.AntiEntropy.Interval = 0
err := c.Start()
if err != nil {
t.Fatalf("starting cluster: %v", err)

View file

@ -884,7 +884,7 @@ func (c *InternalClient) CreateField(ctx context.Context, index, field string) e
return c.CreateFieldWithOptions(ctx, index, field, pilosa.FieldOptions{})
}
// CreateField creates a new field on the server.
// CreateFieldWithOptions creates a new field on the server.
func (c *InternalClient) CreateFieldWithOptions(ctx context.Context, index, field string, opt pilosa.FieldOptions) error {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.CreateFieldWithOptions")
defer span.Finish()
@ -902,20 +902,26 @@ func (c *InternalClient) CreateFieldWithOptions(ctx context.Context, index, fiel
// should probably happen in the field anyway??
fieldOpt := fieldOptions{
Type: opt.Type,
Keys: &opt.Keys,
}
if fieldOpt.Type == pilosa.FieldTypeSet {
switch fieldOpt.Type {
case pilosa.FieldTypeSet, pilosa.FieldTypeMutex:
fieldOpt.CacheType = &opt.CacheType
fieldOpt.CacheSize = &opt.CacheSize
} else if fieldOpt.Type == pilosa.FieldTypeInt {
fieldOpt.Keys = &opt.Keys
case pilosa.FieldTypeInt:
fieldOpt.Min = &opt.Min
fieldOpt.Max = &opt.Max
} else if fieldOpt.Type == pilosa.FieldTypeTime {
case pilosa.FieldTypeTime:
fieldOpt.TimeQuantum = &opt.TimeQuantum
} else if fieldOpt.Type == pilosa.FieldTypeDecimal {
case pilosa.FieldTypeBool:
// pass
case pilosa.FieldTypeDecimal:
fieldOpt.Min = &opt.Min
fieldOpt.Max = &opt.Max
fieldOpt.Scale = &opt.Scale
default:
fieldOpt.Type = pilosa.DefaultFieldType
fieldOpt.Keys = &opt.Keys
}
// TODO: remove buf completely? (depends on whether importer needs to create specific field types)

View file

@ -49,15 +49,16 @@ func TestClient_MultiNode(t *testing.T) {
)
defer c.Close()
hldr := []test.Holder{}
for _, command := range c.Nodes {
hldr = append(hldr, test.Holder{Holder: command.Server.Holder()})
}
hldr0 := c.GetHolder(0)
hldr1 := c.GetHolder(1)
hldr2 := c.GetHolder(2)
// Create a dispersed set of bitmaps across 3 nodes such that each individual node and shard width increment would reveal a different TopN.
// Create a dispersed set of bitmaps across 3 nodes such that each
// individual node and shard width increment would reveal a different TopN.
shardNums := []uint64{1, 2, 6}
// This was generated with: `owns := s[i].Handler.Handler.API.Cluster.OwnsShards("i", 20, s[i].HostURI())`
// This was generated with:
// `owns := s[i].Handler.Handler.API.Cluster.OwnsShards("i", 20, s[i].HostURI())`
owns := [][]uint64{
{1, 3, 4, 8, 10, 13, 17, 19},
{2, 5, 7, 11, 12, 14, 18},
@ -96,26 +97,26 @@ func TestClient_MultiNode(t *testing.T) {
t.Fatalf("creating field: %v", err)
}
hldr[0].MustSetBits("i", "f", 100, baseBit0+10)
hldr[0].MustSetBits("i", "f", 4, baseBit0+10, baseBit0+11, baseBit0+12)
hldr[0].MustSetBits("i", "f", 4, baseBit0+10, baseBit0+11, baseBit0+12, baseBit0+13, baseBit0+14, baseBit0+15)
hldr[0].MustSetBits("i", "f", 2, baseBit0+1, baseBit0+2, baseBit0+3, baseBit0+4)
hldr[0].MustSetBits("i", "f", 3, baseBit0+1, baseBit0+2, baseBit0+3, baseBit0+4, baseBit0+5)
hldr[0].MustSetBits("i", "f", 22, baseBit0+1, baseBit0+2)
hldr0.MustSetBits("i", "f", 100, baseBit0+10)
hldr0.MustSetBits("i", "f", 4, baseBit0+10, baseBit0+11, baseBit0+12)
hldr0.MustSetBits("i", "f", 4, baseBit0+10, baseBit0+11, baseBit0+12, baseBit0+13, baseBit0+14, baseBit0+15)
hldr0.MustSetBits("i", "f", 2, baseBit0+1, baseBit0+2, baseBit0+3, baseBit0+4)
hldr0.MustSetBits("i", "f", 3, baseBit0+1, baseBit0+2, baseBit0+3, baseBit0+4, baseBit0+5)
hldr0.MustSetBits("i", "f", 22, baseBit0+1, baseBit0+2)
hldr[1].MustSetBits("i", "f", 99, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4)
hldr[1].MustSetBits("i", "f", 100, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5, baseBit1+6, baseBit1+7, baseBit1+8, baseBit1+9, baseBit1+10)
hldr[1].MustSetBits("i", "f", 98, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5, baseBit1+6)
hldr[1].MustSetBits("i", "f", 1, baseBit1+4)
hldr[1].MustSetBits("i", "f", 22, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5)
hldr1.MustSetBits("i", "f", 99, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4)
hldr1.MustSetBits("i", "f", 100, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5, baseBit1+6, baseBit1+7, baseBit1+8, baseBit1+9, baseBit1+10)
hldr1.MustSetBits("i", "f", 98, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5, baseBit1+6)
hldr1.MustSetBits("i", "f", 1, baseBit1+4)
hldr1.MustSetBits("i", "f", 22, baseBit1+1, baseBit1+2, baseBit1+3, baseBit1+4, baseBit1+5)
hldr[2].MustSetBits("i", "f", 24, baseBit2+10, baseBit2+11, baseBit2+12, baseBit2+13, baseBit2+14)
hldr[2].MustSetBits("i", "f", 20, baseBit2+10, baseBit2+11, baseBit2+12, baseBit2+13)
hldr[2].MustSetBits("i", "f", 21, baseBit2+10)
hldr[2].MustSetBits("i", "f", 100, baseBit2+10)
hldr[2].MustSetBits("i", "f", 99, baseBit2+10, baseBit2+11, baseBit2+12)
hldr[2].MustSetBits("i", "f", 98, baseBit2+10, baseBit2+11)
hldr[2].MustSetBits("i", "f", 22, baseBit2+10, baseBit2+11, baseBit2+12)
hldr2.MustSetBits("i", "f", 24, baseBit2+10, baseBit2+11, baseBit2+12, baseBit2+13, baseBit2+14)
hldr2.MustSetBits("i", "f", 20, baseBit2+10, baseBit2+11, baseBit2+12, baseBit2+13)
hldr2.MustSetBits("i", "f", 21, baseBit2+10)
hldr2.MustSetBits("i", "f", 100, baseBit2+10)
hldr2.MustSetBits("i", "f", 99, baseBit2+10, baseBit2+11, baseBit2+12)
hldr2.MustSetBits("i", "f", 98, baseBit2+10, baseBit2+11)
hldr2.MustSetBits("i", "f", 22, baseBit2+10, baseBit2+11, baseBit2+12)
// Rebuild the RankCache.
// We have to do this to avoid the 10-second cache invalidation delay
@ -1225,8 +1226,11 @@ func TestClientTransactions(t *testing.T) {
c := test.MustRunCluster(t, 3)
defer c.Close()
client0 := MustNewClient(c.GetNode(0).URL(), http.GetHTTPClient(nil))
client1 := MustNewClient(c.GetNode(1).URL(), http.GetHTTPClient(nil))
coord := c.GetCoordinator()
other := c.GetNonCoordinator()
client0 := MustNewClient(coord.URL(), http.GetHTTPClient(nil))
client1 := MustNewClient(other.URL(), http.GetHTTPClient(nil))
// can create, list, get, and finish a transaction
var expDeadline time.Time

View file

@ -448,7 +448,7 @@ func newRouter(handler *Handler) http.Handler {
// Endpoints to support lattice UI embedded via statik.
// The messiness here reflects the fact that assets live in a nontrivial
// directory structure that is controlled externally.
latticeHandler := NewStatikHandler(handler)
latticeHandler := newStatikHandler(handler)
router.PathPrefix("/static").Handler(latticeHandler)
router.Path("/").Handler(latticeHandler)
router.Path("/favicon.png").Handler(latticeHandler)
@ -499,11 +499,13 @@ type statikHandler struct {
statikFS http.FileSystem
}
// NewStatikHandler returns a new instance of statikHandler
func NewStatikHandler(h *Handler) statikHandler {
// newStatikHandler returns a new instance of statikHandler
func newStatikHandler(h *Handler) statikHandler {
fs, err := h.fileSystem.New()
if err == nil {
h.logger.Printf("enabled Web UI (%s) at %s", h.api.LatticeVersion(), h.api.Node().URI)
// TODO: we need to change the way this works because we don't have a node yet.
//h.logger.Printf("enabled Web UI (%s) at %s", h.api.LatticeVersion(), h.api.Node().URI)
h.logger.Printf("enabled Web UI (%s) at %s", h.api.LatticeVersion(), "TODO")
}
return statikHandler{

179
server.go
View file

@ -75,6 +75,9 @@ type Server struct { // nolint: maligned
sharder disco.Sharder
schemator disco.Schemator
// TODO: this is VERY temporary!!!
Gossiper Gossiper
// External
systemInfo SystemInfo
gcNotifier GCNotifier
@ -499,33 +502,10 @@ func NewServer(opts ...ServerOption) (*Server, error) {
//s.cluster.noder = s.noder
s.cluster.sharder = s.sharder
// Get or create NodeID.
s.nodeID = s.loadNodeID()
if s.isCoordinator {
s.cluster.Coordinator = s.nodeID
}
// Set Cluster Node.
node := &topology.Node{
ID: s.nodeID,
URI: s.uri,
GRPCURI: s.grpcURI,
IsCoordinator: s.cluster.Coordinator == s.nodeID,
State: nodeStateDown,
}
s.cluster.Node = node
if s.clusterDisabled {
err := s.cluster.setStatic(s.hosts)
if err != nil {
return nil, errors.Wrap(err, "setting cluster static")
}
}
// Append the NodeID tag to stats.
s.holder.Stats = s.holder.Stats.WithTags(fmt.Sprintf("node_id:%s", s.nodeID))
s.executor.Holder = s.holder
s.executor.Node = node
s.executor.Cluster = s.cluster
s.executor.MaxWritesPerRequest = s.maxWritesPerRequest
s.cluster.broadcaster = s
@ -534,11 +514,6 @@ func NewServer(opts ...ServerOption) (*Server, error) {
s.cluster.confirmDownSleep = s.confirmDownSleep
s.holder.broadcaster = s
err = s.cluster.setup()
if err != nil {
return nil, errors.Wrap(err, "setting up cluster")
}
return s, nil
}
@ -574,6 +549,10 @@ func (s *Server) UpAndDown() error {
return nil
}
type Gossiper interface {
StartGossip() error
}
// Open opens and initializes the server.
func (s *Server) Open() error {
s.logger.Printf("open server. PID %v", os.Getpid())
@ -591,13 +570,6 @@ func (s *Server) Open() error {
log.Println(errors.Wrap(err, "logging startup"))
}
// Set up the holderSyncer.
s.syncer.Holder = s.holder
s.syncer.Node = s.cluster.Node
s.syncer.Cluster = s.cluster
s.syncer.Closing = s.closing
s.syncer.Stats = s.holder.Stats.WithTags("component:HolderSyncer")
// Start background process listening for translation
// sync resets.
s.wg.Add(1)
@ -614,13 +586,28 @@ func (s *Server) Open() error {
_ = initState
// Set node ID.
// TODO: doesn't work yet, because we depend upon using the disk .id file, tests like
// TestHolderSyncer_BlockIteratorLimits for instance.
// s.nodeID = s.disCo.ID()
s.nodeID = s.disCo.ID()
node := &topology.Node{
ID: s.nodeID,
URI: s.uri,
GRPCURI: s.grpcURI,
IsCoordinator: s.isCoordinator,
State: nodeStateDown,
}
s.cluster.Node = node
s.executor.Node = node
// Set up the holderSyncer.
s.syncer.Holder = s.holder
s.syncer.Node = node
s.syncer.Cluster = s.cluster
s.syncer.Closing = s.closing
s.syncer.Stats = s.holder.Stats.WithTags("component:HolderSyncer")
node := s.cluster.node()
// TODO disco
if node != nil {
if false {
node.URI = s.uri
node.GRPCURI = s.grpcURI
@ -634,6 +621,18 @@ func (s *Server) Open() error {
}
}
err = s.cluster.setup()
if err != nil {
return errors.Wrap(err, "setting up cluster")
}
// ---------- TODO: this is temporary
if s.Gossiper != nil {
if err := s.Gossiper.StartGossip(); err != nil {
return errors.Wrap(err, "starting gossip")
}
}
// Open Cluster management.
if err := s.cluster.waitForStarted(); err != nil {
return errors.Wrap(err, "opening Cluster")
@ -679,65 +678,57 @@ func (s *Server) Open() error {
// Close closes the server and waits for it to shutdown.
func (s *Server) Close() error {
fmt.Println("--- disco: server close:", s.disCo.ID())
errE := s.executor.Close()
select {
case <-s.closing:
return nil
default:
// Notify goroutines to stop.
close(s.closing)
s.wg.Wait()
var errh, errd error
var errhs error
var errc error
fmt.Println("--- disco: server close:", s.disCo.ID())
errE := s.executor.Close()
if s.cluster != nil {
errc = s.cluster.close()
}
errhs = s.syncer.stopTranslationSync()
if s.disCo != nil {
fmt.Println("--- disco: try close:", s.disCo.ID())
errd = s.disCo.Close()
fmt.Println("--- disco: closed", s.disCo.ID(), errd)
}
if s.holder != nil {
errh = s.holder.Close()
}
if s.snapshotQueue != nil {
s.holder.SnapshotQueue = nil
s.snapshotQueue.Stop()
s.snapshotQueue = nil
}
// Notify goroutines to stop.
close(s.closing)
s.wg.Wait()
var errh, errd error
var errhs error
var errc error
// prefer to return holder error over cluster
// error. This order is somewhat arbitrary. It would be better if we had
// some way to combine all the errors, but probably not important enough to
// warrant the extra complexity.
if errh != nil {
return errors.Wrap(errh, "closing holder")
}
if errhs != nil {
return errors.Wrap(errhs, "terminating holder translation sync")
}
if errc != nil {
return errors.Wrap(errc, "closing cluster")
}
if errd != nil {
return errors.Wrap(errd, "closing disco")
}
return errors.Wrap(errE, "closing executor")
}
if s.cluster != nil {
errc = s.cluster.close()
}
errhs = s.syncer.stopTranslationSync()
if s.disCo != nil {
fmt.Println("--- disco: try close:", s.disCo.ID())
errd = s.disCo.Close()
fmt.Println("--- disco: closed", s.disCo.ID(), errd)
}
if s.holder != nil {
errh = s.holder.Close()
}
if s.snapshotQueue != nil {
s.holder.SnapshotQueue = nil
s.snapshotQueue.Stop()
s.snapshotQueue = nil
}
// loadNodeID gets NodeID from disk, or creates a new value.
// If server.NodeID is already set, a new ID is not created.
func (s *Server) loadNodeID() string {
if s.nodeID != "" {
return s.nodeID
// prefer to return holder error over cluster
// error. This order is somewhat arbitrary. It would be better if we had
// some way to combine all the errors, but probably not important enough to
// warrant the extra complexity.
if errh != nil {
return errors.Wrap(errh, "closing holder")
}
if errhs != nil {
return errors.Wrap(errhs, "terminating holder translation sync")
}
if errc != nil {
return errors.Wrap(errc, "closing cluster")
}
if errd != nil {
return errors.Wrap(errd, "closing disco")
}
return errors.Wrap(errE, "closing executor")
}
nodeID, err := s.holder.LoadNodeID()
if err != nil {
s.logger.Printf("loading NodeID: %v", err)
return s.nodeID
}
return nodeID
}
// NodeID returns the server's node id.

View file

@ -695,8 +695,8 @@ func TestCluster_GossipMembership(t *testing.T) {
func TestClusterResize_RemoveNode(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
m0 := cluster.GetNode(0)
m1 := cluster.GetNode(1)
coord := cluster.GetCoordinator()
other := cluster.GetNonCoordinator()
mustNodeID := func(baseURL string) string {
body := test.Do(t, "GET", fmt.Sprintf("%s/status", baseURL), "").Body
@ -712,7 +712,7 @@ func TestClusterResize_RemoveNode(t *testing.T) {
}
t.Run("ErrorRemoveInvalidNode", func(t *testing.T) {
resp := test.Do(t, "POST", m0.URL()+"/cluster/resize/remove-node", `{"id": "invalid-node-id"}`)
resp := test.Do(t, "POST", coord.URL()+"/cluster/resize/remove-node", `{"id": "invalid-node-id"}`)
expBody := "removing node: finding node to remove: node with provided ID does not exist"
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("expected StatusCode %d but got %d", http.StatusNotFound, resp.StatusCode)
@ -722,8 +722,8 @@ func TestClusterResize_RemoveNode(t *testing.T) {
})
t.Run("ErrorRemoveCoordinator", func(t *testing.T) {
nodeID := mustNodeID(m0.URL())
resp := test.Do(t, "POST", m0.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
nodeID := mustNodeID(coord.URL())
resp := test.Do(t, "POST", coord.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
expBody := "removing node: calling node leave: coordinator cannot be removed; first, make a different node the new coordinator"
if resp.StatusCode != http.StatusInternalServerError {
@ -734,9 +734,9 @@ func TestClusterResize_RemoveNode(t *testing.T) {
})
t.Run("ErrorRemoveOnNonCoordinator", func(t *testing.T) {
coordinatorNodeID := mustNodeID(m0.URL())
nodeID := mustNodeID(m1.URL())
resp := test.Do(t, "POST", m1.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
coordinatorNodeID := mustNodeID(coord.URL())
nodeID := mustNodeID(other.URL())
resp := test.Do(t, "POST", other.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
expBody := fmt.Sprintf("removing node: calling node leave: node removal requests are only valid on the coordinator node: %s", coordinatorNodeID)
if resp.StatusCode != http.StatusInternalServerError {
@ -747,7 +747,7 @@ func TestClusterResize_RemoveNode(t *testing.T) {
})
t.Run("ErrorRemoveWithoutReplicas", func(t *testing.T) {
client0 := m0.Client()
client0 := coord.Client()
// Create indexes and fields on one node.
if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists {
@ -763,12 +763,12 @@ func TestClusterResize_RemoveNode(t *testing.T) {
setColumns += fmt.Sprintf("Set(%d, f=1) ", i*pilosa.ShardWidth)
}
if _, err := m0.Query(t, "i", "", setColumns); err != nil {
if _, err := coord.Query(t, "i", "", setColumns); err != nil {
t.Fatal(err)
}
nodeID := mustNodeID(m1.URL())
resp := test.Do(t, "POST", m0.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
nodeID := mustNodeID(other.URL())
resp := test.Do(t, "POST", coord.URL()+"/cluster/resize/remove-node", fmt.Sprintf(`{"id": "%s"}`, nodeID))
expBody := "not enough data to perform resize"
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode)

View file

@ -1402,14 +1402,14 @@ func TestCluster_TranslateStore(t *testing.T) {
)
if err := port.GetPort(func(p int) error {
cluster.GetNode(0).Config.Gossip.Port = fmt.Sprintf("%d", p)
return cluster.GetNode(0).Start()
cluster.GetIdleNode(0).Config.Gossip.Port = fmt.Sprintf("%d", p)
return cluster.GetIdleNode(0).Start()
}, 10); err != nil {
t.Fatalf("starting node 0: %v", err)
}
defer cluster.GetNode(0).Close()
defer cluster.GetIdleNode(0).Close()
test.Do(t, "POST", cluster.GetNode(0).URL()+"/index/i0", "{\"options\": {\"keys\": true}}")
test.Do(t, "POST", cluster.GetIdleNode(0).URL()+"/index/i0", "{\"options\": {\"keys\": true}}")
}
func TestClusterTranslator(t *testing.T) {

View file

@ -152,6 +152,10 @@ func NewCommand(stdin io.Reader, stdout, stderr io.Writer, opts ...CommandOption
return c
}
func (m *Command) StartGossip() (err error) {
return m.setupNetworking()
}
// Start starts the pilosa server - it returns once the server is running.
func (m *Command) Start() (err error) {
// Seed random number generator
@ -163,12 +167,8 @@ func (m *Command) Start() (err error) {
return errors.Wrap(err, "setting up server")
}
// Set up networking (i.e. gossip)
// Gossip no longer unsed under etcd? time to turn it off here?
err = m.setupNetworking()
if err != nil {
return errors.Wrap(err, "setting up networking")
}
// TODO: this is temorary.
m.Server.Gossiper = m
go func() {
err := m.Handler.Serve()
@ -545,30 +545,36 @@ func (m *Command) GossipTransport() *gossip.Transport {
// Close shuts down the server.
func (m *Command) Close() error {
defer close(m.done)
eg := errgroup.Group{}
m.grpcServer.Stop()
eg.Go(m.Handler.Close)
eg.Go(m.Server.Close)
eg.Go(m.API.Close)
eg.Go(m.pgserver.Close)
if m.gossipMemberSet != nil {
eg.Go(m.gossipMemberSet.Close)
}
if closer, ok := m.logOutput.(io.Closer); ok {
// If closer is os.Stdout or os.Stderr, don't close it.
if closer != os.Stdout && closer != os.Stderr {
eg.Go(closer.Close)
select {
case <-m.done:
return nil
default:
defer close(m.done)
eg := errgroup.Group{}
m.grpcServer.Stop()
eg.Go(m.Handler.Close)
eg.Go(m.Server.Close)
eg.Go(m.API.Close)
eg.Go(m.pgserver.Close)
if m.gossipMemberSet != nil {
eg.Go(m.gossipMemberSet.Close)
}
if closer, ok := m.logOutput.(io.Closer); ok {
// If closer is os.Stdout or os.Stderr, don't close it.
if closer != os.Stdout && closer != os.Stderr {
eg.Go(closer.Close)
}
}
// prevent the closed sockets from being re-injected into etcd.
m.Config.DisCo.LPeerSocket = nil
m.Config.DisCo.LClientSocket = nil
err := eg.Wait()
_ = testhook.Closed(pilosa.NewAuditor(), m, nil)
return errors.Wrap(err, "closing everything")
}
// prevent the closed sockets from being re-injected into etcd.
m.Config.DisCo.LPeerSocket = nil
m.Config.DisCo.LClientSocket = nil
err := eg.Wait()
_ = testhook.Closed(pilosa.NewAuditor(), m, nil)
return errors.Wrap(err, "closing everything")
}
// newStatsClient creates a stats client from the config

View file

@ -387,32 +387,31 @@ func TestTransactionsAPI(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
api0 := cluster.GetNode(0).API
api1 := cluster.GetNode(1).API
coord := cluster.GetCoordinator().API
other := cluster.GetNonCoordinator().API
ctx := context.Background()
//api2 := cluster.GetNode(2).API
// can fetch empty transactions
if trnsMap, err := api0.Transactions(ctx); err != nil {
if trnsMap, err := coord.Transactions(ctx); err != nil {
t.Fatalf("getting transactions: %v", err)
} else if len(trnsMap) != 0 {
t.Fatalf("unexpectedly has transactions: %v", trnsMap)
}
// can't fetch transactions from non-coordinator
if _, err := api1.Transactions(ctx); err != pilosa.ErrNodeNotCoordinator {
if _, err := other.Transactions(ctx); err != pilosa.ErrNodeNotCoordinator {
t.Errorf("api1 should return ErrNodeNotCoordinator when asked for transactions but got: %v", err)
}
// can start transaction
if trns, err := api0.StartTransaction(ctx, "a", time.Minute, false, false); err != nil {
if trns, err := coord.StartTransaction(ctx, "a", time.Minute, false, false); err != nil {
t.Errorf("couldn't start transaction: %v", err)
} else {
test.CompareTransactions(t, &pilosa.Transaction{ID: "a", Active: true, Timeout: time.Minute, Deadline: time.Now().Add(time.Minute)}, trns)
}
// can retrieve transaction from other nodes with remote=true
if trns, err := api1.GetTransaction(ctx, "a", true); err != nil {
if trns, err := other.GetTransaction(ctx, "a", true); err != nil {
t.Errorf("couldn't fetch transaction from other node with remote=true: %v", err)
} else {
test.CompareTransactions(t, &pilosa.Transaction{ID: "a", Active: true, Timeout: time.Minute, Deadline: time.Now().Add(time.Minute)}, trns)
@ -420,7 +419,7 @@ func TestTransactionsAPI(t *testing.T) {
// can start transaction with blank id and get uuid back
id := ""
if trns, err := api0.StartTransaction(ctx, id, time.Minute, false, false); err != nil {
if trns, err := coord.StartTransaction(ctx, id, time.Minute, false, false); err != nil {
t.Errorf("couldn't start transaction: %v", err)
} else {
id = trns.ID
@ -431,54 +430,54 @@ func TestTransactionsAPI(t *testing.T) {
}
// can't finish transaction on non-coordinator
if _, err := api1.FinishTransaction(ctx, id, false); err != pilosa.ErrNodeNotCoordinator {
if _, err := other.FinishTransaction(ctx, id, false); err != pilosa.ErrNodeNotCoordinator {
t.Errorf("unexpected error is not ErrNodeNotCoordinator: %v", err)
}
// can finish transaction
if _, err := api0.FinishTransaction(ctx, id, false); err != nil {
if _, err := coord.FinishTransaction(ctx, id, false); err != nil {
t.Errorf("couldn't finish transaction: %v", err)
}
// can finish previous transaction
if _, err := api0.FinishTransaction(ctx, "a", false); err != nil {
if _, err := coord.FinishTransaction(ctx, "a", false); err != nil {
t.Errorf("couldn't finish transaction a: %v", err)
}
// can start exclusive transaction
if te, err := api0.StartTransaction(ctx, "exc", time.Minute, true, false); err != nil {
if te, err := coord.StartTransaction(ctx, "exc", time.Minute, true, false); err != nil {
t.Errorf("couldn't start exclusive transaction: %v", err)
} else if !te.Active {
t.Errorf("expected exclusive transaction to be active: %+v", te)
}
// can finish exclusive transaction
if _, err := api0.FinishTransaction(ctx, "exc", false); err != nil {
if _, err := coord.FinishTransaction(ctx, "exc", false); err != nil {
t.Errorf("couldn't finish exclusive transaction: %v", err)
}
// can start transaction (with same name as previous finished transaction)
if trns, err := api0.StartTransaction(ctx, "a", time.Minute, false, false); err != nil {
if trns, err := coord.StartTransaction(ctx, "a", time.Minute, false, false); err != nil {
t.Errorf("couldn't start transaction: %v", err)
} else {
test.CompareTransactions(t, &pilosa.Transaction{ID: "a", Active: true, Timeout: time.Minute, Deadline: time.Now().Add(time.Minute)}, trns)
}
// can start exclusive transaction and is not immediately active
if te, err := api0.StartTransaction(ctx, "exc", time.Minute, true, false); err != nil {
if te, err := coord.StartTransaction(ctx, "exc", time.Minute, true, false); err != nil {
t.Errorf("couldn't start exclusive transaction: %v", err)
} else if te.Active {
t.Errorf("expected exclusive transaction to be inactive: %+v", te)
}
// can finish non-exclusive transaction
if _, err := api0.FinishTransaction(ctx, "a", false); err != nil {
if _, err := coord.FinishTransaction(ctx, "a", false); err != nil {
t.Errorf("couldn't finish transaction a: %v", err)
}
// can poll exclusive transaction and is active
var excTrns *pilosa.Transaction
if trns, err := api0.GetTransaction(ctx, "exc", false); err != nil {
if trns, err := coord.GetTransaction(ctx, "exc", false); err != nil {
t.Errorf("couldn't poll exclusive transaction: %v", err)
} else {
excTrns = &pilosa.Transaction{ID: "exc", Active: true, Exclusive: true, Timeout: time.Minute, Deadline: time.Now().Add(time.Minute)}
@ -486,7 +485,7 @@ func TestTransactionsAPI(t *testing.T) {
}
// can't start another exclusive transaction
if trns, err := api0.StartTransaction(ctx, "exc2", time.Minute, true, false); errors.Cause(err) != pilosa.ErrTransactionExclusive {
if trns, err := coord.StartTransaction(ctx, "exc2", time.Minute, true, false); errors.Cause(err) != pilosa.ErrTransactionExclusive {
t.Errorf("unexpected error: %v", err)
} else {
// returned transaction should be the exclusive one which is blocking this one
@ -494,14 +493,14 @@ func TestTransactionsAPI(t *testing.T) {
}
// can't keep the second exclusive name but make it nonexclusive and start a transaction
if trns, err := api0.StartTransaction(ctx, "exc2", time.Minute, false, false); errors.Cause(err) != pilosa.ErrTransactionExclusive {
if trns, err := coord.StartTransaction(ctx, "exc2", time.Minute, false, false); errors.Cause(err) != pilosa.ErrTransactionExclusive {
t.Errorf("unexpected error: %v", err)
} else {
test.CompareTransactions(t, excTrns, trns)
}
// transaction is active on other nodes with remote=true
if trns, err := api1.GetTransaction(ctx, "exc", true); err != nil {
if trns, err := other.GetTransaction(ctx, "exc", true); err != nil {
t.Errorf("couldn't poll exclusive transaction: %v", err)
} else {
test.CompareTransactions(t, &pilosa.Transaction{ID: "exc", Active: true, Exclusive: true, Timeout: time.Minute, Deadline: time.Now().Add(time.Minute)}, trns)
@ -631,39 +630,22 @@ func TestClusteringNodesReplica1(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
if err != nil {
if err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond); err != nil {
t.Fatalf("starting cluster: %v", err)
}
if err := cluster.GetNode(2).Command.Close(); err != nil {
if err := cluster.GetNonCoordinator().Command.Close(); err != nil {
t.Fatalf("closing third node: %v", err)
}
if err := cluster.AwaitCoordinatorState(pilosa.ClusterStateStarting, 30*time.Second); err != nil {
t.Fatalf("starting cluster: %v", err)
}
// confirm that cluster stops accepting queries after one node closes
if _, err := cluster.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{}); !strings.Contains(err.Error(), "not allowed in state STARTING") {
if _, err := cluster.GetCoordinator().API.Query(context.Background(), &pilosa.QueryRequest{}); !strings.Contains(err.Error(), "not allowed in state STARTING") {
t.Fatalf("got unexpected error querying an incomplete cluster: %v", err)
}
// Create new main with the same config.
config := cluster.GetNode(2).Command.Config
config.Translation.MapSize = 100000
// this isn't necessary, but makes the test run way faster
config.Gossip.Port = strconv.Itoa(int(cluster.GetNode(2).Command.GossipTransport().URI.Port))
cluster.GetNode(2).Command = server.NewCommand(cluster.GetNode(2).Stdin, cluster.GetNode(2).Stdout, cluster.GetNode(2).Stderr, server.OptCommandServerOptions(pilosa.OptServerOpenTranslateStore(pilosa.OpenInMemTranslateStore)))
cluster.GetNode(2).Command.Config = config
// Run new program.
if err := cluster.GetNode(2).Start(); err != nil {
t.Fatalf("restarting node 2: %v", err)
}
err = cluster.AwaitState(pilosa.ClusterStateNormal, 200*time.Millisecond)
if err != nil {
t.Fatalf("resuming normal operations: %v", err)
}
}
func TestClusteringNodesReplica2(t *testing.T) {
@ -682,75 +664,35 @@ func TestClusteringNodesReplica2(t *testing.T) {
t.Fatalf("starting cluster: %v", err)
}
if err := cluster.GetNode(2).Command.Close(); err != nil {
coord, others := cluster.GetCoordinator(), cluster.GetNonCoordinators()
if err := others[0].Close(); err != nil {
t.Fatalf("closing third node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 100*time.Millisecond)
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 30*time.Second)
if err != nil {
t.Fatalf("after closing first server: %v", err)
}
// confirm that cluster keeps accepting queries if replication > 1
if _, err := cluster.GetNode(0).API.CreateIndex(context.Background(), "anewindex", pilosa.IndexOptions{}); err != nil {
if _, err := coord.API.CreateIndex(context.Background(), "anewindex", pilosa.IndexOptions{}); err != nil {
t.Fatalf("got unexpected error creating index: %v", err)
}
// confirm that cluster stops accepting queries if 2 nodes fail and replication == 2
if err := cluster.GetNode(1).Command.Close(); err != nil {
if err := others[1].Close(); err != nil {
t.Fatalf("closing 2nd node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateStarting, 100*time.Millisecond)
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateStarting, 30*time.Second)
if err != nil {
t.Fatalf("after closing second server: %v", err)
}
if _, err := cluster.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{}); !strings.Contains(err.Error(), "not allowed in state STARTING") {
if _, err := coord.API.Query(context.Background(), &pilosa.QueryRequest{}); !strings.Contains(err.Error(), "not allowed in state STARTING") {
t.Fatalf("got unexpected error querying an incomplete cluster: %v", err)
}
// Create new main with the same config.
config := cluster.GetNode(2).Command.Config
config.Translation.MapSize = 100000
// config.Bind = cluster.GetNode(2).API.Node().URI.HostPort()
// this isn't necessary, but makes the test run way faster
config.Gossip.Port = strconv.Itoa(int(cluster.GetNode(2).Command.GossipTransport().URI.Port))
cluster.GetNode(2).Command = server.NewCommand(cluster.GetNode(2).Stdin, cluster.GetNode(2).Stdout, cluster.GetNode(2).Stderr, server.OptCommandServerOptions(pilosa.OptServerOpenTranslateStore(pilosa.OpenInMemTranslateStore)))
cluster.GetNode(2).Command.Config = config
// Run new program.
if err := cluster.GetNode(2).Start(); err != nil {
t.Fatalf("restarting node 2: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 100*time.Millisecond)
if err != nil {
t.Fatalf("after restarting first server: %v", err)
}
// Create new main with the same config.
config = cluster.GetNode(1).Command.Config
// config.Bind = cluster.GetNode(1).API.Node().URI.HostPort()
config.Translation.MapSize = 100000
// this isn't necessary, but makes the test run way faster
config.Gossip.Port = strconv.Itoa(int(cluster.GetNode(1).Command.GossipTransport().URI.Port))
cluster.GetNode(1).Command = server.NewCommand(cluster.GetNode(1).Stdin, cluster.GetNode(1).Stdout, cluster.GetNode(1).Stderr, server.OptCommandServerOptions(pilosa.OptServerOpenTranslateStore(pilosa.OpenInMemTranslateStore)))
cluster.GetNode(1).Command.Config = config
// Run new program.
if err := cluster.GetNode(1).Start(); err != nil {
t.Fatalf("restarting node 1: %v", err)
}
err = cluster.AwaitState(pilosa.ClusterStateNormal, 200*time.Microsecond)
if err != nil {
t.Fatalf("resuming normal operations: %v", err)
}
}
func TestRemoveNodeAfterItDies(t *testing.T) {
@ -775,27 +717,28 @@ func TestRemoveNodeAfterItDies(t *testing.T) {
t.Fatalf("starting cluster: %v", err)
}
coord, others := cluster.GetCoordinator(), cluster.GetNonCoordinators()
// prevent double-closing cluster.GetNode(2) from the deferred Close above
disabled := cluster.GetNode(2)
if err := cluster.CloseAndRemove(2); err != nil {
disabled := others[0]
if err := disabled.Close(); err != nil {
t.Fatalf("closing third node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 100*time.Millisecond)
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 30*time.Second)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
if _, err := cluster.GetNode(0).API.RemoveNode(disabled.API.Node().ID); err != nil {
if _, err := coord.API.RemoveNode(disabled.API.Node().ID); err != nil {
t.Fatalf("removing failed node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateNormal, 100*time.Millisecond)
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateNormal, 30*time.Second)
if err != nil {
t.Fatalf("removing disabled node: %v", err)
}
hosts := cluster.GetNode(0).API.Hosts(context.Background())
hosts := coord.API.Hosts(context.Background())
if len(hosts) != 2 {
t.Fatalf("unexpected hosts: %v", hosts)
}

View file

@ -21,6 +21,7 @@ import (
"math"
"net"
"path"
"sort"
"strconv"
"strings"
"testing"
@ -56,7 +57,7 @@ func (c *Cluster) Query(t testing.TB, index, query string) pilosa.QueryResponse
t.Fatal("must have at least one node in cluster to query")
}
return c.Nodes[0].QueryAPI(t, &pilosa.QueryRequest{Index: index, Query: query})
return c.GetCoordinator().QueryAPI(t, &pilosa.QueryRequest{Index: index, Query: query})
}
// QueryHTTP executes a PQL query through the HTTP endpoint. It fails
@ -68,7 +69,7 @@ func (c *Cluster) QueryHTTP(t testing.TB, index, query string) (string, error) {
t.Fatal("must have at least one node in cluster to query")
}
return c.Nodes[0].Query(t, index, "", query)
return c.GetCoordinator().Query(t, index, "", query)
}
// QueryGRPC executes a PQL query through the GRPC endpoint. It fails the
@ -79,7 +80,7 @@ func (c *Cluster) QueryGRPC(t testing.TB, index, query string) *proto.TableRespo
t.Fatal("must have at least one node in cluster to query")
}
grpcClient, err := client.NewGRPCClient([]string{fmt.Sprintf("%s:%d", c.Nodes[0].Server.GRPCURI().Host, c.Nodes[0].Server.GRPCURI().Port)}, nil)
grpcClient, err := client.NewGRPCClient([]string{fmt.Sprintf("%s:%d", c.GetCoordinator().Server.GRPCURI().Host, c.GetCoordinator().Server.GRPCURI().Port)}, nil)
if err != nil {
t.Fatalf("getting GRPC client: %v", err)
}
@ -92,12 +93,100 @@ func (c *Cluster) QueryGRPC(t testing.TB, index, query string) *proto.TableRespo
return tableResp
}
func (c *Cluster) GetNode(n int) *Command {
// GetIdleNode gets the node at the given index. This method is used (instead of
// `GetNode()`) when the cluster has yet to be started. In that case, etcd has
// not assigned each node an ID, and therefore the nodes are not in their final,
// sorted order. In other words, this method can only be used to retrieve a node
// when order doesn't matter. An example is if you need to do something like
// this:
// c.GetNode(0).Config.Cluster.ReplicaN = 2
// c.GetNode(1).Config.Cluster.ReplicaN = 2
// In this example, the test needs the replication factor to be set to 2 before
// starting; it's ok to reference each node by its index in the pre-sorted node
// list. It's also safe to use this method after `MustRunCluster()` if the
// cluster contains only one node.
func (c *Cluster) GetIdleNode(n int) *Command {
return c.Nodes[n]
}
// GetNode gets the node at the given index; this method assumes the cluster has
// already been started. Because the node IDs are assigned randomly, they can be
// in an order that does not align with the test's expectations. For example, a
// test might create a 3-node cluster and retrieve them using `GetNode(0)`,
// `GetNode(1)`, and `GetNode(2)` respectively. But if the node IDs are `456`,
// `123`, `789`, then we actually want `GetNode(0)` to return `c.Nodes[1]`, and
// `GetNode(1)` to return `c.Nodes[0]`. This method looks at all the node IDs,
// sorts them, and then returns the node that the test expects.
func (c *Cluster) GetNode(n int) *Command {
// Put all the node IDs into a list to be sorted.
ids := make([]nodePlace, len(c.Nodes))
for i := range c.Nodes {
ids[i].id = c.Nodes[i].ID()
ids[i].idx = i
}
// Sort the list.
sort.SliceStable(ids, func(i, j int) bool {
return ids[i].id < ids[j].id
})
// Return the node which is at the given position in the sorted list.
return c.Nodes[ids[n].idx]
}
// GetCoordinator gets the node which has been determined to be the coordinator.
// This used to be node0 in tests, but since implementing etcd, the coordinator
// can be any node in the cluster, so we have to use this method in tests which
// need to act on the coordinator.
func (c *Cluster) GetCoordinator() *Command {
for _, n := range c.Nodes {
if n.IsCoordinator() {
return n
}
}
return nil
}
// GetNonCoordinator gets first first non-coordinator node in the list of nodes.
func (c *Cluster) GetNonCoordinator() *Command {
for _, n := range c.Nodes {
if !n.IsCoordinator() {
return n
}
}
return nil
}
// GetNonCoordinators gets all nodes except the coordinator.
func (c *Cluster) GetNonCoordinators() []*Command {
rtn := make([]*Command, 0)
for _, n := range c.Nodes {
if !n.IsCoordinator() {
rtn = append(rtn, n)
}
}
return rtn
}
// nodePlace represents a node's ID and its index into the c.Nodes slice.
type nodePlace struct {
id string
idx int
}
func (c *Cluster) GetHolder(n int) *Holder {
return &Holder{Holder: c.Nodes[n].Server.Holder()}
return &Holder{Holder: c.GetNode(n).Server.Holder()}
}
// GetCoordinatorHolder returns the Holder for the coordinator node.
func (c *Cluster) GetCoordinatorHolder() *Holder {
return &Holder{Holder: c.GetCoordinator().Server.Holder()}
}
// GetNonCoordinatorHolder returns the Holder for the the first non-coordinator
// node in the list of nodes.
func (c *Cluster) GetNonCoordinatorHolder() *Holder {
return &Holder{Holder: c.GetNonCoordinator().Server.Holder()}
}
func (c *Cluster) Len() int {
@ -119,7 +208,7 @@ func (c *Cluster) ImportBits(t testing.TB, index, field string, rowcols [][2]uin
rowIDs[i] = bit[0]
colIDs[i] = bit[1]
}
nodes, err := c.Nodes[0].API.ShardNodes(context.Background(), index, shard)
nodes, err := c.GetCoordinator().API.ShardNodes(context.Background(), index, shard)
if err != nil {
t.Fatalf("getting shard nodes: %v", err)
}
@ -162,7 +251,7 @@ func (c *Cluster) ImportKeyKey(t testing.TB, index, field string, valAndRecKeys
importRequest.RowKeys[i] = vk[0]
importRequest.ColumnKeys[i] = vk[1]
}
err := c.Nodes[0].API.Import(context.Background(), nil, importRequest)
err := c.GetCoordinator().API.Import(context.Background(), nil, importRequest)
if err != nil {
t.Fatalf("importing keykey data: %v", err)
}
@ -192,7 +281,7 @@ func (c *Cluster) ImportTimeQuantumKey(t testing.TB, index, field string, entrie
importRequest.Timestamps[i] = entry.Ts
}
err := c.Nodes[0].API.Import(context.Background(), nil, importRequest)
err := c.GetCoordinator().API.Import(context.Background(), nil, importRequest)
if err != nil {
t.Fatalf("importing keykey data: %v", err)
}
@ -218,7 +307,7 @@ func (c *Cluster) ImportIntKey(t testing.TB, index, field string, pairs []IntKey
importRequest.Values[i] = pair.Val
importRequest.ColumnKeys[i] = pair.Key
}
if err := c.Nodes[0].API.ImportValue(context.Background(), nil, importRequest); err != nil {
if err := c.GetCoordinator().API.ImportValue(context.Background(), nil, importRequest); err != nil {
t.Fatalf("importing IntKey data: %v", err)
}
}
@ -242,7 +331,7 @@ func (c *Cluster) ImportIntID(t testing.TB, index, field string, pairs []IntID)
importRequest.Values[i] = pair.Val
importRequest.ColumnIDs[i] = pair.ID
}
if err := c.Nodes[0].API.ImportValue(context.Background(), nil, importRequest); err != nil {
if err := c.GetCoordinator().API.ImportValue(context.Background(), nil, importRequest); err != nil {
t.Fatalf("importing IntID data: %v", err)
}
}
@ -267,7 +356,7 @@ func (c *Cluster) ImportIDKey(t testing.TB, index, field string, pairs []KeyID)
importRequest.RowIDs[i] = pair.ID
importRequest.ColumnKeys[i] = pair.Key
}
err := c.Nodes[0].API.Import(context.Background(), nil, importRequest)
err := c.GetCoordinator().API.Import(context.Background(), nil, importRequest)
if err != nil {
t.Fatalf("importing IDKey data: %v", err)
}
@ -276,11 +365,11 @@ func (c *Cluster) ImportIDKey(t testing.TB, index, field string, pairs []KeyID)
// CreateField creates the index (if necessary) and field specified.
func (c *Cluster) CreateField(t testing.TB, index string, iopts pilosa.IndexOptions, field string, fopts ...pilosa.FieldOption) *pilosa.Field {
t.Helper()
idx, err := c.Nodes[0].API.CreateIndex(context.Background(), index, iopts)
idx, err := c.GetCoordinator().API.CreateIndex(context.Background(), index, iopts)
if err != nil && !strings.Contains(err.Error(), "index already exists") {
t.Fatalf("creating index: %v", err)
} else if err != nil { // index exists
idx, err = c.Nodes[0].API.Index(context.Background(), index)
idx, err = c.GetCoordinator().API.Index(context.Background(), index)
if err != nil {
t.Fatalf("getting index: %v", err)
}
@ -289,7 +378,7 @@ func (c *Cluster) CreateField(t testing.TB, index string, iopts pilosa.IndexOpti
t.Logf("existing index options:\n%v\ndon't match given opts:\n%v\n in pilosa/test.Cluster.CreateField", idx.Options(), iopts)
}
f, err := c.Nodes[0].API.CreateField(context.Background(), index, field, fopts...)
f, err := c.GetCoordinator().API.CreateField(context.Background(), index, field, fopts...)
// we'll assume the field doesn't exist because checking if the options
// match seems painful.
if err != nil {
@ -364,6 +453,15 @@ func (c *Cluster) Close() error {
return nil
}
func (c *Cluster) CloseAndRemoveNonCoordinator() error {
for i, n := range c.Nodes {
if !n.IsCoordinator() {
return c.CloseAndRemove(i)
}
}
return errors.New("could not find non-coordinator node")
}
func (c *Cluster) CloseAndRemove(n int) error {
if n < 0 || n >= len(c.Nodes) {
return fmt.Errorf("close/remove from cluster: index %d out of range (len %d)", n, len(c.Nodes))
@ -380,7 +478,7 @@ func (c *Cluster) AwaitCoordinatorState(expectedState string, timeout time.Durat
if len(c.Nodes) < 1 {
return errors.New("can't await coordinator state on an empty cluster")
}
onlyCoordinator := &Cluster{Nodes: c.Nodes[:1]}
onlyCoordinator := &Cluster{Nodes: []*Command{c.GetCoordinator()}}
return onlyCoordinator.AwaitState(expectedState, timeout)
}

View file

@ -192,6 +192,9 @@ func (m *Command) URL() string { return m.API.Node().URI.String() }
// ID returns the node ID used by the running program.
func (m *Command) ID() string { return m.API.Node().ID }
// IsCoordinator returns true if this is the coordinator.
func (m *Command) IsCoordinator() bool { return m.API.Node().IsCoordinator }
// Client returns a client to connect to the program.
func (m *Command) Client() *http.InternalClient {
return m.Server.InternalClient().(*http.InternalClient)

View file

@ -68,9 +68,9 @@ func NewClusterSnapshot(noder Noder, hasher Hasher, replicas int) *ClusterSnapsh
//////////////////////////////////////////////////////////////////////////////
// shardToShardPartition returns the shard-partition that the given shard
// ShardToShardPartition returns the shard-partition that the given shard
// belongs to. NOTE: This is DIFFERENT from the key-partition.
func (c *ClusterSnapshot) shardToShardPartition(index string, shard uint64) int {
func (c *ClusterSnapshot) ShardToShardPartition(index string, shard uint64) int {
return dedupShardToShardPartition(index, shard, c.PartitionN)
}
@ -88,9 +88,14 @@ func dedupShardToShardPartition(index string, shard uint64, partitionN int) int
return int(h.Sum64() % uint64(partitionN))
}
// keyToKeyPartition returns the key-partition that the given key belongs to.
// IDToShardPartition returns the shard-partition that an id belongs to.
func (c *ClusterSnapshot) IDToShardPartition(index string, id uint64) int {
return c.ShardToShardPartition(index, id/ShardWidth)
}
// KeyToKeyPartition returns the key-partition that the given key belongs to.
// NOTE: The key-partition is DIFFERENT from the shard-partition.
func (c *ClusterSnapshot) keyToKeyPartition(index, key string) int {
func (c *ClusterSnapshot) KeyToKeyPartition(index, key string) int {
// Hash the bytes and mod by partition count.
h := fnv.New64a()
_, _ = h.Write([]byte(index))
@ -100,12 +105,17 @@ func (c *ClusterSnapshot) keyToKeyPartition(index, key string) int {
// ShardNodes returns a list of nodes that own a shard.
func (c *ClusterSnapshot) ShardNodes(index string, shard uint64) []*Node {
return c.PartitionNodes(c.shardToShardPartition(index, shard))
return c.PartitionNodes(c.ShardToShardPartition(index, shard))
}
// OwnsShard returns true if a host owns a fragment.
func (c *ClusterSnapshot) OwnsShard(nodeID string, index string, shard uint64) bool {
return Nodes(c.ShardNodes(index, shard)).ContainsID(nodeID)
}
// KeyNodes returns a list of nodes that own a key.
func (c *ClusterSnapshot) KeyNodes(index, key string) []*Node {
return c.PartitionNodes(c.keyToKeyPartition(index, key))
return c.PartitionNodes(c.KeyToKeyPartition(index, key))
}
// PartitionNodes returns a list of nodes that own the given partition.
@ -129,13 +139,25 @@ func (c *ClusterSnapshot) PartitionNodes(partitionID int) []*Node {
// field keys. The primary could be any node in the cluster, but we arbitrarily
// define it to be the node responsible for partition 0.
func (c *ClusterSnapshot) PrimaryFieldTranslationNode() *Node {
return c.PrimaryPartitionNode(0)
// return c.PrimaryPartitionNode(0)
for _, n := range c.Nodes {
if n.IsCoordinator {
return n
}
}
return nil
}
// IsPrimaryFieldTranslationNode returns true if nodeID represents the primary
// node responsible for field translation.
func (c *ClusterSnapshot) IsPrimaryFieldTranslationNode(nodeID string) bool {
return c.PrimaryFieldTranslationNode().ID == nodeID
// return c.PrimaryFieldTranslationNode().ID == nodeID
for i := range c.Nodes {
if c.Nodes[i].ID == nodeID && c.Nodes[i].IsCoordinator {
return true
}
}
return false
}
// PrimaryPartitionNode returns the primary node of the given partition.
@ -204,7 +226,7 @@ func (c *ClusterSnapshot) ReplicasForPrimary(primary int) (replicaNodeIDs, nonRe
func (c *ClusterSnapshot) ContainsShards(index string, availableShards *roaring.Bitmap, node *Node) []uint64 {
var shards []uint64
_ = availableShards.ForEach(func(i uint64) error {
p := c.shardToShardPartition(index, i)
p := c.ShardToShardPartition(index, i)
// Determine the nodes for partition.
nodes := c.PartitionNodes(p)
for _, n := range nodes {
@ -223,7 +245,7 @@ func (c *ClusterSnapshot) ContainsShards(index string, availableShards *roaring.
// replication. So with 4 nodes and 3-way replication, each node has 3/4 of
// the translation stores on it.
func (c *ClusterSnapshot) PrimaryForColKeyTranslation(index, key string) (primary int) {
partitionID := c.keyToKeyPartition(index, key)
partitionID := c.KeyToKeyPartition(index, key)
return c.PrimaryNodeIndex(partitionID)
}

View file

@ -483,28 +483,28 @@ func TestTranslation_Replication(t *testing.T) {
)
defer c.Close()
node0 := c.GetNode(0)
node1 := c.GetNode(1)
coord := c.GetCoordinator()
other := c.GetNonCoordinator()
ctx := context.Background()
idx := "i"
field := "f"
// Create an index with keys.
if _, err := node0.API.CreateIndex(ctx, idx,
if _, err := coord.API.CreateIndex(ctx, idx,
pilosa.IndexOptions{
Keys: true,
}); err != nil {
t.Fatal(err)
}
if _, err := node0.API.CreateField(ctx, idx, field); err != nil {
if _, err := coord.API.CreateField(ctx, idx, field); err != nil {
t.Fatal(err)
}
// Write data on first node.
// these keys are a minimal example to reproduce the problem for the case of a 3-node cluster with replication factor 2
if _, err := node0.Queryf(t, idx, "", `
if _, err := coord.Queryf(t, idx, "", `
Set("x1", f=1)
Set("x2", f=1)
`); err != nil {
@ -513,22 +513,22 @@ func TestTranslation_Replication(t *testing.T) {
exp := `{"results":[{"attrs":{},"columns":[],"keys":["x1","x2"]}]}`
if !test.CheckClusterState(node0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", node0.API.State())
} else if !test.CheckClusterState(node1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", node1.API.State())
if !test.CheckClusterState(coord, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected coord cluster state: %s", coord.API.State())
} else if !test.CheckClusterState(other, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected other cluster state: %s", other.API.State())
}
// Verify the data exists
node0.QueryExpect(t, idx, "", `Row(f=1)`, exp)
coord.QueryExpect(t, idx, "", `Row(f=1)`, exp)
// Kill one node.
if err := c.CloseAndRemove(1); err != nil {
// Kill a non-coordinator node.
if err := c.CloseAndRemoveNonCoordinator(); err != nil {
t.Fatal(err)
}
// Verify the data exists with one node down
node0.QueryExpect(t, idx, "", `Row(f=1)`, exp)
coord.QueryExpect(t, idx, "", `Row(f=1)`, exp)
})
}
@ -557,8 +557,8 @@ func TestTranslation_Coordinator(t *testing.T) {
)
defer c.Close()
node0 := c.GetNode(0)
node1 := c.GetNode(1)
node0 := c.GetCoordinator()
node1 := c.GetNonCoordinator()
ctx := context.Background()
idx := "i"
@ -643,23 +643,24 @@ func TestTranslation_TranslateIDsOnCluster(t *testing.T) {
)
defer c.Close()
node0 := c.GetNode(0)
node3 := c.GetNode(3)
coord := c.GetCoordinator()
other := c.GetNonCoordinator()
ctx := context.Background()
idx, fld := "i", "f"
// Create an index with keys.
if _, err := node0.API.CreateIndex(ctx, idx, pilosa.IndexOptions{Keys: true}); err != nil {
if _, err := coord.API.CreateIndex(ctx, idx, pilosa.IndexOptions{Keys: true}); err != nil {
t.Fatal(err)
}
// Create an index with keys.
if _, err := node0.API.CreateField(ctx, idx, fld, pilosa.OptFieldKeys()); err != nil {
if _, err := coord.API.CreateField(ctx, idx, fld, pilosa.OptFieldKeys()); err != nil {
t.Fatal(err)
}
keys := []string{"k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"}
// write a new key and get id
req, err := node0.API.Serializer.Marshal(&pilosa.TranslateKeysRequest{
req, err := coord.API.Serializer.Marshal(&pilosa.TranslateKeysRequest{
Index: idx,
Field: fld,
Keys: keys,
@ -668,20 +669,20 @@ func TestTranslation_TranslateIDsOnCluster(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if buf, err := node0.API.TranslateKeys(ctx, bytes.NewReader(req)); err != nil {
if buf, err := coord.API.TranslateKeys(ctx, bytes.NewReader(req)); err != nil {
t.Fatal(err)
} else {
var (
respKeys pilosa.TranslateKeysResponse
respIDs pilosa.TranslateIDsResponse
)
if err = node0.API.Serializer.Unmarshal(buf, &respKeys); err != nil {
if err = other.API.Serializer.Unmarshal(buf, &respKeys); err != nil {
t.Fatal(err)
}
ids := respKeys.IDs
// translate ids
req, err = node3.API.Serializer.Marshal(&pilosa.TranslateIDsRequest{
req, err = other.API.Serializer.Marshal(&pilosa.TranslateIDsRequest{
Index: idx,
Field: fld,
IDs: ids,
@ -689,10 +690,10 @@ func TestTranslation_TranslateIDsOnCluster(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if buf, err = node3.API.TranslateIDs(ctx, bytes.NewReader(req)); err != nil {
if buf, err = other.API.TranslateIDs(ctx, bytes.NewReader(req)); err != nil {
t.Fatal(err)
}
if err = node3.API.Serializer.Unmarshal(buf, &respIDs); err != nil {
if err = other.API.Serializer.Unmarshal(buf, &respIDs); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(respIDs.Keys, keys) {
t.Fatalf("TranslateIDs(%+v): expected: %+v, got: %+v", ids, keys, respIDs.Keys)
@ -734,7 +735,7 @@ func TestTranslation_Cluster_CreateFind(t *testing.T) {
for i, keys := range parts {
i, keys := i, keys
g.Go(func() error {
_, err := c.Nodes[i].API.CreateIndexKeys(ctx, "i", keys...)
_, err := c.GetNode(i).API.CreateIndexKeys(ctx, "i", keys...)
return err
})
}
@ -753,7 +754,7 @@ func TestTranslation_Cluster_CreateFind(t *testing.T) {
}
// Obtain authoritative translations for the keys.
translations, err := c.Nodes[0].API.FindIndexKeys(ctx, "i", keyList...)
translations, err := c.GetCoordinator().API.FindIndexKeys(ctx, "i", keyList...)
if err != nil {
t.Errorf("obtaining authoritative translations: %v", err)
return
@ -820,7 +821,7 @@ func TestTranslation_Cluster_CreateFind(t *testing.T) {
for i, keys := range parts {
i, keys := i, keys
g.Go(func() error {
_, err := c.Nodes[i].API.CreateFieldKeys(ctx, "i", "f", keys...)
_, err := c.GetNode(i).API.CreateFieldKeys(ctx, "i", "f", keys...)
return err
})
}
@ -839,7 +840,7 @@ func TestTranslation_Cluster_CreateFind(t *testing.T) {
}
// Obtain authoritative translations for the keys.
translations, err := c.Nodes[0].API.FindFieldKeys(ctx, "i", "f", keyList...)
translations, err := c.GetCoordinator().API.FindFieldKeys(ctx, "i", "f", keyList...)
if err != nil {
t.Errorf("obtaining authoritative translations: %v", err)
return

View file

@ -255,13 +255,13 @@ func (t *ClusterCluster) WriteTopology(path string, top *Topology) error {
}
func (t *ClusterCluster) addCluster(i int, saveTopology bool) (*cluster, error) {
id := fmt.Sprintf("node%d", i)
uri := NewTestURI("http", fmt.Sprintf("host%d", i), uint16(0))
node := &topology.Node{
ID: id,
URI: uri,
ID: id,
URI: uri,
IsCoordinator: i == 0,
}
// add URI to common