featurebase/internal/clustertests/pause_node_test.go
2023-01-11 18:59:24 +00:00

415 lines
10 KiB
Go

// Copyright 2021 Molecula Corp. All rights reserved.
package clustertest
import (
"context"
"fmt"
"hash/fnv"
"io"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
pilosa "github.com/featurebasedb/featurebase/v3"
"github.com/featurebasedb/featurebase/v3/authn"
"github.com/featurebasedb/featurebase/v3/disco"
"github.com/featurebasedb/featurebase/v3/encoding/proto"
"github.com/featurebasedb/featurebase/v3/net"
"github.com/pkg/errors"
)
// TODO(rdp): add refresh token to this test
func startCmd(cmd string, args ...string) (*exec.Cmd, error) {
pcmd := exec.Command(cmd, args...)
pcmd.Stdout = os.Stdout
pcmd.Stderr = os.Stderr
err := pcmd.Start()
return pcmd, err
}
func sendCmd(cmd string, args ...string) error {
pcmd, err := startCmd(cmd, args...)
if err != nil {
return errors.Wrap(err, "starting cmd")
}
err = pcmd.Wait()
if err != nil {
return errors.Wrap(err, "waiting on cmd")
}
return nil
}
func unpauseNode(t *testing.T, node string) error {
unpauseArgs := []string{"container", "unpause", container(t, node)}
return sendCmd("docker", unpauseArgs...)
}
func pauseNode(t *testing.T, node string) error {
pauseArgs := []string{"container", "pause", container(t, node)}
return sendCmd("docker", pauseArgs...)
}
type keyInserter struct {
client *pilosa.InternalClient
uri *net.URI
index string
keys []string
}
func (ki keyInserter) insertKeys(ctx context.Context) (map[string]uint64, error) {
ts, err := ki.client.CreateIndexKeysNode(ctx, ki.uri, ki.index, ki.keys...)
return ts, err
}
func getAddress(node string) string {
return node + ":10101"
}
func getClients(addrs []string) ([]*pilosa.InternalClient, error) {
clients := make([]*pilosa.InternalClient, 0, len(addrs))
for _, addr := range addrs {
c, err := pilosa.NewInternalClient(addr, pilosa.GetHTTPClient(nil), pilosa.WithSerializer(proto.Serializer{}))
if err != nil {
return nil, err
}
clients = append(clients, c)
}
return clients, nil
}
func getURIsFromAddresses(addrs []string) ([]*net.URI, error) {
uris := make([]*net.URI, 0, len(addrs))
for _, addr := range addrs {
uri, err := net.NewURIFromAddress(addr)
if err != nil {
return nil, err
}
uris = append(uris, uri)
}
return uris, nil
}
func readIndexTranslateData(ctx context.Context, client *pilosa.InternalClient, dirPath, index string, partition int) error {
// read translateStore contents from endpoint
r, err := client.IndexTranslateDataReader(ctx, index, partition)
if err != nil {
return err
}
buf, err := io.ReadAll(r)
if err != nil {
return err
}
r.Close()
// create file and write contents to it
filename := strconv.FormatInt(int64(partition), 10)
filePath := filepath.Join(dirPath, filename)
file, err := os.Create(filePath)
if err != nil {
return err
}
_, err = file.Write(buf)
if err != nil {
file.Close()
return err
}
err = file.Sync()
if err != nil {
file.Close()
return err
}
file.Close()
return nil
}
func openTranslateStores(dirPath, index string) (map[int]pilosa.TranslateStore, error) {
dirEntries, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
}
// in case of error, close any translateStore that has been opened
rollback := make([]pilosa.TranslateStore, 0, disco.DefaultPartitionN)
defer func() {
for _, ts := range rollback {
_ = ts.Close()
}
}()
// filter out non-file entries
filePaths := make([]string, 0, len(dirEntries))
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
filePath := filepath.Join(dirPath, entry.Name())
filePaths = append(filePaths, filePath)
}
translateStores := make(map[int]pilosa.TranslateStore)
for _, filePath := range filePaths {
// extract partition number from filename
file := filepath.Base(filePath)
partition, err := strconv.Atoi(file)
if err != nil {
return nil, err
}
// open bolt db
ts, err := pilosa.OpenTranslateStore(filePath, index, "", partition, disco.DefaultPartitionN, false)
ts.SetReadOnly(true)
if err != nil {
return nil, err
}
translateStores[partition] = ts
rollback = append(rollback, ts)
}
rollback = nil
return translateStores, nil
}
var errOpRetriable = errors.New("If operation failed on this error, it can be retried")
func verifyNodeHasGivenKeys(ctx context.Context, node, index, dirPath string, keys []string) error {
// get client that's connected to node
address := getAddress(node)
client, err := pilosa.NewInternalClient(address, pilosa.GetHTTPClient(nil), pilosa.WithSerializer(proto.Serializer{}))
if err != nil {
return err
}
// create dir to store boltdbs for this node
nodeDirPath := filepath.Join(dirPath, node)
err = os.Mkdir(nodeDirPath, 0o755)
if err != nil {
return err
}
// read in all the translate stores for each partition
for partition := 0; partition < disco.DefaultPartitionN; partition++ {
err := readIndexTranslateData(ctx, client, nodeDirPath, index, partition)
if err != nil {
return err
}
}
// open all the translate stores
translateStores, err := openTranslateStores(nodeDirPath, index)
if err != nil {
return err
}
// close all the translate stores on complete
defer func() {
for _, ts := range translateStores {
ts.Close()
}
}()
// merge all translations
merged := make(map[string]uint64)
for _, ts := range translateStores {
entries, err := ts.FindKeys(keys...)
if err != nil {
return err
}
for key, id := range entries {
merged[key] = id
}
}
// check that all expected keys present in node
if len(merged) != len(keys) {
msg := fmt.Sprintf("entries in node %s: %d. keys inserted: %d",
node, len(merged), len(keys))
return errors.Wrap(errOpRetriable, msg)
}
for _, k := range keys {
if _, ok := merged[k]; !ok {
msg := fmt.Sprintf("Key '%s' not present in node %s", k, node)
return errors.Wrap(errOpRetriable, msg)
}
}
return nil
}
func genKeys(count, maxTries int, keyToNode func(string) string, filterOut []string) []string {
exclusionSet := make(map[string]struct{})
for _, n := range filterOut {
exclusionSet[n] = struct{}{}
}
keys := make([]string, 0, count)
i := 0
for {
if i >= maxTries {
break
}
key := fmt.Sprintf("key-%d", i)
i++
// get primary node for this key
node := keyToNode(key)
// check if we should exclude this key
if _, ok := exclusionSet[node]; ok {
continue
}
// add key
keys = append(keys, key)
if len(keys) >= count {
return keys
}
}
return keys
}
func TestPauseReplica(t *testing.T) {
if os.Getenv("ENABLE_PILOSA_CLUSTER_TESTS") != "1" {
t.Skip("pilosa cluster tests for replication when a replica is paused are not enabled")
}
auth := false
if os.Getenv("ENABLE_AUTH") == "1" {
auth = true
}
// configurations for test
nodeNames := []string{"pilosa1", "pilosa2", "pilosa3"}
nodeToPause := "pilosa3"
addresses := make([]string, len(nodeNames))
for i, node := range nodeNames {
addresses[i] = getAddress(node)
}
clients, err := getClients(addresses)
if err != nil {
t.Fatalf("on init clients from addresses: %v, %v", addresses, err)
}
cli := clients[0]
uris, err := getURIsFromAddresses(addresses)
if err != nil {
t.Fatalf("on init clients from addresses: %v, %v", addresses, err)
}
uri := uris[0]
ctx := context.Background()
if auth {
token := GetAuthToken(t)
ctx = authn.WithAccessToken(ctx, "Bearer "+token)
}
ctx, cancel := context.WithCancel(ctx)
t.Log("start Client")
// first achieve normal cluster status
waitForStatus(t, cli.Status, string(disco.ClusterStateNormal), 30, 1*time.Second, ctx)
// create keyed index
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
index := fmt.Sprintf("keyed-index-%d", rng.Int63())
err = cli.EnsureIndex(ctx, index, pilosa.IndexOptions{
Keys: true,
})
if err != nil {
t.Fatalf("creating/asserting index: %v", err)
}
// generate mapping from partition to primary node
partitionToNode := make([]string, disco.DefaultPartitionN)
for partition := 0; partition < disco.DefaultPartitionN; partition++ {
nodes, err := cli.PartitionNodes(ctx, partition)
if err != nil {
t.Fatal(err)
}
partitionToNode[partition] = nodes[0].URI.Host
}
// generate random keys to insert
// no guarantee that all keys expected to be generated don't fall
// in nodes to be filtered out
keyCount := 100
maxKeyGenTries := 1000
filterOutKeysFromTheseNodes := []string{nodeToPause}
keyToNode := func(key string) string {
// get partition for this key
h := fnv.New64a()
_, _ = h.Write([]byte(index))
_, _ = h.Write([]byte(key))
partition := int(h.Sum64() % uint64(disco.DefaultPartitionN))
// get node for this partition
return partitionToNode[partition]
}
keys := genKeys(keyCount, maxKeyGenTries, keyToNode, filterOutKeysFromTheseNodes)
// pause node
t.Logf("pause %s", nodeToPause)
err = pauseNode(t, nodeToPause)
if err != nil {
t.Fatalf("error on pause node %s: %v", nodeToPause, err)
}
// insert keys
t.Log("start insert")
ts, err := keyInserter{
client: cli,
uri: uri,
index: index,
keys: keys,
}.insertKeys(ctx)
if err != nil {
t.Fatalf("Error: inserting index keys for index(%s) send to node(%s): %v", index, uri.String(), err)
}
t.Logf("successfully end insert: %v", len(ts))
// wait for cluster status to be non-normal
waitForStatus(t, cli.Status, string(disco.ClusterStateDegraded), 30, 1*time.Second, ctx)
// wait for cluster status to get back to normal
t.Logf("unpause %s", nodeToPause)
err = unpauseNode(t, nodeToPause)
if err != nil {
t.Fatalf("error on unpause node %s: %v", nodeToPause, err)
}
waitForStatus(t, cli.Status, string(disco.ClusterStateNormal), 30, 1*time.Second, ctx)
// set up directory to store keys
basePath := "."
keysDirName := "index_keys"
dirPath, err := filepath.Abs(basePath)
if err != nil {
t.Fatal(err)
}
dirPath = filepath.Join(dirPath, keysDirName)
err = os.Mkdir(dirPath, 0o755)
if err != nil {
t.Fatal(err)
}
maxRetries := 10
durationInBetweenRetries := 5 * time.Second
for _, node := range nodeNames {
try := 1
retries:
for {
err = verifyNodeHasGivenKeys(ctx, node, index, dirPath, keys)
if err == nil {
break retries
}
if try <= maxRetries && errors.Is(err, errOpRetriable) {
try++
t.Logf("node %s, retry verify key replication: (%d/%d) after %v\n",
node, try, maxRetries, durationInBetweenRetries)
time.Sleep(durationInBetweenRetries)
continue
}
t.Fatal(errors.Wrap(err, fmt.Sprintf("try: (%d/%d)", try, maxRetries)))
}
}
cancel()
t.Log("Done")
}