redisc

package module
v1.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 18, 2018 License: BSD-3-Clause Imports: 9 Imported by: 7

README

redisc GoDoc Build Status

Package redisc implements a redis cluster client built on top of the redigo package. See the godoc for details.

Installation

$ go get [-u] [-t] github.com/mna/redisc

Releases

  • v1.1.3 : Fix handling of ASK replies in RetryConn.

  • v1.1.2 : Remove mention that StartupNodes in Cluster struct needs to be master nodes (it can be replicas). Add supporting test.

  • v1.1.1 : Fix CI tests.

  • v1.1.0 : This release builds with the github.com/gomodule/redigo package (the new import path of redigo, which also has a breaking change in its v2.0.0, the PMessage type has been removed and consolidated into Message).

  • v1.0.0 : This release builds with the github.com/garyburd/redigo package, which - according to its readme - will not be maintained anymore, having moved to github.com/gomodule/redigo for future development. As such, redisc will not be updated with the old redigo package, this version was created only to avoid causing issues to users of redisc.

Documentation

The godoc is the canonical source for documentation.

The design goal of redisc is to be as compatible as possible with the redigo package. As such, the Cluster type can be used as a drop-in replacement to a redis.Pool, and the connections returned by the cluster implement the redis.Conn interface. The package offers additional features specific to dealing with a cluster that may be needed for more advanced scenarios.

The main features are:

  • Drop-in replacement for redis.Pool (the Cluster type implements the same Get and Close method signatures).
  • Connections are redis.Conn interfaces and use the redigo package to execute commands, redisc only handles the cluster part.
  • Support for all cluster-supported commands including scripting, transactions and pub-sub.
  • Support for READONLY/READWRITE commands to allow reading data from replicas.
  • Client-side smart routing, automatically keeps track of which node holds which key slots.
  • Automatic retry of MOVED, ASK and TRYAGAIN errors when desired, via RetryConn.
  • Manual handling of redirections and retries when desired, via IsTryAgain and ParseRedir.
  • Automatic detection of the node to call based on the command's first parameter (assumed to be the key).
  • Explicit selection of the node to call via BindConn when needed.
  • Support for optimal batch calls via SplitBySlot.

Alternatives

Support

There are a number of ways you can support the project:

  • Use it, star it, build something with it, spread the word!
  • Raise issues to improve the project (note: doc typos and clarifications are issues too!)
    • Please search existing issues before opening a new one - it may have already been adressed.
  • Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
    • Make sure new code is tested.
    • Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.

If you desperately want to send money my way, I have a BuyMeACoffee.com page:

Buy Me A Coffee

License

The BSD 3-Clause license.

Documentation

Overview

Package redisc implements a redis cluster client on top of the redigo client package. It supports all commands that can be executed on a redis cluster, including pub-sub, scripts and read-only connections to read data from replicas. See http://redis.io/topics/cluster-spec for details.

Design

The package defines two main types: Cluster and Conn. Both are described in more details below, but the Cluster manages the mapping of keys (or more exactly, hash slots computed from keys) to a group of nodes that form a redis cluster, and a Conn manages a connection to this cluster.

The package is designed such that for simple uses, or when keys have been carefully named to play well with a redis cluster, a Cluster value can be used as a drop-in replacement for a redis.Pool from the redigo package.

Similarly, the Conn type implements redigo's redis.Conn interface, so the API to execute commands is the same - in fact the redisc package uses the redigo package as its only third-party dependency.

When more control is needed, the package offers some extra behaviour specific to working with a redis cluster:

  • Slot and SplitBySlot functions to compute the slot for a given key and to split a list of keys into groups of keys from the same slot, so that each group can safely be handled using the same connection.

  • *Conn.Bind (or the BindConn package-level helper function) to explicitly specify the keys that will be used with the connection so that the right node is selected, instead of relying on the automatic detection based on the first parameter of the command.

  • *Conn.ReadOnly (or the ReadOnlyConn package-level helper function) to mark a connection as read-only, allowing commands to be served by a replica instead of the master.

  • RetryConn to wrap a connection into one that automatically follows redirections when the cluster moves slots around.

  • Helper functions to deal with cluster-specific errors.

Cluster

The Cluster type manages a redis cluster and offers an interface compatible with redigo's redis.Pool:

