diff --git a/sentinel_test.go b/sentinel_test.go new file mode 100644 index 0000000..2f01fe9 --- /dev/null +++ b/sentinel_test.go @@ -0,0 +1,173 @@ +package redis_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + "text/template" + "time" + + "gopkg.in/redis.v2" +) + +func startRedis(port string) (*exec.Cmd, error) { + cmd := exec.Command("redis-server", "--port", port) + if false { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + if err := cmd.Start(); err != nil { + return nil, err + } + return cmd, nil +} + +func startRedisSlave(port, slave string) (*exec.Cmd, error) { + cmd := exec.Command("redis-server", "--port", port, "--slaveof", "127.0.0.1", slave) + if false { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + if err := cmd.Start(); err != nil { + return nil, err + } + return cmd, nil +} + +func startRedisSentinel(port, masterName, masterPort string) (*exec.Cmd, error) { + dir, err := ioutil.TempDir("", "sentinel") + if err != nil { + return nil, err + } + + sentinelConfFilepath := filepath.Join(dir, "sentinel.conf") + tpl, err := template.New("sentinel.conf").Parse(sentinelConf) + if err != nil { + return nil, err + } + + data := struct { + Port string + MasterName string + MasterPort string + }{ + Port: port, + MasterName: masterName, + MasterPort: masterPort, + } + if err := writeTemplateToFile(sentinelConfFilepath, tpl, data); err != nil { + return nil, err + } + + cmd := exec.Command("redis-server", sentinelConfFilepath, "--sentinel") + if false { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + if err := cmd.Start(); err != nil { + return nil, err + } + + return cmd, nil +} + +func writeTemplateToFile(path string, t *template.Template, data interface{}) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + return t.Execute(f, data) +} + +func TestSentinel(t *testing.T) { + masterName := "mymaster" + masterPort := "8123" + slavePort := "8124" + sentinelPort := "26379" + + _, err := startRedis(masterPort) + if err != nil { + t.Fatal(err) + } + time.Sleep(200 * time.Millisecond) + + master := redis.NewTCPClient(&redis.Options{ + Addr: ":" + masterPort, + }) + if err := master.Ping().Err(); err != nil { + t.Fatal(err) + } + defer master.Shutdown() + + _, err = startRedisSlave(slavePort, masterPort) + if err != nil { + t.Fatal(err) + } + time.Sleep(200 * time.Millisecond) + + slave := redis.NewTCPClient(&redis.Options{ + Addr: ":" + slavePort, + }) + if err := slave.Ping().Err(); err != nil { + t.Fatal(err) + } + defer slave.Shutdown() + + _, err = startRedisSentinel(sentinelPort, masterName, masterPort) + if err != nil { + t.Fatal(err) + } + time.Sleep(200 * time.Millisecond) + + sentinel := redis.NewTCPClient(&redis.Options{ + Addr: ":" + sentinelPort, + }) + if err := sentinel.Ping().Err(); err != nil { + t.Fatal(err) + } + defer sentinel.Shutdown() + + client := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: masterName, + SentinelAddrs: []string{":" + sentinelPort}, + }) + + if err := client.Set("foo", "master").Err(); err != nil { + t.Fatal(err) + } + + val, err := master.Get("foo").Result() + if err != nil { + t.Fatal(err) + } + if val != "master" { + t.Fatalf(`got %q, expected "master"`, val) + } + + // Kill master. + master.Shutdown() + + // Wait for Redis sentinel to elect new master. + time.Sleep(3 * time.Second) + + // Check that client picked up new master. + val, err = client.Get("foo").Result() + if err != nil { + t.Fatal(err) + } + if val != "master" { + t.Fatalf(`got %q, expected "master"`, val) + } +} + +var sentinelConf = ` +port {{ .Port }} + +sentinel monitor {{ .MasterName }} 127.0.0.1 {{ .MasterPort }} 1 +sentinel down-after-milliseconds {{ .MasterName }} 200 +sentinel failover-timeout {{ .MasterName }} 400 +sentinel parallel-syncs {{ .MasterName }} 1 +`