Skip to content

Commit 23a368b

Browse files
authored
Add shardedredis tool (#10580)
Allows testing sharded redis setups locally. Especially useful to test how the app behaves when shards are added, removed, or become intermittently unavailable.
1 parent 4490933 commit 23a368b

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
package(default_visibility = ["//enterprise:__subpackages__"])
4+
5+
go_library(
6+
name = "shardedredis_lib",
7+
srcs = ["shardedredis.go"],
8+
data = [
9+
":redis.conf",
10+
"//enterprise/server/testutil/testredis:redis-server_crossplatform",
11+
],
12+
importpath = "github.com/buildbuddy-io/buildbuddy/enterprise/tools/shardedredis",
13+
visibility = ["//visibility:private"],
14+
x_defs = {
15+
"redisRlocationpath": "$(rlocationpath //enterprise/server/testutil/testredis:redis-server_crossplatform)",
16+
"configRlocationpath": "$(rlocationpath :redis.conf)",
17+
},
18+
deps = [
19+
"//server/util/log",
20+
"//server/util/shlex",
21+
"@com_github_armon_circbuf//:circbuf",
22+
"@com_github_mattn_go_isatty//:go-isatty",
23+
"@io_bazel_rules_go//go/runfiles",
24+
],
25+
)
26+
27+
go_binary(
28+
name = "shardedredis",
29+
embed = [":shardedredis_lib"],
30+
visibility = ["//visibility:public"],
31+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
maxclients 10000
2+
maxmemory 1gb
3+
maxmemory-policy allkeys-lru
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// shardedredis runs a local sharded redis setup.
2+
//
3+
// To run an app pointing at the cluster, run the app like this:
4+
//
5+
// $ bazel run -- enterprise/server $(bazel run enterprise/tools/shardedredis)
6+
//
7+
// This works because this tool runs redis servers in the background and prints
8+
// out the app flags to stdout.
9+
//
10+
// If you prefer to run in the foreground and configure the app flags yourself,
11+
// run this tool in its own terminal, with the -foreground flag:
12+
//
13+
// $ bazel run -- enterprise/tools/shardedredis --foreground
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"flag"
20+
"fmt"
21+
"io"
22+
"net"
23+
"os"
24+
"os/exec"
25+
"strconv"
26+
"time"
27+
28+
"github.com/armon/circbuf"
29+
"github.com/bazelbuild/rules_go/go/runfiles"
30+
"github.com/buildbuddy-io/buildbuddy/server/util/log"
31+
"github.com/buildbuddy-io/buildbuddy/server/util/shlex"
32+
"github.com/mattn/go-isatty"
33+
)
34+
35+
var (
36+
n = flag.Int("replicas", 8, "Number of replicas to run")
37+
basePort = flag.Int("base_port", 8379, "Port number for the first replica; replica i will get port base_port+i")
38+
showOutput = flag.Bool("show_output", false, "Show redis server output")
39+
foreground = flag.Bool("foreground", false, "Run redis servers in the foreground")
40+
)
41+
42+
// Set by x_defs in BUILD fie
43+
var (
44+
redisRlocationpath string
45+
configRlocationpath string
46+
)
47+
48+
func main() {
49+
flag.Parse()
50+
if err := run(); err != nil {
51+
log.Fatal(err.Error())
52+
}
53+
}
54+
55+
type Replica struct {
56+
cmd *exec.Cmd
57+
stderr *circbuf.Buffer
58+
terminatedCh chan struct{}
59+
healthyCh chan error
60+
err error
61+
}
62+
63+
func run() error {
64+
appFlags := make([]string, 0, *n)
65+
for i := range *n {
66+
appFlags = append(appFlags, fmt.Sprintf("--app.default_sharded_redis.shards=localhost:%d", *basePort+i))
67+
}
68+
if isatty.IsTerminal(os.Stdout.Fd()) {
69+
log.Info("App flags:")
70+
}
71+
fmt.Println(shlex.Quote(appFlags...))
72+
73+
redisPath, err := runfiles.Rlocation(redisRlocationpath)
74+
if err != nil {
75+
return fmt.Errorf("rlocation %q: %w", redisRlocationpath, err)
76+
}
77+
configPath, err := runfiles.Rlocation(configRlocationpath)
78+
if err != nil {
79+
return fmt.Errorf("rlocation %q: %w", configRlocationpath, err)
80+
}
81+
82+
var replicas []*Replica
83+
for i := range *n {
84+
buf, err := circbuf.NewBuffer(16 * 1024)
85+
if err != nil {
86+
return fmt.Errorf("create buffer: %w", err)
87+
}
88+
port := *basePort + i
89+
args := []string{configPath, "--port", strconv.Itoa(port)}
90+
// Forward residual args to redis server
91+
args = append(args, flag.Args()...)
92+
cmd := exec.Command(redisPath, args...)
93+
cmd.Stderr = buf
94+
if *showOutput {
95+
cmd.Stdout = log.Writer(fmt.Sprintf("redis-%d:stdout ", i))
96+
cmd.Stderr = io.MultiWriter(log.Writer(fmt.Sprintf("redis-%d:stderr ", i)), buf)
97+
}
98+
if err := cmd.Start(); err != nil {
99+
return fmt.Errorf("start redis-server %d: %w", i, err)
100+
}
101+
log.Infof("Started redis-server %d listening on localhost:%d", i, port)
102+
103+
terminatedCh := make(chan struct{})
104+
healthyCh := make(chan error, 1)
105+
106+
r := &Replica{
107+
cmd: cmd,
108+
stderr: buf,
109+
terminatedCh: terminatedCh,
110+
healthyCh: healthyCh,
111+
}
112+
go func() {
113+
defer close(terminatedCh)
114+
r.err = cmd.Wait()
115+
}()
116+
go func() {
117+
defer close(healthyCh)
118+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
119+
defer cancel()
120+
for ctx.Err() == nil {
121+
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", *basePort+i))
122+
if err == nil {
123+
conn.Close()
124+
return
125+
}
126+
select {
127+
case <-time.After(10 * time.Millisecond):
128+
continue
129+
case <-ctx.Done():
130+
}
131+
}
132+
healthyCh <- ctx.Err()
133+
}()
134+
replicas = append(replicas, r)
135+
}
136+
137+
log.Infof("Waiting for all redis-server replicas to become healthy...")
138+
for i, r := range replicas {
139+
select {
140+
case err := <-r.healthyCh:
141+
if err == nil {
142+
continue
143+
}
144+
return fmt.Errorf("replica %d: %w", i, err)
145+
case <-r.terminatedCh:
146+
return fmt.Errorf("replica %d: %w: %q", i, r.err, r.stderr.String())
147+
}
148+
}
149+
150+
if *foreground {
151+
log.Infof("All redis-server replicas are healthy. Press Ctrl+C to quit")
152+
select {}
153+
}
154+
return nil
155+
}

0 commit comments

Comments
 (0)