Get() redis.Conn
Close() error

Along with some additional methods specific to a cluster:

Dial() (redis.Conn, error)
Refresh() error

If the CreatePool function field is set, then a redis.Pool is created to manage connections to each of the cluster's nodes. A call to Get returns a connection from that pool.

The Dial method, on the other hand, guarantees that the returned connection will not be managed by a pool, even if CreatePool is set. It calls redigo's redis.Dial function to create the unpooled connection, passing along any DialOptions set on the cluster. If the cluster's CreatePool field is nil, Get behaves the same as Dial.

The Refresh method refreshes the cluster's internal mapping of hash slots to nodes. It should typically be called only once, after the cluster is created and before it is used, so that the first connections already benefit from smart routing. It is automatically kept up-to-date based on the redis MOVED responses afterwards.

A cluster must be closed once it is no longer used to release its resources.

Connection

The connection returned from Get or Dial is a redigo redis.Conn interface, with a concrete type of *Conn. In addition to the interface's required methods, *Conn adds the following methods:

Bind(...string) error
ReadOnly() error

The returned connection is not yet connected to any node; it is "bound" to a specific node only when a call to Do, Send, Receive or Bind is made. For Do, Send and Receive, the node selection is implicit, it uses the first parameter of the command, and computes the hash slot assuming that first parameter is a key. It then binds the connection to the node corresponding to that slot. If there are no parameters for the command, or if there is no command (e.g. in a call to Receive), a random node is selected.

Bind is explicit, it gives control to the caller over which node to select by specifying a list of keys that the caller wishes to handle with the connection. All keys must belong to the same slot, and the connection must not already be bound to a node, otherwise an error is returned. On success, the connection is bound to the node holding the slot of the specified key(s).

Because the connection is returned as a redis.Conn interface, a type assertion must be used to access the underlying *Conn and to be able to call Bind:

redisConn := cluster.Get()
if conn, ok := redisConn.(*redisc.Conn); ok {
  if err := conn.Bind("my-key"); err != nil {
    // handle error
  }
}

The BindConn package-level function is provided as a helper for this common use-case.

The ReadOnly method marks the connection as read-only, meaning that it will attempt to connect to a replica instead of the master node for its slot. Once bound to a node, the READONLY redis command is sent automatically, so it doesn't have to be sent explicitly before use. ReadOnly must be called before the connection is bound to a node, otherwise an error is returned.

For the same reason as for Bind, a type assertion must be used to call ReadOnly on a *Conn, so a package-level helper function is also provided, ReadOnlyConn.

There is no ReadWrite method, because it can be sent as a normal redis command and will essentially end that connection (all commands will now return MOVED errors). If the connection was wrapped in a RetryConn call, then it will automatically follow the redirection to the master node (see the Redirections section).

The connection must be closed after use, to release the underlying resources.

Redirections

The redis cluster may return MOVED and ASK errors when the node that received the command doesn't currently hold the slot corresponding to the key. The package cannot reliably handle those redirections automatically because the redirection error may be returned for a pipeline of commands, some of which may have succeeded.

However, a connection can be wrapped by a call to RetryConn, which returns a redis.Conn interface where only calls to Do, Close and Err can succeed. That means pipelining is not supported, and only a single command can be executed at a time, but it will automatically handle MOVED and ASK replies, as well as TRYAGAIN errors.

Note that even if RetryConn is not used, the cluster always updates its mapping of slots to nodes automatically by keeping track of MOVED replies.

Concurrency

The concurrency model is similar to that of the redigo package:

  • Cluster methods are safe to call concurrently (like redis.Pool).

  • Connections do not support concurrent calls to write methods (Send, Flush) or concurrent calls to the read method (Receive).

  • Connections do allow a concurrent reader and writer.

  • Because the Do method combines the functionality of Send, Flush and Receive, it cannot be called concurrently with other methods.

  • The Bind and ReadOnly methods are safe to call concurrently, but there is not much point in doing so for as both will fail if the connection is already bound.

Example

Create and use a cluster.

package main

import (
	"log"
	"time"

	"github.com/gomodule/redigo/redis"
	"github.com/mna/redisc"
)

