RedisServerWrapper

Hex.pmDocs

Manage redis-server processes from Elixir -- single instances, clusters, and sentinel topologies with GenServer lifecycle management.

No Docker required. Just redis-server and redis-cli on your PATH.

Installation

Add redis_server_wrapper to your list of dependencies in mix.exs:

def deps do
[
{:redis_server_wrapper, "~> 0.7"}
]
end

Documentation

Prerequisites

You need redis-server and redis-cli installed and available on your PATH. The wrapper uses kill and lsof for process inspection and management when they are present, but normal server, cluster, and sentinel lifecycle operations degrade safely when minimal runtime images omit them. Occupied ports fail startup; the wrapper does not shut down or signal an existing listener to claim a port. Signal-based functions in RedisServerWrapper.Chaos still require kill.

# macOS
brew install redis
# Ubuntu/Debian
sudo apt-get install redis-server
# Verify
redis-server --version

Supported Redis versions and distributions

The supported Redis lines are 7.2, 7.4, 8.2, and the latest Redis 8.x release. CI runs the full standalone, Cluster, and Sentinel suite against a current patch release from each line, including a custom-module startup test. New patch releases within those lines are supported; older releases may work but are not part of the compatibility contract.

CI verifies Linux, Elixir 1.19 / OTP 27, and Elixir 1.20 / OTP 29. macOS is supported for local development and maintainer-tested; native Windows is not supported. See the support policy for the complete compatibility and platform contract.

Startup uses the core redis-server on PATH by default:

# Redis Open Source / core (default)
RedisServerWrapper.start_server(distribution: :core)
# Redis 8 full distribution; modules are managed by Redis itself
RedisServerWrapper.start_server(distribution: :full)
# Legacy Redis Stack only; enables sibling-module discovery
RedisServerWrapper.start_server(
distribution: :legacy_stack,
redis_server_bin: "/path/to/redis-stack/bin/redis-server"
)

An explicit :redis_server_bin always wins. Legacy Stack selection can also use the REDIS_LEGACY_STACK_SERVER_BIN environment variable. The wrapper never silently switches distributions, and it only discovers sibling Stack modules when distribution: :legacy_stack is explicit. Caller-provided :loadmodule entries work with every distribution.

Quick Start

Single Server

{:ok, server} = RedisServerWrapper.start_server(port: 6400, password: "secret")
RedisServerWrapper.Server.ping(server)
#=> true
RedisServerWrapper.Server.run(server, ["SET", "key", "value"])
#=> {:ok, "OK"}
RedisServerWrapper.Server.run(server, ["GET", "key"])
#=> {:ok, "value"}
RedisServerWrapper.Server.stop(server)

Cluster

{:ok, cluster} = RedisServerWrapper.start_cluster(masters: 3, base_port: 7100)
RedisServerWrapper.Cluster.healthy?(cluster)
#=> true
RedisServerWrapper.Cluster.node_addrs(cluster)
#=> ["127.0.0.1:7100", "127.0.0.1:7101", "127.0.0.1:7102"]
RedisServerWrapper.Cluster.stop(cluster)

Sentinel

{:ok, sentinel} = RedisServerWrapper.start_sentinel(
master_port: 6390,
replicas: 2,
sentinels: 3
)
RedisServerWrapper.Sentinel.healthy?(sentinel)
#=> true
RedisServerWrapper.Sentinel.master_addr(sentinel)
#=> "127.0.0.1:6390"
RedisServerWrapper.Sentinel.stop(sentinel)

Persistent Instances (Manager)

The Manager tracks instances across IEx sessions using a JSON state file:

RedisServerWrapper.Manager.start_basic(name: "dev-redis", port: 6400)
#=> {:ok, %{name: "dev-redis", url: "redis://:password@127.0.0.1:6400", ...}}
RedisServerWrapper.Manager.list()
#=> [%{name: "dev-redis", type: :basic, ...}]
RedisServerWrapper.Manager.credentials("dev-redis")
#=> {:ok, %{password: "password", url: "redis://:password@127.0.0.1:6400"}}
RedisServerWrapper.Manager.stop("dev-redis")

Manager-generated passwords come from the operating system's cryptographic random source. Console output redacts credentials; use Manager.credentials/1 when plaintext access is intentional. The Manager state directory and credential-bearing Redis configuration directories are created with mode 0700, while state and configuration files use 0600. State updates are atomic and serialized across connected BEAM nodes. Independent, unconnected BEAM instances must not write to the same Manager state file concurrently. Invalid state is moved aside as an instances.json.corrupt-* recovery copy instead of being overwritten.

Manager state contains plaintext credentials. Review security and data safety before sharing a state path or binding an instance beyond loopback.

Managed Process Lifecycle

Server.start_link/1 accepts a :managed option controlling how the redis-server OS process is tied to the BEAM:

If a managed Redis process exits unexpectedly, its Server GenServer now exits with an abnormal {:redis_server_exit, backend, reason} reason. A normal OTP supervisor therefore restarts the complete Server child instead of retaining a live GenServer with a missing OS process. Port and Forcola backends follow the same contract.

Cluster and Sentinel make topology loss explicit:

Cluster formation, replica linkage, and Sentinel discovery use bounded polling instead of fixed startup sleeps. Set convergence_timeout: milliseconds to override the default (the value of :timeout). Timeout errors include the last observed health state to make transitional or malformed Redis output diagnosable.

Custom Modules

Use :loadmodule to load one or more Redis modules. A plain path is enough for a module with defaults; use {path, [args]} when it takes load-time arguments. The structured form quotes every path and argument safely in the generated redis.conf.

module_path = System.fetch_env!("EVENT_STREAM_MODULE")
{:ok, server} =
RedisServerWrapper.start_server(
port: 6460,
loadmodule: [
{module_path, ["events", "expired,set", "maxlen", "1000"]}
]
)
RedisServerWrapper.Server.run(server, ["MODULE", "LIST"])

For a cluster, the same modules are loaded into every node. For a Sentinel topology, they are loaded into the master and every replica (not the Sentinel control processes):

RedisServerWrapper.start_cluster(
base_port: 7100,
loadmodule: [
{module_path, ["cluster-streams", "per-node"]}
]
)

The original raw directive form remains supported for compatibility, such as loadmodule: ["/path/module.so events expired"], but the structured form is recommended when arguments are present.

The repository compiles a minimal module and runs both its integration test and the shipped generic example on every Redis version in CI:

REDIS_MODULE=/absolute/path/to/module.so \
REDIS_MODULE_NAME=my_module \
mix run examples/module_loading.exs

For a complete event-stream demo using redis-event-stream-module:

EVENT_STREAM_MODULE=/absolute/path/to/libredis_event_stream_module.so \
mix run examples/event_stream.exs

Redis modules are native libraries and must match the target OS, architecture, Redis Module API, and Redis version. Loading is CI-verified with the repository's minimal module; the external event-stream module must be built separately. See external module support.

Configuration

RedisServerWrapper.Config is a curated typed subset of Redis configuration, not a claim to model every Redis directive. Server, Cluster, and Sentinel add operational lifecycle/topology options, while :extra is the escape hatch for untyped directives. The configuration reference lists every accepted option and calls out fields that are generated, overridden, or intentionally unavailable on a topology.

All values are encoded as individual Redis tokens, including paths, passwords, module arguments, and :extra directives. Use a string for a single extra argument or a list for a multi-argument directive. Extras cannot override wrapper-owned lifecycle or transport settings such as port, bind, daemonize, pidfile, or dir.

RedisServerWrapper.start_server(
port: 6400,
password: "secret",
maxmemory: "256mb",
maxmemory_policy: "allkeys-lru",
extra: [
{"notify-keyspace-events", "KEA"},
{"client-output-buffer-limit", ["pubsub", "32mb", "8mb", "60"]},
{"hz", "100"}
]
)

