Redix.Cluster (Redix v1.8.0)

Copy Markdown View Source

Main API for using Redis Cluster.

For a higher-level guide, see the Cluster guide.

The design of this module follows the same low-level philosophy as the rest of Redix: it builds on existing Redix connections, uses ETS for fast slot lookups, and mostly mirrors the Redix API. By default it opens one connection per cluster node, but that's tunable via options.

Usage

You can start connections to a Redis Cluster similarly to how you'd start a single Redix connection, but specifying a list of seed nodes and a name:

{:ok, cluster} =
  Redix.Cluster.start_link(
    name: :my_cluster,
    nodes: ["redis://localhost:7000"]
  )

Redix.Cluster.command(:my_cluster, ["SET", "mykey", "myvalue"])
#=> {:ok, "OK"}
Redix.Cluster.command(:my_cluster, ["GET", "mykey"])
#=> {:ok, "myvalue"}

Like single-node Redix connections, the cluster starts even if no seed node is currently reachable: the topology is discovered in the background, retrying with exponential backoff. Commands issued while the initial discovery attempt is in flight wait for it to complete, but if that first attempt fails, commands return an error until a seed node becomes reachable. Pass sync_connect: true to start_link/1 to block until the topology has been discovered instead.

Pipelines

Pipelines that span multiple hash slots are transparently split across nodes, executed in parallel, and reassembled in the original order:

Redix.Cluster.pipeline(:my_cluster, [
  ["SET", "key1", "a"],
  ["SET", "key2", "b"],
  ["GET", "key1"],
  ["GET", "key2"]
])
#=> {:ok, ["OK", "OK", "a", "b"]}

Transactions

MULTI/EXEC transactions require all keys to be in the same hash slot. Use hash tags to ensure this:

Redix.Cluster.transaction_pipeline(:my_cluster, [
  ["SET", "{user:1}.name", "Alice"],
  ["SET", "{user:1}.email", "alice@example.com"]
])

Reading from replicas

By default all commands are routed to primaries. To allow reads from replicas, start the cluster with read_from_replicas: true (which opens and supervises connections to each replica, issuing READONLY on them) and pass a :route option per call:

Redix.Cluster.start_link(
  name: :my_cluster,
  nodes: ["redis://localhost:7000"],
  read_from_replicas: true
)

# Read from a replica for the key's slot, failing if none is reachable.
Redix.Cluster.command(:my_cluster, ["GET", "mykey"], route: :replica)

# Prefer a replica but fall back to the primary if none is reachable.
Redix.Cluster.command(:my_cluster, ["GET", "mykey"], route: :prefer_replica)

See command/3 for the full list of :route values.

Limitations

  • Only database 0 is supported (Redis Cluster does not support SELECT).
  • Pub/Sub is not supported through the cluster interface.
  • The :noreply_* functions are not supported in cluster mode.

Telemetry

Redix.Cluster emits cluster-specific Telemetry events for topology changes and redirections. See Redix.Telemetry for details.

Summary

Types

A node endpoint.

Functions

Returns a child spec for use in supervision trees.

Issues a command on the Redis Cluster.

Same as command/3 but raises on errors.

Issues a pipeline of commands on the Redis Cluster.

Starts a connection to a Redis Cluster.

Stops the cluster and all its connections.

Executes a MULTI/EXEC transaction on the Redis Cluster.

Types

endpoint()

(since 1.6.0)
@type endpoint() :: String.t() | [host: String.t(), port: :inet.port_number()]

A node endpoint.

It can be either a Redis URI string (such as "redis://localhost:7000") or a keyword list with :host and :port keys (such as [host: "localhost", port: 7000]).

Only the host and port of a seed are used to discover the cluster, with one exception: the rediss:// scheme enables TLS for every node connection. Credentials (user:pass@) and a non-zero database in a seed URI are not supported and raise. See start_link/1.

Functions

child_spec(opts)

(since 1.6.0)
@spec child_spec(keyword()) :: Supervisor.child_spec()

Returns a child spec for use in supervision trees.

Examples

children = [
  {Redix.Cluster, name: :my_cluster, nodes: ["redis://localhost:7000"]}
]

command(cluster, command, opts \\ [])

(since 1.6.0)
@spec command(atom(), Redix.command(), keyword()) ::
  {:ok, Redix.Protocol.redis_value()}
  | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()}

Issues a command on the Redis Cluster.

The command is routed to the correct node based on the key's hash slot. If the command has no key (PING), it is sent to a random node.

Returns {:ok, response} or {:error, reason}.