func main() {
	// create the cluster
	cluster := redisc.Cluster{
		StartupNodes: []string{":7000", ":7001", ":7002"},
		DialOptions:  []redis.DialOption{redis.DialConnectTimeout(5 * time.Second)},
		CreatePool:   createPool,
	}
	defer cluster.Close()

	// initialize its mapping
	if err := cluster.Refresh(); err != nil {
		log.Fatalf("Refresh failed: %v", err)
	}

	// grab a connection from the pool
	conn := cluster.Get()
	defer conn.Close()

	// call commands on it
	s, err := redis.String(conn.Do("GET", "some-key"))
	if err != nil {
		log.Fatalf("GET failed: %v", err)
	}
	log.Println(s)

	// grab a non-pooled connection
	conn2, err := cluster.Dial()
	if err != nil {
		log.Fatalf("Dial failed: %v", err)
	}
	defer conn2.Close()

	// make it handle redirections automatically
	rc, err := redisc.RetryConn(conn2, 3, 100*time.Millisecond)
	if err != nil {
		log.Fatalf("RetryConn failed: %v", err)
	}

	_, err = rc.Do("SET", "some-key", 2)
	if err != nil {
		log.Fatalf("SET failed: %v", err)
	}
}

func createPool(addr string, opts ...redis.DialOption) (*redis.Pool, error) {
	return &redis.Pool{
		MaxIdle:     5,
		MaxActive:   10,
		IdleTimeout: time.Minute,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", addr, opts...)
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}, nil
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BindConn

func BindConn(c redis.Conn, keys ...string) error

BindConn is a convenience function that checks if c implements a Bind method with the right signature such as the one for a *Conn, and calls that method. If c doesn't implement that method, it returns an error.

func IsCrossSlot

func IsCrossSlot(err error) bool

IsCrossSlot returns true if the error is a redis cluster error of type CROSSSLOT, meaning that a command was sent with keys from different slots.

func IsTryAgain

func IsTryAgain(err error) bool

IsTryAgain returns true if the error is a redis cluster error of type TRYAGAIN, meaning that the command is valid, but the cluster is in an unstable state and it can't complete the request at the moment.

func ReadOnlyConn

func ReadOnlyConn(c redis.Conn) error

ReadOnlyConn is a convenience function that checks if c implements a ReadOnly method with the right signature such as the one for a *Conn, and calls that method. If c doesn't implement that method, it returns an error.

func RetryConn

func RetryConn(c redis.Conn, maxAtt int, tryAgainDelay time.Duration) (redis.Conn, error)

RetryConn wraps the connection c (which must be a *Conn) into a connection that automatically handles cluster redirections (MOVED and ASK replies) and retries for TRYAGAIN errors. Only Do, Close and Err can be called on that connection, all other methods return an error.

The maxAtt parameter indicates the maximum number of attempts to successfully execute the command. The tryAgainDelay is the duration to wait before retrying a TRYAGAIN error.

Example

Automatically retry in case of redirection errors.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/gomodule/redigo/redis"
	"github.com/mna/redisc"
)

func createPool(addr string, opts ...redis.DialOption) (*redis.Pool, error) {
	return &redis.Pool{
		MaxIdle:     5,
		MaxActive:   10,
		IdleTimeout: time.Minute,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", addr, opts...)
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}, nil
}

func main() {
	// create the cluster
	cluster := redisc.Cluster{
		StartupNodes: []string{":7000", ":7001", ":7002"},
		DialOptions:  []redis.DialOption{redis.DialConnectTimeout(5 * time.Second)},
		CreatePool:   createPool,
	}
	defer cluster.Close()

	// initialize its mapping
	if err := cluster.Refresh(); err != nil {
		log.Fatalf("Refresh failed: %v", err)
	}

	// get a connection from the cluster
	conn := cluster.Get()
	defer conn.Close()

	// create the retry connection - only Do, Close and Err are
	// supported on that connection. It will make up to 3 attempts
	// to get a valid response, and will wait 100ms before a retry
	// in case of a TRYAGAIN redis error.
	retryConn, err := redisc.RetryConn(conn, 3, 100*time.Millisecond)
	if err != nil {
		log.Fatalf("RetryConn failed: %v", err)
	}

	// call commands
	v, err := retryConn.Do("GET", "key")
	if err != nil {
		log.Fatalf("GET failed: %v", err)
	}
	fmt.Println("GET returned ", v)
}
Output:

func Slot

func Slot(key string) int

Slot returns the hash slot for the key.

func SplitBySlot

func SplitBySlot(keys ...string) [][]string

SplitBySlot takes a list of keys and returns a list of list of keys, grouped by identical cluster slot. For example:

bySlot := SplitBySlot("k1", "k2", "k3")
for _, keys := range bySlot {
  // keys is a list of keys that belong to the same slot
}

Types

type Cluster

type Cluster struct {
	// StartupNodes is the list of initial nodes that make up
	// the cluster. The values are expected as "address:port"
	// (e.g.: "127.0.0.1:6379").
	StartupNodes []string

	// DialOptions is the list of options to set on each new connection.
	DialOptions []redis.DialOption

	// CreatePool is the function to call to create a redis.Pool for
	// the specified TCP address, using the provided options
	// as set in DialOptions. If this field is not nil, a
	// redis.Pool is created for each node in the cluster and the
	// pool is used to manage the connections returned by Get.
	CreatePool func(address string, options ...redis.DialOption) (*redis.Pool, error)
	// contains filtered or unexported fields
}

Cluster manages a redis cluster. If the CreatePool field is not nil, a redis.Pool is used for each node in the cluster to get connections via Get. If it is nil or if Dial is called, redis.Dial is used to get the connection.

func (*Cluster) Close

func (c *Cluster) Close() error

Close releases the resources used by the cluster. It closes all the pools that were created, if any.

func (*Cluster) Dial

func (c *Cluster) Dial() (redis.Conn, error)

Dial returns a connection the same way as Get, but it guarantees that the connection will not be managed by the pool, even if CreatePool is set. The actual returned type is *Conn, see its documentation for details.

func (*Cluster) Get

func (c *Cluster) Get() redis.Conn

Get returns a redis.Conn interface that can be used to call redis commands on the cluster. The application must close the returned connection. The actual returned type is *Conn, see its documentation for details.

func (*Cluster) Refresh

func (c *Cluster) Refresh() error

Refresh updates the cluster's internal mapping of hash slots to redis node. It calls CLUSTER SLOTS on each known node until one of them succeeds.

It should typically be called after creating the Cluster and before using it. The cluster automatically keeps its mapping up-to-date afterwards, based on the redis commands' MOVED responses.

func (*Cluster) Stats

func (c *Cluster) Stats() map[string]redis.PoolStats

Stats returns the current statistics for all pools. Keys are node's addresses.

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is a redis cluster connection. When returned by Get or Dial, it is not yet bound to any node in the cluster. Only when a call to Do, Send, Receive or Bind is made is a connection to a specific node established:

  • if Do or Send is called first, the command's first parameter is assumed to be the key, and its slot is used to find the node
  • if Receive is called first, or if Do or Send is called first but with no parameter for the command (or no command), a random node is selected in the cluster
  • if Bind is called first, the node corresponding to the slot of the specified key(s) is selected

Because Get and Dial return a redis.Conn interface, a type assertion can be used to call Bind or ReadOnly on this concrete Conn type:

redisConn := cluster.Get()
if conn, ok := redisConn.(*redisc.Conn); ok {
  if err := conn.Bind("my-key"); err != nil {
    // handle error
  }
}

Or call the package-level BindConn or ReadOnlyConn helper functions.

Example

Execute scripts.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/gomodule/redigo/redis"
	"github.com/mna/redisc"
)

func createPool(addr string, opts ...redis.DialOption) (*redis.Pool, error) {
	return &redis.Pool{
		MaxIdle:     5,
		MaxActive:   10,
		IdleTimeout: time.Minute,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", addr, opts...)
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}, nil
}

