mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
The ioutil package is deprecated, with all of its functions having moved into os or io. Do the replacements so we stop having this impending.
269 lines
6.5 KiB
Go
269 lines
6.5 KiB
Go
// Copyright 2022 Molecula Corp. All rights reserved.
|
|
package ctl
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
gohttp "net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
pilosa "github.com/featurebasedb/featurebase/v3"
|
|
"github.com/featurebasedb/featurebase/v3/authn"
|
|
"github.com/featurebasedb/featurebase/v3/disco"
|
|
"github.com/featurebasedb/featurebase/v3/server"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// RestoreTarCommand represents a command for restoring a backup to
|
|
type RestoreTarCommand struct {
|
|
tlsConfig *tls.Config
|
|
|
|
Host string
|
|
|
|
// Filepath to the backup file.
|
|
Path string
|
|
|
|
// Amount of time after first failed request to continue retrying.
|
|
RetryPeriod time.Duration `json:"retry-period"`
|
|
|
|
// Host:port on which to listen for pprof.
|
|
Pprof string `json:"pprof"`
|
|
|
|
// Reusable client.
|
|
client *pilosa.InternalClient
|
|
|
|
// Standard input/output
|
|
*pilosa.CmdIO
|
|
|
|
TLS server.TLSConfig
|
|
|
|
AuthToken string
|
|
}
|
|
|
|
// NewRestoreTarCommand returns a new instance of RestoreTarCommand.
|
|
func NewRestoreTarCommand(stdin io.Reader, stdout, stderr io.Writer) *RestoreTarCommand {
|
|
return &RestoreTarCommand{
|
|
CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr),
|
|
RetryPeriod: time.Second * 30,
|
|
Pprof: "localhost:0",
|
|
}
|
|
}
|
|
|
|
// Run executes the restore.
|
|
func (cmd *RestoreTarCommand) Run(ctx context.Context) (err error) {
|
|
logger := cmd.Logger()
|
|
close, err := startProfilingServer(cmd.Pprof, logger)
|
|
if err != nil {
|
|
return errors.Wrap(err, "starting profiling server")
|
|
}
|
|
defer close()
|
|
|
|
// Validate arguments.
|
|
if cmd.Path == "" {
|
|
return fmt.Errorf("-s flag required")
|
|
}
|
|
useStdin := cmd.Path == "-"
|
|
|
|
var f *os.File
|
|
// read from Stdin if path specified as -
|
|
if useStdin {
|
|
f = os.Stdin
|
|
} else {
|
|
f, err = os.Open(cmd.Path)
|
|
if err != nil {
|
|
return (err)
|
|
}
|
|
defer f.Close()
|
|
}
|
|
|
|
// Parse TLS configuration for node-specific clients.
|
|
tls := cmd.TLSConfiguration()
|
|
if cmd.tlsConfig, err = server.GetTLSConfig(&tls, logger); err != nil {
|
|
return fmt.Errorf("parsing tls config: %w", err)
|
|
}
|
|
// Create a client to the server.
|
|
client, err := commandClient(cmd, pilosa.WithClientRetryPeriod(cmd.RetryPeriod))
|
|
if err != nil {
|
|
return fmt.Errorf("creating client: %w", err)
|
|
}
|
|
cmd.client = client
|
|
|
|
if cmd.AuthToken != "" {
|
|
ctx = context.WithValue(ctx, authn.ContextValueAccessToken, "Bearer "+cmd.AuthToken)
|
|
}
|
|
|
|
var tarReader *tar.Reader
|
|
if strings.HasSuffix(cmd.Path, "gz") {
|
|
gzf, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tarReader = tar.NewReader(gzf)
|
|
} else {
|
|
tarReader = tar.NewReader(f)
|
|
}
|
|
nodes, err := cmd.client.Nodes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var primary *disco.Node
|
|
for _, node := range nodes {
|
|
if node.IsPrimary {
|
|
primary = node
|
|
break
|
|
}
|
|
|
|
}
|
|
if primary == nil {
|
|
return errors.New("no primary")
|
|
}
|
|
c := &gohttp.Client{}
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
record := strings.Split(header.Name, "/")
|
|
if len(record) == 1 {
|
|
switch record[0] {
|
|
case "schema":
|
|
logger.Printf("Load Schema")
|
|
url := primary.URI.Path("/schema")
|
|
_, err = c.Post(url, "application/json", tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "idalloc":
|
|
logger.Printf("Load ids")
|
|
url := primary.URI.Path("/internal/idalloc/restore")
|
|
_, err = c.Post(url, "application/octet-stream", tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return err
|
|
|
|
}
|
|
continue
|
|
}
|
|
indexName := record[1]
|
|
switch record[2] {
|
|
case "shards":
|
|
shard, err := strconv.ParseUint(record[3], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fragmentNodes, err := cmd.client.FragmentNodes(ctx, indexName, shard)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot determine fragmentNodes: %w", err)
|
|
} else if len(fragmentNodes) == 0 {
|
|
return fmt.Errorf("no fragmentNodes available")
|
|
}
|
|
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range fragmentNodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
client := &gohttp.Client{}
|
|
rd := bytes.NewReader(shardBytes)
|
|
logger.Printf("shard %v %v", shard, indexName)
|
|
url := node.URI.Path(fmt.Sprintf("/internal/restore/%v/%v", indexName, shard))
|
|
_, err = client.Post(url, "application/octet-stream", rd)
|
|
return err
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
case "translate":
|
|
partitionID, err := strconv.Atoi(record[3])
|
|
logger.Printf("column keys %v (%v)", indexName, partitionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
partitionNodes, err := cmd.client.PartitionNodes(ctx, partitionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range partitionNodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
// rd := bytes.NewReader(shardBytes)
|
|
rd := func() (io.Reader, error) {
|
|
return bytes.NewReader(shardBytes), nil
|
|
}
|
|
|
|
return cmd.client.ImportIndexKeys(ctx, &node.URI, indexName, partitionID, false, rd)
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
case "attributes":
|
|
//skip
|
|
case "fields":
|
|
fieldName := record[3]
|
|
switch action := record[4]; action {
|
|
case "translate":
|
|
logger.Printf("field keys %v %v", indexName, fieldName)
|
|
//needs to go to all nodes
|
|
shardBytes, err := io.ReadAll(tarReader) // this feels wrong but works for now
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _ := errgroup.WithContext(ctx)
|
|
for _, node := range nodes {
|
|
node := node
|
|
g.Go(func() error {
|
|
// rd := bytes.NewReader(shardBytes)
|
|
rd := func() (io.Reader, error) {
|
|
return bytes.NewReader(shardBytes), nil
|
|
}
|
|
|
|
return cmd.client.ImportFieldKeys(ctx, &node.URI, indexName, fieldName, false, rd)
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
case "attributes":
|
|
//skip
|
|
default:
|
|
return fmt.Errorf("unknown restore action: %v", action)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
/* Fetch the cluster nodes from the target host.
|
|
For each index:
|
|
Upload the RBF snapshot for each shard to the nodes that own the shard.
|
|
Upload the index & field translation BoltDB snapshots to each node.
|
|
If possible, trigger the node to reload itself. Otherwise a restart would be required.
|
|
*/
|
|
|
|
return nil
|
|
}
|
|
func (cmd *RestoreTarCommand) TLSHost() string { return cmd.Host }
|
|
|
|
func (cmd *RestoreTarCommand) TLSConfiguration() server.TLSConfig { return cmd.TLS }
|