Options

  • :timeout - request timeout in milliseconds. Defaults to 5_000.

  • :route - where to send the command. One of:

    • :primary (default) - always route to the slot's primary.
    • :replica - route to a replica for the slot, failing with a connection error if none is reachable.
    • :prefer_replica - route to a replica if one is reachable, otherwise fall back to the primary.

    :replica and :prefer_replica require the cluster to have been started with read_from_replicas: true. Use them only for reads: a write routed to a replica is transparently redirected (via MOVED) to the primary. Keyless commands, such as PING or INFO, always go to a random primary, even when called with route: :replica | :prefer_replica.

Examples

Redix.Cluster.command(:my_cluster, ["SET", "mykey", "foo"])
#=> {:ok, "OK"}

Redix.Cluster.command(:my_cluster, ["GET", "mykey"])
#=> {:ok, "foo"}

Redix.Cluster.command(:my_cluster, ["GET", "mykey"], route: :prefer_replica)
#=> {:ok, "foo"}

command!(cluster, command, opts \\ [])

(since 1.6.0)
@spec command!(atom(), Redix.command(), keyword()) :: Redix.Protocol.redis_value()

Same as command/3 but raises on errors.

pipeline(cluster, commands, opts \\ [])

(since 1.6.0)
@spec pipeline(atom(), [Redix.command()], keyword()) ::
  {:ok, [Redix.Protocol.redis_value()]}
  | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()}

Issues a pipeline of commands on the Redis Cluster.

Commands are grouped by target node based on key hash slots, sent in parallel to the respective nodes, and results are reassembled in the original order.

Returns {:ok, results} or {:error, reason}.

If commands is an empty list ([]) then an ArgumentError exception is raised right away, matching Redix.pipeline/3.

Partial failures across nodes

When a pipeline spans multiple nodes and only some of those nodes fail (for example, it times out or its task crashes), the call still returns {:ok, results}. The positions of the commands routed to a failed node are filled with the relevant error value (a Redix.ConnectionError, or a Redix.Error for a Redis-level error) at their original indices, while the results from nodes that succeeded stay visible. This extends Redix's "errors are values" philosophy to the cross-node connection-error case, so a failure affecting one node no longer discards the work that committed on the others.

This is a deliberate extension of Redix.pipeline/3, which fails the whole call with {:error, reason} on a connection error. There is no partial-failure analog for a single node, so a pipeline that targets a single node (all keys in one slot group) still returns {:error, reason} on a connection error, matching Redix.pipeline/3.

Options

  • :timeout - request timeout in milliseconds. Defaults to 5_000.

  • :route - where to send the pipeline. See command/3 for the accepted values. The option applies to the whole pipeline: every command uses the same routing choice.

Examples

Redix.Cluster.pipeline(:my_cluster, [["SET", "a", "1"], ["SET", "b", "2"]])
#=> {:ok, ["OK", "OK"]}

pipeline!(cluster, commands, opts \\ [])

(since 1.6.0)
@spec pipeline!(atom(), [Redix.command()], keyword()) :: [
  Redix.Protocol.redis_value()
]

Same as pipeline/3 but raises on errors.

start_link(opts)

(since 1.6.0)
@spec start_link(keyword()) :: Supervisor.on_start()

Starts a connection to a Redis Cluster.

Options

These are cluster-specific options:

  • :name (atom/0) - Required. An atom to register the cluster process under. All internal resources are named deterministically based on this name. You use this name to issue commands:

    Redix.Cluster.start_link(name: :my_cluster, nodes: [...])
    Redix.Cluster.command(:my_cluster, ["GET", "key"])
  • :nodes (non-empty list of endpoint/0) - Required. A non-empty list of seed nodes to connect to. Only one reachable node is needed: the full cluster topology is discovered automatically via CLUSTER SLOTS.

  • :sync_connect (boolean/0) - Whether to discover the initial cluster topology before or after start_link/1 returns. When false (the default), start_link/1 returns right away and the topology is fetched in the background, retrying with exponential backoff (driven by the :backoff_initial and :backoff_max options) until a seed node answers. Commands issued while the initial discovery attempt is in flight wait for it to complete (up to their :timeout); once that attempt fails, they return {:error, %Redix.ConnectionError{reason: :closed}} until a seed node becomes reachable. When true, start_link/1 blocks until the topology has been fetched, and returns an error if no seed node is reachable. This mirrors the :sync_connect option of Redix.start_link/1. The default value is false.

  • :topology_refresh_interval (timeout/0) - How often (in milliseconds) to refresh the cluster topology. The default value is 30000.

  • :primary_pool_size (pos_integer/0) - The number of connections to open to each primary node in the cluster. During normal routing, commands from one caller process use the same connection in a node's pool while that connection is available. Redirects keep the original caller choice. If the selected connection is unavailable, Redix uses another live pool member. Workloads with few caller processes might not use all pool members. Redis is single-threaded anyways, so use this only if it makes sense for your workload (and possibly after benchmarking!). Available since 1.7.0. The default value is 1.

  • :replica_pool_size (pos_integer/0) - The number of connections to open to each replica node when read_from_replicas: true is set. This is independent from :primary_pool_size, because primary and replica traffic can need different pool sizes. Redis is single-threaded anyways, so use this only if it makes sense for your workload (and possibly after benchmarking!). Available since 1.7.0. The default value is 1.

  • :read_from_replicas (boolean/0) - If true, the cluster also opens (and supervises) :replica_pool_size connections to every replica node discovered via CLUSTER SLOTS, issuing READONLY on each so it can serve reads. This is required to use route: :replica or route: :prefer_replica (see command/3). Defaults to false, in which case only primaries are connected. The default value is false.

  • :address_mapper - A (host, port -> {host, port}) function applied to every node address the cluster announces, both in CLUSTER SLOTS replies and in MOVED/ASK redirections, before connecting to it. Seed :nodes are dialed as given. Use this when the addresses the cluster advertises are not reachable from the client, such as a cluster running in Docker that announces container IPs, or a NAT'd deployment:

    address_mapper: fn _host, port -> {"127.0.0.1", port} end

    Available since 1.8.0.

    The default value is nil.

