Redix.Cluster (Redix v1.6.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 (one per cluster node), uses ETS for fast slot lookups, and mostly mirrors the Redix API.

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 (just like a single Redix connection postpones commands while it's connecting), so the common "start the cluster, issue a command right away" pattern works without retries. If that first attempt fails, commands return {:error, %Redix.ConnectionError{reason: :closed}} 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 a connection to each replica, issuing READONLY on it) 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 (yet).
  • 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 (ETS tables, Registry, connection supervisor) 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.

  • :read_from_replicas (boolean/0) - if true, the cluster also opens (and supervises) a connection 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.

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).

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.