bind may be one address, a whitespace-separated string, or a list of listen addresses. control_host independently selects the address used by redis-cli, readiness probes, replication, Cluster, and Sentinel:

RedisServerWrapper.start_server(
port: 6400,
bind: ["127.0.0.1", "::1"],
control_host: "127.0.0.1"
)

TCP, TLS, Unix sockets, and ACL authentication

Lifecycle operations use a shared RedisServerWrapper.Connection, so startup readiness, commands, health checks, shutdown, Cluster, Sentinel, and Manager all retain the same endpoint and credentials.

Set username with password to configure and use a named ACL user. Without a username, password retains its password-only requirepass behavior:

RedisServerWrapper.start_server(
port: 6400,
username: "demo-app",
password: "secret"
)

Disable TCP with port: 0 for a Unix-only server:

RedisServerWrapper.start_server(
port: 0,
unixsocket: "/tmp/demo-redis.sock",
unixsocketperm: "700"
)

For a TLS-only server, set port: 0, a tls_port, and the server identity. The CA file or directory is also used by redis-cli for verification:

RedisServerWrapper.start_server(
port: 0,
tls_port: 6400,
tls_cert_file: "/certs/server.crt",
tls_key_file: "/certs/server.key",
tls_ca_cert_file: "/certs/ca.crt",
tls_auth_clients: "no",
tls_server_name: "redis.demo.test"
)

Use tls_ca_cert_dir instead of tls_ca_cert_file for a CA directory. Mutual-TLS clients can set tls_client_cert_file and tls_client_key_file. tls_insecure: true explicitly disables redis-cli certificate verification and is intended only for controlled tests.

Cluster and Sentinel use TLS-only nodes when tls: true; pass the same certificate options:

RedisServerWrapper.start_cluster(
masters: 3,
base_port: 7100,
tls: true,
tls_cert_file: "/certs/server.crt",
tls_key_file: "/certs/server.key",
tls_ca_cert_file: "/certs/ca.crt",
tls_auth_clients: "no"
)

The wrapper enables tls-cluster or tls-replication as required. Sentinel also configures sentinel auth-user/auth-pass for named ACL credentials. Unix sockets are a standalone-server transport; Cluster and Sentinel reject them immediately because their nodes must communicate over network ports.

Chaos / Fault Injection

The Chaos module provides fault injection primitives for testing resilience:

alias RedisServerWrapper.{Chaos, Cluster, Server}
{:ok, cluster} = RedisServerWrapper.start_cluster(
masters: 3,
replicas_per_master: 1,
base_port: 7100
)
# Kill the master owning a key's hash slot
{:ok, killed} = Chaos.kill_master(cluster, "user:123")
# Kill by slot number
{:ok, killed} = Chaos.kill_master(cluster, 5000)
# Freeze a node (SIGSTOP) and resume later
{:ok, os_pid} = Chaos.freeze_node(server)
# ... test timeout/retry behavior ...
Chaos.resume_node(os_pid)
# Freeze with automatic resume after a duration
Chaos.pause_node(server, 5_000)
# Simulate a network partition
nodes = Cluster.nodes(cluster)
{active, isolated} = Enum.split(nodes, 2)
{:ok, frozen_pids} = Chaos.partition(cluster, [active, isolated])
# Recover all frozen nodes
Chaos.recover(frozen_pids)
# Other tools
Chaos.slow_down(server, 2_000) # CLIENT PAUSE
Chaos.flushall(server) # wipe all data
Chaos.fill_memory(server, 10_000) # fill with 1KB dummy keys
Chaos.trigger_save(server) # force BGSAVE
Chaos.random_kill(cluster) # kill a random node
Chaos.trigger_failover(replica) # CLUSTER FAILOVER on a replica

Use Cases

License

Licensed under either of

at your option. See LICENSE for details.