Merge pull request #1378 from travisturner/disco-config-noder

Disco config noder
This commit is contained in:
Travis Turner 2021-02-05 10:37:56 -06:00 committed by GitHub
commit 4a408a895a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 2494 additions and 5672 deletions

View file

@ -229,7 +229,7 @@ docker-test:
# The \-\-\- FAIL avoids counting the extra two FAIL strings at then bottom of log.topt.
topt:
mv log.topt.roar log.topt.roar.prev || true
$(eval SHELL:=/bin/bash) set -o pipefail; go test -v -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) 2>&1 | tee log.topt.roar
$(eval SHELL:=/bin/bash) set -o pipefail; go test -v -timeout 60m -tags='$(BUILD_TAGS) $(TEST_TAGS)' $(TESTFLAGS) 2>&1 | tee log.topt.roar
@echo " log.topt.roar green: \c"; cat log.topt.roar | grep PASS |wc -l
@echo " log.topt.roar red: \c"; cat log.topt.roar | grep '\-\-\- FAIL' | wc -l

151
api.go
View file

@ -112,10 +112,10 @@ func NewAPI(opts ...apiOption) (*API, error) {
// validAPIMethods specifies the api methods that are valid for each
// cluster state.
var validAPIMethods = map[string]map[apiMethod]struct{}{
ClusterStateStarting: methodsCommon,
ClusterStateNormal: appendMap(methodsCommon, methodsNormal),
ClusterStateDegraded: appendMap(methodsCommon, methodsNormal),
ClusterStateResizing: appendMap(methodsCommon, methodsResizing),
string(ClusterStateStarting): methodsCommon,
string(ClusterStateNormal): appendMap(methodsCommon, methodsNormal),
string(ClusterStateDegraded): appendMap(methodsCommon, methodsNormal),
string(ClusterStateResizing): appendMap(methodsCommon, methodsResizing),
}
func appendMap(a, b map[apiMethod]struct{}) map[apiMethod]struct{} {
@ -130,7 +130,10 @@ func appendMap(a, b map[apiMethod]struct{}) map[apiMethod]struct{} {
}
func (api *API) validate(f apiMethod) error {
state := api.cluster.State()
state, err := api.cluster.State()
if err != nil {
return errors.Wrap(err, "getting cluster state")
}
if _, ok := validAPIMethods[state][f]; ok {
return nil
}
@ -207,7 +210,10 @@ func (api *API) CreateIndex(ctx context.Context, indexName string, options Index
return nil, errors.Wrap(err, "validating api method")
}
if !api.holder.isCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(api.Node().ID) {
if err := api.server.defaultClient.CreateIndex(ctx, indexName, options); err != nil {
return nil, errors.Wrap(err, "forwarding CreateIndex to coordinator")
}
@ -303,7 +309,10 @@ func (api *API) CreateField(ctx context.Context, indexName string, fieldName str
}
}
if !api.holder.isCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(api.Node().ID) {
if err := api.server.defaultClient.CreateFieldWithOptions(ctx, indexName, fieldName, fo); err != nil {
return nil, errors.Wrap(err, "forwarding CreateField to coordinator")
}
@ -834,6 +843,13 @@ func (api *API) Node() *topology.Node {
return api.server.node()
}
// PrimaryNode returns the coordinator node for the cluster.
func (api *API) PrimaryNode() *topology.Node {
// 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.PrimaryFieldTranslationNode()
}
// NodeUsage represents all usage measurements for one node.
type NodeUsage struct {
Disk DiskUsage `json:"bytesOnDisk"`
@ -963,10 +979,14 @@ func (err MessageProcessingError) Unwrap() error {
// Schema returns information about each index in Pilosa including which fields
// they contain.
func (api *API) Schema(ctx context.Context) []*IndexInfo {
func (api *API) Schema(ctx context.Context) ([]*IndexInfo, error) {
if err := api.validate(apiSchema); err != nil {
return nil, errors.Wrap(err, "validating api method")
}
span, _ := tracing.StartSpanFromContext(ctx, "API.Schema")
defer span.Finish()
return api.holder.limitedSchema()
return api.holder.limitedSchema(), nil
}
// ApplySchema takes the given schema and applies it across the
@ -1721,38 +1741,6 @@ func (api *API) indexField(indexName string, fieldName string, shard uint64) (*I
return index, field, nil
}
// SetCoordinator makes a new Node the cluster coordinator.
func (api *API) SetCoordinator(ctx context.Context, id string) (oldNode, newNode *topology.Node, err error) {
span, _ := tracing.StartSpanFromContext(ctx, "API.SetCoordinator")
defer span.Finish()
if err := api.validate(apiSetCoordinator); err != nil {
return nil, nil, errors.Wrap(err, "validating api method")
}
oldNode = api.cluster.nodeByID(api.cluster.Coordinator)
newNode = api.cluster.nodeByID(id)
if newNode == nil {
return nil, nil, errors.Wrap(ErrNodeIDNotExists, "getting new node")
}
// If the new coordinator is this node, do the SetCoordinator directly.
if newNode.ID == api.Node().ID {
return oldNode, newNode, api.cluster.setCoordinator(newNode)
}
// Send the set-coordinator message to new node.
err = api.server.SendTo(
newNode,
&SetCoordinatorMessage{
New: newNode,
})
if err != nil {
return nil, nil, fmt.Errorf("problem sending SetCoordinator message: %s", err)
}
return oldNode, newNode, nil
}
// RemoveNode puts the cluster into the "RESIZING" state and begins the job of
// removing the given node.
func (api *API) RemoveNode(id string) (*topology.Node, error) {
@ -1760,21 +1748,19 @@ func (api *API) RemoveNode(id string) (*topology.Node, error) {
return nil, errors.Wrap(err, "validating api method")
}
removeNode := api.cluster.nodeByID(id)
if removeNode == nil {
if !api.cluster.topologyContainsNode(id) {
return nil, errors.Wrap(ErrNodeIDNotExists, "finding node to remove")
}
removeNode = &topology.Node{
ID: id,
}
if api.cluster.disCo.ID() == id {
return nil, errors.Wrapf(ErrPreconditionFailed, "the node %s can not be removed", id)
}
// Start the resize process (similar to NodeJoin)
err := api.cluster.nodeLeave(id)
if err != nil {
return removeNode, errors.Wrap(err, "calling node leave")
removeNode := api.cluster.nodeByID(id)
if removeNode == nil {
return nil, errors.Wrap(ErrNodeIDNotExists, "finding node to remove")
}
if err := api.cluster.removeNode(id); err != nil {
return nil, errors.Wrapf(err, "removing node %s", id)
}
return removeNode, nil
}
@ -1784,14 +1770,17 @@ func (api *API) ResizeAbort() error {
return errors.Wrap(err, "validating api method")
}
err := api.cluster.completeCurrentJob(resizeJobStateAborted)
return errors.Wrap(err, "complete current job")
return api.cluster.resizeAbortAndBroadcast()
}
// State returns the cluster state which is usually "NORMAL", but could be
// "STARTING", "RESIZING", or potentially others. See cluster.go for more
// details.
func (api *API) State() string {
func (api *API) State() (string, error) {
if err := api.validate(apiState); err != nil {
return "", errors.Wrap(err, "validating api method")
}
return api.cluster.State()
}
@ -2125,7 +2114,10 @@ func (api *API) ReserveIDs(key IDAllocKey, session [32]byte, offset uint64, coun
return nil, errors.Wrap(err, "validating api method")
}
if api.holder.isCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(api.Node().ID) {
return api.holder.ida.reserve(key, session, offset, count)
}
@ -2137,7 +2129,10 @@ func (api *API) CommitIDs(key IDAllocKey, session [32]byte, count uint64) error
return errors.Wrap(err, "validating api method")
}
if api.holder.isCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(api.Node().ID) {
return api.holder.ida.commit(key, session, count)
}
@ -2149,7 +2144,10 @@ func (api *API) ResetIDAlloc(index string) error {
return errors.Wrap(err, "validating api method")
}
if api.holder.isCoordinator() {
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(api.cluster.noder, api.cluster.Hasher, api.cluster.ReplicaN)
if !snap.IsPrimaryFieldTranslationNode(api.Node().ID) {
return api.holder.ida.reset(index)
}
@ -2217,10 +2215,9 @@ const (
apiRecalculateCaches
apiRemoveNode
apiResizeAbort
//apiSchema // not implemented
apiSetCoordinator
apiSchema
apiShardNodes
//apiState // not implemented
apiState
//apiStatsWithTags // not implemented
//apiVersion // not implemented
apiViews
@ -2238,13 +2235,36 @@ const (
var methodsCommon = map[apiMethod]struct{}{
apiClusterMessage: {},
apiSetCoordinator: {},
}
var methodsResizing = map[apiMethod]struct{}{
apiFragmentData: {},
apiTranslateData: {},
apiResizeAbort: {},
apiSchema: {},
apiState: {},
}
var methodsDegraded = map[apiMethod]struct{}{
apiExportCSV: {},
apiFragmentBlockData: {},
apiFragmentBlocks: {},
apiField: {},
apiFieldAttrDiff: {},
apiIndex: {},
apiIndexAttrDiff: {},
apiQuery: {},
apiRecalculateCaches: {},
apiRemoveNode: {},
apiShardNodes: {},
apiSchema: {},
apiState: {},
apiViews: {},
apiStartTransaction: {},
apiFinishTransaction: {},
apiTransactions: {},
apiGetTransaction: {},
apiActiveQueries: {},
}
var methodsNormal = map[apiMethod]struct{}{
@ -2267,6 +2287,8 @@ var methodsNormal = map[apiMethod]struct{}{
apiRecalculateCaches: {},
apiRemoveNode: {},
apiShardNodes: {},
apiSchema: {},
apiState: {},
apiViews: {},
apiApplySchema: {},
apiStartTransaction: {},
@ -2275,7 +2297,4 @@ var methodsNormal = map[apiMethod]struct{}{
apiGetTransaction: {},
apiActiveQueries: {},
apiPastQueries: {},
apiIDReserve: {},
apiIDCommit: {},
apiIDReset: {},
}

View file

@ -161,7 +161,6 @@ func TestAPI_ImportColumnAttrs(t *testing.T) {
t.Fatal(err)
}
}
})
}
@ -270,7 +269,11 @@ func TestAPI_Import(t *testing.T) {
// Relies on the previous test creating an index with TrackExistence and
// adding some data.
t.Run("SchemaHasNoExists", func(t *testing.T) {
schema := m1.API.Schema(context.Background())
schema, err := m1.API.Schema(context.Background())
if err != nil {
t.Fatal(err)
}
for _, f := range schema[0].Fields {
if f.Name == "_exists" {
t.Fatalf("found _exists field in schema")

View file

@ -30,19 +30,25 @@ func _() {
_ = x[apiRecalculateCaches-19]
_ = x[apiRemoveNode-20]
_ = x[apiResizeAbort-21]
_ = x[apiSetCoordinator-22]
_ = x[apiSchema-22]
_ = x[apiShardNodes-23]
_ = x[apiViews-24]
_ = x[apiApplySchema-25]
_ = x[apiStartTransaction-26]
_ = x[apiFinishTransaction-27]
_ = x[apiTransactions-28]
_ = x[apiGetTransaction-29]
_ = x[apiState-24]
_ = x[apiViews-25]
_ = x[apiApplySchema-26]
_ = x[apiStartTransaction-27]
_ = x[apiFinishTransaction-28]
_ = x[apiTransactions-29]
_ = x[apiGetTransaction-30]
_ = x[apiActiveQueries-31]
_ = x[apiPastQueries-32]
_ = x[apiIDReserve-33]
_ = x[apiIDCommit-34]
_ = x[apiIDReset-35]
}
const _apiMethod_name = "apiClusterMessageapiCreateFieldapiCreateIndexapiDeleteFieldapiDeleteAvailableShardapiDeleteIndexapiDeleteViewapiExportCSVapiFragmentBlockDataapiFragmentBlocksapiFragmentDataapiTranslateDataapiFieldapiFieldAttrDiffapiImportapiImportValueapiIndexapiIndexAttrDiffapiQueryapiRecalculateCachesapiRemoveNodeapiResizeAbortapiSetCoordinatorapiShardNodesapiViewsapiApplySchemaapiStartTransactionapiFinishTransactionapiTransactionsapiGetTransaction"
const _apiMethod_name = "apiClusterMessageapiCreateFieldapiCreateIndexapiDeleteFieldapiDeleteAvailableShardapiDeleteIndexapiDeleteViewapiExportCSVapiFragmentBlockDataapiFragmentBlocksapiFragmentDataapiTranslateDataapiFieldapiFieldAttrDiffapiImportapiImportValueapiIndexapiIndexAttrDiffapiQueryapiRecalculateCachesapiRemoveNodeapiResizeAbortapiSchemaapiShardNodesapiStateapiViewsapiApplySchemaapiStartTransactionapiFinishTransactionapiTransactionsapiGetTransactionapiActiveQueriesapiPastQueriesapiIDReserveapiIDCommitapiIDReset"
var _apiMethod_index = [...]uint16{0, 17, 31, 45, 59, 82, 96, 109, 121, 141, 158, 173, 189, 197, 213, 222, 236, 244, 260, 268, 288, 301, 315, 332, 345, 353, 367, 386, 406, 421, 438}
var _apiMethod_index = [...]uint16{0, 17, 31, 45, 59, 82, 96, 109, 121, 141, 158, 173, 189, 197, 213, 222, 236, 244, 260, 268, 288, 301, 315, 324, 337, 345, 353, 367, 386, 406, 421, 438, 454, 468, 480, 491, 501}
func (i apiMethod) String() string {
if i < 0 || i >= apiMethod(len(_apiMethod_index)-1) {

View file

@ -64,13 +64,13 @@ const (
messageTypeClusterStatus
messageTypeResizeInstruction
messageTypeResizeInstructionComplete
messageTypeSetCoordinator
messageTypeUpdateCoordinator
messageTypeNodeState
messageTypeRecalculateCaches
messageTypeNodeEvent
messageTypeNodeStatus
messageTypeTransaction
messageTypeResizeNodeMessage
messageTypeResizeAbortMessage
)
// MarshalInternalMessage serializes the pilosa message and adds pilosa internal
@ -106,10 +106,6 @@ func getMessage(typ byte) Message {
return &ResizeInstruction{}
case messageTypeResizeInstructionComplete:
return &ResizeInstructionComplete{}
case messageTypeSetCoordinator:
return &SetCoordinatorMessage{}
case messageTypeUpdateCoordinator:
return &UpdateCoordinatorMessage{}
case messageTypeNodeState:
return &NodeStateMessage{}
case messageTypeRecalculateCaches:
@ -120,6 +116,10 @@ func getMessage(typ byte) Message {
return &NodeStatus{}
case messageTypeTransaction:
return &TransactionMessage{}
case messageTypeResizeNodeMessage:
return &ResizeNodeMessage{}
case messageTypeResizeAbortMessage:
return &ResizeAbortMessage{}
default:
panic(fmt.Sprintf("unknown message type %d", typ))
}
@ -147,10 +147,6 @@ func getMessageType(m Message) byte {
return messageTypeResizeInstruction
case *ResizeInstructionComplete:
return messageTypeResizeInstructionComplete
case *SetCoordinatorMessage:
return messageTypeSetCoordinator
case *UpdateCoordinatorMessage:
return messageTypeUpdateCoordinator
case *NodeStateMessage:
return messageTypeNodeState
case *RecalculateCaches:
@ -161,6 +157,10 @@ func getMessageType(m Message) byte {
return messageTypeNodeStatus
case *TransactionMessage:
return messageTypeTransaction
case *ResizeNodeMessage:
return messageTypeResizeNodeMessage
case *ResizeAbortMessage:
return messageTypeResizeAbortMessage
default:
panic(fmt.Sprintf("don't have type for message %#v", m))
}

View file

@ -93,6 +93,8 @@ type InternalClient interface {
// InternalQueryClient is the internal interface for querying a node.
type InternalQueryClient interface {
SchemaNode(ctx context.Context, uri *pnet.URI, views bool) ([]*IndexInfo, error)
QueryNode(ctx context.Context, uri *pnet.URI, index string, queryRequest *QueryRequest) (*QueryResponse, error)
// Trasnlate keys on the particular node. The parameter writable informs TranslateStore if we can generate a new ID if any of keys does not exist.
@ -108,6 +110,10 @@ type InternalQueryClient interface {
type nopInternalQueryClient struct{}
func (nopInternalQueryClient) SchemaNode(ctx context.Context, uri *pnet.URI, views bool) ([]*IndexInfo, error) {
return nil, nil
}
func (n nopInternalQueryClient) QueryNode(ctx context.Context, uri *pnet.URI, index string, queryRequest *QueryRequest) (*QueryResponse, error) {
return nil, nil
}

1953
cluster.go

File diff suppressed because it is too large Load diff

View file

@ -19,20 +19,13 @@ import (
"fmt"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"testing"
"testing/quick"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/pilosa/pilosa/v2/logger"
pnet "github.com/pilosa/pilosa/v2/net"
"github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/test/port"
@ -288,8 +281,8 @@ func TestFragSources(t *testing.T) {
"node0": {},
"node1": {},
"node2": {
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node1", URI: pnet.URI{Scheme: "http", Host: "host1", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(2)},
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node1", URI: pnet.URI{Scheme: "http", Host: "host1", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(2)},
},
},
err: "",
@ -300,11 +293,11 @@ func TestFragSources(t *testing.T) {
idx: idx,
expected: map[string][]*ResizeSource{
"node0": {
{&topology.Node{ID: "node1", URI: pnet.URI{Scheme: "http", Host: "host1", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(1)},
{&topology.Node{ID: "node1", URI: pnet.URI{Scheme: "http", Host: "host1", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(1)},
},
"node1": {
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(2)},
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(2)},
},
},
err: "",
@ -315,11 +308,11 @@ func TestFragSources(t *testing.T) {
idx: idx,
expected: map[string][]*ResizeSource{
"node0": {
{&topology.Node{ID: "node2", URI: pnet.URI{Scheme: "http", Host: "host2", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node2", URI: pnet.URI{Scheme: "http", Host: "host2", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(2)},
{&topology.Node{ID: "node2", URI: pnet.URI{Scheme: "http", Host: "host2", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(0)},
{&topology.Node{ID: "node2", URI: pnet.URI{Scheme: "http", Host: "host2", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(2)},
},
"node1": {
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsCoordinator: false}, "i", "f", "standard", uint64(3)},
{&topology.Node{ID: "node0", URI: pnet.URI{Scheme: "http", Host: "host0", Port: 10101}, IsPrimary: false}, "i", "f", "standard", uint64(3)},
},
"node2": {},
},
@ -419,22 +412,24 @@ func TestResizeJob(t *testing.T) {
// Ensure the cluster can fairly distribute partitions across the nodes.
func TestCluster_Owners(t *testing.T) {
c := cluster{
nodes: []*topology.Node{
noder: topology.NewLocalNoder([]*topology.Node{
{URI: NewTestURIFromHostPort("serverA", 1000)},
{URI: NewTestURIFromHostPort("serverB", 1000)},
{URI: NewTestURIFromHostPort("serverC", 1000)},
},
}),
Hasher: NewTestModHasher(),
ReplicaN: 2,
}
cNodes := c.noder.Nodes()
// Verify nodes are distributed.
if a := c.partitionNodes(0); !reflect.DeepEqual(a, []*topology.Node{c.nodes[0], c.nodes[1]}) {
if a := c.partitionNodes(0); !reflect.DeepEqual(a, []*topology.Node{cNodes[0], cNodes[1]}) {
t.Fatalf("unexpected owners: %s", spew.Sdump(a))
}
// Verify nodes go around the ring.
if a := c.partitionNodes(2); !reflect.DeepEqual(a, []*topology.Node{c.nodes[2], c.nodes[0]}) {
if a := c.partitionNodes(2); !reflect.DeepEqual(a, []*topology.Node{cNodes[2], cNodes[0]}) {
t.Fatalf("unexpected owners: %s", spew.Sdump(a))
}
}
@ -487,7 +482,8 @@ func TestHasher(t *testing.T) {
func TestCluster_ContainsShards(t *testing.T) {
c := NewTestCluster(t, 5)
c.ReplicaN = 3
shards := c.containsShards("test", roaring.NewBitmap(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), c.nodes[2])
cNodes := c.noder.Nodes()
shards := c.containsShards("test", roaring.NewBitmap(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), cNodes[2])
if !reflect.DeepEqual(shards, []uint64{0, 2, 3, 5, 6, 9, 10}) {
t.Fatalf("unexpected shars for node's index: %v", shards)
@ -614,6 +610,9 @@ func TestCluster_PreviousNode(t *testing.T) {
// NEXT: move this test to internal and unexport IsCoordinator
func TestCluster_Coordinator(t *testing.T) {
// TODO check if this test still makes sense
t.Skip()
const urisCount = 2
var uris []pnet.URI
if err := port.GetPorts(func(ports []int) error {
@ -627,13 +626,16 @@ func TestCluster_Coordinator(t *testing.T) {
node1 := &topology.Node{ID: "node1", URI: uris[0]}
node2 := &topology.Node{ID: "node2", URI: uris[1]}
noder := topology.NewLocalNoder([]*topology.Node{node1, node2})
c1 := *newCluster()
c1.Node = node1
c1.Coordinator = node1.ID
// c1.Coordinator = node1.ID
c1.noder = noder
c2 := *newCluster()
c2.Node = node2
c2.Coordinator = node1.ID
// c2.Coordinator = node1.ID
c2.noder = noder
t.Run("IsCoordinator", func(t *testing.T) {
if !c1.isCoordinator() {
@ -645,6 +647,8 @@ func TestCluster_Coordinator(t *testing.T) {
}
func TestCluster_Topology(t *testing.T) {
t.Skip("these tests don't really apply anymore; they were meant to tests the cluster and adding topology nodes.")
c1 := NewTestCluster(t, 1) // automatically creates Node{ID: "node0"}
const urisCount = 4
@ -664,16 +668,16 @@ func TestCluster_Topology(t *testing.T) {
nodeinvalid := &topology.Node{ID: "nodeinvalid", URI: uris[3]}
t.Run("AddNode", func(t *testing.T) {
err := c1.addNode(node1)
err := c1.addNode(node1.ID)
if err != nil {
t.Fatal(err)
}
// add the same host.
err = c1.addNode(node1)
err = c1.addNode(node1.ID)
if err != nil {
t.Fatal(err)
}
err = c1.addNode(node2)
err = c1.addNode(node2.ID)
if err != nil {
t.Fatal(err)
}
@ -697,7 +701,7 @@ func TestCluster_Topology(t *testing.T) {
// Ensure that general cluster functionality works as expected.
func TestCluster_ResizeStates(t *testing.T) {
t.Skip("these tests don't really apply anymore; they were meant to tests the cluster startup process using memberlist and a topology file")
t.Run("Single node, no data", func(t *testing.T) {
tc := NewClusterCluster(t, 1)
@ -708,9 +712,14 @@ func TestCluster_ResizeStates(t *testing.T) {
node := tc.Clusters[0]
state, err := node.State()
if err != nil {
t.Fatal(err)
}
// Ensure that node comes up in state NORMAL.
if node.State() != ClusterStateNormal {
t.Errorf("expected state: %v, but got: %v", ClusterStateNormal, node.State())
if state != string(ClusterStateNormal) {
t.Errorf("expected state: %v, but got: %v", ClusterStateNormal, state)
}
expectedTop := &Topology{
@ -749,9 +758,14 @@ func TestCluster_ResizeStates(t *testing.T) {
t.Fatal(err)
}
state, err := node.State()
if err != nil {
t.Fatal(err)
}
// Ensure that node comes up in state NORMAL.
if node.State() != ClusterStateNormal {
t.Errorf("expected state: %v, but got: %v", ClusterStateNormal, node.State())
if state != string(ClusterStateNormal) {
t.Errorf("expected state: %v, but got: %v", ClusterStateNormal, state)
}
// Close TestCluster.
@ -805,13 +819,22 @@ func TestCluster_ResizeStates(t *testing.T) {
}
node0 := tc.Clusters[0]
state0, err := node0.State()
if err != nil {
t.Fatal(err)
}
node1 := tc.Clusters[1]
state1, err := node1.State()
if err != nil {
t.Fatal(err)
}
// Ensure that nodes comes up in state NORMAL.
if node0.State() != ClusterStateNormal {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, node0.State())
} else if node1.State() != ClusterStateNormal {
t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, node1.State())
if state0 != string(ClusterStateNormal) {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, state0)
} else if state1 != string(ClusterStateNormal) {
t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, state1)
}
expectedTop := &Topology{
@ -851,27 +874,30 @@ func TestCluster_ResizeStates(t *testing.T) {
t.Fatalf("opening cluster: %v", err)
}
// Ensure that node is in state STARTING before the other node joins.
if node0.State() != ClusterStateStarting {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateStarting, node0.State())
state0, err := node0.State()
if err != nil {
t.Fatal(err)
}
// Expect an error by adding a node not in the topology.
expectedError := "host is not in topology: node1"
if err := tc.addNode(); err == nil || err.Error() != expectedError {
t.Errorf("did not receive expected error: %s", expectedError)
// Ensure that node is in state STARTING before the other node joins.
if state0 != string(ClusterStateStarting) {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateStarting, state0)
}
if err := tc.addNode(); err != nil {
t.Fatalf("adding node: %v", err)
}
node2 := tc.Clusters[2]
node1 := tc.Clusters[1]
state1, err := node1.State()
if err != nil {
t.Fatal(err)
}
// Ensure that node comes up in state NORMAL.
if node0.State() != ClusterStateNormal {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, node0.State())
} else if node2.State() != ClusterStateNormal {
t.Errorf("expected node2 state: %v, but got: %v", ClusterStateNormal, node2.State())
if state0 != string(ClusterStateNormal) {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, state0)
} else if state1 != string(ClusterStateNormal) {
t.Errorf("expected node2 state: %v, but got: %v", ClusterStateNormal, state1)
}
// Close TestCluster.
@ -933,11 +959,21 @@ func TestCluster_ResizeStates(t *testing.T) {
node1 := tc.Clusters[1]
state1, err := node1.State()
if err != nil {
t.Fatal(err)
}
state0, err := node0.State()
if err != nil {
t.Fatal(err)
}
// Ensure that nodes come up in state NORMAL.
if node0.State() != ClusterStateNormal {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, node0.State())
} else if node1.State() != ClusterStateNormal {
t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, node1.State())
if state0 != string(ClusterStateNormal) {
t.Errorf("expected node0 state: %v, but got: %v", ClusterStateNormal, state0)
} else if state1 != string(ClusterStateNormal) {
t.Errorf("expected node1 state: %v, but got: %v", ClusterStateNormal, state1)
}
// INVAR: after node1.State() is normal, the rebalancing should have been done.
@ -1030,120 +1066,9 @@ func TestAE(t *testing.T) {
t.Fatalf("abort should not have blocked this long")
}
})
}
// Ensures that coordinator can be changed.
func TestCluster_UpdateCoordinator(t *testing.T) {
t.Run("UpdateCoordinator", func(t *testing.T) {
c := NewTestCluster(t, 2)
oldNode := c.nodes[0]
newNode := c.nodes[1]
// Update coordinator to the same value.
if c.updateCoordinator(oldNode) {
t.Errorf("did not expect coordinator to change")
} else if c.Coordinator != oldNode.ID {
t.Errorf("expected coordinator: %s, but got: %s", c.Coordinator, oldNode.URI)
}
// Update coordinator to a new value.
if !c.updateCoordinator(newNode) {
t.Errorf("expected coordinator to change")
} else if c.Coordinator != newNode.ID {
t.Errorf("expected coordinator: %s, but got: %s", c.Coordinator, newNode.URI)
}
})
}
func TestCluster_confirmNodeDownUp(t *testing.T) {
t.Skip("does a listen on :0, skip for now. TODO(jea) restore this.")
r := mux.NewRouter()
r.HandleFunc("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ignored")
}))
server := httptest.NewServer(r)
// Close the server when test finishes
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Error("bad test setup")
}
uri := pnet.URI{}
host, port, _ := net.SplitHostPort(u.Host)
uri.Scheme = u.Scheme
uri.Host = host
iport, err := strconv.ParseUint(port, 0, 16)
if err != nil {
t.Error(err)
}
uri.Port = uint16(iport)
c := newCluster()
c.logger = logger.NewVerboseLogger(os.Stdout)
if c.confirmNodeDown(uri) {
t.Errorf("expected node to be up")
}
}
func TestCluster_confirmNodeDownTimeout(t *testing.T) {
t.Skip("does a listen on :0, skip for now. TODO(jea) restore this.")
sleep := 50 * time.Millisecond
retries := 5
if testing.Short() {
t.Skip()
}
r := mux.NewRouter()
r.HandleFunc("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(sleep * time.Duration(retries))
fmt.Fprintln(w, "ignored")
}))
server := httptest.NewServer(r)
// Close the server when test finishes
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Error("bad test setup")
}
uri := pnet.URI{}
host, port, _ := net.SplitHostPort(u.Host)
uri.Scheme = u.Scheme
uri.Host = host
iport, err := strconv.ParseUint(port, 0, 16)
if err != nil {
t.Error(err)
}
uri.Port = uint16(iport)
c := newCluster()
c.confirmDownSleep = sleep
c.confirmDownRetries = retries
c.logger = logger.NewVerboseLogger(os.Stdout)
if !c.confirmNodeDown(uri) {
t.Errorf("expected node to be down")
}
}
func TestCluster_confirmNodeDownDown(t *testing.T) {
if testing.Short() {
t.Skip()
}
uri := pnet.URI{}
uri.Scheme = "http"
uri.Host = "DoesntMatter"
uri.Port = 6666
c := newCluster()
c.confirmDownSleep = 50 * time.Millisecond
c.confirmDownRetries = 5
c.logger = logger.NewVerboseLogger(os.Stdout)
if !c.confirmNodeDown(uri) {
t.Errorf("expected node to be down")
}
}
func TestCluster_GetNonPrimaryReplicas(t *testing.T) {
c := newCluster()
c.ReplicaN = 3
topo := NewTopology(c.Hasher, c.partitionN, c.ReplicaN, c)
@ -1151,7 +1076,7 @@ func TestCluster_GetNonPrimaryReplicas(t *testing.T) {
nNodes := 4
for i := 0; i < nNodes; i++ {
nodeID := fmt.Sprintf("node%d", i)
c.nodes = append(c.nodes, &topology.Node{
c.noder.AppendNode(&topology.Node{
ID: nodeID,
URI: NewTestURI("http", fmt.Sprintf("host%d", i), uint16(0)),
})

View file

@ -1,36 +0,0 @@
.PHONY: install build release
CLONE_URL=github.com/pilosa/pilosa
VERSION := $(shell git describe --tags 2> /dev/null || echo unknown)
LATTICE_COMMIT := $(shell git -C lattice rev-parse --short HEAD 2>/dev/null)
VARIANT = Molecula
VERSION_ID = $(VERSION)-$(GOOS)-$(GOARCH)
BRANCH := $(if $(TRAVIS_BRANCH),$(TRAVIS_BRANCH),$(if $(CIRCLE_BRANCH),$(CIRCLE_BRANCH),$(shell git rev-parse --abbrev-ref HEAD)))
BRANCH_ID := $(BRANCH)-$(GOOS)-$(GOARCH)
BUILD_TIME := $(shell date -u +%FT%T%z)
SHARD_WIDTH = 20
COMMIT := $(shell git describe --exact-match >/dev/null 2>&1 || git rev-parse --short HEAD)
LDFLAGS="-X github.com/pilosa/pilosa/v2.Version=$(VERSION) -X github.com/pilosa/pilosa/v2.BuildTime=$(BUILD_TIME) -X github.com/pilosa/pilosa/v2.Variant=$(VARIANT) -X github.com/pilosa/pilosa/v2.Commit=$(COMMIT) -X github.com/pilosa/pilosa/v2.LatticeCommit=$(LATTICE_COMMIT)"
GOOS = $(shell go env GOOS)
# Install pilosa-fsck
install:
go install -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS)
# Compile pilosa-fsck
build:
go build -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS)
REL = release-pilosa-fsck.$(COMMIT).$(GOOS)
release:
mkdir $(REL)
cd release-pilosa-fsck; tar cf - . |(cd ../$(REL); tar xf - )
go build -tags='$(BUILD_TAGS)' -ldflags $(LDFLAGS) $(FLAGS) -o $(REL)/pilosa-fsck
tar cf - $(REL) | gzip > $(REL).tar.gz
rm -rf $(REL)
mv $(REL).tar.gz ../..
clean:
find . -name pilosa-fsck | xargs rm -f
rm -f release-pilosa-fsck*.tar.gz

View file

@ -1,989 +0,0 @@
// Copyright 2020 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 main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/gogo/protobuf/proto"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/boltdb"
"github.com/pilosa/pilosa/v2/internal"
"github.com/pilosa/pilosa/v2/server"
"github.com/pilosa/pilosa/v2/topology"
"github.com/pkg/errors"
"github.com/zeebo/blake3"
)
// pilosa-fsck :
// an external customer tool (originally for Q2) to do 2 jobs:
// Given a set of cluster backups (and their .id and .topology files)
// mounted on the same file system, we can:
// 1) scan for fragment differences between the primary and its replicas (default); or
// 2) repair those differences by overwriting the replcas with the primary fragments (if -fix is given).
//
// pilosa-chk is deliberately NOT a part of pilosa so that it can run without
// forcing a customer to upgrade or downgrade their installed version.
// FsckConfig configures the dumpcols() and/or read() runs.
type FsckConfig struct {
Fix bool // -fix
FixCol bool // -fixcol
Colkeydump bool // -col
JustThisIndex string // -index
// -col column key dump only options:
// Dir string
// PartitionID int
// ShowHeader bool
// ShowKey bool
// ShowID bool
// not flags, just the Args() left after all other flags. Should be the list
// of pilosa (holder) directories for the cluster.
Dirs []string
Verbose bool // -v
Quiet bool // -q
// manual workaround for not having PilosaConfigPath, if really need be.
ReplicaN int // -replicas
PilosaConfigPath string // -config
ParallelReaders int // -readers
topo *pilosa.Topology
}
// call DefineFlags before myflags.Parse()
func (cfg *FsckConfig) DefineFlags(fs *flag.FlagSet) {
fs.BoolVar(&cfg.Fix, "fix", false, "(warning: alters the backed-up node images on disk) copy primary data to replicas to create a consistent cluster. Implies -fixcol")
fs.BoolVar(&cfg.FixCol, "fixcol", false, "(warning: alters the backed-up node images on disk) repair string key translation tables. Skip repair of index data.")
//fs.BoolVar(&cfg.Verbose, "v", false, "be very verbose during analysis")
fs.BoolVar(&cfg.Quiet, "q", false, "be very quiet")
fs.IntVar(&cfg.ReplicaN, "replicas", 0, "(required) manually entered replicaN; the number of replicas maintained in the cluster. Must be the same as the [cluster] 'replicas = R' entry in the pilosa.conf file for the cluster.")
fs.IntVar(&cfg.ParallelReaders, "readers", 10, "how many parallel readers to use to scan at once. 0 means do everything possible in parallel. 1 means serialize everything through a single reader. Can be adjusted to control memory consumption.")
fs.StringVar(&cfg.PilosaConfigPath, "config", "", "(required: -replicas or -config, with -config preferred) path to the pilosa.conf for the cluster (e.g. /etc/pilosa.conf)")
fs.StringVar(&cfg.JustThisIndex, "index", "", "(optional) restrict to just this index. Otherwise we default to all indexes.")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "pilosa-fsck version: %v\n\n", pilosa.VersionInfo())
fmt.Fprintf(os.Stderr, `Use: pilosa-fsck -replicas R {-fix} {-q} /backup/1/.pilosa /backup/2/.pilosa ... /backup/N/.pilosa
-fix
(warning: alters the backed-up node images on disk) copy primary data to replicas to create a consistent cluster.
-replicas R
(required) R is a positive integer, giving the replicaN or replicator factor for the cluster. This is
the number of replicas maintained in the cluster. Must be the same as the
[cluster] 'replicas = R' entry shared across all the pilosa.conf files on each node.
-index index_name
(optional) restrict to just this index. Otherwise we default to all indexes.
-readers PR
how many parallel readers to use to scan at once. PR==0 means do everything
possible in parallel. PR==1 means serialize everything through a single reader.
Adjust PR to control memory consumption if needed. As a practical limit, setting
PR > 10000 will have no effect. (default is 10).
-q
be very quiet during analysis and repair
`)
fmt.Fprintf(os.Stderr, `
Welcome to pilosa-fsck. This is a scan and repair
tool that is modeled after the classic unix file
system utility fsck.
WARNING: DO NOT RUN ON A LIVE SYSTEM.
The most important point to remember is that analysis
and repair must be done *offline*.
Just as fsck must be run on an unmounted disk,
pilosa-fsck must be run on a backup. It must
not be run on the directories where a live Pilosa system
is serving queries. Instead, take a backup first.
A backup is a set of N Pilosa data directories that have been
copied from your live system. They must all
be visible and mounted on one filesystem together.
pilosa-fsck can be run in scan-mode (without -fix),
or in repair-mode with -fix. The console output
supplies a log documenting the analysis
and showing what data changes would have been made.
REQUIRED COMMAND LINE ARGUMENTS
The paths to all the top-level Pilosa
data directories in a cluster must be given on the command
line. The -replicas R flag is also always required. It
must be correct for your cluser. Here R is the same as
the [cluster] stanza "replicas = R" line from your
pilosa.conf.
Example:
Suppose you are ready to run pilosa-fsck:
you have taken a backup of your four node Pilosa
cluster and stored it all on one filesystem with
all nodes visible and uncompressed. This
is a pre-requisite to running pilosa-fsck.
Let's suppose we have replication R = 3 set.
In this example, have stored our backed-up directories in
/backup/molecula
and the four node backups are in
subdirectories node1/ node2/ node3/ node4/ under this:
/backup/molecula/node1/
/backup/molecula/node1/.pilosa/.id
/backup/molecula/node1/.pilosa/.topology
/backup/molecula/node1/.pilosa/myindex
/backup/molecula/node2/
/backup/molecula/node2/.pilosa/.id
/backup/molecula/node2/.pilosa/.topology
/backup/molecula/node2/.pilosa/myindex
/backup/molecula/node3/
/backup/molecula/node3/.pilosa/.id
/backup/molecula/node3/.pilosa/.topology
/backup/molecula/node3/.pilosa/myindex
/backup/molecula/node4/
/backup/molecula/node4/.pilosa/.id
/backup/molecula/node4/.pilosa/.topology
/backup/molecula/node4/.pilosa/myindex
NOTE: your .pilosa directories need not be named .pilosa. They can
be something else, such as when the -d flag to pilosa server was used.
The .id file, the .topology file, and the index directories must be
found directly underneath.
Then a typical invocation to scan a cluster backup for issues:
$ cd /backup/molecula/
$ pilosa-fsck -replicas 3 node1/.pilosa node2/.pilosa node3/.pilosa node4/.pilosa &> log
A typical invocation to repair the replication in the same backup:
$ pilosa-fsck -replicas 3 -fix node1/.pilosa node2/.pilosa node3/.pilosa node4/.pilosa &> log
In both cases, the .id and .topology files must
be present in the backups.
Without -fix, no modifications will be made to the backups. Only
by running with -fix will repairs be made. The user can safely
always run with -fix to repair only if needed.
A zero error code will be returned to the shell if no repairs were needed.
A zero error code will be also be returned to the shell if
repairs were needed and they were accomplished under -fix.
A non-zero error code indicates that repairs were needed but
were not made.
`)
}
}
// call c.ValidateConfig() after myflags.Parse()
func (c *FsckConfig) ValidateConfig() error {
if c.Fix {
c.FixCol = true
}
if c.ReplicaN == 0 && c.PilosaConfigPath == "" {
return fmt.Errorf("must supply -replicas with the replica count from your pilosa.conf (positive integer count)")
}
if c.ReplicaN == 0 && c.PilosaConfigPath != "" {
if !FileExists(c.PilosaConfigPath) {
return fmt.Errorf(" -config path '%v' does not exist", c.PilosaConfigPath)
}
by, err := ioutil.ReadFile(c.PilosaConfigPath)
if err != nil {
return fmt.Errorf("error: could not read the -config path '%v': '%v'", c.PilosaConfigPath, err)
}
srvcfg, err := server.ParseConfig(string(by))
if err != nil {
//vv("warning: -config path '%v' problem, could not parse toml: '%v'", c.PilosaConfigPath, err)
// fall back to manual parsing of config
lines := strings.Split(string(by), "\n")
clusterStart := -1
for i, line := range lines {
if strings.Contains(line, `[cluster]`) {
clusterStart = i
}
if i > clusterStart {
if strings.Contains(line, "replicas") {
split := strings.Split(line, "=")
ns := strings.TrimSpace(split[1])
n, err := strconv.Atoi(ns)
if err != nil {
return fmt.Errorf("error: could not parse the replicaN from line %v in -config path '%v' (%v): '%v'", i+1, c.PilosaConfigPath, line, err)
}
c.ReplicaN = n
}
}
}
} else {
c.ReplicaN = srvcfg.Cluster.ReplicaN
}
if c.ReplicaN == 0 {
return fmt.Errorf("error: -config path '%v' did not list the Replica count: cannot be 0. See the [cluster] section, the 'replicas = R' line.", c.PilosaConfigPath)
}
//vv("c.ReplicaN = %v", c.ReplicaN)
}
return nil
}
var ProgramName = "pilosa-fsck"
func main() {
myflags := flag.NewFlagSet(ProgramName, flag.ContinueOnError)
cfg := &FsckConfig{}
cfg.DefineFlags(myflags)
cfg.Verbose = true
err := myflags.Parse(os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "\n%v\n", err.Error())
os.Exit(1)
}
err = cfg.ValidateConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "%s error: %s\n", ProgramName, err)
os.Exit(1)
}
dirs := myflags.Args()
nDir := len(dirs)
if nDir <= 0 && !cfg.Colkeydump {
fmt.Fprintf(os.Stderr, "error: %v command line arguments missing error: provide all of the top-level pilosa directories for the cluster as command line arguments.\n", ProgramName)
os.Exit(1)
}
cmdline := strings.Join(os.Args, " ")
// make sure all the dir are distinct
dup := make(map[string]bool)
for _, dir := range dirs {
if dup[dir] {
fmt.Fprintf(os.Stderr, "%v error: duplicate data directory '%v' given in command line '%v'. Each backup directory must be distinct.\n", ProgramName, dir, cmdline)
os.Exit(1)
} else {
dup[dir] = true
}
}
fmt.Fprintf(os.Stdout, "#!/bin/bash\n\n# pilosa-fsck version: %v\n", pilosa.VersionInfo())
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not read current dir: '%v'\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "# cwd: %v\n", cwd)
fmt.Fprintf(os.Stdout, "# command line: %v\n", cmdline)
t0 := time.Now()
fmt.Fprintf(os.Stdout, "# started at %v\n\n", t0.Format(RFC3339MsecTz0))
defer func() {
fmt.Fprintf(os.Stdout, "# finished at %v (elapsed %v)\n\n", time.Now().Format(RFC3339MsecTz0), time.Since(t0))
}()
cfg.Dirs = dirs
fixNeeded, err := cfg.Run()
if err != nil {
fmt.Fprintf(os.Stdout, "# finished at %v (elapsed %v)\n\n", time.Now().Format(RFC3339MsecTz0), time.Since(t0))
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if fixNeeded && !cfg.Fix {
fmt.Fprintf(os.Stdout, "# finished at %v (elapsed %v)\n\n", time.Now().Format(RFC3339MsecTz0), time.Since(t0))
fmt.Fprintf(os.Stderr, "# pilosa-fsck exiting with non-zero error code because a repair is needed, but -fix was not given.\n")
os.Exit(1)
}
}
func (cfg *FsckConfig) Run() (fixNeeded bool, err error) {
// if cfg.Colkeydump {
// cfg.dumpcols()
//}
perNodeIndexMaps, clusterNodes, ats, err := cfg.read()
if err != nil {
return false, err
}
if cfg.FixCol {
err := cfg.RepairTranslationStores(ats)
if err != nil {
return false, fmt.Errorf("error fixing key translation stores with cfg.RepairTranslationStores(): '%v'\n", err)
}
}
//vv("perNodeIndexMaps='%#v', clusterNodes='%#v'", perNodeIndexMaps, clusterNodes)
fixme, reports, err := cfg.analyze(clusterNodes, perNodeIndexMaps, ats)
if err != nil {
return false, fmt.Errorf("error in FsckConfig.analyze(): '%v'", err)
}
fixNeeded = ats.RepairNeeded || fixme
for _, report := range reports {
fmt.Printf("%v\n", report)
}
if len(reports) == 0 {
fmt.Fprintf(os.Stderr, "pilosa-fsck: no index found to analyze. cmdline was: %v\n", strings.Join(os.Args, " "))
}
return
}
var _ = (&FsckConfig{}).dumpAts
func (cfg *FsckConfig) dumpAts(ats *pilosa.AllTranslatorSummary) {
fmt.Printf("# dumpAts: RepairNeeded=%v\n", ats.RepairNeeded)
for _, sum := range ats.Sums {
fmt.Printf("# sum = '%#v'\n", sum)
}
}
type group struct {
elem []*pilosa.TranslatorSummary
partitionID int
}
func (g *group) String() (s string) {
for i, e := range g.elem {
s += fmt.Sprintf("partition %v, group elem [%v] out of %v: %v\n", g.partitionID, i, len(g.elem), e.String())
}
return
}
func indexesFromAts(ats *pilosa.AllTranslatorSummary) (indexes []string) {
indexMap := make(map[string]bool)
for _, sum := range ats.Sums {
if !indexMap[sum.Index] {
indexMap[sum.Index] = true
indexes = append(indexes, sum.Index)
}
}
sort.Strings(indexes)
return
}
func (cfg *FsckConfig) RepairTranslationStores(ats *pilosa.AllTranslatorSummary) (err error) {
verbose := cfg.Verbose
// group by index first. then repair.
indexes := indexesFromAts(ats)
for _, index := range indexes {
if !cfg.DoingIndex(index) {
continue
}
m := make(map[int]*group)
for _, sum := range ats.Sums {
if !sum.IsColKey || sum.Index != index {
continue
}
grp := m[sum.PartitionID]
if grp == nil {
grp = &group{
partitionID: sum.PartitionID,
}
m[sum.PartitionID] = grp
}
grp.elem = append(grp.elem, sum)
}
for partitionID, group := range m {
_ = partitionID
prim := -1
keyCount := 0
for k, e := range group.elem {
if e.IsPrimary {
prim = k
}
keyCount += e.KeyCount
}
if prim == -1 {
panic(fmt.Sprintf("no primary found for group '%v'", group.String()))
}
primary := group.elem[prim]
primaryChecksum := primary.Checksum
for _, e := range group.elem {
if e.IsPrimary {
continue
}
// is e a replica? not necessarily! have to check.
if !e.IsReplica {
//if verbose {
// since this will happen even on a fix point, where it is already empty,
// we don't report it again.
//fmt.Printf("# non-replica should have no data: creating an empty translation store here at '%v'\n", e.StorePath)
//}
err := os.RemoveAll(e.StorePath)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("RepairTranslationStores() os.RemoveAll(e.StorePath='%v')", e.StorePath))
}
store, err := boltdb.OpenTranslateStore(e.StorePath, e.Index, e.Field, e.PartitionID, topology.DefaultPartitionN)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("RepairTranslationStores() create empty boldtdb: boltdb.OpenTranslateStore e.StorePath='%v'", e.StorePath))
}
err = store.Close()
if err != nil {
return errors.Wrap(err, fmt.Sprintf("RepairTranslationStores() closing empty boltdb at path '%v'", e.StorePath))
}
continue
}
// INVAR: e is a replica for this paritionID.
// Copy from primary if checksums are different.
if e.Checksum != primaryChecksum {
from := group.elem[prim].StorePath
dest := e.StorePath
if verbose {
fmt.Printf("# e.Checksum '%v' != primaryChecksum '%v': copying from primary translation store '%v' -> '%v'\n", e.Checksum, primaryChecksum, from, dest)
}
err := cp(from, dest)
if err != nil {
return fmt.Errorf("error: could not copy from primary '%v' to replica translation store '%v': '%v' ... try to keep going...\n", from, dest, err)
}
}
}
}
}
return nil
}
/*
func (cfg *FsckConfig) dumpcols() {
verbose := cfg.Verbose
quiet := cfg.Quiet
_, _ = verbose, quiet
dir := cfg.Dir
index := cfg.Index
partitionID := cfg.PartitionID
showKey := cfg.ShowKey
showID := cfg.ShowID
if !quiet {
fmt.Printf("# dumpcols: opening dir '%v'... this may take a few minutes...\n", dir)
}
holder := pilosa.NewHolder(dir, nil)
holder.OpenTranslateStore = boltdb.OpenTranslateStore
err := holder.Open()
if err != nil {
log.Fatal(err)
}
if cfg.ShowHeader {
fmt.Println("# columnKey columId")
}
id_key := make(map[uint64]string)
key_id := make(map[string]uint64)
for _, idx := range holder.Indexes() {
fmt.Printf("# Looking '%v'\n", idx.Name())
if idx.Name() == index {
store := idx.TranslateStore(partitionID)
fmt.Printf("# Key By ID partitionID = %v\n", partitionID)
err := store.KeyWalker(func(key string, col uint64) {
key_id[key] = col
if showKey {
fmt.Printf("# '%v' %v shard: %v partition: %v\n", key, col, col/pilosa.ShardWidth, partitionID)
}
})
panicOn(err)
}
}
for _, idx := range holder.Indexes() {
if idx.Name() == index {
store := idx.TranslateStore(partitionID)
//fmt.Printf("# ID ByKey\n")
err := store.IDWalker(func(key string, col uint64) {
id_key[col] = key
if showID {
fmt.Printf("# '%v' %v\n", key, col)
}
})
panicOn(err)
}
}
fmt.Printf("# k: %d i: %d\n", len(key_id), len(id_key))
fmt.Println("id_key")
for k, v := range id_key {
l, ok := key_id[v]
if ok {
if k != l {
fmt.Printf("# X: %v %v %v\n", k, l, v)
}
} else {
fmt.Printf("# key not in id %v\n", v)
}
}
fmt.Println("key_id")
for k, v := range key_id {
l, ok := id_key[v]
if ok {
if k != l {
fmt.Printf("# T: %v %v %v\n", k, l, v)
}
} else {
fmt.Printf("# id not in key %v\n", v)
}
}
}
*/
func (cfg *FsckConfig) read() (perNodeIndexMaps []map[string]*pilosa.IndexFragmentSummary, clusterNodes []string, final *pilosa.AllTranslatorSummary, err error) {
final = pilosa.NewAllTranslatorSummary()
dirs := cfg.Dirs
for _, dir := range dirs {
idx2frag, nodeID, atsNode, err := cfg.readOneDir(dir)
if err != nil {
return nil, nil, nil, err
}
final.Append(atsNode)
clusterNodes = append(clusterNodes, nodeID)
perNodeIndexMaps = append(perNodeIndexMaps, idx2frag)
}
return
}
func (cfg *FsckConfig) readOneDir(dir string) (idx2frag map[string]*pilosa.IndexFragmentSummary, nodeID string, atsNode *pilosa.AllTranslatorSummary, err error) {
verbose := cfg.Verbose
quiet := cfg.Quiet
if !quiet {
fmt.Printf("# opening dir '%v'... this may take a few minutes...\n\n", dir)
}
jmphasher := &topology.Jmphasher{}
partitionN := topology.DefaultPartitionN
replicaN := cfg.ReplicaN
topo, err := loadTopology(dir, jmphasher, partitionN, replicaN)
if err != nil {
return nil, "", nil, err
}
cfg.topo = topo
//vv("topo = '%#v'", topo)
nodeIDs := topo.GetNodeIDs()
//vv("nodeIDs = '%#v'", nodeIDs)
nNodes := len(nodeIDs)
nDir := len(cfg.Dirs)
if nDir != nNodes {
return nil, "", nil, fmt.Errorf("command line had %v directories (%#v) but the .topology had %v nodes (%#v)", nDir, cfg.Dirs, nNodes, nodeIDs)
}
holder := pilosa.NewHolder(dir, nil)
holder.OpenTranslateStore = boltdb.OpenTranslateStore
nodeID, err = holder.LoadNodeID()
panicOn(err)
//vv("nodeID = '%v'", nodeID)
err = holder.Open()
if err != nil {
log.Fatal(err)
}
if !quiet {
fmt.Printf("\n# calculating hashes of row and column key translation maps on data from dir '%v'...\n", dir)
}
var indexes []*pilosa.Index
const checkKeys = true
atsNode = pilosa.NewAllTranslatorSummary()
for _, idx := range holder.Indexes() {
if !cfg.DoingIndex(idx.Name()) {
continue
}
//vv("calling idx.ComputeTranslatorSummary(verbose, checkKeys=%v, cfg.FixCol='%v')", checkKeys, cfg.FixCol)
asum, err := idx.ComputeTranslatorSummary(verbose, checkKeys, cfg.FixCol, topo, nodeID, cfg.ParallelReaders)
if err != nil {
log.Fatal(err)
}
atsNode.Append(asum)
indexes = append(indexes, idx)
}
atsNode.Sort()
hasher := blake3.New()
if !quiet {
fmt.Printf("\n# summary of col/row translations in dir: %v:\n", dir)
}
for _, sum := range atsNode.Sums {
if !quiet {
fmt.Printf("# index: %v partitionID: %v blake3-%v keyCount: %v idCount: %v\n", sum.Index, sum.PartitionID, sum.Checksum, sum.KeyCount, sum.IDCount)
}
_, _ = hasher.Write([]byte(sum.Checksum))
}
var buf [16]byte
_, _ = hasher.Digest().Read(buf[0:])
if !quiet {
fmt.Printf("# all-checksum = blake3-%x\n", buf)
}
// fragment analysis
showBits := false
showOpsLog := false
idx2frag = make(map[string]*pilosa.IndexFragmentSummary) // on this node.
for _, idx := range indexes {
if verbose {
fmt.Printf("# ==============================\n")
fmt.Printf("# index: %v\n", idx.Name())
fmt.Printf("# ==============================\n")
}
frgsum := idx.WriteFragmentChecksums(os.Stdout, showBits, showOpsLog, topo, verbose)
frgsum.Dir = dir
frgsum.NodeID = nodeID
idx2frag[idx.Name()] = frgsum
}
_ = holder.Close()
//vv("idx2frag = '%v'", idx2frag) // tons of output. see 1234.out.full for examaple.
return
}
func (cfg *FsckConfig) DoingIndex(index string) bool {
if cfg.JustThisIndex == "" {
// scan all indexes
return true
}
if index == cfg.JustThisIndex {
// scan just this one
return true
}
return false
}
// from cluster.go:1924
func loadTopology(holderDir string, hasher topology.Hasher, partitionN, replicaN int) (*pilosa.Topology, error) {
buf, err := ioutil.ReadFile(filepath.Join(holderDir, ".topology"))
if err != nil {
return nil, err
}
var pb internal.Topology
err = proto.Unmarshal(buf, &pb)
if err != nil {
return nil, err
}
return pilosa.DecodeTopology(&pb, hasher, partitionN, replicaN, nil)
}
func (cfg *FsckConfig) analyze(clusterNodes []string, perNodeIndexMaps []map[string]*pilosa.IndexFragmentSummary, ats *pilosa.AllTranslatorSummary) (fixNeeded bool, reports []string, err error) {
verbose := cfg.Verbose
quiet := cfg.Quiet
_, _ = verbose, quiet
allIndex := make(map[string]bool)
for _, mp := range perNodeIndexMaps {
for index := range mp {
allIndex[index] = true
}
}
if !quiet {
vv("allIndex = '%#v'", allIndex)
}
for index := range allIndex {
if !quiet {
vv("on index '%v'", index)
}
nodes2fragsum := make(map[string]*pilosa.IndexFragmentSummary)
for _, mp := range perNodeIndexMaps {
sum := mp[index]
if sum == nil {
continue
}
nodes2fragsum[sum.NodeID] = sum
}
fixme, report, err := cfg.analyzeThisIndex(index, nodes2fragsum, ats)
if err != nil {
return false, reports, fmt.Errorf("error in analyze of index '%v': '%v'", index, err)
}
fixNeeded = fixNeeded || fixme
reports = append(reports, report)
}
return fixNeeded, reports, nil
}
func (cfg *FsckConfig) analyzeThisIndex(
index string,
nodes2fragsum map[string]*pilosa.IndexFragmentSummary,
ats *pilosa.AllTranslatorSummary,
) (fixNeeded bool, report string, err error) {
verbose := cfg.Verbose
quiet := cfg.Quiet
_, _ = verbose, quiet
var removedBytes int64
var copiedBytes int64
var changedFiles int64
var totalFiles int64
var overwrittenBytes int64
var totalBytes int64
if !quiet {
vv("top of analyzeThisIndex(index='%v'); len of nodes2fragsum = %v; nodes2fragsum='%#v'",
index, len(nodes2fragsum), nodes2fragsum)
}
// Create a snapshot of the cluster to use for node/partition calculations.
snap := topology.NewClusterSnapshot(cfg.topo, cfg.topo.Hasher, cfg.topo.ReplicaN)
for node, sum := range nodes2fragsum {
if !quiet {
fmt.Printf("# on node '%v'\n", node)
}
// do they disagree on who is the primary?
// for each fragment, do they disagree on the checksum?
// Q: which nodes are supposed to have data, and which
// nodes are not supposed to have data?
// loopFragSum:
for relpath, fragsum := range sum.RelPath2fsum {
fragsum.NodeID = node
totalFiles++
//vv("checking %v on node %v", relpath, node)
replicas, nonReplicas := snap.ReplicasForPrimary(fragsum.Primary)
_, _ = replicas, nonReplicas
//vv("replicas = '%#v'", replicas)
//vv("nonReplicas = '%#v'", nonReplicas)
err := cfg.verifyReplicasAvailable(replicas, nonReplicas, nodes2fragsum, fragsum)
if err != nil {
return fixNeeded, "", err
}
// find the primary's checksum
primaryChecksum := ""
var primaryFragSum *pilosa.FragSum
for node, isPrimary := range replicas {
if isPrimary {
primarySum := nodes2fragsum[node]
primaryFragSum = primarySum.RelPath2fsum[relpath]
if primaryFragSum == nil {
// This seems clear indication that we have the topology wrong.
// When the topology is right, there are NO errors of this kind.
//
msg := fmt.Sprintf("# ugh. BAD. Stopping because any fix will be wrong. We see wrong -replica %v param, OR the .id files are mis-assigned with respect to the topology file. Could not find primary FragSum for relpath = '%v'. replicas = '%#v', nonReplicas = '%#v'\n", cfg.ReplicaN, relpath, replicas, nonReplicas)
vv(msg)
fmt.Fprintf(os.Stderr, "%v\n", msg)
panic(msg) // stop. the fixes are going to be wrong.
} else {
primaryChecksum = primaryFragSum.Checksum
primaryFragSum.NodeID = node
primaryFragSum.ScanDone = true
}
break
}
}
if primaryChecksum == "" {
return fixNeeded, "", fmt.Errorf("could not find primary replica??? replicas='%#v', nodes2fragsum='%v'; for fragsum='%#v'", replicas, nodes2fragsum, fragsum)
}
// is this a non-replica?
_, isNon := nonReplicas[fragsum.NodeID]
if isNon {
removedBytes += FileSize(fragsum.AbsPath)
changedFiles++
//vv("yes, is nonReplica: fragsum.NodeID='%v'", fragsum.NodeID)
if !quiet {
fmt.Printf("rm %v #### REPAIR REMOVE data from non-replica at node '%v' (fragsum='%#v') vs. primary (%#v)\n\n", fragsum.AbsPath, node, fragsum, primaryFragSum)
}
if cfg.Fix {
err := os.Remove(fragsum.AbsPath)
if err != nil {
return fixNeeded, "", fmt.Errorf("error removing non-replica extra fragment '%v': '%v'", fragsum.AbsPath, err)
}
}
} else {
presz := FileSize(fragsum.AbsPath)
totalBytes += presz
checksum := fragsum.Checksum
if checksum != primaryChecksum {
copiedBytes += FileSize(primaryFragSum.AbsPath)
changedFiles++
overwrittenBytes += presz
if !quiet {
fmt.Printf("cp %v %v #### REPAIR OVERWRITE replica at node '%v' (%#v) from primary '%v' (%#v)\n", primaryFragSum.AbsPath, fragsum.AbsPath, node, fragsum, primaryFragSum.NodeID, primaryFragSum)
}
if cfg.Fix {
err := cp(primaryFragSum.AbsPath, fragsum.AbsPath)
if err != nil {
return fixNeeded, "", fmt.Errorf("error copying from '%v' to '%v': '%v'",
primaryFragSum.AbsPath, fragsum.AbsPath, err)
}
}
}
}
fragsum.ScanDone = true
}
}
nDir := len(nodes2fragsum)
keyCount, idCount := cfg.getKeyIDCounts(index, ats)
fixNeeded = changedFiles > 0 || ats.RepairNeeded
var actionTaken string
var wouldBe string
if cfg.Fix || cfg.FixCol {
if fixNeeded {
actionTaken = "*REPAIRS WERE MADE TO THE BACKUPS*"
wouldBe = "sync repairs made:"
} else {
wouldBe = ""
actionTaken = "NO REPAIR NEEDED."
}
} else {
if fixNeeded {
wouldBe = "sync actions that would be taken under -fix:"
actionTaken = "*REPAIRS NEEDED BUT WERE NOT APPLIED* ; pilosa-fsck -fix was omitted."
} else {
wouldBe = ""
actionTaken = "NO REPAIR NEEDED."
}
}
var fragUpdate string
if changedFiles > 0 {
fragUpdate = fmt.Sprintf(`
# %v
# copied bytes: %v
# file bytes overwritten: %v
# new bytes added: %v
# new bytes is %0.01f%% of %v total bytes
# removed %v bytes from non-replicas
# changed file count %v (%0.01f%%; total files=%v)
#
`, wouldBe, humanize.Comma(copiedBytes), humanize.Comma(overwrittenBytes), humanize.Comma(copiedBytes-overwrittenBytes), 100*float64(copiedBytes-overwrittenBytes)/float64(totalBytes), humanize.Comma(totalBytes), humanize.Comma(removedBytes), changedFiles, 100*float64(changedFiles)/float64(totalFiles), humanize.Comma(totalFiles))
}
report = fmt.Sprintf(`
# ========================================================
# pilosa-fsck final report
#
# run with -fix: %v
#
# index examined: '%v'
#
# nodes examined: %v
# -replicas %v replication factor used
#
# feature data examined: %v bytes
# feature files examined: %v files
#
# key-translation-stores examined: %v
# key-count: %v over all replicas
# id-count: %v over all replicas
#
# %v
# %v
# ========================================================
`,
cfg.Fix, index, nDir, cfg.ReplicaN, humanize.Comma(totalBytes), humanize.Comma(totalFiles), humanize.Comma(int64(nDir*topology.DefaultPartitionN)), humanize.Comma(int64(keyCount)), humanize.Comma(int64(idCount)), actionTaken, fragUpdate)
return
}
func (cfg *FsckConfig) verifyReplicasAvailable(replicas, nonReplicas map[string]bool, nodes2fragsum map[string]*pilosa.IndexFragmentSummary, fragsum *pilosa.FragSum) error {
for node := range replicas {
if nodes2fragsum[node] == nil {
return fmt.Errorf("error: node '%v' needed for a replica set was not availabe. Did you give ALL the directories for your cluster on the command line at once? In nodes2fragsum '%#v' (replicas: '%#v'; non-replicas '%#v') for fragsum '%v'", node, nodes2fragsum, replicas, nonReplicas, fragsum)
}
}
return nil
}
func cp(fromPath, toPath string) (err error) {
tmpTo := toPath + ".fsck.tmp"
toFd, err := os.Create(tmpTo)
if err != nil {
return err
}
defer toFd.Close()
fromFd, err := os.Open(fromPath)
if err != nil {
return err
}
defer fromFd.Close()
_, err = io.Copy(toFd, fromFd)
if err != nil {
return err
}
err = toFd.Close()
if err != nil {
return err
}
return os.Rename(tmpTo, toPath)
}
func (cfg *FsckConfig) getKeyIDCounts(index string, ats *pilosa.AllTranslatorSummary) (keyCount, idCount int) {
for _, sum := range ats.Sums {
if sum.Index == index {
keyCount += sum.KeyCount
idCount += sum.IDCount
}
}
return
}

View file

@ -1,448 +0,0 @@
// Copyright 2020 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 main
import (
"context"
"fmt"
"io/ioutil"
"reflect"
"strconv"
"testing"
"time"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/boltdb"
"github.com/pilosa/pilosa/v2/hash"
"github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/server"
"github.com/pilosa/pilosa/v2/test"
)
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
nReplicas := 3
name := t.Name()
var nodeid []string
for i := 0; i < nNodes; i++ {
// work around a bug in the test.MustRunCluster that corrupts
// the .topology file if we only join name with one "_" underscore.
nodeid = append(nodeid, name+"__"+strconv.Itoa(i))
}
c := test.MustRunCluster(t, nNodes,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID(nodeid[0]),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(nReplicas),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID(nodeid[1]),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(nReplicas),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID(nodeid[2]),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(nReplicas),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerNodeID(nodeid[3]),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(nReplicas),
)},
)
// note: do not defer c.Close() here. We manually close below.
var nodes []*test.Command
var dirs []string
for i := 0; i < nNodes; i++ {
nd := c.GetNode(i)
nodes = append(nodes, nd)
dirs = append(dirs, nd.Server.Holder().Path())
}
ctx := context.Background()
index := []string{"rick", "morty"}
fieldName := []string{"f", "flying_car"}
idx := make([]*pilosa.Index, len(index))
field := make([]*pilosa.Field, len(index))
var err error
for i := range index {
idx[i], err = nodes[0].API.CreateIndex(ctx, index[i], pilosa.IndexOptions{Keys: true, TrackExistence: true})
if err != nil {
t.Fatalf("creating index: %v", err)
}
if idx[i].CreatedAt() == 0 {
t.Fatal("index createdAt is empty")
}
field[i], err = nodes[0].API.CreateField(ctx, index[i], fieldName[i], pilosa.OptFieldTypeSet(pilosa.DefaultCacheType, 100))
if err != nil {
t.Fatalf("creating field: %v", err)
}
if field[i].CreatedAt() == 0 {
t.Fatal("field createdAt is empty")
}
}
rowID := uint64(1)
timestamp := int64(0)
for i := range index {
// Generate some keyed records.
rowIDs := []uint64{}
timestamps := []int64{}
N := 10
for j := 1; j <= N; j++ {
rowIDs = append(rowIDs, rowID)
timestamps = append(timestamps, timestamp)
}
var colKeys []string
switch i {
case 0:
// Keys are sharded so ordering is not guaranteed.
colKeys = []string{"col10", "col8", "col9", "col6", "col7", "col4", "col5", "col2", "col3", "col1"}
colKeys = colKeys[:N]
case 1:
colKeys = []string{"col11", "col12"}
N = len(colKeys)
rowIDs = rowIDs[:N]
timestamps = timestamps[:N]
}
// 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)
req := &pilosa.ImportRequest{
Index: index[i],
IndexCreatedAt: idx[i].CreatedAt(),
Field: fieldName[i],
FieldCreatedAt: field[i].CreatedAt(),
// even though this says Shard: 0, that won't matter. The column keys
// get hashed and that decides the actual shard.
Shard: 0,
RowIDs: rowIDs,
ColumnKeys: colKeys,
Timestamps: timestamps,
}
qcx := nodes[0].API.Txf().NewQcx()
if err := nodes[0].API.Import(ctx, qcx, req); err != nil {
t.Fatal(err)
}
panicOn(qcx.Finish())
//qcx.Reset()
pql := fmt.Sprintf("Row(%s=%d)", fieldName[i], rowID)
// Query node0.
if res, err := nodes[0].API.Query(ctx, &pilosa.QueryRequest{Index: index[i], Query: pql}); err != nil {
t.Fatal(err)
} else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) {
t.Fatalf("expected colKeys='%#v'; observed column keys: %#v", colKeys, keys)
}
// Query node1.
if err := test.RetryUntil(5*time.Second, func() error {
if res, err := nodes[1].API.Query(ctx, &pilosa.QueryRequest{Index: index[i], Query: pql}); err != nil {
return err
} else if keys := res.Results[0].(*pilosa.Row).Keys; !reflect.DeepEqual(keys, colKeys) {
return fmt.Errorf("unexpected column keys: %#v", keys)
}
return nil
}); err != nil {
t.Fatal(err)
}
}
// end of setup.
// partitionID in use: 6, 31, 57, 133, 185, 235
targetPartition := 31 // which partitionID we mess with.
targetNode := nodes[0] // this is the first replica.
targetIndex := index[0]
// 0 first replica
// 1 second replica
// 2 -- not a replica
// 3 primary
cfg := &FsckConfig{
Fix: false,
FixCol: false,
Quiet: true,
//Verbose: true,
ReplicaN: nReplicas,
Dirs: dirs,
ParallelReaders: 5,
}
panicOn(cfg.ValidateConfig())
// for this test, mess up a replica that is not the primary.
h := targetNode.API.Holder()
idx[0] = h.Index(index[0])
store := idx[0].TranslateStore(targetPartition)
fwd, rev := getFwdRev(store, targetPartition)
//vv("targetPartition=%v, store.PartitionID=%v, before corruption, fwd='%#v', rev='%#v'", targetPartition, store.PartitionID, fwd, rev)
// # fsck_test.go:288 2020-10-01T13:39:57.718995-05:00 partition 31, key 'col5' -> db00001
presz := len(rev)
delete(rev, fwd["col5"])
postsz := len(rev)
if postsz == presz {
panic("did not delete any key!")
}
bolt := store.(*boltdb.TranslateStore)
//vv("pre corruption, bolt = '%v'", fileChecksum(bolt.Path))
//bolt.DumpBolt("pre-corruption")
if err := bolt.SetFwdRevMaps(nil, fwd, rev); err != nil {
t.Fatal(err)
}
//vv("post corruption, bolt = '%v'", fileChecksum(bolt.Path))
//bolt.DumpBolt("post-corruption")
//fwd3, rev3 := getFwdRev(store, targetPartition)
//vv("after corruption, fwd='%#v', rev='%#v'", fwd3, rev3)
targetIndex1 := "morty"
targetPartition1 := 226 // for "col11"
// # fsck_test.go:248 2020-10-06T20:24:33.755576-05:00 on k=47, idx[1]: targetPartition=47, store.PartitionID=0x4abe160, before corruption, fwd1='map[string]uint64{"col12":0xcf00001}', rev1='map[uint64]string{0xcf00001:"col12"}'
//# fsck_test.go:248 2020-10-06T20:24:35.608568-05:00 on k=226, idx[1]: targetPartition=226, store.PartitionID=0x4abe160, before corruption, fwd1='map[string]uint64{"col11":0xcc00001}', rev1='map[uint64]string{0xcc00001:"col11"}'
idx[1] = h.Index(index[1])
store1 := idx[1].TranslateStore(targetPartition1)
fwd1, rev1 := getFwdRev(store1, targetPartition1)
//vv("on k=%v, idx[1]: targetPartition=%v, store.PartitionID=%v, before corruption, fwd1='%#v', rev1='%#v'", k, targetPartition1, store.PartitionID, fwd1, rev1)
presz1 := len(rev1)
delete(rev1, fwd1["col11"])
postsz1 := len(rev1)
if postsz1 == presz1 {
panic("did not delete any key!")
}
bolt1 := store1.(*boltdb.TranslateStore)
if err := bolt1.SetFwdRevMaps(nil, fwd1, rev1); err != nil {
t.Fatal(err)
}
// done corrupting.
for _, nd := range nodes {
nd.Command.Close()
}
//panicOn(bolt.Open())
//bolt.DumpBolt("post-corruption, after Close. bolt:")
//bolt.Close()
//chksums := getChecksums(dirs, cfg, targetPartition)
//vv("post corruption, pre repair chksums = '%#v'", chksums)
// first we check that the corruption can be detected
// by our test with the checksums.
chk, err := check(dirs, cfg, targetIndex, targetPartition)
_ = chk
//vv("pre-fix, chk='%v'; err='%v'", chk, err)
if err == nil {
panic("expected to see checksums not match! but no corruption detected.")
}
chk1, err := check(dirs, cfg, targetIndex1, targetPartition1)
_ = chk1
//vv("pre-fix, chk1='%v'; err='%v'", chk1, err)
if err == nil {
panic("expected to see checksums not match! but no corruption detected.")
}
// b) running in reporting mode only should report that a fix is needed.
fixNeeded, err := cfg.Run()
panicOn(err)
if !fixNeeded {
panic("fix should be needed now, before repair")
}
// c) run the fix.
cfg.Fix = true
cfg.FixCol = true
fixNeeded, err = cfg.Run()
panicOn(err)
if !fixNeeded {
panic("fix should be marked needed if repair was made")
}
// d) check that the replicas all look like the primary.
//chksums = getChecksums(dirs, cfg, targetPartition)
//vv("after repair chksums = '%#v'", chksums)
chk, err = check(dirs, cfg, targetIndex, targetPartition)
_ = chk
//vv("chk = '%v' after repair; err='%v'", chk, err)
panicOn(err)
chk1, err = check(dirs, cfg, targetIndex1, targetPartition1)
_ = chk1
//vv("chk = '%v' after repair; err='%v'", chk, err)
panicOn(err)
// e) run again, should see no fix needed.
fixNeeded, err = cfg.Run()
panicOn(err)
if fixNeeded {
panic("should see no fix needed after the prior repair")
}
}
func getFwdRev(store pilosa.TranslateStore, partitionID int) (fwd map[string]uint64, rev map[uint64]string) {
fwd = make(map[string]uint64)
rev = make(map[uint64]string)
_ = store.KeyWalker(func(key string, col uint64) {
//vv("partition %v, key '%v' -> %x", partitionID, key, col)
fwd[key] = col
})
_ = store.IDWalker(func(key string, col uint64) {
//vv("partition %v, id %x -> '%v'", partitionID, col, key)
rev[col] = key
})
return
}
func check(dirs []string, cfg *FsckConfig, targetIndex string, targetPartition int) (chksum string, err error) {
//vv("top of check, dirs = '%#v', targetIndex='%v', targetPartition='%v'", dirs, targetIndex, targetPartition)
//defer vv("returning from check()")
firstChecksum := ""
firstDir := ""
firstStorePath := ""
quiet := cfg.Quiet
defer func() {
cfg.Quiet = quiet
}()
cfg.Quiet = true
for i := range dirs {
dir := dirs[i]
_, _, ats, err := cfg.readOneDir(dir)
panicOn(err)
indexes := indexesFromAts(ats)
//vv("indexes = '%#v'", indexes)
for _, index := range indexes {
if index != targetIndex {
continue
}
for _, s := range ats.Sums {
//vv(" s= '%#v'", s)
if s.Index != index {
//vv("skipping s.Index '%v' != index '%v'", s.Index, index)
continue
}
if s.PartitionID != targetPartition {
continue
}
//vv("accepting s.PartitionID(%v) == targetPartition(%v); s.Index '%v'; "+
//"index '%v'; s.IsPrimary=%v, s.IsReplica=%v, s='%#v'; s.Checksum='%v', firstChecksum='%v'",
//s.PartitionID, targetPartition, s.Index, index,
//s.IsPrimary, s.IsReplica, s, s.Checksum, firstChecksum)
if s.IsPrimary || s.IsReplica {
chksum := s.Checksum
if firstChecksum == "" {
firstChecksum = chksum
firstDir = dir
firstStorePath = s.StorePath
} else {
//vv("targetIndex = '%v'; firstChecksum='%v', chksum='%v'", targetIndex, firstChecksum, chksum)
if chksum != firstChecksum {
return chksum, fmt.Errorf("bolt chksum on node %v '%v' disagrees with '%v' on '%v'; index='%v'; s.StorePath = '%v'; firstStorePath='%v'", dir, chksum, firstChecksum, firstDir, index, s.StorePath, firstStorePath)
}
}
}
}
}
}
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) {
for i := range dirs {
dir := dirs[i]
_, _, ats, err := cfg.readOneDir(dir)
panicOn(err)
for _, s := range ats.Sums {
if s.PartitionID != targetPartition {
continue
}
chksum = append(chksum, s.Checksum)
}
}
return
}
/* on shardwidth 20
# fsck_test.go:211 2020-09-30T17:19:05.823278-05:00 partition 6, key 'col2' -> dc00001
# fsck_test.go:214 2020-09-30T17:19:05.823309-05:00 partition 6, id dc00001 -> 'col2'
# fsck_test.go:211 2020-09-30T17:19:05.823430-05:00 partition 31, key 'col5' -> db00001
# fsck_test.go:214 2020-09-30T17:19:05.823447-05:00 partition 31, id db00001 -> 'col5'
# fsck_test.go:211 2020-09-30T17:19:05.823970-05:00 partition 57, key 'col10' -> 5d00001
# fsck_test.go:214 2020-09-30T17:19:05.823998-05:00 partition 57, id 5d00001 -> 'col10'
# fsck_test.go:211 2020-09-30T17:19:05.827007-05:00 partition 133, key 'col7' -> d900001
# fsck_test.go:214 2020-09-30T17:19:05.827071-05:00 partition 133, id d900001 -> 'col7'
# fsck_test.go:211 2020-09-30T17:19:05.827549-05:00 partition 185, key 'col3' -> dd00001
# fsck_test.go:214 2020-09-30T17:19:05.827573-05:00 partition 185, id dd00001 -> 'col3'
# fsck_test.go:211 2020-09-30T17:19:05.827792-05:00 partition 235, key 'col9' -> d700001
# fsck_test.go:214 2020-09-30T17:19:05.827809-05:00 partition 235, id d700001 -> 'col9'
*/
var _ = fileChecksum
func fileChecksum(path string) string {
by, err := ioutil.ReadFile(path)
panicOn(err)
return hash.Blake3sum16(by)
}

View file

@ -1 +0,0 @@
pilosa-fsck

View file

@ -1,252 +0,0 @@
Design for pilosa-fsck
======================
Problem Background
------------------
Molecula Pilosa provides replication for fault-tolerance within a Pilosa cluster.
Three kinds of data are replicated: Roaring bitmap data, Column-Key translation data,
and Row-Key data are replicated. Only the first two, Roaring data and Column-Key
data are relevant here. Broadly, the Roaring bitmap data
forms the central features -- the bits -- of a large, sparse bitmap matrix.
The Column-Keys are the labels for the columns at the top margin of this matrix.
For speed, the Roaring bitmap data is stored separately from the
Key data. The Roaring data is stored in sharded files
within a directory heirarchy under PILOSA-DATA-DIR/index_name/field_name/...
The Key translation data is stored in sharded BoltDB databases within
the PILOSA-DATA-DIR/index_name/_key directory.
The current approach to Roaring file replication involves an
eventually consistent mechanism that uses an Anti-Entropy agent to
fix partial or incomplete replication from the primary shard to all
replica shards.
Unfortunately, the Anti-Entropy agent approach has proved inadequate on two
fronts. First, it does not provide for immediately consistent reads in the
event that the primary is lost. Second, the Anti-Entropy agent itself experienced
out-of-memory issues that have yet to be resolved.
Therefore, work is now underway to replace this replication
approach with a more consistent design.
However, in the meantime, for our customers in production with Molecula
Pilosa, we wish to provide a means to re-establish correct replication.
Thus even in the event of a node failure followed by a read from a replica, the
returned read will be correct.
The pilosa-fsck tool can therefore be seen as a temporary, stop-gap
measure to address immediate issues while the cluster replication
mechanism is replaced.
The second factor motivating the creation of pilosa-fsck was the discovery
of a bug in the Key-translation process. Unfortunately this was a hard
to reproduce bug. It happened only on the customer's premises,
and only after running the system for a long time, with a
large amount of data, and with various eccentric node failures
and recoveries.
However, we were able to reproduce a plausible explanation.
Non-primary replicas were creating keys when they should have been
forwarding the request to the primary. Correcting this bug is impetus
for the v2.1.4 release of Molecula Pilosa.
A fine point here: since we were not able to precisely reproduce the customer's
issue in the development environment, we cannot guarantee with 100%
certainty that we have actually addressed the bug that the customer
was seeing.
Therefore we also desired an additional insurance
policy. We wished to be able to empower customers to proactively discover any
future Key-translation issues that happen in their on-premise systems.
To do this, we proposed providing select customers with the pilosa-fsck
tool which can analyze their offline backups for issues.
Optionally, these issues can also be repaired in-place in the
offline backup on which pilosa-fsck is run.
The -fix flag repairs both kinds of replication issues.
Solution Approach: mechanism of action
--------------------------------------
The pilosa-fsck is run offline on a full set of backups taken from
all nodes in a Pilosa cluster. It runs on a single computer that
must be separate from the production or staging Pilosa environments.
When run, pilosa-fsck analyzes the differences between the
primary and its replicas. Both the Roaring
files and the Key translation databases are analyzed.
The computer running pilosa-fsck must have the same or more
memory as the Pilosa nodes in the cluster, as it will
"pretend" to be each Pilosa node in turn. However, as each
node's backup is closed before the next node's backup is
opened, we do not require substantially more memory than a single
production node. Short Blake3 cryptographic checksums are
computed for each Roaring fragment and each Key translation
database. These are held in memory (and printed to the log)
for comparing nodes. This comparison forms the heart of
the consistency checks, and is the basis for any subsequent
repair.
We recommend capturing both stdout and stderr to a log.
Use `&> log` or `2>&1 > log` at the end of the
pilosa-fsck invocation to save a log of the run to disk.
In a typical cluster, the Replication factor R may be less
than the number of nodes N in the cluster. For example, while
N may be 4, the R may be only 3. In this example, within
each replicated shard, one node will be the primary for
that shard, two nodes will be non-primary replicas, and one
node will be a non-replica. Note that the designation
of primary changes for different Roaring shards within an index,
even on a single node.
The essence of the the -fix repair operation that pilosa-fsck
can do is this: it will copy from the primary to the
the non-primary replicas. Further, it will remove data from
any non-replica node if it was mistakenly present.
The pilosa-fsck output log will contain
a sequence of command line 'cp' and 'rm' commands.
These commands are merely a record (with
accompanying justifcation in the comment following the
command) of what actions would be performed to repair
the Roaring file data.
Only with -fix will the repair actions actually happen
during the pilosa-fsck run.
Details: running pilosa-fsck
----------------------------
Errors in invocation are reported on stderr and the program will exit with a non-zero
error code if invocation errors are present. A non-zero error code
is returned if a repair is needed and -fix was not given.
A -fix run will return a zero error code to the shell if the fix was
successfully made; or if no fix was required.
The log of the run is printed to stdout.
The -h flag to pilosa-fsck prints a summary of its operation
and a guide to laying out the backup directories.
The help is reproduced below.
~~~
$ pilosa-fsck version: Molecula Pilosa v2.2.1-43-g9dacbccf (Oct 5 2020 1:28PM, 9dacbccf)
Use: pilosa-fsck -replicas R {-fix} {-q} /backup/1/.pilosa /backup/2/.pilosa ... /backup/N/.pilosa
-fix
(warning: alters the backed-up node images on disk) copy primary data to replicas to create a consistent cluster.
-replicas R
(required) R is a positive integer, giving the replicaN or replicator factor for the cluster. This is
the number of replicas maintained in the cluster. Must be the same as the
[cluster] 'replicas = R' entry shared across all the pilosa.conf files on each node.
-q
be very quiet during analysis and repair
Welcome to pilosa-fsck. This is a scan and repair
tool that is modeled after the classic unix file
system utility fsck.
WARNING: DO NOT RUN ON A LIVE SYSTEM.
The most important point to remember is that analysis
and repair must be done *offline*.
Just as fsck must be run on an unmounted disk,
pilosa-fsck must be run on a backup. It must
not be run on the directories where a live Pilosa system
is serving queries. Instead, take a backup first.
A backup is a set of N Pilosa data directories that have been
copied from your live system. They must all
be visible and mounted on one filesystem together.
pilosa-fsck can be run in scan-mode (without -fix),
or in repair-mode with -fix. The console output
supplies a log documenting the analysis
and showing what data changes would have been made.
REQUIRED COMMAND LINE ARGUMENTS
The paths to all the top-level Pilosa
data directories in a cluster must be given on the command
line. The -replicas R flag is also always required. It
must be correct for your cluser. Here R is the same as
the [cluster] stanza "replicas = R" line from your
pilosa.conf.
Example:
Suppose you are ready to run pilosa-fsck:
you have taken a backup of your four node Pilosa
cluster and stored it all on one filesystem with
all nodes visible and uncompressed. This
is a pre-requisite to running pilosa-fsck.
Let's suppose we have replication R = 3 set.
In this example, have stored our backed-up directories in
/backup/molecula
and the four node backups are in
subdirectories node1/ node2/ node3/ node4/ under this:
/backup/molecula/node1/
/backup/molecula/node1/.pilosa/.id
/backup/molecula/node1/.pilosa/.topology
/backup/molecula/node1/.pilosa/myindex
/backup/molecula/node2/
/backup/molecula/node2/.pilosa/.id
/backup/molecula/node2/.pilosa/.topology
/backup/molecula/node2/.pilosa/myindex
/backup/molecula/node3/
/backup/molecula/node3/.pilosa/.id
/backup/molecula/node3/.pilosa/.topology
/backup/molecula/node3/.pilosa/myindex
/backup/molecula/node4/
/backup/molecula/node4/.pilosa/.id
/backup/molecula/node4/.pilosa/.topology
/backup/molecula/node4/.pilosa/myindex
NOTE: your .pilosa directories need not be named .pilosa. They can
be something else, such as when the -d flag to pilosa server was used.
The .id file, the .topology file, and the index directories must be
found directly underneath.
Then a typical invocation to scan a cluster backup for issues:
$ cd /backup/molecula/
$ pilosa-fsck -replicas 3 node1/.pilosa node2/.pilosa node3/.pilosa node4/.pilosa &> log
A typical invocation to repair the replication in the same backup:
$ pilosa-fsck -replicas 3 -fix node1/.pilosa node2/.pilosa node3/.pilosa node4/.pilosa &> log
In both cases, the .id and .topology files must
be present in the backups.
Without -fix, no modifications will be made to the backups. Only
by running with -fix will repairs be made. The user can safely
always run with -fix to repair only if needed.
A zero error code will be returned to the shell if no repairs were needed.
A zero error code will be also be returned to the shell if
repairs were needed and they were accomplished under -fix.
A non-zero error code indicates that repairs were needed but
were not made.
~~~

View file

@ -1,21 +0,0 @@
#!/bin/bash
set +x
export PATH=.:${PATH}
# unpack the sample Molecula Pilosa cluster.
tar xf backups.tar.gz
# check if repair is needed.
pilosa-fsck -replicas 3 backups/node0/pilosa backups/node1/pilosa backups/node2/pilosa backups/node3/pilosa
# yes, so do the repairs. This can be done first (only) as well.
#
pilosa-fsck -fix -replicas 3 backups/node0/pilosa backups/node1/pilosa backups/node2/pilosa backups/node3/pilosa
# check again if you like
#
pilosa-fsck -replicas 3 backups/node0/pilosa backups/node1/pilosa backups/node2/pilosa backups/node3/pilosa

View file

@ -1,177 +0,0 @@
// home: https://github.com/glycerine/vprint
// Copyright 2019 Jason E. Aten, Ph.D. All rights reserved.
// License: MIT
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main
import (
"fmt"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"sync"
"time"
)
const RFC3339MsecTz0 = "2006-01-02T15:04:05.000Z07:00"
const RFC3339UsecTz0 = "2006-01-02T15:04:05.000000Z07:00"
// for tons of debug output
var VerboseVerbose bool = false
// convience functions for . import
var pp = PP
var vv = VV
var panicOn = PanicOn
func init() {
// keeper linter happy
_ = pp
_ = vv
}
func PanicOn(err error) {
if err != nil {
panic(err)
}
}
func PP(format string, a ...interface{}) {
if VerboseVerbose {
TSPrintf(format, a...)
}
}
func VV(format string, a ...interface{}) {
TSPrintf(format, a...)
}
func AlwaysPrintf(format string, a ...interface{}) {
TSPrintf(format, a...)
}
var tsPrintfMut sync.Mutex
// time-stamped printf
func TSPrintf(format string, a ...interface{}) {
tsPrintfMut.Lock()
Printf("# %s %s ", FileLine(3), ts())
Printf(format+"\n", a...)
tsPrintfMut.Unlock()
}
// get timestamp for logging purposes
func ts() string {
return time.Now().Format(RFC3339UsecTz0)
}
// so we can multi write easily, use our own printf
var OurStdout io.Writer = os.Stdout
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(OurStdout, format, a...)
}
func FileLine(depth int) string {
_, fileName, fileLine, ok := runtime.Caller(depth)
var s string
if ok {
s = fmt.Sprintf("%s:%d", path.Base(fileName), fileLine)
} else {
s = ""
}
return s
}
func stack() string {
return string(debug.Stack())
}
func FileExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return false
}
return true
}
func DirExists(name string) bool {
fi, err := os.Stat(name)
if err != nil {
return false
}
if fi.IsDir() {
return true
}
return false
}
func FileSize(name string) int64 {
fi, err := os.Stat(name)
if err != nil {
return 0
}
return fi.Size()
}
// Caller returns the name of the calling function.
func Caller(upStack int) string {
// elide ourself and runtime.Callers
target := upStack + 2
pc := make([]uintptr, target+2)
n := runtime.Callers(0, pc)
f := runtime.Frame{Function: "unknown"}
if n > 0 {
frames := runtime.CallersFrames(pc[:n])
for i := 0; i <= target; i++ {
contender, more := frames.Next()
if i == target {
f = contender
}
if !more {
break
}
}
}
return f.Function
}
// happy linter:
var _ = DirExists
var _ = FileExists
var _ = Caller
var _ = stack
var _ = RFC3339MsecTz0
var _ = RFC3339UsecTz0
var _ = AlwaysPrintf
var _ = FileSize

View file

@ -27,24 +27,24 @@ import (
"time"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/pql"
)
// RandomQueryConfig
type RandomQueryConfig struct {
// user facing flags
HostPort string // -hostport
TreeDepth int // -d
QueryCount int // -n
Verbose bool // -v
VeryVerbose bool // -V
TimeFromArg string // --time.from
TimeToArg string // --time.to
TimeFrom time.Time // parsed time
TimeTo time.Time // parsed time
TimeRange int64 // hours between parsed times
HostPort string // -hostport
TreeDepth int // -d
QueryCount int // -n
Verbose bool // -v
VeryVerbose bool // -V
TimeFromArg string // --time.from
TimeToArg string // --time.to
TimeFrom time.Time // parsed time
TimeTo time.Time // parsed time
TimeRange int64 // hours between parsed times
IndexMap map[string]*Features
@ -73,7 +73,7 @@ type wrapper struct {
}
func (w *wrapper) Schema(ctx context.Context) ([]*pilosa.IndexInfo, error) {
return w.api.Schema(ctx), nil
return w.api.Schema(ctx)
}
func (w *wrapper) Query(ctx context.Context, index string, queryRequest *pilosa.QueryRequest) (*pilosa.QueryResponse, error) {
@ -234,11 +234,11 @@ NewSetup:
}
type Features struct {
Slc []IndexFieldRow
Ranges []IndexFieldRange
Slc []IndexFieldRow
Ranges []IndexFieldRange
Distinctables []IndexFieldRange
SlcWeight int
RangeWeight int
SlcWeight int
RangeWeight int
}
// Pick either a feature entry or a random query on a range, weighted
@ -274,7 +274,7 @@ func (fea *IndexFieldRow) Query(cfg *RandomQueryConfig) *Tree {
// anyway.
if fea.HasTime && cfg.Rnd.Int63n(20) != 0 {
startHours := (cfg.Rnd.Int63n(cfg.TimeRange - 1))
endHours := cfg.Rnd.Int63n(cfg.TimeRange - startHours) + 1 + startHours
endHours := cfg.Rnd.Int63n(cfg.TimeRange-startHours) + 1 + startHours
startTime := cfg.TimeFrom.Add(time.Duration(startHours) * time.Hour)
endTime := cfg.TimeFrom.Add(time.Duration(endHours) * time.Hour)
fromTo = fmt.Sprintf(", from=%s, to=%s",
@ -288,11 +288,11 @@ func (fea *IndexFieldRow) Query(cfg *RandomQueryConfig) *Tree {
}
type IndexFieldRange struct {
Index string
Field string
Index string
Field string
Min, Max, Scale int64
ScaleDiv float64
Range uint64
ScaleDiv float64
Range uint64
}
// We want to pick one of (1) a single-operation filter, (2) a
@ -316,8 +316,8 @@ func (i *IndexFieldRange) Query(cfg *RandomQueryConfig) *Tree {
v2 = v2 + uint64(i.Min)
var v1s, v2s string
if i.Scale != 0 {
v1s = fmt.Sprintf("%.*f", i.Scale, float64(int64(v1)) / i.ScaleDiv)
v2s = fmt.Sprintf("%.*f", i.Scale, float64(int64(v2)) / i.ScaleDiv)
v1s = fmt.Sprintf("%.*f", i.Scale, float64(int64(v1))/i.ScaleDiv)
v2s = fmt.Sprintf("%.*f", i.Scale, float64(int64(v2))/i.ScaleDiv)
} else {
v1s = strconv.FormatInt(int64(v1), 10)
v2s = strconv.FormatInt(int64(v2), 10)
@ -332,7 +332,7 @@ func (i *IndexFieldRange) Query(cfg *RandomQueryConfig) *Tree {
if cfg.Rnd.Int63n(2) == 1 {
v1s = v2s
}
return &Tree{S: fmt.Sprintf("Row(%s %s %s)", i.Field, binaryOps[r - 4], v1s)}
return &Tree{S: fmt.Sprintf("Row(%s %s %s)", i.Field, binaryOps[r-4], v1s)}
}
}
@ -463,8 +463,8 @@ func (cfg *RandomQueryConfig) GenQuery(index string) (pql string, err error) {
}
type Tree struct {
Chd []*Tree
S string
Chd []*Tree
S string
Args []string // Extra args to pass after children, such as a field for Distinct.
}
@ -496,6 +496,7 @@ func (tr *Tree) StringIndent(ind int) (s string) {
}
const pilosaTimeFmt = "2006-01-02T15:04"
func (cfg *RandomQueryConfig) GenTree(index string, depth int) (tr *Tree) {
features := cfg.IndexMap[index]
if depth == 0 {

View file

@ -49,7 +49,7 @@ func TestServerConfig(t *testing.T) {
tests := []commandTest{
// TEST 0
{
args: []string{"server", "--data-dir", actualDataDir, "--cluster.hosts", "localhost:42454,localhost:10110", "--bind", "localhost:42454", "--bind-grpc", "localhost:30112", "--translation.map-size", "100000"},
args: []string{"server", "--data-dir", actualDataDir, "--bind", "localhost:42454", "--bind-grpc", "localhost:30112", "--translation.map-size", "100000"},
env: map[string]string{
"PILOSA_DATA_DIR": "/tmp/myEnvDatadir",
"PILOSA_LONG_QUERY_TIME": "1m30s",
@ -66,11 +66,7 @@ func TestServerConfig(t *testing.T) {
long-query-time = "1m10s"
[cluster]
disabled = true
replicas = 2
hosts = [
"localhost:19444",
]
long-query-time = "1m10s"
[profile]
block-rate = 100
@ -81,7 +77,6 @@ func TestServerConfig(t *testing.T) {
v.Check(cmd.Server.Config.DataDir, actualDataDir)
v.Check(cmd.Server.Config.Bind, "localhost:42454")
v.Check(cmd.Server.Config.Cluster.ReplicaN, 2)
v.Check(cmd.Server.Config.Cluster.Hosts, []string{"localhost:42454", "localhost:10110"})
v.Check(cmd.Server.Config.LongQueryTime, toml.Duration(time.Second*90))
v.Check(cmd.Server.Config.Cluster.LongQueryTime, toml.Duration(time.Second*90))
v.Check(cmd.Server.Config.MaxWritesPerRequest, 2000)
@ -109,18 +104,12 @@ func TestServerConfig(t *testing.T) {
bind = ` + nextPort() + `
bind-grpc = ` + nextPort() + `
data-dir = "` + actualDataDir + `"
[cluster]
disabled = true
hosts = [
"localhost:19444",
]
[profile]
block-rate = 100
mutex-fraction = 10
`,
validation: func() error {
v := validator{}
v.Check(cmd.Server.Config.Cluster.Hosts, []string{"localhost:1110", "localhost:1111"})
v.Check(cmd.Server.Config.AntiEntropy.Interval, toml.Duration(time.Minute*9))
v.Check(cmd.Server.Config.Translation.MapSize, 100000)
v.Check(cmd.Server.Config.Profile.BlockRate, 4832)
@ -136,10 +125,6 @@ func TestServerConfig(t *testing.T) {
bind = "localhost:19444"
bind-grpc = "localhost:29444"
data-dir = "` + actualDataDir + `"
[cluster]
hosts = [
"localhost:19444",
]
[anti-entropy]
interval = "11m0s"
[metric]
@ -152,7 +137,6 @@ func TestServerConfig(t *testing.T) {
`,
validation: func() error {
v := validator{}
v.Check(cmd.Server.Config.Cluster.Hosts, []string{"localhost:19444"})
v.Check(cmd.Server.Config.AntiEntropy.Interval, toml.Duration(time.Minute*11))
v.Check(cmd.Server.Config.LogPath, logFile.Name())
v.Check(cmd.Server.Config.Metric.Service, "statsd")
@ -217,8 +201,6 @@ func TestServerConfig_DeprecateLongQueryTime(t *testing.T) {
bind = ` + nextPort() + `
bind-grpc = ` + nextPort() + `
data-dir = "` + actualDataDir + `"
[gossip]
port = "14321"
`,
validation: func() error {
v := validator{}
@ -234,8 +216,6 @@ func TestServerConfig_DeprecateLongQueryTime(t *testing.T) {
cfgFileContent: `
bind = ` + nextPort() + `
bind-grpc = ` + nextPort() + `
[gossip]
port = "14321"
`,
validation: func() error {
v := validator{}
@ -251,8 +231,6 @@ func TestServerConfig_DeprecateLongQueryTime(t *testing.T) {
cfgFileContent: `
bind = ` + nextPort() + `
bind-grpc = ` + nextPort() + `
[gossip]
port = "14321"
`,
validation: func() error {
v := validator{}

View file

@ -26,77 +26,75 @@ import (
// BuildServerFlags attaches a set of flags to the command for a server instance.
func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags := cmd.Flags()
flags.StringVar(&srv.Config.Name, "name", srv.Config.Name, "Name of the node in the cluster.")
flags.StringVarP(&srv.Config.DataDir, "data-dir", "d", srv.Config.DataDir, "Directory to store pilosa data files.")
flags.StringVarP(&srv.Config.Bind, "bind", "b", srv.Config.Bind, "Default URI on which pilosa should listen.")
flags.StringVar(&srv.Config.BindGRPC, "bind-grpc", srv.Config.BindGRPC, "URI on which pilosa should listen for gRPC requests.")
flags.StringVar(&srv.Config.Advertise, "advertise", srv.Config.Advertise, "Address to advertise externally.")
flags.StringVar(&srv.Config.AdvertiseGRPC, "advertise-grpc", srv.Config.AdvertiseGRPC, "Address to advertise externally for gRPC.")
flags.IntVarP(&srv.Config.MaxWritesPerRequest, "max-writes-per-request", "", srv.Config.MaxWritesPerRequest, "Number of write commands per request.")
flags.IntVar(&srv.Config.MaxWritesPerRequest, "max-writes-per-request", srv.Config.MaxWritesPerRequest, "Number of write commands per request.")
flags.StringVar(&srv.Config.LogPath, "log-path", srv.Config.LogPath, "Log path")
flags.BoolVar(&srv.Config.Verbose, "verbose", srv.Config.Verbose, "Enable verbose logging")
flags.Uint64Var(&srv.Config.MaxMapCount, "max-map-count", srv.Config.MaxMapCount, "Limits the maximum number of active mmaps. Pilosa will fall back to reading files once this is exhausted. Set below your system's vm.max_map_count.")
flags.Uint64Var(&srv.Config.MaxFileCount, "max-file-count", srv.Config.MaxFileCount, "Soft limit on the maximum number of fragment files Pilosa keeps open simultaneously.")
flags.DurationVarP((*time.Duration)(&srv.Config.LongQueryTime), "long-query-time", "", time.Duration(srv.Config.LongQueryTime), "Duration that will trigger log and stat messages for slow queries. Zero to disable.")
flags.DurationVar((*time.Duration)(&srv.Config.LongQueryTime), "long-query-time", time.Duration(srv.Config.LongQueryTime), "Duration that will trigger log and stat messages for slow queries. Zero to disable.")
flags.IntVar(&srv.Config.QueryHistoryLength, "query-history-length", srv.Config.QueryHistoryLength, "Number of queries to remember in history.")
// TLS
SetTLSConfig(flags, "", &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.CACertPath, &srv.Config.TLS.SkipVerify, &srv.Config.TLS.EnableClientVerification)
// Handler
flags.StringSliceVarP(&srv.Config.Handler.AllowedOrigins, "handler.allowed-origins", "", []string{}, "Comma separated list of allowed origin URIs (for CORS/Web UI).")
flags.StringSliceVar(&srv.Config.Handler.AllowedOrigins, "handler.allowed-origins", []string{}, "Comma separated list of allowed origin URIs (for CORS/Web UI).")
// Cluster
flags.BoolVarP(&srv.Config.Cluster.Disabled, "cluster.disabled", "", srv.Config.Cluster.Disabled, "Disabled multi-node cluster communication (used for testing)")
flags.BoolVarP(&srv.Config.Cluster.Coordinator, "cluster.coordinator", "", srv.Config.Cluster.Coordinator, "Host that will act as cluster coordinator during startup and resizing.")
flags.IntVarP(&srv.Config.Cluster.ReplicaN, "cluster.replicas", "", 1, "Number of hosts each piece of data should be stored on.")
flags.StringSliceVarP(&srv.Config.Cluster.Hosts, "cluster.hosts", "", []string{}, "Comma separated list of hosts in cluster. Only used for testing.")
flags.DurationVarP((*time.Duration)(&srv.Config.Cluster.LongQueryTime), "cluster.long-query-time", "", time.Duration(srv.Config.Cluster.LongQueryTime), "RENAMED TO 'long-query-time': Duration that will trigger log and stat messages for slow queries.") // negative duration indicates invalid value because 0 is meaningful
flags.IntVar(&srv.Config.Cluster.ReplicaN, "cluster.replicas", 1, "Number of hosts each piece of data should be stored on.")
flags.DurationVar((*time.Duration)(&srv.Config.Cluster.LongQueryTime), "cluster.long-query-time", time.Duration(srv.Config.Cluster.LongQueryTime), "RENAMED TO 'long-query-time': Duration that will trigger log and stat messages for slow queries.") // negative duration indicates invalid value because 0 is meaningful
flags.StringVar(&srv.Config.Cluster.Name, "cluster.name", srv.Config.Cluster.Name, "Human-readable name for the cluster.")
// Translation
flags.StringVarP(&srv.Config.Translation.PrimaryURL, "translation.primary-url", "", srv.Config.Translation.PrimaryURL, "DEPRECATED: URL for primary translation node for replication.")
flags.IntVarP(&srv.Config.Translation.MapSize, "translation.map-size", "", srv.Config.Translation.MapSize, "Size in bytes of mmap to allocate for key translation.")
flags.StringVar(&srv.Config.Translation.PrimaryURL, "translation.primary-url", srv.Config.Translation.PrimaryURL, "DEPRECATED: URL for primary translation node for replication.")
flags.IntVar(&srv.Config.Translation.MapSize, "translation.map-size", srv.Config.Translation.MapSize, "Size in bytes of mmap to allocate for key translation.")
// Gossip
flags.StringVarP(&srv.Config.Gossip.Port, "gossip.port", "", srv.Config.Gossip.Port, "Port to which pilosa should bind for internal state sharing.")
flags.StringVarP(&srv.Config.Gossip.AdvertiseHost, "gossip.advertise-host", "", srv.Config.Gossip.AdvertiseHost, "Host on which memberlist should advertise.")
flags.StringVarP(&srv.Config.Gossip.AdvertisePort, "gossip.advertise-port", "", srv.Config.Gossip.AdvertisePort, "Port on which memberlist should advertise.")
flags.StringVar(&srv.Config.Gossip.Port, "gossip.port", srv.Config.Gossip.Port, "Port to which pilosa should bind for internal state sharing.")
flags.StringVar(&srv.Config.Gossip.AdvertiseHost, "gossip.advertise-host", srv.Config.Gossip.AdvertiseHost, "Host on which memberlist should advertise.")
flags.StringVar(&srv.Config.Gossip.AdvertisePort, "gossip.advertise-port", srv.Config.Gossip.AdvertisePort, "Port on which memberlist should advertise.")
flags.StringSliceVarP(&srv.Config.Gossip.Seeds, "gossip.seeds", "", srv.Config.Gossip.Seeds, "Host with which to seed the gossip membership.")
flags.StringVarP(&srv.Config.Gossip.Key, "gossip.key", "", srv.Config.Gossip.Key, "The path to file of the encryption key for gossip. The contents of the file should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.StreamTimeout), "gossip.stream-timeout", "", (time.Duration)(srv.Config.Gossip.StreamTimeout), "Timeout for establishing a stream connection with a remote node for a full state sync.")
flags.IntVarP(&srv.Config.Gossip.SuspicionMult, "gossip.suspicion-mult", "", srv.Config.Gossip.SuspicionMult, "Multiplier for determining the time an inaccessible node is considered suspect before declaring it dead.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.PushPullInterval), "gossip.push-pull-interval", "", (time.Duration)(srv.Config.Gossip.PushPullInterval), "Interval between complete state syncs.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ProbeTimeout), "gossip.probe-timeout", "", (time.Duration)(srv.Config.Gossip.ProbeTimeout), "Timeout to wait for an ack from a probed node before assuming it is unhealthy.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ProbeInterval), "gossip.probe-interval", "", (time.Duration)(srv.Config.Gossip.ProbeInterval), "Interval between random node probes.")
flags.IntVarP(&srv.Config.Gossip.Nodes, "gossip.nodes", "", srv.Config.Gossip.Nodes, "Number of random nodes to send gossip messages to per GossipInterval.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.Interval), "gossip.interval", "", (time.Duration)(srv.Config.Gossip.Interval), "Interval between sending messages that need to be gossiped that haven't piggybacked on probing messages.")
flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ToTheDeadTime), "gossip.to-the-dead-time", "", (time.Duration)(srv.Config.Gossip.ToTheDeadTime), "Interval after which a node has died that we will still try to gossip to it.")
flags.StringSliceVar(&srv.Config.Gossip.Seeds, "gossip.seeds", srv.Config.Gossip.Seeds, "Host with which to seed the gossip membership.")
flags.StringVar(&srv.Config.Gossip.Key, "gossip.key", srv.Config.Gossip.Key, "The path to file of the encryption key for gossip. The contents of the file should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.StreamTimeout), "gossip.stream-timeout", (time.Duration)(srv.Config.Gossip.StreamTimeout), "Timeout for establishing a stream connection with a remote node for a full state sync.")
flags.IntVar(&srv.Config.Gossip.SuspicionMult, "gossip.suspicion-mult", srv.Config.Gossip.SuspicionMult, "Multiplier for determining the time an inaccessible node is considered suspect before declaring it dead.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.PushPullInterval), "gossip.push-pull-interval", (time.Duration)(srv.Config.Gossip.PushPullInterval), "Interval between complete state syncs.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.ProbeTimeout), "gossip.probe-timeout", (time.Duration)(srv.Config.Gossip.ProbeTimeout), "Timeout to wait for an ack from a probed node before assuming it is unhealthy.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.ProbeInterval), "gossip.probe-interval", (time.Duration)(srv.Config.Gossip.ProbeInterval), "Interval between random node probes.")
flags.IntVar(&srv.Config.Gossip.Nodes, "gossip.nodes", srv.Config.Gossip.Nodes, "Number of random nodes to send gossip messages to per GossipInterval.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.Interval), "gossip.interval", (time.Duration)(srv.Config.Gossip.Interval), "Interval between sending messages that need to be gossiped that haven't piggybacked on probing messages.")
flags.DurationVar((*time.Duration)(&srv.Config.Gossip.ToTheDeadTime), "gossip.to-the-dead-time", (time.Duration)(srv.Config.Gossip.ToTheDeadTime), "Interval after which a node has died that we will still try to gossip to it.")
// DisCo
flags.StringVarP(&srv.Config.DisCo.Name, "disco.name", "", srv.Config.DisCo.Name, "Name of node in DisCo.")
flags.StringVarP(&srv.Config.DisCo.Dir, "disco.dir", "", srv.Config.DisCo.Dir, "Directory to use for DisCo.")
flags.StringVarP(&srv.Config.DisCo.LClientURL, "disco.listen-client-addr", "", srv.Config.DisCo.LClientURL, "Listen client address.")
flags.StringVarP(&srv.Config.DisCo.AClientURL, "disco.advertise-client-addr", "", srv.Config.DisCo.AClientURL, "Advertise client address.")
flags.StringVarP(&srv.Config.DisCo.LPeerURL, "disco.listen-peer-addr", "", srv.Config.DisCo.LPeerURL, "Listen peer address.")
flags.StringVarP(&srv.Config.DisCo.APeerURL, "disco.advertise-peer-addr", "", srv.Config.DisCo.APeerURL, "Advertise peer address.")
flags.StringVarP(&srv.Config.DisCo.ClusterURL, "disco.cluster-url", "", srv.Config.DisCo.ClusterURL, "Cluster URL to join.")
flags.StringVarP(&srv.Config.DisCo.ClusterName, "disco.cluster-name", "", srv.Config.DisCo.ClusterName, "Cluster name.")
flags.StringVarP(&srv.Config.DisCo.InitCluster, "disco.initial-cluster", "", srv.Config.DisCo.InitCluster, "Initial cluster name1=apurl1,name2=apurl2")
// Etcd
// Etcd.Name used Config.Name for it's value.
// Etcd.Dir defaults to a directory under the pilosa data directory.
flags.StringVar(&srv.Config.Etcd.LClientURL, "etcd.listen-client-address", srv.Config.Etcd.LClientURL, "Listen client address.")
flags.StringVar(&srv.Config.Etcd.AClientURL, "etcd.advertise-client-address", srv.Config.Etcd.AClientURL, "Advertise client address. If not provided, uses the listen client address.")
flags.StringVar(&srv.Config.Etcd.LPeerURL, "etcd.listen-peer-address", srv.Config.Etcd.LPeerURL, "Listen peer address.")
flags.StringVar(&srv.Config.Etcd.APeerURL, "etcd.advertise-peer-address", srv.Config.Etcd.APeerURL, "Advertise peer address. If not provided, uses the listen peer address.")
flags.StringVar(&srv.Config.Etcd.ClusterURL, "etcd.cluster-url", srv.Config.Etcd.ClusterURL, "Cluster URL to join.")
// Etcd.ClusterName uses Cluster.Name for its value.
flags.StringVar(&srv.Config.Etcd.InitCluster, "etcd.initial-cluster", srv.Config.Etcd.InitCluster, "Initial cluster name1=apurl1,name2=apurl2")
// AntiEntropy
flags.DurationVarP((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", "", (time.Duration)(srv.Config.AntiEntropy.Interval), "Interval at which to run anti-entropy routine.")
flags.DurationVar((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", (time.Duration)(srv.Config.AntiEntropy.Interval), "Interval at which to run anti-entropy routine.")
// Metric
flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", srv.Config.Metric.Service, "Where to send stats: can be expvar (in-memory served at /debug/vars), prometheus, statsd or none.")
flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", srv.Config.Metric.Host, "URI to send metrics when metric.service is statsd.")
flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", (time.Duration)(srv.Config.Metric.PollInterval), "Polling interval metrics.")
flags.BoolVarP((&srv.Config.Metric.Diagnostics), "metric.diagnostics", "", srv.Config.Metric.Diagnostics, "Enabled diagnostics reporting.")
flags.StringVar(&srv.Config.Metric.Service, "metric.service", srv.Config.Metric.Service, "Where to send stats: can be expvar (in-memory served at /debug/vars), prometheus, statsd or none.")
flags.StringVar(&srv.Config.Metric.Host, "metric.host", srv.Config.Metric.Host, "URI to send metrics when metric.service is statsd.")
flags.DurationVar((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", (time.Duration)(srv.Config.Metric.PollInterval), "Polling interval metrics.")
flags.BoolVar((&srv.Config.Metric.Diagnostics), "metric.diagnostics", srv.Config.Metric.Diagnostics, "Enabled diagnostics reporting.")
// Tracing
flags.StringVarP(&srv.Config.Tracing.AgentHostPort, "tracing.agent-host-port", "", srv.Config.Tracing.AgentHostPort, "Jaeger agent host:port.")
flags.StringVarP(&srv.Config.Tracing.SamplerType, "tracing.sampler-type", "", srv.Config.Tracing.SamplerType, "Jaeger sampler type (remote, const, probabilistic, ratelimiting) or 'off' to disable tracing completely.")
flags.Float64VarP(&srv.Config.Tracing.SamplerParam, "tracing.sampler-param", "", srv.Config.Tracing.SamplerParam, "Jaeger sampler parameter.")
flags.StringVar(&srv.Config.Tracing.AgentHostPort, "tracing.agent-host-port", srv.Config.Tracing.AgentHostPort, "Jaeger agent host:port.")
flags.StringVar(&srv.Config.Tracing.SamplerType, "tracing.sampler-type", srv.Config.Tracing.SamplerType, "Jaeger sampler type (remote, const, probabilistic, ratelimiting) or 'off' to disable tracing completely.")
flags.Float64Var(&srv.Config.Tracing.SamplerParam, "tracing.sampler-param", srv.Config.Tracing.SamplerParam, "Jaeger sampler parameter.")
// Profiling
flags.IntVar(&srv.Config.Profile.BlockRate, "profile.block-rate", srv.Config.Profile.BlockRate, "Sampling rate for goroutine blocking profiler. One sample per <rate> ns.")
@ -112,7 +110,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags.BoolVar(&srv.Config.Storage.FsyncEnabled, "storage.fsync", true, "enable fsync fully safe flush-to-disk")
// RowcacheOn
flags.BoolVarP((&srv.Config.RowcacheOn), "rowcache-on", "", srv.Config.RowcacheOn, "turn on the rowcache for all backends (may speed some queries)")
flags.BoolVar((&srv.Config.RowcacheOn), "rowcache-on", srv.Config.RowcacheOn, "turn on the rowcache for all backends (may speed some queries)")
// RBF specific flags. See pilosa/rbf/cfg/cfg.go for definitions.
srv.Config.RBFConfig.DefineFlags(flags)
@ -125,5 +123,4 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) {
flags.DurationVar((*time.Duration)(&srv.Config.Postgres.WriteTimeout), "postgres.write-timeout", time.Duration(srv.Config.Postgres.WriteTimeout), "Timeout for writes on a postgres connection. (set 0 to disable)")
flags.Uint32Var(&srv.Config.Postgres.MaxStartupSize, "postgres.max-startup-size", srv.Config.Postgres.MaxStartupSize, "Maximum acceptable size of a postgres startup packet, in bytes. (set 0 to disable)")
flags.Uint16Var(&srv.Config.Postgres.ConnectionLimit, "postgres.connection-limit", srv.Config.Postgres.ConnectionLimit, "Maximum number of simultaneous postgres connections to allow. (set 0 to disable)")
}

View file

@ -173,7 +173,7 @@ type nopStator struct{}
// ClusterState is a no-op implementation of the Stator ClusterState method.
func (n *nopStator) ClusterState(context.Context) (ClusterState, error) {
return "", nil
return ClusterStateUnknown, nil
}
func (n *nopStator) Started(ctx context.Context) error {

View file

@ -138,22 +138,6 @@ func (s Serializer) Unmarshal(buf []byte, m pilosa.Message) error {
}
s.decodeResizeInstructionComplete(msg, mt)
return nil
case *pilosa.SetCoordinatorMessage:
msg := &internal.SetCoordinatorMessage{}
err := proto.Unmarshal(buf, msg)
if err != nil {
return errors.Wrap(err, "unmarshaling SetCoordinatorMessage")
}
s.decodeSetCoordinatorMessage(msg, mt)
return nil
case *pilosa.UpdateCoordinatorMessage:
msg := &internal.UpdateCoordinatorMessage{}
err := proto.Unmarshal(buf, msg)
if err != nil {
return errors.Wrap(err, "unmarshaling UpdateCoordinatorMessage")
}
s.decodeUpdateCoordinatorMessage(msg, mt)
return nil
case *pilosa.NodeStateMessage:
msg := &internal.NodeStateMessage{}
err := proto.Unmarshal(buf, msg)
@ -322,6 +306,25 @@ func (s Serializer) Unmarshal(buf []byte, m pilosa.Message) error {
}
*mt = s.decodeRowMatrix(msg)
return nil
case *pilosa.ResizeNodeMessage:
msg := &internal.ResizeNodeMessage{}
err := proto.Unmarshal(buf, msg)
if err != nil {
return errors.Wrap(err, "unmarshaling ResizeNodeMessage")
}
decodeResizeNodeMessage(msg, mt)
return nil
case *pilosa.ResizeAbortMessage:
msg := &internal.ResizeAbortMessage{}
err := proto.Unmarshal(buf, msg)
if err != nil {
return errors.Wrap(err, "unmarshaling ResizeAbortMessage")
}
decodeResizeAbortMessage(msg, mt)
return nil
default:
panic(fmt.Sprintf("unhandled pilosa.Message of type %T: %#v", mt, m))
}
@ -351,10 +354,6 @@ func (s Serializer) encodeToProto(m pilosa.Message) proto.Message {
return s.encodeResizeInstruction(mt)
case *pilosa.ResizeInstructionComplete:
return s.encodeResizeInstructionComplete(mt)
case *pilosa.SetCoordinatorMessage:
return s.encodeSetCoordinatorMessage(mt)
case *pilosa.UpdateCoordinatorMessage:
return s.encodeUpdateCoordinatorMessage(mt)
case *pilosa.NodeStateMessage:
return s.encodeNodeStateMessage(mt)
case *pilosa.RecalculateCaches:
@ -395,6 +394,10 @@ func (s Serializer) encodeToProto(m pilosa.Message) proto.Message {
return s.encodeTransactionMessage(mt)
case *pilosa.AtomicRecord:
return s.encodeAtomicRecord(mt)
case *pilosa.ResizeNodeMessage:
return s.encodeResizeNodeMessage(mt)
case *pilosa.ResizeAbortMessage:
return s.encodeResizeAbortMessage(mt)
}
return nil
}
@ -574,7 +577,7 @@ func (s Serializer) encodeResizeInstruction(m *pilosa.ResizeInstruction) *intern
return &internal.ResizeInstruction{
JobID: m.JobID,
Node: s.encodeNode(m.Node),
Coordinator: s.encodeNode(m.Coordinator),
Primary: s.encodeNode(m.Primary),
Sources: s.encodeResizeSources(m.Sources),
TranslationSources: s.encodeTranslationResizeSources(m.TranslationSources),
NodeStatus: s.encodeNodeStatus(m.NodeStatus),
@ -691,13 +694,12 @@ func (s Serializer) encodeNodes(a []*topology.Node) []*internal.Node {
// s.encodeNode converts a Node into its internal representation.
func (s Serializer) encodeNode(m *topology.Node) *internal.Node {
n := m.ProtectedClone()
n := m.Clone()
return &internal.Node{
ID: n.ID,
URI: s.encodeURI(n.URI),
IsCoordinator: n.IsCoordinator,
State: n.State,
GRPCURI: s.encodeURI(n.GRPCURI),
ID: n.ID,
URI: s.encodeURI(n.URI),
State: n.State,
GRPCURI: s.encodeURI(n.GRPCURI),
}
}
@ -795,18 +797,6 @@ func (s Serializer) encodeResizeInstructionComplete(m *pilosa.ResizeInstructionC
}
}
func (s Serializer) encodeSetCoordinatorMessage(m *pilosa.SetCoordinatorMessage) *internal.SetCoordinatorMessage {
return &internal.SetCoordinatorMessage{
New: s.encodeNode(m.New),
}
}
func (s Serializer) encodeUpdateCoordinatorMessage(m *pilosa.UpdateCoordinatorMessage) *internal.UpdateCoordinatorMessage {
return &internal.UpdateCoordinatorMessage{
New: s.encodeNode(m.New),
}
}
func (s Serializer) encodeNodeStateMessage(m *pilosa.NodeStateMessage) *internal.NodeStateMessage {
return &internal.NodeStateMessage{
NodeID: m.NodeID,
@ -953,8 +943,8 @@ func (s Serializer) decodeResizeInstruction(ri *internal.ResizeInstruction, m *p
m.JobID = ri.JobID
m.Node = &topology.Node{}
s.decodeNode(ri.Node, m.Node)
m.Coordinator = &topology.Node{}
s.decodeNode(ri.Coordinator, m.Coordinator)
m.Primary = &topology.Node{}
s.decodeNode(ri.Primary, m.Primary)
m.Sources = make([]*pilosa.ResizeSource, len(ri.Sources))
s.decodeResizeSources(ri.Sources, m.Sources)
m.TranslationSources = make([]*pilosa.TranslationResizeSource, len(ri.TranslationSources))
@ -1073,7 +1063,6 @@ func (s Serializer) decodeNode(node *internal.Node, m *topology.Node) {
m.ID = node.ID
s.decodeURI(node.URI, &m.URI)
s.decodeURI(node.GRPCURI, &m.GRPCURI)
m.IsCoordinator = node.IsCoordinator
m.State = node.State
}
@ -1145,16 +1134,6 @@ func (s Serializer) decodeResizeInstructionComplete(pb *internal.ResizeInstructi
m.Error = pb.Error
}
func (s Serializer) decodeSetCoordinatorMessage(pb *internal.SetCoordinatorMessage, m *pilosa.SetCoordinatorMessage) {
m.New = &topology.Node{}
s.decodeNode(pb.New, m.New)
}
func (s Serializer) decodeUpdateCoordinatorMessage(pb *internal.UpdateCoordinatorMessage, m *pilosa.UpdateCoordinatorMessage) {
m.New = &topology.Node{}
s.decodeNode(pb.New, m.New)
}
func (s Serializer) decodeNodeStateMessage(pb *internal.NodeStateMessage, m *pilosa.NodeStateMessage) {
m.NodeID = pb.NodeID
m.State = pb.State
@ -1946,3 +1925,23 @@ func (s Serializer) encodeAttr(key string, value interface{}) *internal.Attr {
}
return pb
}
func (s Serializer) encodeResizeNodeMessage(m *pilosa.ResizeNodeMessage) *internal.ResizeNodeMessage {
return &internal.ResizeNodeMessage{
NodeID: m.NodeID,
Action: m.Action,
}
}
func (s Serializer) encodeResizeAbortMessage(*pilosa.ResizeAbortMessage) *internal.ResizeAbortMessage {
return &internal.ResizeAbortMessage{}
}
func decodeResizeNodeMessage(pb *internal.ResizeNodeMessage, m *pilosa.ResizeNodeMessage) {
m.NodeID = pb.NodeID
m.Action = pb.Action
}
func decodeResizeAbortMessage(pb *internal.ResizeAbortMessage, m *pilosa.ResizeAbortMessage) {
}

View file

@ -16,10 +16,14 @@ package etcd
import (
"context"
"encoding/json"
"log"
"sort"
"sync"
"time"
"github.com/pilosa/pilosa/v2/disco"
"github.com/pilosa/pilosa/v2/topology"
)
// EtcdWithCache is a wrapper around the Etcd type which will return a
@ -146,3 +150,40 @@ func (c *EtcdWithCache) NodeState(ctx context.Context, peerID string) (disco.Nod
c.nodeStates[peerID] = ns
return ns.val, nil
}
// Nodes implements the Noder interface.
func (c *EtcdWithCache) Nodes() []*topology.Node {
peers := c.Peers()
nodes := make([]*topology.Node, len(peers))
for i, peer := range peers {
node := &topology.Node{}
if meta, err := c.Metadata(context.Background(), peer.ID); err != nil {
log.Println(err, "getting metadata") // TODO: handle this with a logger
} else if err := json.Unmarshal(meta, node); err != nil {
log.Println(err, "unmarshaling json metadata")
}
node.ID = peer.ID
nodes[i] = node
}
// Nodes must be sorted.
sort.Sort(topology.ByID(nodes))
return nodes
}
// SetNodes implements the Noder interface as NOP
// (because we can't force to set nodes for etcd).
func (c *EtcdWithCache) SetNodes(nodes []*topology.Node) {}
// AppendNode implements the Noder interface as NOP
// (because resizer is responsible for adding new nodes).
func (c *EtcdWithCache) AppendNode(node *topology.Node) {}
// RemoveNode implements the Noder interface as NOP
// (because resizer is responsible for removing existing nodes)
func (c *EtcdWithCache) RemoveNode(nodeID string) bool {
return false
}

View file

@ -41,12 +41,12 @@ import (
type Options struct {
Name string `toml:"name"`
Dir string `toml:"dir"`
LClientURL string `toml:"listen-client-addr"`
AClientURL string `toml:"advertise-client-addr"`
LPeerURL string `toml:"listen-peer-addr"`
APeerURL string `toml:"advertise-peer-addr"`
InitCluster string `toml:"initial-cluster"`
LClientURL string `toml:"listen-client-url"`
AClientURL string `toml:"advertise-client-url"`
LPeerURL string `toml:"listen-peer-url"`
APeerURL string `toml:"advertise-peer-url"`
ClusterURL string `toml:"cluster-url"`
InitCluster string `toml:"initial-cluster"`
ClusterName string `toml:"cluster-name"`
HeartbeatTTL int64 `toml:"heartbeat-ttl"`
@ -127,9 +127,17 @@ func parseOptions(opt Options) *embed.Config {
cfg.Dir = opt.Dir
cfg.InitialClusterToken = opt.ClusterName
cfg.LCUrls = types.MustNewURLs([]string{opt.LClientURL})
cfg.ACUrls = types.MustNewURLs([]string{opt.AClientURL})
if opt.AClientURL != "" {
cfg.ACUrls = types.MustNewURLs([]string{opt.AClientURL})
} else {
cfg.ACUrls = cfg.LCUrls
}
cfg.LPUrls = types.MustNewURLs([]string{opt.LPeerURL})
cfg.APUrls = types.MustNewURLs([]string{opt.APeerURL})
if opt.APeerURL != "" {
cfg.APUrls = types.MustNewURLs([]string{opt.APeerURL})
} else {
cfg.APUrls = cfg.LPUrls
}
lps := make([]*net.TCPListener, len(opt.LPeerSocket))
copy(lps, opt.LPeerSocket)
@ -720,7 +728,7 @@ func (e *Etcd) leaseKeepAlive(ttl int64) (clientv3.LeaseID, func(context.Context
leaseResp, err := cli.Grant(context.TODO(), ttl)
if err != nil {
return 0, nil, errors.Wrapf(err, "leaseKeepAlive: creates a new lease (TTL: %d)", ttl)
return 0, nil, errors.Wrapf(err, "leaseKeepAlive: creates a new lease (TTL: %v)", ttl)
}
keepaliveFunc := func(ctx context.Context, tick time.Duration) {
@ -731,6 +739,15 @@ func (e *Etcd) leaseKeepAlive(ttl int64) (clientv3.LeaseID, func(context.Context
select {
case <-ctx.Done():
log.Printf("leaseKeepAlive: %v\n", ctx.Err())
if cli, err := e.client(); err != nil {
log.Printf("leaseKeepAlive: creates a new client: %v\n", err)
} else {
if _, err := cli.Revoke(context.TODO(), leaseResp.ID); err != nil {
log.Printf("leaseKeepAlive: revokes the lease (ID: %x): %v\n", leaseResp.ID, err)
}
cli.Close()
}
return
case <-ticker.C:
@ -738,7 +755,7 @@ func (e *Etcd) leaseKeepAlive(ttl int64) (clientv3.LeaseID, func(context.Context
log.Printf("leaseKeepAlive: creates a new client: %v\n", err)
} else {
if _, err = cli.KeepAliveOnce(ctx, leaseResp.ID); err != nil {
log.Printf("leaseKeepAlive: renews the lease (ID: %v): %v\n", leaseResp.ID, err)
log.Printf("leaseKeepAlive: renews the lease (ID: %x): %v\n", leaseResp.ID, err)
}
cli.Close()
}
@ -751,10 +768,12 @@ func (e *Etcd) leaseKeepAlive(ttl int64) (clientv3.LeaseID, func(context.Context
func (e *Etcd) client() (*clientv3.Client, error) {
urls := e.e.Server.Cluster().ClientURLs()
cli, err := clientv3.NewFromURLs(urls)
if err != nil {
return nil, errors.Wrapf(err, "creates a new etcd client from URLs (%v)", urls)
}
return cli, nil
}
@ -1025,16 +1044,15 @@ func (e *Etcd) RemoveShard(ctx context.Context, index, field string, shard uint6
return nil
}
// Nodes implements the Noder interface.
func (n *Etcd) Nodes() []*topology.Node {
// If we have looked up nodes within a certain time, then we're going to
// use the cached value for now. This is temporary and will be addressed
// correctly in #1133.
peers := n.Peers()
// Nodes implements the Noder interface. It returns the sorted list of nodes
// based on the etcd peers.
func (e *Etcd) Nodes() []*topology.Node {
peers := e.Peers()
nodes := make([]*topology.Node, len(peers))
for i, peer := range peers {
node := &topology.Node{}
if meta, err := n.Metadata(context.Background(), peer.ID); err != nil {
if meta, err := e.Metadata(context.Background(), peer.ID); err != nil {
log.Println(err, "getting metadata") // TODO: handle this with a logger
} else if err := json.Unmarshal(meta, node); err != nil {
log.Println(err, "unmarshaling json metadata")
@ -1051,16 +1069,31 @@ func (n *Etcd) Nodes() []*topology.Node {
return nodes
}
// PrimaryNodeID implements the Noder interface.
func (e *Etcd) PrimaryNodeID(hasher topology.Hasher) string {
return topology.PrimaryNodeID(e.NodeIDs(), hasher)
}
// NodeIDs returns the list of node IDs in the etcd cluster.
func (e *Etcd) NodeIDs() []string {
peers := e.Peers()
ids := make([]string, len(peers))
for i, peer := range peers {
ids[i] = peer.ID
}
return ids
}
// SetNodes implements the Noder interface as NOP
// (because we can't force to set nodes for etcd).
func (n *Etcd) SetNodes(nodes []*topology.Node) {}
func (e *Etcd) SetNodes(nodes []*topology.Node) {}
// AppendNode implements the Noder interface as NOP
// (because resizer is responsible for adding new nodes).
func (n *Etcd) AppendNode(node *topology.Node) {}
func (e *Etcd) AppendNode(node *topology.Node) {}
// RemoveNode implements the Noder interface as NOP
// (because resizer is responsible for removing existing nodes)
func (n *Etcd) RemoveNode(nodeID string) bool {
func (e *Etcd) RemoveNode(nodeID string) bool {
return false
}

View file

@ -1,76 +0,0 @@
// Copyright 2021 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 etcd
import (
"context"
"encoding/json"
"log"
"sort"
"github.com/pilosa/pilosa/v2/topology"
)
var _ topology.Noder = &Noder{}
type Noder struct {
*EtcdWithCache
}
func NewNoder(opt Options, replicas int) *Noder {
return &Noder{
EtcdWithCache: NewEtcdWithCache(opt, replicas),
}
}
// Nodes implements the Noder interface.
func (n *Noder) Nodes() []*topology.Node {
// If we have looked up nodes within a certain time, then we're going to
// use the cached value for now. This is temporary and will be addressed
// correctly in #1133.
peers := n.Peers()
nodes := make([]*topology.Node, len(peers))
for i, peer := range peers {
node := &topology.Node{}
if meta, err := n.Metadata(context.Background(), peer.ID); err != nil {
log.Println(err, "getting metadata") // TODO: handle this with a logger
} else if err := json.Unmarshal(meta, node); err != nil {
log.Println(err, "unmarshaling json metadata")
}
node.ID = peer.ID
nodes[i] = node
}
// Nodes must be sorted.
sort.Sort(topology.ByID(nodes))
return nodes
}
// SetNodes implements the Noder interface as NOP
// (because we can't force to set nodes for etcd).
func (n *Noder) SetNodes(nodes []*topology.Node) {}
// AppendNode implements the Noder interface as NOP
// (because resizer is responsible for adding new nodes).
func (n *Noder) AppendNode(node *topology.Node) {}
// RemoveNode implements the Noder interface as NOP
// (because resizer is responsible for removing existing nodes)
func (n *Noder) RemoveNode(nodeID string) bool {
return false
}

View file

@ -5266,7 +5266,7 @@ func (e *executor) executeSetRowAttrs(ctx context.Context, qcx *Qcx, index strin
}
// Execute on remote nodes in parallel.
nodes := topology.Nodes(e.Cluster.nodes).FilterID(e.Node.ID)
nodes := topology.Nodes(e.Cluster.noder.Nodes()).FilterID(e.Node.ID)
resp := make(chan error, len(nodes))
for _, node := range nodes {
go func(node *topology.Node) {
@ -5378,7 +5378,7 @@ func (e *executor) executeBulkSetRowAttrs(ctx context.Context, qcx *Qcx, index s
}
// Execute on remote nodes in parallel.
nodes := topology.Nodes(e.Cluster.nodes).FilterID(e.Node.ID)
nodes := topology.Nodes(e.Cluster.noder.Nodes()).FilterID(e.Node.ID)
resp := make(chan error, len(nodes))
for _, node := range nodes {
go func(node *topology.Node) {
@ -5430,7 +5430,7 @@ func (e *executor) executeSetColumnAttrs(ctx context.Context, qcx *Qcx, index st
}
// Execute on remote nodes in parallel.
nodes := topology.Nodes(e.Cluster.nodes).FilterID(e.Node.ID)
nodes := topology.Nodes(e.Cluster.noder.Nodes()).FilterID(e.Node.ID)
resp := make(chan error, len(nodes))
for _, node := range nodes {
go func(node *topology.Node) {
@ -5484,7 +5484,12 @@ func (e *executor) shardsByNode(nodes []*topology.Node, index string, shards []u
loop:
for _, shard := range shards {
for _, node := range snap.ShardNodes(index, shard) {
if topology.Nodes(nodes).Contains(node) {
// If the node being considered is in any state other than STARTED,
// then exclude it from the map. This way, one of that node's
// healthy replicas will be included instead.
// TODO: check state once stator is implemented
//if topology.Nodes(nodes).ContainsID(node.ID) && node.State == disco.NodeStateStarted {
if topology.Nodes(nodes).ContainsID(node.ID) {
m[node] = append(m[node], shard)
continue loop
}
@ -5537,7 +5542,7 @@ func (e *executor) mapReduce(ctx context.Context, index string, shards []uint64,
if resp.err != nil {
// Filter out unavailable nodes.
nodes = topology.Nodes(nodes).Filter(resp.node)
nodes = topology.Nodes(nodes).FilterID(resp.node.ID)
// Begin mapper against secondary nodes.
if err := e.mapper(ctx, cancel, ch, nodes, index, resp.shards, c, opt, mapFn, reduceFn); errors.Cause(err) == errShardUnavailable {

View file

@ -3526,8 +3526,9 @@ func TestExecutor_Execute_Existence(t *testing.T) {
t.Fatal(err)
}
node0 := c.GetNode(0)
// Set bits.
if _, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `` +
if _, err := node0.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `` +
fmt.Sprintf("Set(%d, f=%d)\n", 3, 10) +
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+1, 10) +
fmt.Sprintf("Set(%d, f=%d)\n", ShardWidth+2, 20),
@ -3535,25 +3536,27 @@ func TestExecutor_Execute_Existence(t *testing.T) {
t.Fatal(err)
}
//index.Dump("after Set 3x")
if res, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
if res, err := node0.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Row(f=10)`}); err != nil {
t.Fatal(err)
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{3, ShardWidth + 1}) {
t.Fatalf("unexpected columns: %+v", bits)
}
if res, err := c.GetNode(0).API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Not(Row(f=10))`}); err != nil {
if res, err := node0.API.Query(context.Background(), &pilosa.QueryRequest{Index: "i", Query: `Not(Row(f=10))`}); err != nil {
t.Fatal(err)
} else if bits := res.Results[0].(*pilosa.Row).Columns(); !reflect.DeepEqual(bits, []uint64{ShardWidth + 2}) {
t.Fatalf("unexpected columns after Not: %+v", bits)
}
// Reopen cluster to ensure existence field is reloaded.
if err := c.GetNode(0).Reopen(); err != nil {
if err := node0.Reopen(); err != nil {
t.Fatal(err)
}
if err := node0.AwaitState(string(pilosa.ClusterStateNormal), 10*time.Second); err != nil {
t.Fatalf("restarting cluster: %v", err)
}
hldr2 := c.GetHolder(0)
index2 := hldr2.Index("i")
_ = index2
@ -6959,7 +6962,7 @@ toronto,3
{ // 2019 All, this excludes userC (who likes pangolin & icecream) from the count.
// UserC visited Paris and Toronto in 2019
query: `GroupBy(
Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'),
Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'),
filter=Not(Intersect(Row(likes='pangolin'), Row(likes='icecream')))
)`,
csvVerifier: `nairobi,1
@ -6969,7 +6972,7 @@ toronto,2
},
{ // After excluding UserC, this gets the sum of the networth of everyone per cities travelled
query: `GroupBy(
Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'),
Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'),
filter=Not(Intersect(Row(likes='pangolin'), Row(likes='icecream'))),
aggregate=Sum(field=net_worth)
)`,

View file

@ -507,6 +507,14 @@ func (f *Field) unprotectedSaveAvailableShards() error {
return nil
}
// SetRemoteAvailableShards replaces remoteAvailableShards with the provided
// value.
func (f *Field) SetRemoteAvailableShards(b *roaring.Bitmap) {
f.mu.Lock()
defer f.mu.Unlock()
f.remoteAvailableShards = b
}
// RemoveAvailableShard removes a shard from the bitmap cache.
//
// NOTE: This can be overridden on the next sync so all nodes should be updated.

View file

@ -15,551 +15,9 @@
package gossip
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/hashicorp/memberlist"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/logger"
pnet "github.com/pilosa/pilosa/v2/net"
"github.com/pilosa/pilosa/v2/roaring"
"github.com/pilosa/pilosa/v2/toml"
"github.com/pilosa/pilosa/v2/topology"
"github.com/pkg/errors"
)
// Ensure GossipMemberSet implements interfaces.
var _ memberlist.Delegate = &memberSet{}
// memberSet represents a gossip implementation of MemberSet using memberlist.
type memberSet struct {
mu sync.RWMutex
memberlist *memberlist.Memberlist
broadcasts *memberlist.TransmitLimitedQueue
papi *pilosa.API
config *config
Logger logger.Logger
// stdLogger is only used when passed into memberlist library things that take a std library logger rather than an interface.
stdLogger *log.Logger
// logOutput is similar to stdLogger in that it's passed to memberlist things which can't take a pilosa Logger.
logOutput io.Writer
transport *Transport
eventReceiver *eventReceiver
}
// Open implements the MemberSet interface to start network activity.
func (g *memberSet) Open() (err error) {
g.mu.Lock()
defer g.mu.Unlock()
g.memberlist, err = memberlist.Create(g.config.memberlistConfig)
if err != nil {
return errors.Wrap(err, "creating memberlist")
}
g.broadcasts = &memberlist.TransmitLimitedQueue{
NumNodes: func() int {
g.mu.RLock()
defer g.mu.RUnlock()
return g.memberlist.NumMembers()
},
RetransmitMult: 3,
}
var uris = make([]*pnet.URI, len(g.config.gossipSeeds))
for i, addr := range g.config.gossipSeeds {
uris[i], err = pnet.NewURIFromAddress(addr)
if err != nil {
return fmt.Errorf("new uri from address: %s", err)
}
}
var nodes = make([]*topology.Node, len(uris))
for i, uri := range uris {
nodes[i] = &topology.Node{URI: *uri}
}
err = g.joinWithRetry(pnet.URIs(topology.Nodes(nodes).URIs()).HostPortStrings())
if err != nil {
return errors.Wrap(err, "joinWithRetry")
}
return nil
}
// Close attempts to gracefully leave the cluster, and finally calls shutdown
// after (at most) a timeout period.
func (g *memberSet) Close() error {
defer g.eventReceiver.Close()
leaveErr := g.memberlist.Leave(5 * time.Second)
shutdownErr := g.memberlist.Shutdown()
if leaveErr != nil || shutdownErr != nil {
return fmt.Errorf("leaving: '%v', shutting down: '%v'", leaveErr, shutdownErr)
}
return nil
}
// joinWithRetry wraps the standard memberlist Join function in a retry.
func (g *memberSet) joinWithRetry(hosts []string) error {
err := retry(60, 2*time.Second, func() error {
_, err := g.memberlist.Join(hosts)
return err
})
return err
}
// retry periodically retries function fn a specified number of attempts.
func retry(attempts int, sleep time.Duration, fn func() error) (err error) { // nolint: unparam
for i := 0; ; i++ {
err = fn()
if err == nil {
return
}
if i >= (attempts - 1) {
break
}
time.Sleep(sleep)
log.Println("retrying after error:", err)
}
return fmt.Errorf("after %d attempts, last error: %s", attempts, err)
}
////////////////////////////////////////////////////////////////
type config struct {
gossipSeeds []string
memberlistConfig *memberlist.Config
}
// memberSetOption describes a functional option for GossipMemberSet.
type memberSetOption func(*memberSet) error
// WithTransport is a functional option for providing a transport to NewMemberSet.
func WithTransport(transport *Transport) memberSetOption {
return func(g *memberSet) error {
g.transport = transport
return nil
}
}
// WithLogger is a functional option for providing a Go logger to NewMemberSet.
// If the memberSet's transport is nil, this logger will be used when creating
// one. If WithLogOutput is not used, this logger will be passed to memberlist
// for it to use internally. This logger is not used for logging by code in this
// (gossip) package - for that, use the WithPilosaLogger option.
func WithLogger(logger *log.Logger) memberSetOption {
return func(g *memberSet) error {
g.stdLogger = logger
return nil
}
}
// WithLogOutput allows one to pass a Writer which will in turn be passed to
// memberlist for use in logging.
func WithLogOutput(o io.Writer) memberSetOption {
return func(g *memberSet) error {
g.logOutput = o
return nil
}
}
// WithPilosaLogger allows one to configure a memberSet with a logger of their
// choice which satisfies the pilosa logger interface.
func WithPilosaLogger(l logger.Logger) memberSetOption {
return func(g *memberSet) error {
g.Logger = l
return nil
}
}
// NewMemberSet returns a new instance of GossipMemberSet based on options. The
// logging options which can be passed to NewMemberSet are complicated for
// historical reasons - please pass WithPilosaLogger, and either WithLogOutput
// or WithLogger. If you pass WithLogOutput, be sure to also pass in a Transport
// using WithTransport.
func NewMemberSet(cfg Config, api *pilosa.API, options ...memberSetOption) (*memberSet, error) {
host := api.Node().URI.Host
g := &memberSet{
papi: api,
Logger: logger.NopLogger,
}
// options
for _, opt := range options {
if err := opt(g); err != nil {
return nil, errors.Wrap(err, "executing option")
}
}
ger := newEventReceiver(g.Logger, api)
g.eventReceiver = ger
if g.transport == nil {
port, err := strconv.Atoi(cfg.Port)
if err != nil {
return nil, fmt.Errorf("convert port: %s", err)
}
if g.stdLogger == nil {
if g.logOutput != nil {
g.stdLogger = logger.NewStandardLogger(g.logOutput).Logger()
} else {
g.stdLogger = log.New(os.Stderr, "", log.LstdFlags)
}
}
// Set up the transport.
transport, err := NewTransport(host, port, g.stdLogger)
if err != nil {
return nil, fmt.Errorf("new tranport: %s", err)
}
g.transport = transport
}
port := g.transport.net.GetAutoBindPort()
var gossipKey []byte
var err error
if cfg.Key != "" {
gossipKey, err = ioutil.ReadFile(cfg.Key)
if err != nil {
return nil, fmt.Errorf("reading gossip key: %s", err)
}
}
////////////////////
// memberlist config
conf := memberlist.DefaultWANConfig()
conf.Transport = g.transport.net
conf.Name = api.Node().ID
conf.BindAddr = api.Node().URI.Host
conf.BindPort = port
// AdvertisePort
if cfg.AdvertisePort != "" {
if p, err := strconv.Atoi(cfg.Port); err != nil {
return nil, fmt.Errorf("convert advertise port: %s", err)
} else {
conf.AdvertisePort = p
}
} else {
conf.AdvertisePort = port
}
// AdvertiseHost
if cfg.AdvertiseHost != "" {
conf.AdvertiseAddr = cfg.AdvertiseHost
} else {
conf.AdvertiseAddr = hostToIP(api.Node().URI.Host)
}
//
conf.TCPTimeout = time.Duration(cfg.StreamTimeout)
conf.SuspicionMult = cfg.SuspicionMult
conf.PushPullInterval = time.Duration(cfg.PushPullInterval)
conf.ProbeTimeout = time.Duration(cfg.ProbeTimeout)
conf.ProbeInterval = time.Duration(cfg.ProbeInterval)
conf.GossipNodes = cfg.Nodes
conf.GossipInterval = time.Duration(cfg.Interval)
conf.GossipToTheDeadTime = time.Duration(cfg.ToTheDeadTime)
//
conf.Delegate = g
conf.SecretKey = gossipKey
conf.Events = ger
if g.logOutput != nil {
conf.LogOutput = g.logOutput
} else {
conf.Logger = g.stdLogger
}
g.config = &config{
memberlistConfig: conf,
gossipSeeds: cfg.Seeds,
}
return g, nil
}
// NodeMeta implementation of the memberlist.Delegate interface.
func (g *memberSet) NodeMeta(limit int) []byte {
buf, err := g.papi.Serializer.Marshal(g.papi.Node())
if err != nil {
g.Logger.Printf("marshal message error: %s", err)
return []byte{}
}
return buf
}
// NotifyMsg implementation of the memberlist.Delegate interface
// called when a user-data message is received.
func (g *memberSet) NotifyMsg(b []byte) {
err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(b))
if err != nil {
g.Logger.Printf("cluster message error: %s", err)
}
}
// GetBroadcasts implementation of the memberlist.Delegate interface
// called when user data messages can be broadcast.
func (g *memberSet) GetBroadcasts(overhead, limit int) [][]byte {
return g.broadcasts.GetBroadcasts(overhead, limit)
}
// LocalState implementation of the memberlist.Delegate interface
// sends this Node's state data.
func (g *memberSet) LocalState(join bool) []byte {
m := &pilosa.NodeStatus{
Node: g.papi.Node(),
Schema: &pilosa.Schema{Indexes: g.papi.Schema(context.Background())},
}
for _, idx := range m.Schema.Indexes {
is := &pilosa.IndexStatus{Name: idx.Name, CreatedAt: idx.CreatedAt}
for _, f := range idx.Fields {
availableShards := roaring.NewBitmap()
if field, _ := g.papi.Field(context.Background(), idx.Name, f.Name); field != nil {
availableShards = field.AvailableShards(false)
}
fs := &pilosa.FieldStatus{
Name: f.Name,
CreatedAt: f.CreatedAt,
AvailableShards: availableShards,
}
is.Fields = append(is.Fields, fs)
}
m.Indexes = append(m.Indexes, is)
}
// Marshal nodestate data to bytes.
buf, err := pilosa.MarshalInternalMessage(m, g.papi.Serializer)
if err != nil {
g.Logger.Printf("error marshalling nodestate data, err=%s", err)
return []byte{}
}
return buf
}
// MergeRemoteState implementation of the memberlist.Delegate interface
// receive and process the remote side's LocalState.
func (g *memberSet) MergeRemoteState(buf []byte, join bool) {
err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(buf))
if err != nil {
g.Logger.Printf("merge state error: %s", err)
}
}
// eventReceiver is used to enable an application to receive
// events about joins and leaves over a channel.
//
// Care must be taken that events are processed in a timely manner from
// the channel, since this delegate will block until an event can be sent.
type eventReceiver struct {
ch chan memberlist.NodeEvent
closed chan struct{}
papi *pilosa.API
logger logger.Logger
}
// newEventReceiver returns a new instance of GossipEventReceiver.
func newEventReceiver(logger logger.Logger, papi *pilosa.API) *eventReceiver {
ger := &eventReceiver{
ch: make(chan memberlist.NodeEvent, 1),
closed: make(chan struct{}),
logger: logger,
papi: papi,
}
go ger.listen()
return ger
}
func (g *eventReceiver) NotifyJoin(n *memberlist.Node) {
// copy node to avoid data race
n2 := *n
n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta)
select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeJoin, Node: &n2}:
case <-g.closed:
}
}
func (g *eventReceiver) NotifyLeave(n *memberlist.Node) {
// copy node to avoid data race
n2 := *n
n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta)
select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeLeave, Node: &n2}:
case <-g.closed:
}
}
func (g *eventReceiver) NotifyUpdate(n *memberlist.Node) {
// copy node to avoid data race
n2 := *n
n2.Meta = make([]byte, len(n.Meta))
copy(n2.Meta, n.Meta)
select {
case g.ch <- memberlist.NodeEvent{Event: memberlist.NodeUpdate, Node: &n2}:
case <-g.closed:
}
}
func (g *eventReceiver) Close() {
// TODO workaround to make tests pass. We are going to delete this code anyways.
select {
case <-g.closed:
return
default:
close(g.closed)
}
}
func (g *eventReceiver) listen() {
var nodeEventType pilosa.NodeEventType
for {
var e memberlist.NodeEvent
select {
case <-g.closed:
return
case e = <-g.ch:
}
switch e.Event {
case memberlist.NodeJoin:
nodeEventType = pilosa.NodeJoin
case memberlist.NodeLeave:
nodeEventType = pilosa.NodeLeave
case memberlist.NodeUpdate:
nodeEventType = pilosa.NodeUpdate
default:
continue
}
// Get the node from the event.Node meta data.
var n topology.Node
if err := g.papi.Serializer.Unmarshal(e.Node.Meta, &n); err != nil {
panic("failed to unmarshal event node meta into node")
}
ne := &pilosa.NodeEvent{
Event: nodeEventType,
Node: &n,
}
buf, err := pilosa.MarshalInternalMessage(ne, g.papi.Serializer)
if err != nil {
panic(err)
}
if err := g.papi.ClusterMessage(context.Background(), bytes.NewBuffer(buf)); err != nil {
g.logger.Printf("receive event error: %s", err)
}
}
}
// Transport is a gossip transport for binding to a port.
type Transport struct {
//memberlist.Transport
net *memberlist.NetTransport
URI *pnet.URI
}
// NewTransport returns a NetTransport based on the given host and port.
// It will dynamically bind to a port if port is 0.
// This is useful for test cases where specifying a port is not reasonable.
//func NewTransport(host string, port int) (*memberlist.NetTransport, error) {
func NewTransport(host string, port int, logger *log.Logger) (*Transport, error) {
// memberlist config
conf := memberlist.DefaultWANConfig()
conf.BindAddr = host
conf.BindPort = port
conf.AdvertisePort = port
conf.Logger = logger
net, err := newTransport(conf)
if err != nil {
return nil, fmt.Errorf("new transport: %s", err)
}
uri, err := pnet.NewURIFromHostPort(host, uint16(net.GetAutoBindPort()))
if err != nil {
return nil, fmt.Errorf("new uri from host port: %s", err)
}
return &Transport{
net: net,
URI: uri,
}, nil
}
// newTransport returns a NetTransport based on the memberlist configuration.
// It will dynamically bind to a port if conf.BindPort is 0.
func newTransport(conf *memberlist.Config) (*memberlist.NetTransport, error) {
nc := &memberlist.NetTransportConfig{
BindAddrs: []string{conf.BindAddr},
BindPort: conf.BindPort,
Logger: conf.Logger,
}
if conf.BindPort == 0 {
panic("TODO: remove this. problem: gossip conf.BindPort was 0!")
}
// See comment below for details about the retry in here.
makeNetRetry := func(limit int) (*memberlist.NetTransport, error) {
var err error
for try := 0; try < limit; try++ {
var nt *memberlist.NetTransport
if nt, err = memberlist.NewNetTransport(nc); err == nil {
return nt, nil
}
if strings.Contains(err.Error(), "address already in use") {
conf.Logger.Printf("[DEBUG] Got bind error: %v", err)
continue
}
}
return nil, fmt.Errorf("failed to obtain an address: %v", err)
}
// The dynamic bind port operation is inherently racy because
// even though we are using the kernel to find a port for us, we
// are attempting to bind multiple protocols (and potentially
// multiple addresses) with the same port number. We build in a
// few retries here since this often gets transient errors in
// busy unit tests.
limit := 1
if conf.BindPort == 0 {
limit = 10
}
nt, err := makeNetRetry(limit)
if err != nil {
return nil, errors.Wrap(err, "could not set up network transport")
}
return nt, nil
}
// Config holds toml-friendly memberlist configuration.
type Config struct {
// Port indicates the port to which pilosa should bind for internal state sharing.
@ -633,21 +91,3 @@ type Config struct {
Nodes int `toml:"nodes"`
ToTheDeadTime toml.Duration `toml:"to-the-dead-time"`
}
// hostToIP converts host to an IP4 address based on net.LookupIP().
func hostToIP(host string) string {
// if host is not an IP addr, check net.LookupIP()
if net.ParseIP(host) == nil {
hosts, err := net.LookupIP(host)
if err != nil {
return host
}
for _, h := range hosts {
// this restricts pilosa to IP4
if h.To4() != nil {
return h.String()
}
}
}
return host
}

View file

@ -647,7 +647,7 @@ func (h *Holder) Open() error {
return errors.Wrap(err, "opening index")
}
if h.isCoordinator() {
if h.isPrimary() {
index.createdAt = timestamp()
err = index.OpenWithTimestamp()
} else {
@ -1200,9 +1200,10 @@ func (h *Holder) recalculateCaches() {
}
}
func (h *Holder) isCoordinator() bool {
// TODO: this needs to be removed
func (h *Holder) isPrimary() bool {
if s, ok := h.broadcaster.(*Server); ok {
return s.isCoordinator
return s.IsPrimary()
}
return false
}
@ -1426,7 +1427,7 @@ func (s *holderSyncer) syncIndex(index string) error {
s.Stats.CountWithCustomTags(MetricColumnAttrStoreBlocks, int64(len(blks)), 1.0, []string{indexTag})
// Sync with every other host.
for _, node := range topology.Nodes(s.Cluster.nodes).FilterID(s.Node.ID) {
for _, node := range topology.Nodes(s.Cluster.noder.Nodes()).FilterID(s.Node.ID) {
// Retrieve attributes from differing blocks.
// Skip update and recomputation if no attributes have changed.
m, err := s.Cluster.InternalClient.ColumnAttrDiff(ctx, &node.URI, index, blks)
@ -1473,7 +1474,7 @@ func (s *holderSyncer) syncField(index, name string) error {
s.Stats.CountWithCustomTags(MetricRowAttrStoreBlocks, int64(len(blks)), 1.0, []string{indexTag, fieldTag})
// Sync with every other host.
for _, node := range topology.Nodes(s.Cluster.nodes).FilterID(s.Node.ID) {
for _, node := range topology.Nodes(s.Cluster.noder.Nodes()).FilterID(s.Node.ID) {
// Retrieve attributes from differing blocks.
// Skip update and recomputation if no attributes have changed.
m, err := s.Cluster.InternalClient.RowAttrDiff(ctx, &node.URI, index, name, blks)
@ -1822,6 +1823,11 @@ type holderCleaner struct {
Closing <-chan struct{}
}
// TODO: this is here to satisfy the linter since holderCleaner was removed from
// the gossip implementation of removeNode. But presumably we will use it once
// we have ported over the etcd implementation.
var _ holderCleaner
// IsClosing returns true if the cleaner has been marked to close.
func (c *holderCleaner) IsClosing() bool {
select {
@ -1836,7 +1842,7 @@ func (c *holderCleaner) IsClosing() bool {
// 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)
snap := topology.NewClusterSnapshot(c.Cluster.noder, c.Cluster.Hasher, c.Cluster.ReplicaN)
for _, index := range c.Holder.Indexes() {
// Verify cleaner has not closed.

View file

@ -104,6 +104,39 @@ func (c *InternalClient) maxShardByIndex(ctx context.Context) (map[string]uint64
return rsp.Standard, nil
}
// SchemaNode returns all index and field schema information from the specified
// node.
func (c *InternalClient) SchemaNode(ctx context.Context, uri *pnet.URI, views bool) ([]*pilosa.IndexInfo, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.Schema")
defer span.Finish()
// TODO: /?views parameter will be ignored, till we implement schemator!
// Execute request against the host.
u := uri.Path(fmt.Sprintf("/schema?views=%v", views))
// Build request.
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, errors.Wrap(err, "creating request")
}
req.Header.Set("User-Agent", "pilosa/"+pilosa.Version)
req.Header.Set("Accept", "application/json")
// Execute request.
resp, err := c.executeRequest(req.WithContext(ctx))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var rsp getSchemaResponse
if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil {
return nil, fmt.Errorf("json decode: %s", err)
}
return rsp.Indexes, nil
}
// Schema returns all index and field schema information.
func (c *InternalClient) Schema(ctx context.Context) ([]*pilosa.IndexInfo, error) {
span, ctx := tracing.StartSpanFromContext(ctx, "InternalClient.Schema")
@ -173,7 +206,7 @@ func (c *InternalClient) CreateIndex(ctx context.Context, index string, opt pilo
if err != nil {
return fmt.Errorf("getting nodes: %s", err)
}
coord := getCoordinatorNode(nodes)
coord := getPrimaryNode(nodes)
if coord == nil {
return fmt.Errorf("could not find the coordinator node")
}
@ -370,9 +403,9 @@ func (c *InternalClient) Import(ctx context.Context, index, field string, shard
return nil
}
func getCoordinatorNode(nodes []*topology.Node) *topology.Node {
func getPrimaryNode(nodes []*topology.Node) *topology.Node {
for _, node := range nodes {
if node.IsCoordinator {
if node.IsPrimary {
return node
}
}
@ -417,7 +450,7 @@ func (c *InternalClient) ImportK(ctx context.Context, index, field string, bits
if err != nil {
return fmt.Errorf("getting nodes: %s", err)
}
coord := getCoordinatorNode(nodes)
coord := getPrimaryNode(nodes)
if coord == nil {
return fmt.Errorf("could not find the coordinator node")
}
@ -629,7 +662,7 @@ func (c *InternalClient) ImportValueK(ctx context.Context, index, field string,
if err != nil {
return fmt.Errorf("getting nodes: %s", err)
}
coord := getCoordinatorNode(nodes)
coord := getPrimaryNode(nodes)
if coord == nil {
return fmt.Errorf("could not find the coordinator node")
}
@ -939,7 +972,7 @@ func (c *InternalClient) CreateFieldWithOptions(ctx context.Context, index, fiel
if err != nil {
return fmt.Errorf("getting nodes: %s", err)
}
coord := getCoordinatorNode(nodes)
coord := getPrimaryNode(nodes)
if coord == nil {
return fmt.Errorf("could not find the coordinator node")
}

View file

@ -367,7 +367,6 @@ func newRouter(handler *Handler) http.Handler {
router := mux.NewRouter()
router.HandleFunc("/cluster/resize/abort", handler.handlePostClusterResizeAbort).Methods("POST").Name("PostClusterResizeAbort")
router.HandleFunc("/cluster/resize/remove-node", handler.handlePostClusterResizeRemoveNode).Methods("POST").Name("PostClusterResizeRemoveNode")
router.HandleFunc("/cluster/resize/set-coordinator", handler.handlePostClusterResizeSetCoordinator).Methods("POST").Name("PostClusterResizeSetCoordinator")
router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux).Methods("GET")
router.Handle("/debug/vars", expvar.Handler()).Methods("GET")
router.Handle("/metrics", promhttp.Handler())
@ -669,7 +668,11 @@ func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
schema := h.api.Schema(r.Context())
schema, err := h.api.Schema(r.Context())
if err != nil {
h.logger.Printf("getting schema error: %s", err)
}
if err := json.NewEncoder(w).Encode(pilosa.Schema{Indexes: schema}); err != nil {
h.logger.Printf("write schema response error: %s", err)
}
@ -736,8 +739,15 @@ func (h *Handler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
return
}
state, err := h.api.State()
if err != nil {
http.Error(w, "getting cluster state error: "+err.Error(), http.StatusInternalServerError)
return
}
status := getStatusResponse{
State: h.api.State(),
State: state,
Nodes: h.api.Hosts(r.Context()),
LocalID: h.api.Node().ID,
ClusterName: h.api.ClusterName(),
@ -971,7 +981,12 @@ func (h *Handler) handleGetIndex(w http.ResponseWriter, r *http.Request) {
return
}
indexName := mux.Vars(r)["index"]
for _, idx := range h.api.Schema(r.Context()) {
schema, err := h.api.Schema(r.Context())
if err != nil {
h.logger.Printf("getting schema error: %s", err)
}
for _, idx := range schema {
if idx.Name == indexName {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(idx); err != nil {
@ -2022,47 +2037,6 @@ func parseUint64Slice(s string) ([]uint64, error) {
return a, nil
}
func (h *Handler) handlePostClusterResizeSetCoordinator(w http.ResponseWriter, r *http.Request) {
if !validHeaderAcceptJSON(r.Header) {
http.Error(w, "JSON only acceptable response", http.StatusNotAcceptable)
return
}
// Decode request.
var req setCoordinatorRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "decoding request "+err.Error(), http.StatusBadRequest)
return
}
oldNode, newNode, err := h.api.SetCoordinator(r.Context(), req.ID)
if err != nil {
if errors.Cause(err) == pilosa.ErrNodeIDNotExists {
http.Error(w, "setting new coordinator: "+err.Error(), http.StatusNotFound)
} else {
http.Error(w, "setting new coordinator: "+err.Error(), http.StatusInternalServerError)
}
return
}
// Encode response.
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(setCoordinatorResponse{
Old: oldNode,
New: newNode,
}); err != nil {
h.logger.Printf("response encoding error: %s", err)
}
}
type setCoordinatorRequest struct {
ID string `json:"id"`
}
type setCoordinatorResponse struct {
Old *topology.Node `json:"old"`
New *topology.Node `json:"new"`
}
// handlePostClusterResizeRemoveNode handles POST /cluster/resize/remove-node request.
func (h *Handler) handlePostClusterResizeRemoveNode(w http.ResponseWriter, r *http.Request) {
if !validHeaderAcceptJSON(r.Header) {

File diff suppressed because it is too large Load diff

View file

@ -112,7 +112,7 @@ message URI {
message Node {
string ID = 1;
URI URI = 2;
bool IsCoordinator = 3;
bool IsPrimary = 3;
string State = 4;
URI GRPCURI = 5;
}
@ -174,7 +174,7 @@ message DeleteViewMessage {
message ResizeInstruction {
int64 JobID = 1;
Node Node = 2;
Node Coordinator = 3;
Node Primary = 3;
repeated ResizeSource Sources = 4;
repeated TranslationResizeSource TranslationSources = 8;
NodeStatus NodeStatus = 7;
@ -201,14 +201,6 @@ message ResizeInstructionComplete {
string Error = 3;
}
message SetCoordinatorMessage {
Node New = 1;
}
message UpdateCoordinatorMessage {
Node New = 1;
}
message Topology {
string ClusterID = 1;
repeated string NodeIDs = 2;
@ -230,4 +222,13 @@ message Transaction {
TransactionStats Stats = 6;
}
message TransactionStats {}
message TransactionStats {}
message ResizeAbortMessage {
}
message ResizeNodeMessage {
string NodeID = 1;
string Action = 2;
}

View file

@ -6748,7 +6748,10 @@ func (m *Row) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -6833,7 +6836,10 @@ func (m *RowMatrix) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -6956,7 +6962,10 @@ func (m *SignedRow) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7115,7 +7124,10 @@ func (m *RowIdentifiers) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7242,7 +7254,10 @@ func (m *IDList) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7346,7 +7361,10 @@ func (m *ExtractedIDColumn) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7463,7 +7481,10 @@ func (m *ExtractedIDMatrix) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7546,7 +7567,10 @@ func (m *KeyList) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7760,7 +7784,10 @@ func (m *ExtractedTableValue) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -7897,7 +7924,10 @@ func (m *ExtractedTableColumn) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8012,7 +8042,10 @@ func (m *ExtractedTableField) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8131,7 +8164,10 @@ func (m *ExtractedTable) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8252,7 +8288,10 @@ func (m *Pair) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8371,7 +8410,10 @@ func (m *PairField) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8488,7 +8530,10 @@ func (m *PairsField) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8558,7 +8603,10 @@ func (m *Int64) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8728,7 +8776,10 @@ func (m *FieldRow) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8851,7 +8902,10 @@ func (m *GroupCount) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -8987,7 +9041,10 @@ func (m *ValCount) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9076,7 +9133,10 @@ func (m *Decimal) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9212,7 +9272,10 @@ func (m *ColumnAttrSet) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9396,7 +9459,10 @@ func (m *Attr) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9481,7 +9547,10 @@ func (m *AttrMap) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9774,7 +9843,10 @@ func (m *QueryRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -9925,7 +9997,10 @@ func (m *QueryResponse) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -10538,7 +10613,10 @@ func (m *QueryResult) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -11022,7 +11100,10 @@ func (m *ImportRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -11484,7 +11565,10 @@ func (m *ImportValueRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -11654,7 +11738,10 @@ func (m *AtomicRecord) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -11737,7 +11824,10 @@ func (m *AtomicImportResponse) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -11904,7 +11994,10 @@ func (m *TranslateKeysRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12031,7 +12124,10 @@ func (m *TranslateKeysResponse) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12222,7 +12318,10 @@ func (m *TranslateIDsRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12305,7 +12404,10 @@ func (m *TranslateIDsResponse) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12422,7 +12524,10 @@ func (m *ImportRoaringRequestView) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12616,7 +12721,10 @@ func (m *ImportRoaringRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12877,7 +12985,10 @@ func (m *ImportColumnAttrsRequest) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {
@ -12994,7 +13105,10 @@ func (m *GroupCounts) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
if skippy < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) < 0 {
return ErrInvalidLengthPublic
}
if (iNdEx + skippy) > l {

View file

@ -181,30 +181,6 @@ func validateName(name string) error {
return nil
}
// stringSlicesAreEqual determines if two string slices are equal.
func stringSlicesAreEqual(a, b []string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func timestamp() int64 {
return time.Now().UnixNano()
}

203
server.go
View file

@ -62,8 +62,6 @@ type Server struct { // nolint: maligned
diagnostics *diagnosticsCollector
executor *executor
executorPoolSize int
hosts []string
clusterDisabled bool
serializer Serializer
// Distributed Consensus
@ -75,9 +73,6 @@ type Server struct { // nolint: maligned
sharder disco.Sharder
schemator disco.Schemator
// TODO: this is VERY temporary!!!
Gossiper Gossiper
// External
systemInfo SystemInfo
gcNotifier GCNotifier
@ -93,7 +88,6 @@ type Server struct { // nolint: maligned
maxWritesPerRequest int
confirmDownSleep time.Duration
confirmDownRetries int
isCoordinator bool
syncer holderSyncer
translationSyncer TranslationSyncer
@ -281,16 +275,6 @@ func OptServerGRPCURI(uri *pnet.URI) ServerOption {
}
}
// OptServerClusterDisabled tells the server whether to use a static cluster with the
// defined hosts. Mostly used for testing.
func OptServerClusterDisabled(disabled bool, hosts []string) ServerOption {
return func(s *Server) error {
s.hosts = hosts
s.clusterDisabled = disabled
return nil
}
}
// OptServerClusterName sets the human-readable cluster name.
func OptServerClusterName(name string) ServerOption {
return func(s *Server) error {
@ -308,15 +292,6 @@ func OptServerSerializer(ser Serializer) ServerOption {
}
}
// OptServerIsCoordinator is a functional option on Server
// used to specify whether or not this server is the coordinator.
func OptServerIsCoordinator(is bool) ServerOption {
return func(s *Server) error {
s.isCoordinator = is
return nil
}
}
// OptServerNodeID is a functional option on Server
// used to set the server node ID.
func OptServerNodeID(nodeID string) ServerOption {
@ -444,7 +419,7 @@ func NewServer(opts ...ServerOption) (*Server, error) {
stator: disco.NopStator,
metadator: disco.NopMetadator,
resizer: disco.NopResizer,
noder: topology.NewLocalNoder(nil),
noder: topology.NewEmptyLocalNoder(),
sharder: disco.NopSharder,
confirmDownRetries: defaultConfirmDownRetries,
@ -499,7 +474,7 @@ func NewServer(opts ...ServerOption) (*Server, error) {
s.cluster.disCo = s.disCo
s.cluster.stator = s.stator
s.cluster.resizer = s.resizer
//s.cluster.noder = s.noder
s.cluster.noder = s.noder
s.cluster.sharder = s.sharder
// Append the NodeID tag to stats.
@ -549,10 +524,6 @@ 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())
@ -582,18 +553,26 @@ func (s *Server) Open() error {
if err != nil {
return errors.Wrap(err, "starting DisCo")
}
fmt.Println("--- disco: open:", s.disCo.ID())
_ = initState
// Set node ID.
s.nodeID = s.disCo.ID()
node := &topology.Node{
ID: s.nodeID,
URI: s.uri,
GRPCURI: s.grpcURI,
IsCoordinator: s.isCoordinator,
State: nodeStateDown,
ID: s.nodeID,
URI: s.uri,
GRPCURI: s.grpcURI,
State: nodeStateDown,
IsPrimary: s.IsPrimary(),
}
// Set metadata for this node.
data, err := json.Marshal(node)
if err != nil {
return errors.Wrap(err, "marshaling json metadata")
}
if err := s.metadator.SetMetadata(context.Background(), data); err != nil {
return errors.Wrap(err, "setting metadata")
}
s.cluster.Node = node
@ -606,33 +585,11 @@ func (s *Server) Open() error {
s.syncer.Closing = s.closing
s.syncer.Stats = s.holder.Stats.WithTags("component:HolderSyncer")
// TODO disco
if false {
node.URI = s.uri
node.GRPCURI = s.grpcURI
// Set metadata for this node.
data, err := json.Marshal(node)
if err != nil {
return errors.Wrap(err, "marshaling json metadata")
}
if err := s.metadator.SetMetadata(context.Background(), data); err != nil {
return errors.Wrap(err, "setting metadata")
}
}
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")
@ -645,24 +602,13 @@ func (s *Server) Open() error {
// bring up the background tasks for the holder.
s.holder.SnapshotQueue = s.snapshotQueue
s.holder.Activate()
if err := s.cluster.setNodeState(nodeStateReady); err != nil {
return errors.Wrap(err, "setting nodeState")
}
// Listen for joining nodes.
// This needs to start after the Holder has opened so that nodes can join
// the cluster without waiting for data to load on the coordinator. Before
// this starts, the joins are queued up in the Cluster.joiningLeavingNodes
// buffered channel.
s.cluster.listenForJoins()
// if we joined existing cluster then broadcast "resize on add" message
// TODO
// if initState == disco.InitialClusterStateExisting {
// if err := s.cluster.addNode(s.nodeID); err != nil {
// return errors.Wrap(err, "adding a node to the existing cluster")
// }
// }
if initState == disco.InitialClusterStateExisting {
if err := s.cluster.addNode(s.nodeID); err != nil {
return errors.Wrap(err, "adding a node to the existing cluster")
}
}
if err := s.stator.Started(context.Background()); err != nil {
return errors.Wrap(err, "setting nodeState")
@ -682,8 +628,6 @@ func (s *Server) Close() error {
case <-s.closing:
return nil
default:
fmt.Println("--- disco: server close:", s.disCo.ID())
errE := s.executor.Close()
// Notify goroutines to stop.
@ -698,9 +642,7 @@ func (s *Server) Close() error {
}
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()
@ -791,7 +733,14 @@ func (s *Server) monitorAntiEntropy() {
s.holder.Stats.Count(MetricAntiEntropy, 1, 1.0)
}
t := time.Now()
if s.cluster.State() == ClusterStateResizing {
state, err := s.cluster.State()
if err != nil {
s.logger.Printf("cluster state error: err=%s", err)
continue
}
if state == string(ClusterStateResizing) {
continue // don't launch anti-entropy during resize.
// the cluster sets its state to resizing and *then* sends to
// abortAntiEntropyCh before starting to resize
@ -837,6 +786,7 @@ func (s *Server) receiveMessage(m Message) error {
if err := f.AddRemoteAvailableShards(roaring.NewBitmap(obj.Shard)); err != nil {
return errors.Wrap(err, "adding remote available shards")
}
case *CreateIndexMessage:
opt := obj.Meta
idx, err := s.holder.CreateIndex(obj.Index, *opt)
@ -846,10 +796,12 @@ func (s *Server) receiveMessage(m Message) error {
idx.mu.Lock()
idx.createdAt = obj.CreatedAt
idx.mu.Unlock()
case *DeleteIndexMessage:
if err := s.holder.DeleteIndex(obj.Index); err != nil {
return err
}
case *CreateFieldMessage:
idx := s.holder.Index(obj.Index)
if idx == nil {
@ -863,16 +815,19 @@ func (s *Server) receiveMessage(m Message) error {
fld.mu.Lock()
fld.createdAt = obj.CreatedAt
fld.mu.Unlock()
case *DeleteFieldMessage:
idx := s.holder.Index(obj.Index)
if err := idx.DeleteField(obj.Field); err != nil {
return err
}
case *DeleteAvailableShardMessage:
f := s.holder.Field(obj.Index, obj.Field)
if err := f.RemoveAvailableShard(obj.ShardID); err != nil {
return err
}
case *CreateViewMessage:
f := s.holder.Field(obj.Index, obj.Field)
if f == nil {
@ -881,6 +836,7 @@ func (s *Server) receiveMessage(m Message) error {
if _, _, err := f.createViewIfNotExistsBase(obj.View); err != nil {
return err
}
case *DeleteViewMessage:
f := s.holder.Field(obj.Index, obj.Field)
if f == nil {
@ -890,45 +846,41 @@ func (s *Server) receiveMessage(m Message) error {
if err != nil {
return err
}
case *ClusterStatus:
err := s.cluster.mergeClusterStatus(obj)
if err != nil {
return err
}
if !s.isCoordinator {
if obj.Schema != nil {
s.holder.applyCreatedAt(obj.Schema.Indexes)
case *ResizeNodeMessage:
switch obj.Action {
case resizeJobActionRemove:
if err := s.cluster.resizeNodeOnRemove(obj.NodeID); err != nil {
return errors.Wrapf(err, "resizing node %s on remove %s", s.cluster.disCo.ID(), obj.NodeID)
}
case resizeJobActionAdd:
if err := s.cluster.resizeNodeOnAdd(obj.NodeID); err != nil {
return errors.Wrapf(err, "resizing node %s on remove %s", s.cluster.disCo.ID(), obj.NodeID)
}
default:
return fmt.Errorf("incorrect resizing node action: %s", obj.Action)
}
case *ResizeInstruction:
err := s.cluster.followResizeInstruction(obj)
err := s.cluster.followResizeInstruction(context.Background(), obj)
if err != nil {
return err
}
case *ResizeInstructionComplete:
err := s.cluster.markResizeInstructionComplete(obj)
if err != nil {
return err
}
case *SetCoordinatorMessage:
return s.cluster.setCoordinator(obj.New)
case *UpdateCoordinatorMessage:
s.cluster.updateCoordinator(obj.New)
case *NodeStateMessage:
err := s.cluster.receiveNodeState(obj.NodeID, obj.State)
case *ResizeAbortMessage:
err := s.cluster.resizeAbort()
if err != nil {
return err
}
case *RecalculateCaches:
s.holder.recalculateCaches()
case *NodeEvent:
err := s.cluster.ReceiveEvent(obj)
if err != nil {
return errors.Wrapf(err, "cluster receiving NodeEvent %v", obj)
}
case *NodeStatus:
s.handleRemoteStatus(obj)
case *TransactionMessage:
err := s.handleTransactionMessage(obj)
if err != nil {
@ -976,11 +928,7 @@ func (s *Server) SendSync(m Message) error {
for _, node := range s.cluster.Nodes() {
node := node
// prevent race against cluster.addNodeBasicSorted() in cluster.go
node.Mu.Lock()
uri := node.URI // URI is a struct value
node.Mu.Unlock()
// Don't forward the message to ourselves.
if s.uri == uri {
@ -1008,10 +956,7 @@ func (s *Server) SendTo(node *topology.Node, m Message) error {
}
msg = append([]byte{getMessageType(m)}, msg...)
// prevent race against cluster.addNodeBasicSorted() in cluster.go
node.Mu.Lock()
uri := node.URI // URI is a struct value
node.Mu.Unlock()
return s.defaultClient.SendMessage(context.Background(), &uri, msg)
}
@ -1024,8 +969,14 @@ func (s *Server) node() *topology.Node {
// handleRemoteStatus receives incoming NodeStatus from remote nodes.
func (s *Server) handleRemoteStatus(pb Message) {
state, err := s.cluster.State()
if err != nil {
s.logger.Printf("getting cluster state: %s", err)
return
}
// Ignore NodeStatus messages until the cluster is in a Normal state.
if s.cluster.State() != ClusterStateNormal {
if state != string(ClusterStateNormal) {
return
}
@ -1071,6 +1022,11 @@ func (s *Server) mergeRemoteStatus(ns *NodeStatus) error {
return nil
}
// IsPrimary returns if this node is primary right now or not.
func (s *Server) IsPrimary() bool {
return s.nodeID == s.noder.PrimaryNodeID(s.cluster.Hasher)
}
// monitorDiagnostics periodically polls the Pilosa Indexes for cluster info.
func (s *Server) monitorDiagnostics() {
// Do not send more than once a minute
@ -1084,7 +1040,7 @@ func (s *Server) monitorDiagnostics() {
s.diagnostics.SetVersion(Version)
s.diagnostics.Set("Host", s.uri.Host)
s.diagnostics.Set("Cluster", strings.Join(s.cluster.nodeIDs(), ","))
s.diagnostics.Set("NumNodes", len(s.cluster.nodes))
s.diagnostics.Set("NumNodes", len(s.cluster.noder.Nodes()))
s.diagnostics.Set("NumCPU", runtime.NumCPU())
s.diagnostics.Set("NodeID", s.nodeID)
s.diagnostics.Set("ClusterID", s.cluster.id)
@ -1170,11 +1126,12 @@ func (s *Server) monitorRuntime() {
}
func (srv *Server) StartTransaction(ctx context.Context, id string, timeout time.Duration, exclusive bool, remote bool) (*Transaction, error) {
snap := topology.NewClusterSnapshot(srv.cluster.noder, srv.cluster.Hasher, srv.cluster.partitionN)
node := srv.node()
if !remote && !node.IsCoordinator && len(srv.cluster.Nodes()) > 1 {
if !remote && !snap.IsPrimaryFieldTranslationNode(node.ID) && len(srv.cluster.Nodes()) > 1 {
return nil, ErrNodeNotCoordinator
}
if remote && (node.IsCoordinator || len(srv.cluster.Nodes()) == 1) {
if remote && (snap.IsPrimaryFieldTranslationNode(node.ID) || len(srv.cluster.Nodes()) == 1) {
return nil, errors.New("unexpected remote start call to coordinator or single node cluster")
}
@ -1216,11 +1173,12 @@ func (srv *Server) StartTransaction(ctx context.Context, id string, timeout time
}
func (srv *Server) FinishTransaction(ctx context.Context, id string, remote bool) (*Transaction, error) {
snap := topology.NewClusterSnapshot(srv.cluster.noder, srv.cluster.Hasher, srv.cluster.partitionN)
node := srv.node()
if !remote && !node.IsCoordinator && len(srv.cluster.Nodes()) > 1 {
if !remote && !snap.IsPrimaryFieldTranslationNode(node.ID) && len(srv.cluster.Nodes()) > 1 {
return nil, ErrNodeNotCoordinator
}
if remote && (node.IsCoordinator || len(srv.cluster.Nodes()) == 1) {
if remote && (snap.IsPrimaryFieldTranslationNode(node.ID) || len(srv.cluster.Nodes()) == 1) {
return nil, errors.New("unexpected remote finish call to coordinator or single node cluster")
}
@ -1245,8 +1203,9 @@ func (srv *Server) FinishTransaction(ctx context.Context, id string, remote bool
}
func (srv *Server) Transactions(ctx context.Context) (map[string]*Transaction, error) {
snap := topology.NewClusterSnapshot(srv.cluster.noder, srv.cluster.Hasher, srv.cluster.partitionN)
node := srv.node()
if !node.IsCoordinator && len(srv.cluster.Nodes()) > 1 {
if !snap.IsPrimaryFieldTranslationNode(node.ID) && len(srv.cluster.Nodes()) > 1 {
return nil, ErrNodeNotCoordinator
}
@ -1254,12 +1213,14 @@ func (srv *Server) Transactions(ctx context.Context) (map[string]*Transaction, e
}
func (srv *Server) GetTransaction(ctx context.Context, id string, remote bool) (*Transaction, error) {
snap := topology.NewClusterSnapshot(srv.cluster.noder, srv.cluster.Hasher, srv.cluster.partitionN)
node := srv.node()
if !remote && !node.IsCoordinator && len(srv.cluster.Nodes()) > 1 {
if !remote && !snap.IsPrimaryFieldTranslationNode(node.ID) && len(srv.cluster.Nodes()) > 1 {
return nil, ErrNodeNotCoordinator
}
if remote && (node.IsCoordinator || len(srv.cluster.Nodes()) == 1) {
if remote && (snap.IsPrimaryFieldTranslationNode(node.ID) || len(srv.cluster.Nodes()) == 1) {
return nil, errors.New("unexpected remote get call to coordinator or single node cluster")
}

View file

@ -29,7 +29,6 @@ import (
"github.com/pilosa/pilosa/v2/server"
"github.com/pilosa/pilosa/v2/test"
"github.com/pilosa/pilosa/v2/test/port"
"golang.org/x/sync/errgroup"
)
// Ensure program can send/receive broadcast messages.
@ -121,8 +120,9 @@ func TestClusterResize_EmptyNode(t *testing.T) {
m0 := test.RunCommand(t)
defer m0.Close()
if m0.API.State() != pilosa.ClusterStateNormal {
t.Fatalf("unexpected cluster state: %s", m0.API.State())
state0, err := m0.API.State()
if err != nil || state0 != string(pilosa.ClusterStateNormal) {
t.Fatalf("unexpected cluster state: %s, error: %v", state0, err)
}
}
@ -131,10 +131,12 @@ func TestClusterResize_EmptyNodes(t *testing.T) {
clus := test.MustRunCluster(t, 2)
defer clus.Close()
if clus.GetNode(0).API.State() != pilosa.ClusterStateNormal {
t.Fatalf("unexpected node0 cluster state: %s", clus.GetNode(0).API.State())
} else if clus.GetNode(1).API.State() != pilosa.ClusterStateNormal {
t.Fatalf("unexpected node1 cluster state: %s", clus.GetNode(1).API.State())
state0, err0 := clus.GetNode(0).API.State()
state1, err1 := clus.GetNode(1).API.State()
if err0 != nil || state0 != string(pilosa.ClusterStateNormal) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || state1 != string(pilosa.ClusterStateNormal) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
}
@ -157,10 +159,12 @@ func TestClusterResize_AddNode(t *testing.T) {
clus := test.MustRunCluster(t, 2)
defer clus.Close()
if !test.CheckClusterState(clus.GetNode(0), pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", clus.GetNode(0).API.State())
} else if !test.CheckClusterState(clus.GetNode(1), pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", clus.GetNode(1).API.State())
state0, err0 := clus.GetNode(0).API.State()
state1, err1 := clus.GetNode(1).API.State()
if err0 != nil || !test.CheckClusterState(clus.GetNode(0), string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(clus.GetNode(1), string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
})
t.Run("WithIndex", func(t *testing.T) {
@ -168,8 +172,6 @@ func TestClusterResize_AddNode(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -181,27 +183,28 @@ func TestClusterResize_AddNode(t *testing.T) {
}
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error; %v", state1, err1)
}
})
t.Run("ContinuousShards", func(t *testing.T) {
@ -210,8 +213,6 @@ func TestClusterResize_AddNode(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -238,27 +239,28 @@ func TestClusterResize_AddNode(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
// Verify the data exists on both nodes.
@ -270,8 +272,6 @@ func TestClusterResize_AddNode(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -295,26 +295,28 @@ func TestClusterResize_AddNode(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
// Verify the data exists on both nodes.
@ -328,8 +330,6 @@ func TestClusterResize_AddNode(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -357,27 +357,29 @@ func TestClusterResize_AddNode(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
// Verify the data exists on both nodes.
@ -395,8 +397,6 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -414,24 +414,26 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
}()
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
if err := <-errc; err != nil {
@ -443,8 +445,6 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -472,16 +472,16 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
errc := make(chan error, 1)
@ -491,10 +491,12 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
}()
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
// Verify the data exists on both nodes.
@ -507,8 +509,6 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -536,13 +536,13 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
errc := make(chan error, 1)
@ -551,15 +551,17 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
errc <- err
}()
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m1.Close()
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
// Verify the data exists on both nodes.
@ -571,8 +573,6 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
// Create a client for each node.
client0 := m0.Client()
@ -598,13 +598,13 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
// Configure node1
m1 := test.NewCommandNode(t, false)
m1.Config.Gossip.Seeds = []string{seed}
m1 := test.NewCommandNode(t)
if err := port.GetListeners(func(lsns []*net.TCPListener) error {
portsCfg := test.GenPortsConfig(test.NewPorts(lsns))
m1.Config.Gossip.Port = portsCfg[0].Gossip.Port
m1.Config.DisCo = portsCfg[0].DisCo
m1.Config.Etcd = portsCfg[0].Etcd
m1.Config.Name = portsCfg[0].Name
m1.Config.Cluster.Name = portsCfg[0].Cluster.Name
m1.Config.BindGRPC = portsCfg[0].BindGRPC
errc := make(chan error, 1)
@ -613,85 +613,22 @@ func TestClusterResize_AddNodeConcurrentIndex(t *testing.T) {
errc <- err
}()
return m1.Start()
}, 4, 10); err != nil {
}, 3, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
state0, err0 := m0.API.State()
state1, err1 := m1.API.State()
if err0 != nil || !test.CheckClusterState(m0, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node0 cluster state: %s, error: %v", state0, err0)
} else if err1 != nil || !test.CheckClusterState(m1, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected node1 cluster state: %s, error: %v", state1, err1)
}
m0.QueryExpect(t, "i", "", `Row(f=1)`, exp)
m1.QueryExpect(t, "i", "", `Row(f=1)`, exp)
})
}
// Ensure that redundant gossip seeds are used
func TestCluster_GossipMembership(t *testing.T) {
t.Skip("skipping gossip test")
t.Run("Node0Down", func(t *testing.T) {
// Configure node0
m0 := test.MustRunCluster(t, 1).GetNode(0)
defer m0.Close()
seed := m0.GossipAddress()
var eg errgroup.Group
// Configure node1
m1 := test.NewCommandNode(t, false)
defer m1.Close()
eg.Go(func() error {
// Pass invalid seed as first in list
m1.Config.Gossip.Seeds = []string{"http://localhost:8765", seed}
if err := port.GetPort(func(p int) error {
m1.Config.Gossip.Port = fmt.Sprintf("%d", p)
return m1.Start()
}, 10); err != nil {
t.Fatalf("starting second main: %v", err)
}
return nil
})
// Configure node1
m2 := test.NewCommandNode(t, false)
defer m2.Close()
eg.Go(func() error {
// Pass invalid seed as first in list
m2.Config.Gossip.Seeds = []string{seed, "http://localhost:8765"}
err := port.GetPort(func(p int) error {
m2.Config.Gossip.Port = fmt.Sprintf("%d", p)
return m2.Start()
}, 10)
if err != nil {
t.Fatalf("starting second main: %v", err)
}
defer m2.Close()
return nil
})
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
if !test.CheckClusterState(m0, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node0 cluster state: %s", m0.API.State())
} else if !test.CheckClusterState(m1, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node1 cluster state: %s", m1.API.State())
} else if !test.CheckClusterState(m2, pilosa.ClusterStateNormal, 1000) {
t.Fatalf("unexpected node2 cluster state: %s", m2.API.State())
}
numNodes := len(m0.API.Hosts(context.Background()))
if numNodes != 3 {
t.Fatalf("Expected 3 nodes, got %d", numNodes)
}
})
}
func TestClusterResize_RemoveNode(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
@ -725,7 +662,7 @@ func TestClusterResize_RemoveNode(t *testing.T) {
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"
expBody := fmt.Sprintf("removing node: the node %s can not be removed: precondition failed", nodeID)
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode)
} else if strings.TrimSpace(resp.Body) != expBody {
@ -734,11 +671,10 @@ func TestClusterResize_RemoveNode(t *testing.T) {
})
t.Run("ErrorRemoveOnNonCoordinator", func(t *testing.T) {
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)
expBody := fmt.Sprintf("removing node: the node %s can not be removed: precondition failed", nodeID)
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode)
} else if strings.TrimSpace(resp.Body) != expBody {
@ -747,6 +683,7 @@ func TestClusterResize_RemoveNode(t *testing.T) {
})
t.Run("ErrorRemoveWithoutReplicas", func(t *testing.T) {
t.Skip("TODO: Unskip the test if you understand it")
client0 := coord.Client()
// Create indexes and fields on one node.

View file

@ -53,6 +53,9 @@ type TLSConfig struct {
// Config represents the configuration for the command.
type Config struct {
// Name a unique name for this node in the cluster.
Name string `toml:"name"`
// DataDir is the directory where Pilosa stores both indexed data and
// running state such as cluster topology information.
DataDir string `toml:"data-dir"`
@ -120,18 +123,14 @@ type Config struct {
ImportWorkerPoolSize int `toml:"-"`
Cluster struct {
// Disabled controls whether clustering functionality is enabled.
Disabled bool `toml:"disabled"`
Coordinator bool `toml:"coordinator"`
ReplicaN int `toml:"replicas"`
Hosts []string `toml:"hosts"`
Name string `toml:"name"`
ReplicaN int `toml:"replicas"`
Name string `toml:"name"`
// This LongQueryTime is deprecated but still exists for backward compatibility
LongQueryTime toml.Duration `toml:"long-query-time"`
} `toml:"cluster"`
// DisCo config is based on embedded etcd.
DisCo petcd.Options `toml:"disco"`
// Etcd config is based on embedded etcd.
Etcd petcd.Options `toml:"etcd"`
LongQueryTime toml.Duration `toml:"long-query-time"`
// Gossip config is based around memberlist.Config.
@ -225,24 +224,23 @@ type Config struct {
// We disallow zero because the tests need to be using from the pre-allocated
// block of ports maintained by the pilosa/test/port port-mapper.
func (c *Config) MustValidate() {
err := c.Validate()
err := c.validate()
if err != nil {
panic(err)
}
}
func (c *Config) Validate() error {
fmt.Printf("Validate() called on Config = '%#v'\n", c)
func (c *Config) validate() error {
hostPort := []string{
"Bind", c.Bind, // :10101
"BindGRPC", c.BindGRPC, // :20101
"Advertise", c.Advertise, // on hp = 'http://localhost:63002'
"AdvertiseGRPC", c.AdvertiseGRPC, // on hp = 'http://localhost:63003'
"DisCo.LClientURL", c.DisCo.LClientURL, // on hp = ':14000'
//c.DisCo.AClientURL, // hardcoded to same as LClientURL
"DisCo.LPeerURL", c.DisCo.LPeerURL, // ":"
//c.DisCo.APeerURL, // hardcoded to same as LPeerURL
"DisCo.ClusterURL", c.DisCo.ClusterURL,
"Etcd.LClientURL", c.Etcd.LClientURL, // on hp = ':14000'
"Etcd.AClientURL", c.Etcd.AClientURL, // ""
"Etcd.LPeerURL", c.Etcd.LPeerURL, // ":"
"Etcd.APeerURL", c.Etcd.APeerURL, // ""
"Etcd.ClusterURL", c.Etcd.ClusterURL,
"Gossip.Port", fmt.Sprintf(":%v", c.Gossip.Port),
"Gossip.AdvertisePort", fmt.Sprintf(":%v", c.Gossip.AdvertisePort),
"Postgres.Bind", c.Postgres.Bind,
@ -265,7 +263,6 @@ func (c *Config) Validate() error {
continue
}
fmt.Printf(" on name = '%v', hp = '%v'\n", name, hp)
hp = strings.TrimPrefix(hp, "http://")
hp = strings.TrimPrefix(hp, "https://")
splt := strings.Split(hp, ":")
@ -291,6 +288,7 @@ func (c *Config) Validate() error {
// NewConfig returns an instance of Config with default options.
func NewConfig() *Config {
c := &Config{
Name: "pilosa0",
DataDir: "~/.pilosa",
Bind: ":" + defaultBindPort,
BindGRPC: ":" + defaultBindGRPCPort,
@ -318,9 +316,8 @@ func NewConfig() *Config {
}
// Cluster config.
c.Cluster.Disabled = false
c.Cluster.Name = "cluster0"
c.Cluster.ReplicaN = 1
c.Cluster.Hosts = []string{}
c.Cluster.LongQueryTime = toml.Duration(-time.Minute) //TODO remove this once cluster.longQueryTime is fully deprecated
// Gossip config.
@ -356,13 +353,14 @@ func NewConfig() *Config {
c.Postgres.WriteTimeout = toml.Duration(10 * time.Second)
// we don't really need a connection limit
c.DisCo.AClientURL = "http://localhost:10301"
c.DisCo.LClientURL = "http://localhost:10301"
c.DisCo.APeerURL = "http://localhost:10401"
c.DisCo.LPeerURL = "http://localhost:10401"
c.DisCo.Dir = ""
c.DisCo.Name = "nodeName"
c.DisCo.ClusterName = "clusterName"
c.Etcd.AClientURL = ""
c.Etcd.LClientURL = "http://localhost:10301"
c.Etcd.APeerURL = ""
c.Etcd.LPeerURL = "http://localhost:10401"
c.Etcd.Dir = ""
c.Etcd.Name = ""
c.Etcd.ClusterName = ""
c.Etcd.InitCluster = c.Name + "=" + c.Etcd.LPeerURL
return c
}
@ -373,34 +371,34 @@ func NewConfig() *Config {
// completely empty, or have both a host part and a port part
// separated by a colon. In the latter case either can be empty to
// indicate it's left unspecified.
func (cfg *Config) validateAddrs(ctx context.Context) error {
func (c *Config) validateAddrs(ctx context.Context) error {
// Validate the advertise address.
advScheme, advHost, advPort, err := validateAdvertiseAddr(ctx, cfg.Advertise, cfg.Bind, defaultBindPort)
advScheme, advHost, advPort, err := validateAdvertiseAddr(ctx, c.Advertise, c.Bind, defaultBindPort)
if err != nil {
return errors.Wrapf(err, "validating advertise address")
}
cfg.Advertise = schemeHostPortString(advScheme, advHost, advPort)
c.Advertise = schemeHostPortString(advScheme, advHost, advPort)
// Validate the listen address.
listenScheme, listenHost, listenPort, err := validateListenAddr(ctx, cfg.Bind, defaultBindPort)
listenScheme, listenHost, listenPort, err := validateListenAddr(ctx, c.Bind, defaultBindPort)
if err != nil {
return errors.Wrap(err, "validating listen address")
}
cfg.Bind = schemeHostPortString(listenScheme, listenHost, listenPort)
c.Bind = schemeHostPortString(listenScheme, listenHost, listenPort)
// Validate the gRPC advertise address.
_, grpcAdvHost, grpcAdvPort, err := validateAdvertiseAddr(ctx, cfg.AdvertiseGRPC, cfg.BindGRPC, defaultBindGRPCPort)
_, grpcAdvHost, grpcAdvPort, err := validateAdvertiseAddr(ctx, c.AdvertiseGRPC, c.BindGRPC, defaultBindGRPCPort)
if err != nil {
return errors.Wrapf(err, "validating grpc advertise address")
}
cfg.AdvertiseGRPC = schemeHostPortString("grpc", grpcAdvHost, grpcAdvPort)
c.AdvertiseGRPC = schemeHostPortString("grpc", grpcAdvHost, grpcAdvPort)
// Validate the gRPC listen address.
_, grpcListenHost, grpcListenPort, err := validateListenAddr(ctx, cfg.BindGRPC, defaultBindGRPCPort)
_, grpcListenHost, grpcListenPort, err := validateListenAddr(ctx, c.BindGRPC, defaultBindGRPCPort)
if err != nil {
return errors.Wrap(err, "validating grpc listen address")
}
cfg.BindGRPC = schemeHostPortString("grpc", grpcListenHost, grpcListenPort)
c.BindGRPC = schemeHostPortString("grpc", grpcListenHost, grpcListenPort)
return nil
}

View file

@ -23,14 +23,6 @@ import (
"github.com/pilosa/pilosa/v2/toml"
)
func Test_NewConfig(t *testing.T) {
c := server.NewConfig()
if c.Cluster.Disabled {
t.Fatalf("unexpected Cluster.Disabled: %v", c.Cluster.Disabled)
}
}
func Test_ValidateConfig(t *testing.T) {
c := server.NewConfig()
c.MustValidate()

View file

@ -284,7 +284,11 @@ func (h *GRPCHandler) CreateIndex(ctx context.Context, req *pb.CreateIndexReques
// GetIndex returns a single Index given a name
func (h *GRPCHandler) GetIndex(ctx context.Context, req *pb.GetIndexRequest) (*pb.GetIndexResponse, error) {
schema := h.api.Schema(ctx)
schema, err := h.api.Schema(ctx)
if err != nil {
return nil, errToStatusError(err)
}
for _, index := range schema {
if req.Name == index.Name {
return &pb.GetIndexResponse{Index: &pb.Index{Name: index.Name}}, nil
@ -295,7 +299,11 @@ func (h *GRPCHandler) GetIndex(ctx context.Context, req *pb.GetIndexRequest) (*p
// GetIndexes returns a list of all Indexes
func (h *GRPCHandler) GetIndexes(ctx context.Context, req *pb.GetIndexesRequest) (*pb.GetIndexesResponse, error) {
schema := h.api.Schema(ctx)
schema, err := h.api.Schema(ctx)
if err != nil {
return nil, errToStatusError(err)
}
indexes := make([]*pb.Index, len(schema))
for i, index := range schema {
indexes[i] = &pb.Index{Name: index.Name}
@ -341,7 +349,11 @@ func (h *VDSMGRPCHandler) GetVDS(ctx context.Context, req *vdsm_pb.GetVDSRequest
case *vdsm_pb.GetVDSRequest_Id:
return nil, status.Error(codes.InvalidArgument, "VDS IDs are no longer supported")
case *vdsm_pb.GetVDSRequest_Name:
schema := h.api.Schema(ctx)
schema, err := h.api.Schema(ctx)
if err != nil {
return nil, errToStatusError(err)
}
for _, index := range schema {
if idOrName.Name == index.Name {
return &vdsm_pb.GetVDSResponse{Vds: &vdsm_pb.VDS{Name: index.Name}}, nil
@ -355,7 +367,11 @@ func (h *VDSMGRPCHandler) GetVDS(ctx context.Context, req *vdsm_pb.GetVDSRequest
// GetVDSs returns a list of all VDSs
func (h *VDSMGRPCHandler) GetVDSs(ctx context.Context, req *vdsm_pb.GetVDSsRequest) (*vdsm_pb.GetVDSsResponse, error) {
schema := h.api.Schema(ctx)
schema, err := h.api.Schema(ctx)
if err != nil {
return nil, errToStatusError(err)
}
vdss := make([]*vdsm_pb.VDS, len(schema))
for i, index := range schema {
vdss[i] = &vdsm_pb.VDS{Name: index.Name}

View file

@ -1009,7 +1009,10 @@ func TestCRUDIndexes(t *testing.T) {
t.Fatal(err)
}
schema := m.API.Schema(ctx)
schema, err := m.API.Schema(ctx)
if err != nil {
t.Fatal("Getting schema error", err)
}
if len(schema) != 1 {
t.Fatal("Schema should include one index")
}
@ -1029,14 +1032,22 @@ func TestCRUDIndexes(t *testing.T) {
t.Fatal(err)
}
schema = m.API.Schema(ctx)
schema, err = m.API.Schema(ctx)
if err != nil {
t.Fatal("Getting schema error", err)
}
if len(schema) != 2 {
t.Fatal("Schema should include two indexes")
}
_ = m.API.DeleteIndex(ctx, "testindex1")
schema = m.API.Schema(ctx)
schema, err = m.API.Schema(ctx)
if err != nil {
t.Fatal("Getting schema error", err)
}
if len(schema) != 1 {
t.Fatal("Schema should include one index")
}
@ -1146,7 +1157,11 @@ func TestCRUDIndexes(t *testing.T) {
t.Fatal(err)
}
schema := m.API.Schema(ctx)
schema, err := m.API.Schema(ctx)
if err != nil {
t.Fatal("Getting schema error", err)
}
if len(schema) != 0 {
t.Fatal("Schema should include no index")
}

View file

@ -40,7 +40,6 @@ import (
pb "github.com/pilosa/pilosa/v2/proto"
"github.com/pilosa/pilosa/v2/server"
"github.com/pilosa/pilosa/v2/test"
"github.com/pilosa/pilosa/v2/test/port"
)
func TestHandler_PostSchemaCluster(t *testing.T) {
@ -226,8 +225,12 @@ func TestHandler_Endpoints(t *testing.T) {
})
t.Run("Import", func(t *testing.T) {
indexInfo := cmd.API.Schema(context.Background())
err := cmd.API.ApplySchema(context.Background(), &pilosa.Schema{Indexes: indexInfo}, false)
indexInfo, err := cmd.API.Schema(context.Background())
if err != nil {
t.Fatalf("getting schema: %v", err)
}
err = cmd.API.ApplySchema(context.Background(), &pilosa.Schema{Indexes: indexInfo}, false)
if err != nil {
t.Fatalf("applying schema: %v", err)
}
@ -1060,8 +1063,8 @@ func TestHandler_Endpoints(t *testing.T) {
}
body := mustJSONDecodeSlice(t, w.Body)
bmap := body[0].(map[string]interface{})
if bmap["isCoordinator"] != true {
t.Fatalf("expected true coordinator")
if bmap["isPrimary"] != true {
t.Fatalf("expected true primary, got: %+v", bmap)
}
// invalid argument should return BadRequest
@ -1394,17 +1397,14 @@ func TestHandler_Endpoints(t *testing.T) {
func TestCluster_TranslateStore(t *testing.T) {
cluster := test.MustNewCluster(t, 1)
cluster.Nodes[0] = test.NewCommandNode(t, true,
cluster.Nodes[0] = test.NewCommandNode(t,
server.OptCommandServerOptions(
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderWithLockerFunc(nil, &sync.Mutex{})),
),
)
if err := port.GetPort(func(p int) error {
cluster.GetIdleNode(0).Config.Gossip.Port = fmt.Sprintf("%d", p)
return cluster.GetIdleNode(0).Start()
}, 10); err != nil {
if err := cluster.GetIdleNode(0).Start(); err != nil {
t.Fatalf("starting node 0: %v", err)
}
defer cluster.GetIdleNode(0).Close()
@ -1499,7 +1499,7 @@ func TestQueryHistory(t *testing.T) {
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/query-history", nil))
if w.Code != gohttp.StatusOK {
t.Fatalf("unexpected status code: %d", w.Code)
t.Fatalf("unexpected status code: %d %s", w.Code, w.Body.String())
}
ret := make([]pilosa.PastQueryStatus, 4)

View file

@ -20,10 +20,8 @@
package server
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"log"
@ -48,7 +46,6 @@ import (
petcd "github.com/pilosa/pilosa/v2/etcd"
"github.com/pilosa/pilosa/v2/gcnotify"
"github.com/pilosa/pilosa/v2/gopsutil"
"github.com/pilosa/pilosa/v2/gossip"
"github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/logger"
pnet "github.com/pilosa/pilosa/v2/net"
@ -73,10 +70,6 @@ type Command struct {
// Configuration.
Config *Config
// Gossip transport
gossipTransport *gossip.Transport
gossipMemberSet io.Closer
// Standard input/output
*pilosa.CmdIO
@ -85,7 +78,6 @@ type Command struct {
// done will be closed when Command.Close() is called
done chan struct{}
// Passed to the Gossip implementation.
logOutput io.Writer
logger loggerLogger
@ -122,8 +114,7 @@ func OptCommandConfig(config *Config) CommandOption {
return func(c *Command) error {
defer c.Config.MustValidate()
if c.Config != nil {
c.Config.DisCo = config.DisCo
fmt.Printf("setting c.ConfigDisCo to '%#v'", config.DisCo)
c.Config.Etcd = config.Etcd
return nil
}
c.Config = config
@ -153,10 +144,6 @@ 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
@ -168,9 +155,6 @@ func (m *Command) Start() (err error) {
return errors.Wrap(err, "setting up server")
}
// TODO: this is temporary.
m.Server.Gossiper = m
if runtime.GOOS == "linux" {
result, err := ioutil.ReadFile("/proc/sys/vm/max_map_count")
if err != nil {
@ -242,11 +226,6 @@ func (m *Command) UpAndDown() (err error) {
return errors.Wrap(err, "setting up server")
}
// SetupNetworking (so we'll have profiling)
err = m.setupNetworking()
if err != nil {
return errors.Wrap(err, "setting up networking")
}
go func() {
err := m.Handler.Serve()
if err != nil {
@ -389,22 +368,25 @@ func (m *Command) SetupServer() error {
m.logger.Printf("DEPRECATED: Configuration parameter cluster.long-query-time has been renamed to long-query-time")
}
// Set Coordinator.
coordinatorOpt := pilosa.OptServerIsCoordinator(false)
if m.Config.Cluster.Coordinator || len(m.Config.Gossip.Seeds) == 0 {
coordinatorOpt = pilosa.OptServerIsCoordinator(true)
}
// If a DisCo.Dir is not provided, nest a default under the pilosa data dir.
if m.Config.DisCo.Dir == "" {
// Use other config parameters to set Etcd parameters which we don't want to
// expose in the user-facing config.
//
// Use cluster.name for etcd.cluster-name
m.Config.Etcd.ClusterName = m.Config.Cluster.Name
//
// Use name for etcd.name
m.Config.Etcd.Name = m.Config.Name
//
// If an Etcd.Dir is not provided, nest a default under the pilosa data dir.
if m.Config.Etcd.Dir == "" {
path, err := expandDirName(m.Config.DataDir)
if err != nil {
return errors.Wrapf(err, "expanding directory name: %s", m.Config.DataDir)
}
m.Config.DisCo.Dir = filepath.Join(path, pilosa.DefaultDiscoDir)
m.Config.Etcd.Dir = filepath.Join(path, pilosa.DefaultDiscoDir)
}
e := petcd.NewEtcd(m.Config.DisCo, m.Config.Cluster.ReplicaN)
e := petcd.NewEtcdWithCache(m.Config.Etcd, m.Config.Cluster.ReplicaN)
discoOpt := pilosa.OptServerDisCo(e, e, e, e, e, e, e)
serverOptions := []pilosa.ServerOption{
@ -427,14 +409,12 @@ func (m *Command) SetupServer() error {
pilosa.OptServerURI(advertiseURI),
pilosa.OptServerGRPCURI(advertiseGRPCURI),
pilosa.OptServerInternalClient(http.NewInternalClientFromURI(uri, c)),
pilosa.OptServerClusterDisabled(m.Config.Cluster.Disabled, m.Config.Cluster.Hosts),
pilosa.OptServerClusterName(m.Config.Cluster.Name),
pilosa.OptServerSerializer(proto.Serializer{}),
pilosa.OptServerStorageConfig(m.Config.Storage),
pilosa.OptServerRowcacheOn(m.Config.RowcacheOn),
pilosa.OptServerRBFConfig(m.Config.RBFConfig),
pilosa.OptServerQueryHistoryLength(m.Config.QueryHistoryLength),
coordinatorOpt,
discoOpt,
}
@ -477,39 +457,6 @@ func (m *Command) SetupServer() error {
return errors.Wrap(err, "new handler")
}
// setupNetworking sets up internode communication based on the configuration.
func (m *Command) setupNetworking() error {
if m.Config.Cluster.Disabled {
return nil
}
gossipPort, err := strconv.Atoi(m.Config.Gossip.Port)
if err != nil {
return errors.Wrap(err, "parsing port")
}
// get the host portion of addr to use for binding
gossipHost := m.listenURI.Host
m.gossipTransport, err = gossip.NewTransport(gossipHost, gossipPort, m.logger.Logger())
if err != nil {
return errors.Wrap(err, "getting transport")
}
gossipMemberSet, err := gossip.NewMemberSet(
m.Config.Gossip,
m.API,
gossip.WithLogOutput(&filteredWriter{logOutput: m.logOutput, v: m.Config.Verbose}),
gossip.WithPilosaLogger(m.logger),
gossip.WithTransport(m.gossipTransport),
)
if err != nil {
return errors.Wrap(err, "getting memberset")
}
m.gossipMemberSet = gossipMemberSet
return errors.Wrap(gossipMemberSet.Open(), "opening gossip memberset")
}
// setupLogger sets up the logger based on the configuration.
func (m *Command) setupLogger() error {
var f *logger.FileWriter
@ -551,30 +498,18 @@ func (m *Command) setupLogger() error {
return nil
}
// GossipTransport allows a caller to return the gossip transport created when
// setting up the GossipMemberSet. This is useful if one needs to determine the
// allocated ephemeral port programmatically. (usually used in tests)
func (m *Command) GossipTransport() *gossip.Transport {
return m.gossipTransport
}
// Close shuts down the server.
func (m *Command) Close() error {
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 {
@ -583,11 +518,13 @@ func (m *Command) Close() error {
}
// prevent the closed sockets from being re-injected into etcd.
m.Config.DisCo.LPeerSocket = nil
m.Config.DisCo.LClientSocket = nil
m.Config.Etcd.LPeerSocket = nil
m.Config.Etcd.LClientSocket = nil
err := eg.Wait()
_ = testhook.Closed(pilosa.NewAuditor(), m, nil)
close(m.done)
return errors.Wrap(err, "closing everything")
}
}
@ -629,27 +566,6 @@ func getListener(uri pnet.URI, tlsconf *tls.Config) (ln net.Listener, err error)
return ln, nil
}
type filteredWriter struct {
v bool
logOutput io.Writer
}
// Write forwards the write to logOutput if verbose is true, or it doesn't
// contain [DEBUG] or [INFO]. This implementation isn't technically correct
// since Write could be called with only part of a log line, but I don't think
// that actually happens, so until it becomes a problem, I don't think it's
// worth dealing with the extra complexity. (jaffee)
func (f *filteredWriter) Write(p []byte) (n int, err error) {
if bytes.Contains(p, []byte("[DEBUG]")) || bytes.Contains(p, []byte("[INFO]")) {
if f.v {
return f.logOutput.Write(p)
}
} else {
return f.logOutput.Write(p)
}
return len(p), nil
}
// ParseConfig parses s into a Config.
func ParseConfig(s string) (Config, error) {
var c Config

View file

@ -26,12 +26,12 @@ import (
"os"
"reflect"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/pilosa/pilosa/v2"
"github.com/pilosa/pilosa/v2/disco"
"github.com/pilosa/pilosa/v2/http"
"github.com/pilosa/pilosa/v2/pql"
"github.com/pilosa/pilosa/v2/roaring"
@ -107,6 +107,10 @@ func TestMain_Set_Quick(t *testing.T) {
t.Fatal(err)
}
if err := m.AwaitState(string(pilosa.ClusterStateNormal), 10*time.Second); err != nil {
t.Fatalf("restarting cluster: %v", err)
}
// Validate data after reopening.
for field, fieldSet := range SetCommands(cmds).Fields() {
for id, columnIDs := range fieldSet {
@ -186,6 +190,10 @@ func TestMain_SetRowAttrs(t *testing.T) {
t.Fatal(err)
}
if err := m.AwaitState(string(pilosa.ClusterStateNormal), 10*time.Second); err != nil {
t.Fatalf("restarting cluster: %v", err)
}
// Query rows after reopening.
if res, err := m.Query(t, "i", "columnAttrs=true", `Row(x=1)`); err != nil {
t.Fatal(err)
@ -242,6 +250,10 @@ func TestMain_SetColumnAttrs(t *testing.T) {
t.Fatal(err)
}
if err := m.AwaitState(string(pilosa.ClusterStateNormal), 10*time.Second); err != nil {
t.Fatalf("restarting cluster: %v", err)
}
// Query row after reopening.
if res, err := m.Query(t, "i", "columnAttrs=true", `Row(x=1)`); err != nil {
t.Fatal(err)
@ -358,12 +370,13 @@ func TestConcurrentFieldCreation(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
node0 := cluster.GetNode(0)
err := node0.AwaitState(string(pilosa.ClusterStateNormal), 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
api0 := cluster.GetNode(0).API
api0 := node0.API
if _, err := api0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil {
t.Fatalf("creating index: %v", err)
}
@ -509,7 +522,7 @@ func TestTransactionsAPI(t *testing.T) {
// LATER, test deadline extension on non-coordinator blocks active, exclusive transaction being returned
}
func TestMain_RecalculateHashes(t *testing.T) {
func TestMain_RecalculateCaches(t *testing.T) {
const clusterSize = 5
cluster := test.MustRunCluster(t, clusterSize)
defer cluster.Close()
@ -630,7 +643,7 @@ func TestClusteringNodesReplica1(t *testing.T) {
cluster := test.MustRunCluster(t, 3)
defer cluster.Close()
if err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond); err != nil {
if err := cluster.GetNode(0).AwaitState(string(disco.ClusterStateNormal), 100*time.Millisecond); err != nil {
t.Fatalf("starting cluster: %v", err)
}
@ -638,18 +651,21 @@ func TestClusteringNodesReplica1(t *testing.T) {
t.Fatalf("closing third node: %v", err)
}
if err := cluster.AwaitCoordinatorState(pilosa.ClusterStateStarting, 30*time.Second); err != nil {
if err := cluster.GetCoordinator().AwaitState(string(disco.ClusterStateDown), 30*time.Second); err != nil {
t.Fatalf("starting cluster: %v", err)
}
// confirm that cluster stops accepting queries after one node closes
if _, err := cluster.GetCoordinator().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 DOWN") {
t.Fatalf("got unexpected error querying an incomplete cluster: %v", err)
}
}
func TestClusteringNodesReplica2(t *testing.T) {
cluster := test.MustNewCluster(t, 3)
// Because this test shuts down 2 nodes, it needs to start as a 5-node
// cluster in order to retain enough available nodes for raft leader
// election.
cluster := test.MustNewCluster(t, 5)
for _, c := range cluster.Nodes {
c.Config.Cluster.ReplicaN = 2
}
@ -659,43 +675,43 @@ func TestClusteringNodesReplica2(t *testing.T) {
}
defer cluster.Close()
err = cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
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, 30*time.Second)
err = coord.AwaitState(string(disco.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 := coord.API.CreateIndex(context.Background(), "anewindex", pilosa.IndexOptions{}); err != nil {
t.Fatalf("got unexpected error creating index: %v", err)
}
// We no longer support mutations or schema changes when the cluster is in
// state DEGRADED, so this test doesn't apply anymore.
//
// // confirm that cluster keeps accepting queries if replication > 1
// 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 := others[1].Close(); err != nil {
t.Fatalf("closing 2nd node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateStarting, 30*time.Second)
err = coord.AwaitState(string(pilosa.ClusterStateDown), 30*time.Second)
if err != nil {
t.Fatalf("after closing second server: %v", err)
}
if _, err := coord.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 DOWN") {
t.Fatalf("got unexpected error querying an incomplete cluster: %v", err)
}
}
func TestRemoveNodeAfterItDies(t *testing.T) {
t.Skip("TestRemoveNodeAfterItDies won't be supported unless we implement resizer.")
cluster := test.MustNewCluster(t, 3)
for _, c := range cluster.Nodes {
c.Config.Cluster.ReplicaN = 2
@ -712,19 +728,20 @@ func TestRemoveNodeAfterItDies(t *testing.T) {
cluster.Close()
}()
err = cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
coord, others := cluster.GetCoordinator(), cluster.GetNonCoordinators()
err = coord.AwaitState(string(pilosa.ClusterStateNormal), 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
coord, others := cluster.GetCoordinator(), cluster.GetNonCoordinators()
// prevent double-closing cluster.GetNode(2) from the deferred Close above
disabled := others[0]
if err := disabled.Close(); err != nil {
t.Fatalf("closing third node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateDegraded, 30*time.Second)
err = coord.AwaitState(string(pilosa.ClusterStateDegraded), 30*time.Second)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
@ -733,7 +750,7 @@ func TestRemoveNodeAfterItDies(t *testing.T) {
t.Fatalf("removing failed node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateNormal, 30*time.Second)
err = coord.AwaitState(string(pilosa.ClusterStateNormal), 30*time.Second)
if err != nil {
t.Fatalf("removing disabled node: %v", err)
}
@ -756,27 +773,28 @@ func TestRemoveConcurrentIndexCreation(t *testing.T) {
}
defer cluster.Close()
err = cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
node0 := cluster.GetNode(0)
err = node0.AwaitState(string(pilosa.ClusterStateNormal), 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
errc := make(chan error)
go func() {
_, err := cluster.GetNode(0).API.CreateIndex(context.Background(), "blah", pilosa.IndexOptions{})
_, err := node0.API.CreateIndex(context.Background(), "blah", pilosa.IndexOptions{})
errc <- err
}()
if _, err := cluster.GetNode(0).API.RemoveNode(cluster.GetNode(2).API.Node().ID); err != nil {
if _, err := node0.API.RemoveNode(cluster.GetNode(2).API.Node().ID); err != nil {
t.Fatalf("removing node: %v", err)
}
err = cluster.AwaitCoordinatorState(pilosa.ClusterStateNormal, 100*time.Millisecond)
err = cluster.GetCoordinator().AwaitState(string(pilosa.ClusterStateNormal), 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
hosts := cluster.GetNode(0).API.Hosts(context.Background())
hosts := node0.API.Hosts(context.Background())
if len(hosts) != 2 {
t.Fatalf("unexpected hosts: %v", hosts)
}
@ -903,7 +921,7 @@ func TestClusterQueriesAfterRestart(t *testing.T) {
defer cluster.Close()
cmd1 := cluster.GetNode(1)
err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
err := cmd1.AwaitState(string(pilosa.ClusterStateNormal), 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
@ -946,7 +964,7 @@ func TestClusterQueriesAfterRestart(t *testing.T) {
err = cmd1.Command.Close()
if err != nil {
t.Fatalf("closing node0: %v", err)
t.Fatalf("closing node1: %v", err)
}
// confirm that cluster stops accepting queries after one node closes
@ -958,16 +976,18 @@ func TestClusterQueriesAfterRestart(t *testing.T) {
config := cmd1.Command.Config
config.Bind = cmd1.API.Node().URI.HostPort()
// this isn't necessary, but makes the test run way faster
config.Gossip.Port = strconv.Itoa(int(cmd1.Command.GossipTransport().URI.Port))
cmd1.Command = server.NewCommand(cmd1.Stdin, cmd1.Stdout, cmd1.Stderr, server.OptCommandServerOptions(pilosa.OptServerOpenTranslateStore(pilosa.OpenInMemTranslateStore)))
cmd1.Command.Config = config
err = cmd1.Start()
if err != nil {
t.Fatalf("reopening node 0: %v", err)
t.Fatalf("reopening node 1: %v", err)
}
for cmd1.API.State() != pilosa.ClusterStateNormal {
state1, err1 := cmd1.API.State()
if err1 != nil {
t.Fatalf("getting state foor node 1: %v", err)
}
for state1 != string(pilosa.ClusterStateNormal) {
time.Sleep(time.Millisecond)
}
@ -1196,20 +1216,15 @@ func TestClusterCreatedAtRace(t *testing.T) {
cluster := test.MustRunCluster(t, 4)
defer cluster.Close()
err := cluster.AwaitState(pilosa.ClusterStateNormal, 100*time.Millisecond)
if err != nil {
t.Fatalf("starting cluster: %v", err)
}
for _, com := range cluster.Nodes {
nodes := com.API.Hosts(context.Background())
for _, n := range nodes {
if n.State != "READY" {
t.Fatalf("unexpected node state after upping cluster: %v", nodes) // server_test.go:1245: unexpected node state after upping cluster: [Node:http://localhost:43075:READY:TestClusterCreatedAtRace/run-0__0 Node:http://localhost:42301:READY:TestClusterCreatedAtRace/run-0__1 Node:http://localhost:42031:DOWN:TestClusterCreatedAtRace/run-0__2 Node:http://localhost:43671:READY:TestClusterCreatedAtRace/run-0__3]
if n.State != string(disco.NodeStateStarted) {
t.Fatalf("unexpected node state (%s) after upping cluster: %v", n.State, nodes)
}
}
}
_, err = cluster.Nodes[0].API.CreateIndex(context.Background(), "anindex", pilosa.IndexOptions{})
_, err := cluster.Nodes[0].API.CreateIndex(context.Background(), "anindex", pilosa.IndexOptions{})
if err != nil && errors.Cause(err).Error() != pilosa.ErrIndexExists.Error() {
t.Fatal(err)
}
@ -1235,7 +1250,12 @@ func TestClusterCreatedAtRace(t *testing.T) {
schemas := make([]*pilosa.IndexInfo, len(cluster.Nodes))
for i, cmd := range cluster.Nodes {
schemas[i] = cmd.API.Schema(context.Background())[0]
s, err := cmd.API.Schema(context.Background())
if err != nil {
t.Fatalf("getting schema: %v", err)
}
schemas[i] = s[0]
}
createdAtField := schemas[0].Fields[0].CreatedAt

View file

@ -54,7 +54,10 @@ func (s *ShowHandler) Handle(ctx context.Context, mapped *MappedSQL) (pproto.ToR
}
func (s *ShowHandler) execShowTables(ctx context.Context, showStmt *sqlparser.Show) (pproto.ToRowser, error) {
indexInfo := s.api.Schema(ctx)
indexInfo, err := s.api.Schema(ctx)
if err != nil {
return nil, errors.Wrap(err, "getting schema")
}
result := make(pproto.ConstRowser, len(indexInfo))
for i, ii := range indexInfo {

View file

@ -17,12 +17,9 @@ package test
import (
"context"
"fmt"
"io/ioutil"
"math"
"net"
"path"
"sort"
"strconv"
"strings"
"testing"
"time"
@ -140,7 +137,7 @@ func (c *Cluster) GetNode(n int) *Command {
// need to act on the coordinator.
func (c *Cluster) GetCoordinator() *Command {
for _, n := range c.Nodes {
if n.IsCoordinator() {
if n.IsPrimary() {
return n
}
}
@ -150,7 +147,7 @@ func (c *Cluster) GetCoordinator() *Command {
// 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() {
if !n.IsPrimary() {
return n
}
}
@ -161,7 +158,7 @@ func (c *Cluster) GetNonCoordinator() *Command {
func (c *Cluster) GetNonCoordinators() []*Command {
rtn := make([]*Command, 0)
for _, n := range c.Nodes {
if !n.IsCoordinator() {
if !n.IsPrimary() {
rtn = append(rtn, n)
}
}
@ -404,43 +401,26 @@ func (c *Cluster) Start() error {
}()
portsCfg := GenPortsConfig(sliceOfPorts)
var gossipSeeds []string
for i, cc := range c.Nodes {
i := i
// get the bind uri to use as the host portion of the gossip seed.
uri, err := pilosa.AddressWithDefaults(cc.Config.Bind)
if err != nil {
return errors.Wrap(err, "processing bind address")
}
cc.Config.Gossip.Port = portsCfg[i].Gossip.Port
gossipHost := uri.Host
gossipPort := cc.Config.Gossip.Port
gossipSeeds = append(gossipSeeds, fmt.Sprintf("%s:%s", gossipHost, gossipPort))
}
for i, cc := range c.Nodes {
cc := cc
cc.Config.DisCo = portsCfg[i].DisCo
cc.Config.Etcd = portsCfg[i].Etcd
cc.Config.Name = portsCfg[i].Name
cc.Config.Cluster.Name = portsCfg[i].Cluster.Name
cc.Config.BindGRPC = portsCfg[i].BindGRPC
eg.Go(func() error {
fmt.Printf("DISCO CONFIG: %+v\n", cc.Config.DisCo)
cc.Config.Gossip.Seeds = gossipSeeds
return cc.Start()
})
}
return eg.Wait()
}, 4*len(c.Nodes), 10)
}, 3*len(c.Nodes), 10)
if err != nil {
return err
}
return c.AwaitState(pilosa.ClusterStateNormal, 30*time.Second)
return c.GetNode(0).AwaitState(string(pilosa.ClusterStateNormal), 30*time.Second)
}
// Close stops a Cluster
@ -455,7 +435,7 @@ func (c *Cluster) Close() error {
func (c *Cluster) CloseAndRemoveNonCoordinator() error {
for i, n := range c.Nodes {
if !n.IsCoordinator() {
if !n.IsPrimary() {
return c.CloseAndRemove(i)
}
}
@ -472,47 +452,6 @@ func (c *Cluster) CloseAndRemove(n int) error {
return err
}
// AwaitState waits for the cluster coordinator (assumed to be the first
// node) to reach a specified state.
func (c *Cluster) AwaitCoordinatorState(expectedState string, timeout time.Duration) error {
if len(c.Nodes) < 1 {
return errors.New("can't await coordinator state on an empty cluster")
}
onlyCoordinator := &Cluster{Nodes: []*Command{c.GetCoordinator()}}
return onlyCoordinator.AwaitState(expectedState, timeout)
}
// ExceptionalState returns an error if any node in the cluster is not
// in the expected state.
func (c *Cluster) ExceptionalState(expectedState string) error {
for _, node := range c.Nodes {
state := node.API.State()
if state != expectedState {
return fmt.Errorf("node %q: state %s", node.ID(), state)
}
}
return nil
}
// AwaitState waits for the whole cluster to reach a specified state.
func (c *Cluster) AwaitState(expectedState string, timeout time.Duration) (err error) {
if len(c.Nodes) < 1 {
return errors.New("can't await state of an empty cluster")
}
startTime := time.Now()
var elapsed time.Duration
for elapsed = 0; elapsed <= timeout; elapsed = time.Since(startTime) {
// Counterintuitive: We're returning if the err *is* nil,
// meaning we've reached the expected state.
if err = c.ExceptionalState(expectedState); err == nil {
return err
}
time.Sleep(1 * time.Millisecond)
}
return fmt.Errorf("waited %v for cluster to reach state %q: %v",
elapsed, expectedState, err)
}
// MustNewCluster creates a new cluster. If opts contains only one
// slice of command options, those options are used with every node.
// If it is empty, default options are used. Otherwise, it must contain size
@ -536,7 +475,12 @@ func MustNewCluster(tb testing.TB, size int, opts ...[]server.CommandOption) *Cl
// receives a matching state. It polls up to n times before returning.
func CheckClusterState(m *Command, state string, n int) bool {
for i := 0; i < n; i++ {
if m.API.State() == state {
apiState, err := m.API.State()
if err != nil {
return false
}
if apiState == state {
return true
}
time.Sleep(10 * time.Millisecond)
@ -555,17 +499,12 @@ func newCluster(tb testing.TB, size int, opts ...[]server.CommandOption) (*Clust
}
cluster := &Cluster{Nodes: make([]*Command, size)}
name := tb.Name()
for i := 0; i < size; i++ {
var commandOpts []server.CommandOption
if len(opts) > 0 {
commandOpts = opts[i%len(opts)]
}
m := NewCommandNode(tb, i == 0, commandOpts...)
err := ioutil.WriteFile(path.Join(m.Config.DataDir, ".id"), []byte(name+"__"+strconv.Itoa(i)), 0600)
if err != nil {
return nil, errors.Wrap(err, "writing node id")
}
m := NewCommandNode(tb, commandOpts...)
cluster.Nodes[i] = m
}

View file

@ -22,7 +22,6 @@ import (
"time"
"github.com/pilosa/pilosa/v2/etcd"
"github.com/pilosa/pilosa/v2/gossip"
"github.com/pilosa/pilosa/v2/server"
)
@ -33,8 +32,7 @@ type Ports struct {
LsnP *net.TCPListener
PortP int
Grpc int
Gossip int //TODO remove
Grpc int
}
func (ports *Ports) Close() error {
@ -52,6 +50,7 @@ func GenPortsConfig(ports []Ports) []*server.Config {
clusterURLs := make([]string, len(ports))
for i := range cfgs {
name := fmt.Sprintf("server%d", i)
clusterName := "cluster-abc123"
lsnC, portC := ports[i].LsnC, ports[i].PortC
lClientURL := fmt.Sprintf("http://localhost:%d", portC)
@ -59,19 +58,15 @@ func GenPortsConfig(ports []Ports) []*server.Config {
lPeerURL := fmt.Sprintf("http://localhost:%d", portP)
discoDir := ""
if d, err := ioutil.TempDir("/tmp", "disco."); err == nil {
if d, err := ioutil.TempDir("", "disco."); err == nil {
discoDir = d
}
cfgs[i] = &server.Config{
Gossip: gossip.Config{
Port: fmt.Sprint(ports[i].Gossip),
},
Name: name,
BindGRPC: fmt.Sprintf(":%d", ports[i].Grpc),
DisCo: etcd.Options{
Name: name,
Etcd: etcd.Options{
Dir: discoDir,
ClusterName: "bartholemuuuuu",
LClientURL: lClientURL,
AClientURL: lClientURL,
LPeerURL: lPeerURL,
@ -81,13 +76,12 @@ func GenPortsConfig(ports []Ports) []*server.Config {
LClientSocket: []*net.TCPListener{lsnC},
},
}
cfgs[i].Cluster.Name = clusterName
clusterURLs[i] = fmt.Sprintf("%s=%s", name, lPeerURL)
fmt.Printf("\ndebug test/disco.go: on i=%v, GenPortsConfig Gossip: %v, DisCo.Client: %v, DisCo.Peer: %v, BindGRPC: %v\n",
i, ports[i].Gossip, portC, portP, ports[i].Grpc)
}
for i := range cfgs {
cfgs[i].DisCo.InitCluster = strings.Join(clusterURLs, ",")
cfgs[i].Etcd.InitCluster = strings.Join(clusterURLs, ",")
}
return cfgs
@ -102,20 +96,18 @@ func NewPorts(lsn []*net.TCPListener) []Ports {
ports[i] = lsn[i].Addr().(*net.TCPAddr).Port
}
for i := 0; i < n; i = i + 4 {
for i := 0; i < n; i = i + 3 {
out = append(out, Ports{
LsnC: lsn[i],
PortC: ports[i],
LsnP: lsn[i+1],
PortP: ports[i+1],
Grpc: ports[i+2],
Gossip: ports[i+3],
Grpc: ports[i+2],
})
// make Grpc and Gossip ports available to
// make Grpc port available to
// be rebound.
lsn[i+2].Close()
lsn[i+3].Close()
}
return out

View file

@ -90,14 +90,12 @@ func newCommand(tb testing.TB, opts ...server.CommandOption) *Command {
}
// NewCommandNode returns a new instance of Command with clustering enabled.
func NewCommandNode(tb testing.TB, isCoordinator bool, opts ...server.CommandOption) *Command {
func NewCommandNode(tb testing.TB, opts ...server.CommandOption) *Command {
// We want tests to default to using the in-memory translate store, so we
// prepend opts with that functional option. If a different translate store
// has been specified, it will override this one.
opts = prependTestServerOpts(opts)
m := newCommand(tb, opts...)
m.Config.Cluster.Disabled = false
m.Config.Cluster.Coordinator = isCoordinator
return m
}
@ -110,13 +108,6 @@ func RunCommand(t *testing.T) *Command {
return MustRunCluster(t, 1).GetNode(0)
}
// GossipAddress returns the address on which gossip is listening after a Main
// has been setup. Useful to pass as a seed to other nodes when creating and
// testing clusters.
func (m *Command) GossipAddress() string {
return m.GossipTransport().URI.String()
}
// Close closes the program and removes the underlying data directory.
func (m *Command) Close() error {
// leave the removing part to the test logic. Some tests are closing and opening again the command
@ -192,8 +183,14 @@ 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 }
// IsPrimary returns true if this is the primary.
func (m *Command) IsPrimary() bool {
coord := m.API.PrimaryNode()
if coord == nil {
return false
}
return coord.ID == m.API.Node().ID
}
// Client returns a client to connect to the program.
func (m *Command) Client() *http.InternalClient {
@ -389,3 +386,28 @@ func RetryUntil(timeout time.Duration, fn func() error) (err error) {
}
}
}
// AwaitState waits for the whole cluster to reach a specified state.
func (m *Command) AwaitState(expectedState string, timeout time.Duration) (err error) {
startTime := time.Now()
var elapsed time.Duration
for elapsed = 0; elapsed <= timeout; elapsed = time.Since(startTime) {
// Counterintuitive: We're returning if the err *is* nil,
// meaning we've reached the expected state.
if err = m.exceptionalState(expectedState); err == nil {
return err
}
time.Sleep(1 * time.Millisecond)
}
return fmt.Errorf("waited %v for command to reach state %q: %v",
elapsed, expectedState, err)
}
// exceptionalState returns an error if the node is not in the expected state.
func (m *Command) exceptionalState(expectedState string) error {
state, err := m.API.State()
if err != nil || state != expectedState {
return fmt.Errorf("node %q: state %s: err %v", m.ID(), state, err)
}
return nil
}

View file

@ -77,7 +77,7 @@ func TestNewCluster(t *testing.T) {
t.Fatalf("wrong number of nodes in status: %s", bytes)
}
if body.State != pilosa.ClusterStateNormal {
if body.State != string(pilosa.ClusterStateNormal) {
t.Fatalf("cluster state should be %s but is %s", pilosa.ClusterStateNormal, body.State)
}
}
@ -85,7 +85,7 @@ func TestNewCluster(t *testing.T) {
func getCoordinator(m *test.Command) string {
hosts := m.API.Hosts(context.Background())
for _, host := range hosts {
if host.IsCoordinator {
if host.IsPrimary {
return host.ID
}
}

View file

@ -16,26 +16,17 @@ package topology
import (
"fmt"
"sync"
"github.com/pilosa/pilosa/v2/net"
)
// Node represents a node in the cluster.
type Node struct {
Mu sync.Mutex
ID string `json:"id"`
URI net.URI `json:"uri"`
GRPCURI net.URI `json:"grpc-uri"`
IsCoordinator bool `json:"isCoordinator"`
State string `json:"state"`
}
func (n *Node) ProtectedClone() *Node {
n.Mu.Lock()
defer n.Mu.Unlock()
return n.Clone()
ID string `json:"id"`
URI net.URI `json:"uri"`
GRPCURI net.URI `json:"grpc-uri"`
IsPrimary bool `json:"isPrimary"`
State string `json:"state"`
}
func (n *Node) Clone() *Node {
@ -46,13 +37,13 @@ func (n *Node) Clone() *Node {
other.ID = n.ID
other.URI = n.URI
other.GRPCURI = n.GRPCURI
other.IsCoordinator = n.IsCoordinator
other.IsPrimary = n.IsPrimary
other.State = n.State
return &other
}
func (n *Node) String() string {
return fmt.Sprintf("Node:%s:%s:%s", n.URI, n.State, n.ID)
return fmt.Sprintf("Node:%s:%s:%s(%v)", n.URI, n.State, n.ID, n.IsPrimary)
}
// Nodes represents a list of nodes.

View file

@ -22,6 +22,7 @@ import (
// nodes in a cluster can be maintained outside of the cluster struct.
type Noder interface {
Nodes() []*Node // Remember: this has to be sorted correctly!!
PrimaryNodeID(hasher Hasher) string
SetNodes([]*Node)
AppendNode(*Node)
RemoveNode(nodeID string) bool
@ -41,11 +42,45 @@ func NewLocalNoder(nodes []*Node) *localNoder {
}
}
// NewEmptyLocalNoder is an empty Noder used for testing.
func NewEmptyLocalNoder() *localNoder {
return &localNoder{}
}
// NewIDNoder is a helper function for wrapping an existing slice of Node IDs
// with something which implements Noder.
func NewIDNoder(ids []string) *localNoder {
nodes := make([]*Node, len(ids))
for i, id := range ids {
node := &Node{
ID: id,
}
nodes[i] = node
}
// Nodes must be sorted.
sort.Sort(ByID(nodes))
return &localNoder{
nodes: nodes,
}
}
// Nodes implements the Noder interface.
func (n *localNoder) Nodes() []*Node {
return n.nodes
}
// PrimaryNodeID implements the Noder interface.
func (n *localNoder) PrimaryNodeID(hasher Hasher) string {
snap := NewClusterSnapshot(NewLocalNoder(n.nodes), hasher, 1)
primaryNode := snap.PrimaryFieldTranslationNode()
if primaryNode == nil {
return ""
}
return primaryNode.ID
}
// SetNodes implements the Noder interface.
func (n *localNoder) SetNodes(nodes []*Node) {
n.nodes = nodes

View file

@ -139,25 +139,13 @@ 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)
for _, n := range c.Nodes {
if n.IsCoordinator {
return n
}
}
return nil
return c.PrimaryPartitionNode(0)
}
// 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
for i := range c.Nodes {
if c.Nodes[i].ID == nodeID && c.Nodes[i].IsCoordinator {
return true
}
}
return false
return c.PrimaryFieldTranslationNode().ID == nodeID
}
// PrimaryPartitionNode returns the primary node of the given partition.
@ -292,3 +280,15 @@ func NodePositionByID(nodes []*Node, nodeID string) int {
}
return -1
}
// PrimaryNodeID returns the ID of the primary node, given a list of node IDs
// and a hasher. The order of the node IDs provided does not matter because this
// function will re-order them in a deterministic way.
func PrimaryNodeID(nodeIDs []string, hasher Hasher) string {
snap := NewClusterSnapshot(NewIDNoder(nodeIDs), hasher, 1)
primaryNode := snap.PrimaryFieldTranslationNode()
if primaryNode == nil {
return ""
}
return primaryNode.ID
}

View file

@ -204,28 +204,24 @@ func TestTranslation_Reset(t *testing.T) {
c := test.MustRunCluster(t, 4,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(true),
pilosa.OptServerNodeID("2node0"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("4node1"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("3node2"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("1node3"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
@ -267,17 +263,12 @@ func TestTranslation_Reset(t *testing.T) {
if err := node0.SoftOpen(); err != nil {
t.Fatal(err)
}
gossipSeeds := []string{node0.GossipAddress()}
node1.Config.Gossip.Seeds = gossipSeeds
if err := node1.SoftOpen(); err != nil {
t.Fatal(err)
}
node2.Config.Gossip.Seeds = gossipSeeds
if err := node2.SoftOpen(); err != nil {
t.Fatal(err)
}
node3.Config.Gossip.Seeds = gossipSeeds
if err := node3.SoftOpen(); err != nil {
t.Fatal(err)
}
@ -304,28 +295,24 @@ func TestTranslation_KeyNotFound(t *testing.T) {
c := test.MustRunCluster(t, 4,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(true),
pilosa.OptServerNodeID("node0"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node1"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node2"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node3"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
@ -458,24 +445,22 @@ func TestInMemTranslateStore_ReadKey(t *testing.T) {
// Test index key translation replication under node failure.
func TestTranslation_Replication(t *testing.T) {
t.Run("Replication", func(t *testing.T) {
t.Skip("this test is fragile and doesn't work with randomly ordered nodes. it also seems to assume failover for index key partitions, which does not exist")
c := test.MustRunCluster(t, 3,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(true),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(2),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(2),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
pilosa.OptServerReplicaN(2),
@ -513,10 +498,14 @@ func TestTranslation_Replication(t *testing.T) {
exp := `{"results":[{"attrs":{},"columns":[],"keys":["x1","x2"]}]}`
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())
coordState, err := coord.API.State()
if err != nil || !test.CheckClusterState(coord, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected coord cluster state: %s, got: %s, err: %v", pilosa.ClusterStateNormal, coordState, err)
}
otherState, err := other.API.State()
if err != nil || !test.CheckClusterState(other, string(pilosa.ClusterStateNormal), 1000) {
t.Fatalf("unexpected other cluster state: %s, got: %s, err: %v", pilosa.ClusterStateNormal, otherState, err)
}
// Verify the data exists
@ -527,6 +516,11 @@ func TestTranslation_Replication(t *testing.T) {
t.Fatal(err)
}
coordState, err = coord.API.State()
if err != nil || !test.CheckClusterState(coord, string(pilosa.ClusterStateDegraded), 1000) {
t.Fatalf("unexpected coord cluster state: %s, got: %s", pilosa.ClusterStateDegraded, coordState)
}
// Verify the data exists with one node down
coord.QueryExpect(t, idx, "", `Row(f=1)`, exp)
})
@ -542,14 +536,12 @@ func TestTranslation_Coordinator(t *testing.T) {
c := test.MustRunCluster(t, 2,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(true),
pilosa.OptServerNodeID("node0"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node1"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
@ -614,28 +606,24 @@ func TestTranslation_TranslateIDsOnCluster(t *testing.T) {
c := test.MustRunCluster(t, 4,
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(true),
pilosa.OptServerNodeID("node0"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node1"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node2"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),
)},
[]server.CommandOption{
server.OptCommandServerOptions(
pilosa.OptServerIsCoordinator(false),
pilosa.OptServerNodeID("node3"),
pilosa.OptServerOpenTranslateStore(boltdb.OpenTranslateStore),
pilosa.OptServerOpenTranslateReader(http.GetOpenTranslateReaderFunc(nil)),

View file

@ -75,16 +75,15 @@ func NewTestCluster(tb testing.TB, n int) *cluster {
c.Topology = NewTopology(c.Hasher, c.partitionN, c.ReplicaN, c)
for i := 0; i < n; i++ {
c.nodes = append(c.nodes, &topology.Node{
c.noder.AppendNode(&topology.Node{
ID: fmt.Sprintf("node%d", i),
URI: NewTestURI("http", fmt.Sprintf("host%d", i), uint16(0)),
})
}
c.Node = c.nodes[0]
c.Coordinator = c.nodes[0].ID
c.SetState(ClusterStateNormal)
cNodes := c.noder.Nodes()
c.Node = cNodes[0]
return c
}
@ -212,35 +211,6 @@ func (t *ClusterCluster) clusterByID(id string) *cluster {
// addNode adds a node to the cluster and (potentially) starts a resize job.
func (t *ClusterCluster) addNode() error {
id := len(t.Clusters)
c, err := t.addCluster(id, false)
if err != nil {
return err
}
// Send NodeJoin event to coordinator.
if id > 0 {
coord := t.Clusters[0]
ev := &NodeEvent{
Event: NodeJoin,
Node: c.Node,
}
if err := coord.ReceiveEvent(ev); err != nil {
return err
}
// Wait for the AddNode job to finish.
if c.State() != ClusterStateNormal {
t.resizeDone = make(chan struct{})
t.mu.Lock()
t.resizing = true
t.mu.Unlock()
<-t.resizeDone
}
}
return nil
}
@ -259,9 +229,8 @@ func (t *ClusterCluster) addCluster(i int, saveTopology bool) (*cluster, error)
uri := NewTestURI("http", fmt.Sprintf("host%d", i), uint16(0))
node := &topology.Node{
ID: id,
URI: uri,
IsCoordinator: i == 0,
ID: id,
URI: uri,
}
// add URI to common
@ -289,13 +258,13 @@ func (t *ClusterCluster) addCluster(i int, saveTopology bool) (*cluster, error)
c.Topology = NewTopology(c.Hasher, c.partitionN, c.ReplicaN, c)
c.holder = h
c.Node = node
c.Coordinator = t.common.Nodes[0].ID // the first node is the coordinator
// c.Coordinator = t.common.Nodes[0].ID // the first node is the coordinator
c.broadcaster = t.broadcaster(c)
// add nodes
if saveTopology {
for _, n := range t.common.Nodes {
if err := c.addNode(n); err != nil {
if err := c.addNode(n.ID); err != nil {
return nil, err
}
}
@ -325,13 +294,6 @@ func NewClusterCluster(tb testing.TB, n int) *ClusterCluster {
return tc
}
// SetState sets the state of the cluster on each node.
func (t *ClusterCluster) SetState(state string) {
for _, c := range t.Clusters {
c.SetState(state)
}
}
// Open opens all clusters in the test cluster.
func (t *ClusterCluster) Open() error {
for _, c := range t.Clusters {
@ -341,17 +303,7 @@ func (t *ClusterCluster) Open() error {
if err := c.holder.Open(); err != nil {
return err
}
if err := c.setNodeState(nodeStateReady); err != nil {
return err
}
}
// Start the listener on the coordinator.
if len(t.Clusters) == 0 {
return nil
}
t.Clusters[0].listenForJoins()
return nil
}
@ -377,17 +329,8 @@ type bcast struct {
func (b bcast) SendSync(m Message) error {
switch obj := m.(type) {
case *ClusterStatus:
// Apply the send message to all nodes (except the coordinator).
for _, c := range b.t.Clusters {
if c != b.c {
err := c.mergeClusterStatus(obj)
if err != nil {
return err
}
}
}
b.t.mu.RLock()
if obj.State == ClusterStateNormal && b.t.resizing {
if obj.State == string(ClusterStateNormal) && b.t.resizing {
close(b.t.resizeDone)
}
b.t.mu.RUnlock()
@ -415,23 +358,9 @@ func (b bcast) SendTo(to *topology.Node, m Message) error {
if err != nil {
return err
}
case *ResizeInstructionComplete:
coord := b.t.clusterByID(to.ID)
// this used to be async, but that prevented us from checking
// its error status...
return coord.markResizeInstructionComplete(obj)
case *ClusterStatus:
// Apply the send message to the node.
for _, c := range b.t.Clusters {
if c.Node.ID == to.ID {
err := c.mergeClusterStatus(obj)
if err != nil {
return err
}
}
}
b.t.mu.RLock()
if obj.State == ClusterStateNormal && b.t.resizing {
if obj.State == string(ClusterStateNormal) && b.t.resizing {
close(b.t.resizeDone)
}
b.t.mu.RUnlock()
@ -526,7 +455,7 @@ func (t *ClusterCluster) FollowResizeInstruction(instr *ResizeInstruction) error
complete.Error = err.Error()
}
node := instr.Coordinator
node := instr.Primary
return bcast{t: t}.SendTo(node, complete)
}
@ -553,16 +482,16 @@ func NewTestClusterWithReplication(tb testing.TB, nNodes, nReplicas, partitionN
for i := 0; i < nNodes; i++ {
nodeID := fmt.Sprintf("node%d", i)
c.nodes = append(c.nodes, &topology.Node{
c.noder.AppendNode(&topology.Node{
ID: nodeID,
URI: NewTestURI("http", fmt.Sprintf("host%d", i), uint16(0)),
})
c.Topology.addID(nodeID)
}
c.Node = c.nodes[0]
c.Coordinator = c.nodes[0].ID
c.SetState(ClusterStateNormal)
cNodes := c.noder.Nodes()
c.Node = cNodes[0]
if err := c.holder.Open(); err != nil {
panic(err)