func main() {
	// create the cluster
	cluster := redisc.Cluster{
		StartupNodes: []string{":7000", ":7001", ":7002"},
		DialOptions:  []redis.DialOption{redis.DialConnectTimeout(5 * time.Second)},
		CreatePool:   createPool,
	}
	defer cluster.Close()

	// initialize its mapping
	if err := cluster.Refresh(); err != nil {
		log.Fatalf("Refresh failed: %v", err)
	}

	// create a script that takes 2 keys and 2 values, and returns 1
	var script = redis.NewScript(2, `
		redis.call("SET", KEYS[1], ARGV[1])
		redis.call("SET", KEYS[2], ARGV[2])
		return 1
	`)

	// get a connection from the cluster
	conn := cluster.Get()
	defer conn.Close()

	// bind it to the right node for the required keys, ahead of time
	if err := redisc.BindConn(conn, "scr{a}1", "src{a}2"); err != nil {
		log.Fatalf("BindConn failed: %v", err)
	}

	// script.Do, sends the whole script on first use
	v, err := script.Do(conn, "scr{a}1", "scr{a}2", "x", "y")
	if err != nil {
		log.Fatalf("script.Do failed: %v", err)
	}
	fmt.Println("Do returned ", v)

	// it is also possible to send only the hash, once it has been
	// loaded on that node
	if err := script.SendHash(conn, "scr{a}1", "scr{a}2", "x", "y"); err != nil {
		log.Fatalf("script.SendHash failed: %v", err)
	}
	if err := conn.Flush(); err != nil {
		log.Fatalf("Flush failed: %v", err)
	}

	// and receive the script's result
	v, err = conn.Receive()
	if err != nil {
		log.Fatalf("Receive failed: %v", err)
	}
	fmt.Println("Receive returned ", v)
}
Output:

func (*Conn) Bind

func (c *Conn) Bind(keys ...string) error

Bind binds the connection to the cluster node corresponding to the slot of the provided keys. If the keys don't belong to the same slot, an error is returned and the connection is not bound. If the connection is already bound, an error is returned. If no key is provided, it binds to a random node.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the connection.

func (*Conn) Do

func (c *Conn) Do(cmd string, args ...interface{}) (interface{}, error)

Do sends a command to the server and returns the received reply. If the connection is not yet bound to a cluster node, it will be after this call, based on the rules documented in the Conn type.

func (*Conn) Err

func (c *Conn) Err() error

Err returns a non-nil value if the connection is broken. Applications should close broken connections.

func (*Conn) Flush

func (c *Conn) Flush() error

Flush flushes the output buffer to the server.

func (*Conn) ReadOnly

func (c *Conn) ReadOnly() error

ReadOnly marks the connection as read-only, meaning that when it is bound to a cluster node, it will attempt to connect to a replica instead of the master and will automatically emit a READONLY command so that the replica agrees to serve read commands. Be aware that reading from a replica may return stale data. Sending write commands on a read-only connection will fail with a MOVED error. See http://redis.io/commands/readonly for more details.

If the connection is already bound to a node, either via a call to Do, Send, Receive or Bind, ReadOnly returns an error.

func (*Conn) Receive

func (c *Conn) Receive() (interface{}, error)

Receive receives a single reply from the server. If the connection is not yet bound to a cluster node, it will be after this call, based on the rules documented in the Conn type.

func (*Conn) Send

func (c *Conn) Send(cmd string, args ...interface{}) error

Send writes the command to the client's output buffer. If the connection is not yet bound to a cluster node, it will be after this call, based on the rules documented in the Conn type.

type RedirError

type RedirError struct {
	// Type indicates if the redirection is a MOVED or an ASK.
	Type string
	// NewSlot is the slot number of the redirection.
	NewSlot int
	// Addr is the node address to redirect to.
	Addr string
	// contains filtered or unexported fields
}

RedirError is a cluster redirection error. It indicates that the redis node returned either a MOVED or an ASK error, as specified by the Type field.

func ParseRedir

func ParseRedir(err error) *RedirError

ParseRedir parses err into a RedirError. If err is not a MOVED or ASK error or if it is nil, it returns nil.

func (*RedirError) Error

func (e *RedirError) Error() string

Error returns the error message of a RedirError. This is the message as received from redis.

Directories

Path Synopsis
Command ccheck implements the consistency checker redis cluster client as described in http://redis.io/topics/cluster-tutorial.
Command ccheck implements the consistency checker redis cluster client as described in http://redis.io/topics/cluster-tutorial.
Package redistest provides test helpers to manage a redis server.
Package redistest provides test helpers to manage a redis server.
resp
Package resp implements an efficient decoder for the Redis Serialization Protocol (RESP).
Package resp implements an efficient decoder for the Redis Serialization Protocol (RESP).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL