Packages

A Redix wrapper with a Poolboy-managed connection pool and typed read helpers.

Current section

Files

Jump to
cistern README.md
Raw

README.md

# Cistern
An Elixir library that wraps [Redix](https://github.com/whatyouhide/redix) with a [Poolboy](https://github.com/devinus/poolboy)-managed connection pool, providing a simple, typed API for common Redis operations.
Available on [Hex](https://hex.pm/packages/cistern).
## Features
- Connection pooling via Poolboy (configurable size and overflow)
- Type coercion on reads (configurable, on by default): `"true"`/`"false"``boolean`, numeric strings → `integer`
- High-level helpers: `get`, `set` (with optional TTL), `multiple`, `set_many`, `delete`, `delete_many`, `increment`
- Low-level escape hatches: `command/2` and `pipeline/2` for arbitrary Redis commands
- Fire-and-forget `noreply_pipeline/2` for write-heavy workloads
- Composable key validation — accepts binaries, integers, or iodata lists
## Requirements
- Erlang 26+
- Elixir 1.17+
- A running Redis instance
## Installation
Add `cistern` to your dependencies in `mix.exs`:
```elixir
{:cistern, "~> 0.1"}
```
Then fetch dependencies:
```bash
mix deps.get
```
## Configuration
Add the following to your `config/config.exs` (or environment-specific file):
```elixir
config :cistern,
host: "localhost",
port: 6379,
password: "", # omit or leave blank for no auth
pool_size: 10, # number of persistent connections
pool_max_overflow: 5, # extra connections allowed under load
pool_timeout: 5_000, # ms to wait for a free connection
sync_connect: true,
exit_on_disconnection: true,
coerce: true # global read coercion (default); override per call with `coerce:`
```
### Read coercion
On reads, `get/2` and `multiple/2` coerce values by default (`"true"`/`"false"`
`boolean`, numeric strings → `integer`). Control it at two levels:
- **Globally** via `config :cistern, coerce: false` — e.g. to keep zero-padded
codes or ids intact application-wide.
- **Per call** via the `coerce:` option, which overrides the global setting:
`Cistern.get("zip_key", coerce: false)`.
The resolution order is: per-call opt → global config → `true`.
### Runtime configuration (production)
For production it's common to read connection settings from environment
variables at boot via `config/runtime.exs`:
```elixir
import Config
if config_env() == :prod do
config :cistern,
host: System.fetch_env!("REDIS_HOST"),
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
password: System.get_env("REDIS_PASSWORD", ""),
pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE", "10"))
end
```
## Usage
### Starting under a Supervision tree
```elixir
children = [
Cistern,
# ...
]
Supervisor.start_link(children, strategy: :one_for_one)
```
Or start manually:
```elixir
{:ok, _pid} = Cistern.start_link()
```
### Basic operations
```elixir
# Store a value
{:ok, "bar"} = Cistern.set("foo", "bar")
# Store with a TTL (milliseconds)
{:ok, "bar"} = Cistern.set("foo", "bar", ttl: 10_000)
# Retrieve — strings are coerced to boolean or integer by default
{:ok, "bar"} = Cistern.get("foo")
{:ok, true} = Cistern.get("flag_key") # stored as "true"
{:ok, 42} = Cistern.get("count_key") # stored as "42"
{:ok, nil} = Cistern.get("missing")
# Opt out of coercion to keep the raw string (e.g. zero-padded codes)
{:ok, "01001"} = Cistern.get("zip_key", coerce: false) # stored as "01001"
# Bulk write
:ok = Cistern.set_many([{"foo", "bar"}, {"count", 1}])
:ok = Cistern.set_many([{"a", 1}, {"b", 2}], ttl: 60_000)
# Bulk read — coercion applies here too, and is overridable the same way
{:ok, ["bar", 1]} = Cistern.multiple(["foo", "count"])
{:ok, ["bar", "1"]} = Cistern.multiple(["foo", "count"], coerce: false)
# Atomic increment
{:ok, 2} = Cistern.increment("count")
# Delete
{:ok, 1} = Cistern.delete("foo") # returns count of deleted keys
{:ok, 2} = Cistern.delete_many(["a", "b"])
```
### Iodata keys
Keys can be iodata lists — they are joined into a single binary before being
sent to Redis. Non-binary elements (e.g. integer ids) are stringified and kept,
so `["user:", 1]` and `["user:", 2]` produce distinct keys:
```elixir
key = ["user:", user_id] # user_id may be a binary or an integer
Cistern.set(key, data)
Cistern.get(key)
Cistern.delete(key)
```
### Raw Redis commands
```elixir
# Single command
{:ok, "PONG"} = Cistern.command(["PING"])
{:ok, "OK"} = Cistern.command(["SET", "foo", "bar"])
# Pipelined commands (returns list of results)
{:ok, ["OK", "OK"]} = Cistern.pipeline([["SET", "a", 1], ["SET", "b", 2]])
# Fire-and-forget pipeline (no reply waited for)
:ok = Cistern.noreply_pipeline([["SET", "a", 1], ["SET", "b", 2]])
```
> **Note:** `noreply_pipeline/2` uses `CLIENT REPLY OFF` under the hood. If your Redis server or proxy does not support the `CLIENT` command, use `pipeline/2` instead.
## Testing
The library ships with a `RedixMock` test double that replaces the real Redix module via `Application.compile_env`. Configure it in `config/test.exs`:
```elixir
config :cistern, redis_module: RedixMock
```
Run the test suite:
```bash
mix test
```
Run with coverage:
```bash
mix test --cover
```
## Development
```bash
# Fetch deps
mix deps.get
# Start an interactive session
iex -S mix
# Static analysis
mix credo
# Generate docs
mix docs
```
## Architecture
```
Cistern — public API and type coercion
└── Cistern.Redis.Pool — Supervisor that owns the Poolboy pool
└── Cistern.Redis.Client — GenServer worker; one per connection
```
Each `command/2`, `pipeline/2`, or `noreply_pipeline/2` call checks out a connection from the pool, executes the command via Redix, and returns it — all within a single `:poolboy.transaction/3`.