Skip to content

Commit

Permalink
Support dumping from redis clusters (#7)
Browse files Browse the repository at this point in the history
Signed-off-by: Tamal Saha <[email protected]>
  • Loading branch information
tamalsaha authored Apr 29, 2023
1 parent c2884a3 commit 3c7919b
Show file tree
Hide file tree
Showing 17 changed files with 884 additions and 33 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module github.com/yannh/redis-dump-go

go 1.18

require github.com/mediocregopher/radix/v3 v3.8.0
require (
github.com/mediocregopher/radix/v3 v3.8.0
github.com/pkg/errors v0.9.1
)

require golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mediocregopher/radix/v3 v3.8.0 h1:HI8EgkaM7WzsrFpYAkOXIgUKbjNonb2Ne7K6Le61Pmg=
github.com/mediocregopher/radix/v3 v3.8.0/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
Expand Down
15 changes: 14 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,22 @@ func realMain() int {
TlsHandler: tlshandler,
}

if err = redisdump.DumpServer(s, db, c.Filter, c.NWorkers, c.WithTTL, c.BatchSize, c.Noscan, logger, serializer, progressNotifs); err != nil {
if hosts, err := redisdump.GetHosts(s, c.NWorkers); err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
return 1
} else {
totalSize := 0
for _, host := range hosts {
if size, err := redisdump.DumpServer(host, db, c.Filter, c.NWorkers, c.WithTTL, c.BatchSize, c.Noscan, logger, serializer, progressNotifs); err != nil {
fmt.Fprintf(os.Stderr, "%s", err)
return 1
} else {
totalSize += size
}
}
if c.SetTotalKeys {
logger.Print(serializer(redisdump.StringToRedisCmd(config.KeyTotalKeys, fmt.Sprint(totalSize+1))))
}
}

return 0
Expand Down
38 changes: 21 additions & 17 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@ import (
"fmt"
)

const KeyTotalKeys = "__redisdump_total_keys"

type Config struct {
Host string
Port int
Db int
Username string
Filter string
Noscan bool
BatchSize int
NWorkers int
WithTTL bool
Output string
Silent bool
Tls bool
Insecure bool
CaCert string
Cert string
Key string
Help bool
Host string
Port int
Db int
Username string
Filter string
Noscan bool
BatchSize int
NWorkers int
WithTTL bool
Output string
Silent bool
Tls bool
Insecure bool
CaCert string
Cert string
Key string
Help bool
SetTotalKeys bool
}

func isFlagPassed(flags *flag.FlagSet, name string) bool {
Expand Down Expand Up @@ -60,6 +63,7 @@ func FromFlags(progName string, args []string) (Config, string, error) {
flags.StringVar(&c.Cert, "cert", "", "Private key file to authenticate with")
flags.StringVar(&c.Key, "key", "", "SSL private key file path")
flags.BoolVar(&c.Help, "h", false, "show help information")
flags.BoolVar(&c.SetTotalKeys, "set-total-keys", false, "set total number of keys as "+KeyTotalKeys)
flags.Usage = func() {
fmt.Fprintf(&outBuf, "Usage: %s [OPTION]...\n", progName)
flags.PrintDefaults()
Expand Down
117 changes: 117 additions & 0 deletions pkg/redisdump/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package redisdump

import (
"net"
"strconv"
"strings"

"github.com/mediocregopher/radix/v3"
"github.com/pkg/errors"
)

func ParseRedisInfo(s string) (map[string]string, error) {
lines := strings.Split(s, "\n")

info := map[string]string{}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, val, found := strings.Cut(line, ":")
if !found {
continue
}
info[key] = val
}
return info, nil
}

type Node struct {
Host string
Port int
Slots []Range
}

type Range struct {
Start, End int
}

func GetMasterNodeAddresses(s string) ([]Node, error) {
lines := strings.Split(s, "\n")

var masters []Node
for _, line := range lines {
if strings.Contains(line, "master") {
fields := strings.FieldsFunc(line, func(r rune) bool {
return r == ' ' || r == '@'
})

host, port, err := net.SplitHostPort(fields[1])
if err != nil {
return nil, errors.Wrapf(err, "failed to split addr %s", fields[1])
}
p, err := strconv.Atoi(port)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse port in addr %s", fields[1])
}
m := Node{Host: host, Port: p}

for i := len(fields) - 1; i >= 0; i-- {
start, end, found := strings.Cut(fields[i], "-")
if !found {
break
}
s, err := strconv.Atoi(start)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse slot start for redis master %s with slots %s", m.Host, fields[i])
}
e, err := strconv.Atoi(end)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse slot end for redis master %s with slots %s", m.Host, fields[i])
}
m.Slots = append(m.Slots, Range{Start: s, End: e})
}
masters = append(masters, m)
}
}
return masters, nil
}

