// Copyright 2017 Pilosa Corp. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package pilosa_test import ( "bytes" "math/rand" "reflect" "testing" "testing/quick" "github.com/davecgh/go-spew/spew" "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/internal" "github.com/pilosa/pilosa/test" ) // Ensure the cluster can fairly distribute partitions across the nodes. func TestCluster_Owners(t *testing.T) { c := pilosa.Cluster{ Nodes: []*pilosa.Node{ {URI: test.NewURIFromHostPort("serverA", 1000)}, {URI: test.NewURIFromHostPort("serverB", 1000)}, {URI: test.NewURIFromHostPort("serverC", 1000)}, }, Hasher: test.NewModHasher(), ReplicaN: 2, } // Verify nodes are distributed. if a := c.PartitionNodes(0); !reflect.DeepEqual(a, []*pilosa.Node{c.Nodes[0], c.Nodes[1]}) { t.Fatalf("unexpected owners: %s", spew.Sdump(a)) } // Verify nodes go around the ring. if a := c.PartitionNodes(2); !reflect.DeepEqual(a, []*pilosa.Node{c.Nodes[2], c.Nodes[0]}) { t.Fatalf("unexpected owners: %s", spew.Sdump(a)) } } // Ensure the partitioner can assign a fragment to a partition. func TestCluster_Partition(t *testing.T) { if err := quick.Check(func(index string, slice uint64, partitionN int) bool { c := pilosa.NewCluster() c.PartitionN = partitionN partitionID := c.Partition(index, slice) if partitionID < 0 || partitionID >= partitionN { t.Errorf("partition out of range: slice=%d, p=%d, n=%d", slice, partitionID, partitionN) } return true }, &quick.Config{ Values: func(values []reflect.Value, rand *rand.Rand) { values[0], _ = quick.Value(reflect.TypeOf(""), rand) values[1] = reflect.ValueOf(uint64(rand.Uint32())) values[2] = reflect.ValueOf(rand.Intn(1000) + 1) }, }); err != nil { t.Fatal(err) } } // Ensure the hasher can hash correctly. func TestHasher(t *testing.T) { for _, tt := range []struct { key uint64 bucket []int }{ // Generated from the reference C++ code {0, []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, {1, []int{0, 0, 0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 17, 17}}, {0xdeadbeef, []int{0, 1, 2, 3, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 16, 16, 16}}, {0x0ddc0ffeebadf00d, []int{0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 15, 15, 15, 15}}, } { for i, v := range tt.bucket { if got := pilosa.NewHasher().Hash(tt.key, i+1); got != v { t.Errorf("hash(%v,%v)=%v, want %v", tt.key, i+1, got, v) } } } } // Ensure OwnsSlices can find the actual slice list for node and index. func TestCluster_OwnsSlices(t *testing.T) { c := test.NewCluster(5) slices := c.OwnsSlices("test", 10, test.NewURIFromHostPort("host2", 0)) if !reflect.DeepEqual(slices, []uint64{0, 3, 6, 10}) { t.Fatalf("unexpected slices for node's index: %v", slices) } } func TestCluster_Nodes(t *testing.T) { uri0 := test.NewURIFromHostPort("node0", 0) uri1 := test.NewURIFromHostPort("node1", 0) uri2 := test.NewURIFromHostPort("node2", 0) uri3 := test.NewURIFromHostPort("node3", 0) nodes := []*pilosa.Node{ {URI: uri0}, {URI: uri1}, {URI: uri2}, } t.Run("NodeSet", func(t *testing.T) { actual := pilosa.Nodes(nodes).URIs() expected := []pilosa.URI{uri0, uri1, uri2} if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) t.Run("Filter", func(t *testing.T) { actual := pilosa.Nodes(pilosa.Nodes(nodes).Filter(nodes[1])).URIs() expected := []pilosa.URI{uri0, uri2} if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) t.Run("FilterURI", func(t *testing.T) { actual := pilosa.Nodes(pilosa.Nodes(nodes).FilterURI(uri1)).URIs() expected := []pilosa.URI{uri0, uri2} if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) t.Run("Contains", func(t *testing.T) { actualTrue := pilosa.Nodes(nodes).Contains(nodes[1]) actualFalse := pilosa.Nodes(nodes).Contains(&pilosa.Node{}) if !reflect.DeepEqual(actualTrue, true) { t.Errorf("expected: %v, but got: %v", true, actualTrue) } if !reflect.DeepEqual(actualFalse, false) { t.Errorf("expected: %v, but got: %v", false, actualTrue) } }) t.Run("ContainsURI", func(t *testing.T) { actualTrue := pilosa.Nodes(nodes).ContainsURI(uri1) actualFalse := pilosa.Nodes(nodes).ContainsURI(uri3) if !reflect.DeepEqual(actualTrue, true) { t.Errorf("expected: %v, but got: %v", true, actualTrue) } if !reflect.DeepEqual(actualFalse, false) { t.Errorf("expected: %v, but got: %v", false, actualTrue) } }) t.Run("Clone", func(t *testing.T) { clone := pilosa.Nodes(nodes).Clone() actual := pilosa.Nodes(clone).URIs() expected := []pilosa.URI{uri0, uri1, uri2} if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) } func TestCluster_Coordinator(t *testing.T) { uri1 := test.NewURIFromHostPort("node1", 0) uri2 := test.NewURIFromHostPort("node2", 0) c1 := *pilosa.NewCluster() c1.URI = uri1 c1.Coordinator = uri1 c2 := *pilosa.NewCluster() c2.URI = uri2 c2.Coordinator = uri1 t.Run("IsCoordinator", func(t *testing.T) { if !c1.IsCoordinator() { t.Errorf("!IsCoordinator error: %v", c1.URI) } else if c2.IsCoordinator() { t.Errorf("IsCoordinator error: %v", c2.URI) } }) } func TestCluster_Topology(t *testing.T) { c1 := test.NewCluster(1) uri1 := test.NewURIFromHostPort("node1", 0) uri2 := test.NewURIFromHostPort("node2", 0) base := test.NewURIFromHostPort("host0", 0) invalid := test.NewURIFromHostPort("invalid", 0) t.Run("AddNode", func(t *testing.T) { err := c1.AddNode(uri1) if err != nil { t.Fatal(err) } // add the same host. err = c1.AddNode(uri1) if err != nil { t.Fatal(err) } err = c1.AddNode(uri2) if err != nil { t.Fatal(err) } actual := c1.NodeSet() expected := []pilosa.URI{base, uri1, uri2} if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) t.Run("ContainsURI", func(t *testing.T) { if !c1.Topology.ContainsURI(uri1) { t.Errorf("!ContainsHost error: %v", uri1) } else if c1.Topology.ContainsURI(invalid) { t.Errorf("ContainsHost error: %v", invalid) } }) } // Ensure DataDiff can generate the expected source map. func TestCluster_Resize(t *testing.T) { // Given two clusters, ensure DataDiff can determine the sources of data // needed in order to respond to queries. t.Run("DataDiff", func(t *testing.T) { // Holder h1 := test.NewHolder() i, err := h1.CreateIndex("i", pilosa.IndexOptions{}) if err != nil { t.Fatal(err) } f, err := i.CreateFrame("f", pilosa.FrameOptions{InverseEnabled: true}) if err != nil { t.Fatal(err) } _, err = f.CreateViewIfNotExists("v") if err != nil { t.Fatal(err) } _, err = f.CreateViewIfNotExists("inverse") if err != nil { t.Fatal(err) } // Set max slices. i.SetRemoteMaxSlice(2) i.SetRemoteMaxInverseSlice(5) // Cluster 1 c1 := test.NewCluster(3) c1.ReplicaN = 2 // Cluster 2 c2 := test.NewCluster(4) c2.ReplicaN = 2 u0 := test.NewURIFromHostPort("host0", 0) u1 := test.NewURIFromHostPort("host1", 0) u2 := test.NewURIFromHostPort("host2", 0) u3 := test.NewURIFromHostPort("host3", 0) uri0 := u0.Encode() uri1 := u1.Encode() uri2 := u2.Encode() //uri3 := u3.Encode() expected := map[pilosa.URI][]*internal.ResizeSource{ u0: []*internal.ResizeSource{ {URI: uri1, Index: "i", Frame: "f", View: "inverse", Slice: 5}, }, u1: []*internal.ResizeSource{ {URI: uri2, Index: "i", Frame: "f", View: "v", Slice: 0}, {URI: uri2, Index: "i", Frame: "f", View: "inverse", Slice: 0}, }, u2: []*internal.ResizeSource{ {URI: uri0, Index: "i", Frame: "f", View: "inverse", Slice: 3}, }, u3: []*internal.ResizeSource{ {URI: uri0, Index: "i", Frame: "f", View: "v", Slice: 1}, {URI: uri1, Index: "i", Frame: "f", View: "v", Slice: 2}, {URI: uri0, Index: "i", Frame: "f", View: "inverse", Slice: 1}, {URI: uri1, Index: "i", Frame: "f", View: "inverse", Slice: 2}, {URI: uri1, Index: "i", Frame: "f", View: "inverse", Slice: 5}, }, } actual := c1.DataDiff(c2, i) if !reflect.DeepEqual(actual, expected) { t.Errorf("expected: %v, but got: %v", expected, actual) } }) } // TestTestCluster ensures that general cluster functionality works as expected. func TestCluster_ResizeStates(t *testing.T) { /* test conditions: x- single node, no data, comes up in NORMAL with topology x- single node, in topology, comes up NORMAL x- single node, not in topology, raises error x- two node, no data, comes up in NORMAL, with topology x- two node, in topology, comes up NORMAL x- two node, STARTING, not in topology, raises error x- two node, NORMAL, not in topology, triggers resize x- resize of nodes with data moves data appropriately */ t.Run("Single node, no data", func(t *testing.T) { tc := test.NewTestCluster(1) // Open TestCluster. if err := tc.Open(); err != nil { t.Fatal(err) } node := tc.Clusters[0] // Ensure that node comes up in state NORMAL. if node.State != pilosa.ClusterStateNormal { t.Errorf("expected state: %v, but got: %v", pilosa.ClusterStateNormal, node.State) } expectedTop := &pilosa.Topology{ NodeSet: []pilosa.URI{node.URI}, } // Verify topology file. if !reflect.DeepEqual(node.Topology, expectedTop) { t.Errorf("expected topology: %v, but got: %v", expectedTop, node.Topology) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) t.Run("Single node, in topology", func(t *testing.T) { tc := test.NewTestCluster(0) tc.AddNode(false) node := tc.Clusters[0] // write topology to data file top := &pilosa.Topology{ NodeSet: []pilosa.URI{node.URI}, } tc.WriteTopology(node.Path, top) // Open TestCluster. if err := tc.Open(); err != nil { t.Fatal(err) } // Ensure that node comes up in state NORMAL. if node.State != pilosa.ClusterStateNormal { t.Errorf("expected state: %v, but got: %v", pilosa.ClusterStateNormal, node.State) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) t.Run("Single node, not in topology", func(t *testing.T) { tc := test.NewTestCluster(0) tc.AddNode(false) node := tc.Clusters[0] // write topology to data file top := &pilosa.Topology{ NodeSet: []pilosa.URI{ test.NewURIFromHostPort("some-other-host", 0), }, } tc.WriteTopology(node.Path, top) // Open TestCluster. expected := "considerTopology: coordinator http://host0:0 is not in topology: [http://some-other-host:0]" err := tc.Open() if err == nil || err.Error() != expected { t.Errorf("did not receive expected error: %s", expected) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) t.Run("Multiple nodes, no data", func(t *testing.T) { tc := test.NewTestCluster(0) tc.AddNode(false) // Open TestCluster. if err := tc.Open(); err != nil { t.Fatal(err) } tc.AddNode(false) node0 := tc.Clusters[0] node1 := tc.Clusters[1] // Ensure that nodes comes up in state NORMAL. if node0.State != pilosa.ClusterStateNormal { t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State) } else if node1.State != pilosa.ClusterStateNormal { t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node1.State) } expectedTop := &pilosa.Topology{ NodeSet: []pilosa.URI{node0.URI, node1.URI}, } // Verify topology file. if !reflect.DeepEqual(node0.Topology, expectedTop) { t.Errorf("expected node0 topology: %v, but got: %v", expectedTop, node0.Topology) } else if !reflect.DeepEqual(node1.Topology, expectedTop) { t.Errorf("expected node1 topology: %v, but got: %v", expectedTop, node1.Topology) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) t.Run("Multiple nodes, in/not in topology", func(t *testing.T) { tc := test.NewTestCluster(0) tc.AddNode(false) node0 := tc.Clusters[0] u0 := test.NewURIFromHostPort("host0", 0) //u1 := test.NewURIFromHostPort("host1", 0) u2 := test.NewURIFromHostPort("host2", 0) // write topology to data file top := &pilosa.Topology{ NodeSet: []pilosa.URI{u0, u2}, } tc.WriteTopology(node0.Path, top) // Open TestCluster. if err := tc.Open(); err != nil { t.Fatal(err) } // Ensure that node is in state STARTING before the other node joins. if node0.State != pilosa.ClusterStateStarting { t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateStarting, node0.State) } // Expect an error by adding a node not in the topology. expectedError := "host is not in topology: http://host1:0" err := tc.AddNode(false) if err == nil || err.Error() != expectedError { t.Errorf("did not receive expected error: %s", expectedError) } tc.AddNode(false) node2 := tc.Clusters[2] // Ensure that node comes up in state NORMAL. if node0.State != pilosa.ClusterStateNormal { t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State) } else if node2.State != pilosa.ClusterStateNormal { t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node2.State) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) t.Run("Multiple nodes, with data", func(t *testing.T) { tc := test.NewTestCluster(0) tc.AddNode(false) // Open TestCluster. if err := tc.Open(); err != nil { t.Fatal(err) } // Add Data to node0. tc.CreateFrame("i", "f", pilosa.FrameOptions{}) tc.SetBit("i", "f", "standard", 1, 101, nil) tc.SetBit("i", "f", "standard", 1, 1300000, nil) // AddNode needs to block until the resize process has completed. tc.AddNode(false) node0 := tc.Clusters[0] node1 := tc.Clusters[1] // Ensure that nodes comes up in state NORMAL. if node0.State != pilosa.ClusterStateNormal { t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State) } else if node1.State != pilosa.ClusterStateNormal { t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node1.State) } expectedTop := &pilosa.Topology{ NodeSet: []pilosa.URI{node0.URI, node1.URI}, } // Verify topology file. if !reflect.DeepEqual(node0.Topology, expectedTop) { t.Errorf("expected node0 topology: %v, but got: %v", expectedTop, node0.Topology) } else if !reflect.DeepEqual(node1.Topology, expectedTop) { t.Errorf("expected node1 topology: %v, but got: %v", expectedTop, node1.Topology) } // Verify that node-1 contains the fragment (i/f/standard/1) transferred from node-0. node0Frame := node0.Holder.Frame("i", "f") node0View := node0Frame.View("standard") node0Fragment := node0View.Fragment(1) node1Frame := node1.Holder.Frame("i", "f") node1View := node1Frame.View("standard") node1Fragment := node1View.Fragment(1) // Ensure checksums are the same. orig := node0Fragment.Checksum() if chksum := node1Fragment.Checksum(); !bytes.Equal(chksum, orig) { t.Fatalf("expected checksum to match: %x - %x", chksum, orig) } // Close TestCluster. if err := tc.Close(); err != nil { t.Fatal(err) } }) }