diff --git a/cluster.go b/cluster.go index d6d4e842..52f357a4 100644 --- a/cluster.go +++ b/cluster.go @@ -34,7 +34,8 @@ type ClusterOptions struct { // Following options are copied from Options struct. - Password string + MaxRetries int + Password string DialTimeout time.Duration ReadTimeout time.Duration @@ -63,8 +64,9 @@ func (opt *ClusterOptions) clientOptions() *Options { const disableIdleCheck = -1 return &Options{ - Password: opt.Password, - ReadOnly: opt.ReadOnly, + MaxRetries: opt.MaxRetries, + Password: opt.Password, + ReadOnly: opt.ReadOnly, DialTimeout: opt.DialTimeout, ReadTimeout: opt.ReadTimeout, diff --git a/cluster_test.go b/cluster_test.go index 4a7c4c1e..49cb13ca 100644 --- a/cluster_test.go +++ b/cluster_test.go @@ -39,12 +39,16 @@ func (s *clusterScenario) slaves() []*redis.Client { return result } -func (s *clusterScenario) clusterClient(opt *redis.ClusterOptions) *redis.ClusterClient { +func (s *clusterScenario) addrs() []string { addrs := make([]string, len(s.ports)) for i, port := range s.ports { addrs[i] = net.JoinHostPort("127.0.0.1", port) } - opt.Addrs = addrs + return addrs +} + +func (s *clusterScenario) clusterClient(opt *redis.ClusterOptions) *redis.ClusterClient { + opt.Addrs = s.addrs() return redis.NewClusterClient(opt) } diff --git a/example_test.go b/example_test.go index 3ecd0ba4..7899ab01 100644 --- a/example_test.go +++ b/example_test.go @@ -367,3 +367,31 @@ func ExampleScanCmd_Iterator() { panic(err) } } + +func ExampleNewUniversalClient_simple() { + client := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{":6379"}, + }) + defer client.Close() + + client.Ping() +} + +func ExampleNewUniversalClient_failover() { + client := redis.NewUniversalClient(&redis.UniversalOptions{ + MasterName: "master", + Addrs: []string{":26379"}, + }) + defer client.Close() + + client.Ping() +} + +func ExampleNewUniversalClient_cluster() { + client := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"}, + }) + defer client.Close() + + client.Ping() +} diff --git a/universal.go b/universal.go new file mode 100644 index 00000000..3c13b196 --- /dev/null +++ b/universal.go @@ -0,0 +1,134 @@ +package redis + +import "time" + +// UniversalOptions information is required by UniversalClient to establish +// connections. +type UniversalOptions struct { + // Either a single address or a seed list of host:port addresses + // of cluster/sentinel nodes. + Addrs []string + + // The sentinel master name. + // Only failover clients. + MasterName string + + // Database to be selected after connecting to the server. + // Only single-node and failover clients. + DB int + + // Enables read only queries on slave nodes. + // Only cluster and single-node clients. + ReadOnly bool + + // Only cluster clients. + + MaxRedirects int + RouteByLatency bool + + // Common options + + MaxRetries int + Password string + DialTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration + PoolSize int + PoolTimeout time.Duration + IdleTimeout time.Duration + IdleCheckFrequency time.Duration +} + +func (o *UniversalOptions) cluster() *ClusterOptions { + if len(o.Addrs) == 0 { + o.Addrs = []string{"127.0.0.1:6379"} + } + + return &ClusterOptions{ + Addrs: o.Addrs, + MaxRedirects: o.MaxRedirects, + RouteByLatency: o.RouteByLatency, + ReadOnly: o.ReadOnly, + + MaxRetries: o.MaxRetries, + Password: o.Password, + DialTimeout: o.DialTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + PoolSize: o.PoolSize, + PoolTimeout: o.PoolTimeout, + IdleTimeout: o.IdleTimeout, + IdleCheckFrequency: o.IdleCheckFrequency, + } +} + +func (o *UniversalOptions) failover() *FailoverOptions { + if len(o.Addrs) == 0 { + o.Addrs = []string{"127.0.0.1:26379"} + } + + return &FailoverOptions{ + SentinelAddrs: o.Addrs, + MasterName: o.MasterName, + DB: o.DB, + + MaxRetries: o.MaxRetries, + Password: o.Password, + DialTimeout: o.DialTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + PoolSize: o.PoolSize, + PoolTimeout: o.PoolTimeout, + IdleTimeout: o.IdleTimeout, + IdleCheckFrequency: o.IdleCheckFrequency, + } +} + +func (o *UniversalOptions) simple() *Options { + addr := "127.0.0.1:6379" + if len(o.Addrs) > 0 { + addr = o.Addrs[0] + } + + return &Options{ + Addr: addr, + DB: o.DB, + ReadOnly: o.ReadOnly, + + MaxRetries: o.MaxRetries, + Password: o.Password, + DialTimeout: o.DialTimeout, + ReadTimeout: o.ReadTimeout, + WriteTimeout: o.WriteTimeout, + PoolSize: o.PoolSize, + PoolTimeout: o.PoolTimeout, + IdleTimeout: o.IdleTimeout, + IdleCheckFrequency: o.IdleCheckFrequency, + } +} + +// -------------------------------------------------------------------- + +// UniversalClient is an abstract client which - based on the provided options - +// can connect to either clusters, or sentinel-backed failover instances or simple +// single-instance servers. This can be useful for testing cluster-specific +// applications locally. +type UniversalClient interface { + Cmdable + Close() error +} + +// NewUniversalClient returns a new multi client. The type of client returned depends +// on the following three conditions: +// +// 1. if a MasterName is passed a sentinel-backed FailoverClient will be returned +// 2. if the number of Addrs is two or more, a ClusterClient will be returned +// 3. otherwise, a single-node redis Client will be returned. +func NewUniversalClient(opts *UniversalOptions) UniversalClient { + if opts.MasterName != "" { + return NewFailoverClient(opts.failover()) + } else if len(opts.Addrs) > 1 { + return NewClusterClient(opts.cluster()) + } + return NewClient(opts.simple()) +} diff --git a/universal_test.go b/universal_test.go new file mode 100644 index 00000000..b05736c4 --- /dev/null +++ b/universal_test.go @@ -0,0 +1,41 @@ +package redis_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "gopkg.in/redis.v5" +) + +var _ = Describe("UniversalClient", func() { + var client redis.UniversalClient + + AfterEach(func() { + if client != nil { + Expect(client.Close()).To(Succeed()) + } + }) + + It("should connect to failover servers", func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + MasterName: sentinelName, + Addrs: []string{":" + sentinelPort}, + }) + Expect(client.Ping().Err()).NotTo(HaveOccurred()) + }) + + It("should connect to simple servers", func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{redisAddr}, + }) + Expect(client.Ping().Err()).NotTo(HaveOccurred()) + }) + + It("should connect to clusters", func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: cluster.addrs(), + }) + Expect(client.Ping().Err()).NotTo(HaveOccurred()) + }) + +})