func GetHosts(s Host, nWorkers int) ([]Host, error) {
client, err := NewClient(s, nil, nWorkers)
if err != nil {
return nil, err
}
defer client.Close()

var val string
err = client.Do(radix.Cmd(&val, "INFO"))
if err != nil {
return nil, err
}
info, err := ParseRedisInfo(val)
if err != nil {
return nil, err
}
if info["cluster_enabled"] == "0" {
return []Host{s}, nil
}

err = client.Do(radix.Cmd(&val, "CLUSTER", "nodes"))
if err != nil {
return nil, err
}
masters, err := GetMasterNodeAddresses(val)
if err != nil {
panic(err)
}
hosts := make([]Host, 0, len(masters))
for _, m := range masters {
scopy := s
scopy.Host = m.Host
scopy.Port = m.Port
hosts = append(hosts, scopy)
}
return hosts, nil
}
40 changes: 27 additions & 13 deletions pkg/redisdump/redisdump.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func ttlToRedisCmd(k string, val int64) []string {
return []string{"EXPIREAT", k, fmt.Sprint(time.Now().Unix() + val)}
}

func stringToRedisCmd(k, val string) []string {
func StringToRedisCmd(k, val string) []string {
return []string{"SET", k, val}
}

Expand Down Expand Up @@ -166,7 +166,7 @@ func dumpKeys(client radix.Client, cmd radixCmder, keys []string, withTTL bool,
if err = client.Do(cmd(&val, "GET", key)); err != nil {
return err
}
redisCmds = [][]string{stringToRedisCmd(key, val)}
redisCmds = [][]string{StringToRedisCmd(key, val)}

case "list":
var val []string
Expand Down Expand Up @@ -394,10 +394,7 @@ type Host struct {
TlsHandler *TlsHandler
}

// DumpServer dumps all Keys from the redis server given by redisURL,
// to the Logger logger. Progress notification informations
// are regularly sent to the channel progressNotifications
func DumpServer(s Host, db *uint8, filter string, nWorkers int, withTTL bool, batchSize int, noscan bool, logger *log.Logger, serializer func([]string) string, progress chan<- ProgressNotification) error {
func NewClient(s Host, db *uint8, nWorkers int) (*radix.Pool, error) {
redisURL := RedisURL(s.Host, fmt.Sprint(s.Port))
getConnFunc := func(db *uint8) func(network, addr string) (radix.Conn, error) {
return func(network, addr string) (radix.Conn, error) {
Expand All @@ -409,34 +406,51 @@ func DumpServer(s Host, db *uint8, filter string, nWorkers int, withTTL bool, ba
return radix.Dial(network, addr, dialOpts...)
}
}
return radix.NewPool("tcp", redisURL, nWorkers, radix.PoolConnFunc(getConnFunc(db)))
}

// DumpServer dumps all Keys from the redis server given by redisURL,
// to the Logger logger. Progress notification informations
// are regularly sent to the channel progressNotifications
func DumpServer(s Host, db *uint8, filter string, nWorkers int, withTTL bool, batchSize int, noscan bool, logger *log.Logger, serializer func([]string) string, progress chan<- ProgressNotification) (int, error) {
dbs := []uint8{}
if db != AllDBs {
dbs = []uint8{*db}
} else {
client, err := radix.NewPool("tcp", redisURL, nWorkers, radix.PoolConnFunc(getConnFunc(nil)))
client, err := NewClient(s, nil, nWorkers)
if err != nil {
return err
return -1, err
}

dbs, err = getDBIndexes(client)
if err != nil {
return err
return -1, err
}
client.Close()
}

totalSize := 0
for _, db := range dbs {
client, err := radix.NewPool("tcp", redisURL, nWorkers, radix.PoolConnFunc(getConnFunc(&db)))
client, err := NewClient(s, &db, nWorkers)
if err != nil {
return err
return -1, err
}
defer client.Close()

if err = dumpDB(client, &db, filter, nWorkers, withTTL, batchSize, noscan, logger, serializer, progress); err != nil {
return err
return -1, err
}

var dbSize string
if err = client.Do(radix.Cmd(&dbSize, "dbsize")); err != nil {
return -1, err
}
if size, err := strconv.Atoi(dbSize); err != nil {
return -1, err
} else {
totalSize += size
}
}

return nil
return totalSize, nil
}
2 changes: 1 addition & 1 deletion pkg/redisdump/redisdump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestStringToRedisCmd(t *testing.T) {
}

for _, test := range testCases {
res := stringToRedisCmd(test.key, test.value)
res := StringToRedisCmd(test.key, test.value)
if !testEqString(res, test.expected) {
t.Errorf("Failed generating redis command from string for: %s %s", test.key, test.value)
}
Expand Down
24 changes: 24 additions & 0 deletions vendor/github.com/pkg/errors/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vendor/github.com/pkg/errors/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions vendor/github.com/pkg/errors/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 3c7919b

Please sign in to comment.