RedisServerWrapper
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
Prerequisites
You need redis-server and redis-cli installed and available on your PATH.
# macOS
brew install redis
# Ubuntu/Debian
sudo apt-get install redis-server
# Verify
redis-server --version
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.stop("dev-redis")
Managed Process Lifecycle
Server.start_link/1 accepts a :managed option controlling how the
redis-server OS process is tied to the BEAM:
managed: true(default) -redis-serverruns as a foreground Port. Teardown runs interminate/2, which OTP skips on a:brutal_killsupervisor shutdown or a hard BEAM death (SIGKILL, OOM), so the OS process can be stranded on those paths, keeping its port bound.managed: :forcola-redis-serverruns in the foreground under aForcola.Daemon, whose Rust shim guarantees the OS process group is killed and confirmed dead on owner death or supervisor shutdown, including the paths whereterminate/2never runs. This requires the optional dependency and is opt-in:# mix.exs{:forcola, "~> 0.3"}{:ok, server} = RedisServerWrapper.Server.start_link(port: 6400, managed: :forcola)Without
:forcolaon the path,start_linkreturns{:error, :forcola_not_available}. Forcola is POSIX-only and ships precompiled, SHA256-verified shim binaries, so it needs no Rust toolchain at build time.managed: false-redis-serverdaemonizes independently; the caller owns the OS process lifecycle.
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.
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
Configuration
All Redis configuration directives can be set via the Config struct options.
Use the :extra option as an escape hatch for any directive not covered by
the typed fields:
RedisServerWrapper.start_server(
port: 6400,
password: "secret",
maxmemory: "256mb",
maxmemory_policy: "allkeys-lru",
extra: [
{"notify-keyspace-events", "KEA"},
{"hz", "100"}
]
)
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
- Testing -- spin up real Redis instances in ExUnit setup/teardown
- Development -- run Redis alongside your app without system services
- CI -- no Docker layer needed, just install redis-server
- Cluster/Sentinel testing -- create full topologies in a single function call
- Resilience testing -- fault injection with the Chaos module (kill nodes, simulate partitions, inject latency)
License
Licensed under either of
- MIT License
- Apache License, Version 2.0
at your option. See LICENSE for details.