All other standard Redix connection options (:password, :ssl, :socket_opts, :timeout, and so on) are passed through to each underlying node connection. The exceptions are :sentinel and :exit_on_disconnection, which are not supported in cluster mode (the cluster supervises node connections and handles disconnections itself).

Resources

Each cluster uses:

  • Two ETS tables.
  • One Registry (which starts another ETS table).
  • :primary_pool_size connections for each primary node.
  • :replica_pool_size connections for each replica node when read_from_replicas: true is set.

TLS and credentials in seed URIs

The full topology is discovered from a seed node, and every node connection (the seeds and the nodes discovered from them) is made with a single shared configuration. Only the host and port of a seed are used to reach it, so the other parts of a seed URI are handled specially rather than silently dropped:

  • The rediss:// scheme enables TLS for every node connection (equivalent to ssl: true), since TLS in a cluster is all-or-nothing. Combining a rediss:// seed with an explicit ssl: false raises an error, so a rediss:// seed is never silently downgraded to plain TCP.

  • Credentials in a seed URI (redis://user:pass@host) raise an error: every node is authenticated with the shared configuration, so pass :username and :password as connection options instead.

  • A non-zero database in a seed URI raises an error, as a cluster only supports database 0.

Examples

Redix.Cluster.start_link(
  name: :my_cluster,
  nodes: ["redis://localhost:7000", "redis://localhost:7001"]
)

Redix.Cluster.start_link(
  name: :my_cluster,
  nodes: [[host: "redis1.example.com", port: 6379]],
  password: "secret"
)

stop(cluster, timeout \\ 60000)

(since 1.6.0)
@spec stop(atom(), timeout()) :: :ok

Stops the cluster and all its connections.

Examples

Redix.Cluster.stop(:my_cluster)

transaction_pipeline(cluster, commands, opts \\ [])

(since 1.6.0)
@spec transaction_pipeline(atom(), [Redix.command()], keyword()) ::
  {:ok, [Redix.Protocol.redis_value()]}
  | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()}

Executes a MULTI/EXEC transaction on the Redis Cluster.

All commands must target the same hash slot (use hash tags to ensure this). Returns {:error, %Redix.Error{message: "CROSSSLOT" <> _}} if commands span multiple slots. At least one command must contain a key to route the transaction on; a pipeline of only keyless commands (such as PING) returns an error since there's no slot to send it to.

Transactions always run on the slot's primary; passing route: anything other than :primary raises an ArgumentError.

Like command/3 and pipeline/3, a "transaction" follows MOVED/ASK redirections: since all commands target one slot, a redirect means the whole transaction belongs on another node, so the entire MULTI/EXEC is re-run there (bounded by the internal redirection limit). A MOVED also triggers a reactive topology refresh, so a transaction-only workload self-heals after a failover instead of failing until the next periodic refresh.

Options

  • :timeout - request timeout in milliseconds. Defaults to 5_000.

Examples

Redix.Cluster.transaction_pipeline(:my_cluster, [
  ["SET", "{user:1}.name", "Alice"],
  ["SET", "{user:1}.email", "alice@example.com"]
])
#=> {:ok, ["OK", "OK"]}

transaction_pipeline!(cluster, commands, opts \\ [])

(since 1.6.0)
@spec transaction_pipeline!(atom(), [Redix.command()], keyword()) :: [
  Redix.Protocol.redis_value()
]

Same as transaction_pipeline/3 but raises on errors.