README.md

# Vela

A distributed cache library for Elixir with pluggable backends, topologies, and near-cache support.

## Features

- **Pluggable backends** — ETS (in-memory), DETS (disk-persistent), Redis (external)
- **Pluggable topologies** — Local, Replicated, Partitioned (consistent hash ring)
- **Near cache** — local ETS read-through layer in front of any topology
- **Stampede protection** — only one process fetches on a cache miss, others wait
- **Tag-based invalidation** — bulk-evict groups of entries across all topologies
- **TTL management** — per-entry TTL with background sweep
- **Telemetry** — built-in instrumentation for all operations
- **Stats** — lock-free atomic counters for hits, misses, writes, deletes

## Installation

Add `vela` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:vela, "~> 0.1.0"}
  ]
end
```

## Quick Start

```elixir
defmodule MyApp.Cache do
  use Vela.Cache,
    backend: Vela.Backend.ETS,
    default_ttl: :timer.minutes(5)
end
```

Add it to your supervision tree:

```elixir
children = [
  MyApp.Cache
]
```

Use it:

```elixir
MyApp.Cache.put(:user_1, %{name: "Alice"})
MyApp.Cache.get(:user_1)
# => {:ok, %{name: "Alice"}}

MyApp.Cache.get(:missing)
# => {:error, :not_found}
```

## API

```elixir
get(key, opts \\ [])                    # {:ok, value} | {:error, :not_found}
get!(key, opts \\ [])                   # value | raises KeyError
put(key, value, opts \\ [])             # :ok | {:error, reason}
delete(key, opts \\ [])                 # :ok
exists?(key, opts \\ [])               # boolean
flush(opts \\ [])                       # :ok
size()                                  # integer
stats()                                 # %{hits: n, misses: n, writes: n, ...}
get_or_fetch(key, fetch_fn, opts \\ []) # {:ok, value} | {:error, reason}
invalidate_tag(tag, opts \\ [])         # {:ok, evicted_count}
```

### Options

- `ttl:` — time-to-live in milliseconds, or `:infinity` (default: cache's `default_ttl`)
- `tags:` — list of atoms for group invalidation (e.g., `tags: [:users, :active]`)

## Backends

### ETS (default)

In-memory, fastest. Data lost on restart.

```elixir
use Vela.Cache, backend: Vela.Backend.ETS
```

### DETS

Disk-persistent. Survives restarts. Slower than ETS.

```elixir
use Vela.Cache,
  backend: Vela.Backend.DETS,
  backend_opts: [data_dir: "/var/data/my_cache"]
```

### Redis

External Redis server. Requires the `:redix` dependency.

```elixir
# Add to deps: {:redix, "~> 1.3"}

use Vela.Cache,
  backend: Vela.Backend.Redis,
  backend_opts: [url: "redis://localhost:6379"]
```

## Topologies

### Local (default)

Single node. No distribution. Fastest.

```elixir
use Vela.Cache, topology: Vela.Topology.Local
```

### Replicated

Every node holds a full copy. Writes broadcast to all nodes. Reads are always local.

Best for: small datasets, read-heavy workloads, feature flags, config caches.

```elixir
use Vela.Cache, topology: Vela.Topology.Replicated
```

### Partitioned

Each key lives on one node, determined by a consistent hash ring. Reads for remote keys use RPC.

Best for: large datasets that don't fit on a single node.

```elixir
use Vela.Cache, topology: Vela.Topology.Partitioned
```

The hash ring updates automatically when nodes join or leave the cluster.

## Near Cache

Adds a local ETS read-through layer (L1) in front of the real topology (L2). Hot keys are served from local memory without hitting the network.

```elixir
use Vela.Cache,
  backend: Vela.Backend.Redis,
  topology: Vela.Topology.Local,
  near_cache: true,
  near_cache_l1_ttl: :timer.seconds(30)
```

Read flow: L1 hit -> return | L1 miss -> L2 -> promote to L1 -> return.

## Stampede Protection

When a cached value expires and many processes request it simultaneously, only one fetches from the source. Others wait and read from cache.

```elixir
MyApp.Cache.get_or_fetch(:expensive_key, fn _key ->
  {:ok, MyApp.Repo.get_expensive_data()}
end)
```

Enabled by default. Configure with:

```elixir
use Vela.Cache,
  stampede_protection: true,
  stampede_timeout: 5_000  # max wait time in ms
```

## Tag-Based Invalidation

Group related entries with tags, then invalidate them in bulk:

```elixir
MyApp.Cache.put(:user_1, alice, tags: [:users])
MyApp.Cache.put(:user_2, bob, tags: [:users])
MyApp.Cache.put(:product_1, widget, tags: [:products])

MyApp.Cache.invalidate_tag(:users)
# => {:ok, 2}  — both user entries removed, product untouched
```

Works across all topologies. Replicated and Partitioned broadcast the invalidation to all nodes.

## Telemetry Events

All events are prefixed with `[:vela, :cache]` by default (configurable via `telemetry_prefix`).

| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:vela, :cache, :get, :stop]` | `duration` | `cache, key, result` |
| `[:vela, :cache, :put, :stop]` | `duration` | `cache, key, ttl` |
| `[:vela, :cache, :fetch, :stop]` | `duration` | `cache, key, result` |
| `[:vela, :cache, :invalidate_tag, :stop]` | `count` | `cache, tag` |

## Configuration

All options with defaults:

```elixir
use Vela.Cache,
  backend: Vela.Backend.ETS,
  backend_opts: [],
  topology: Vela.Topology.Local,
  topology_opts: [],
  default_ttl: :infinity,
  max_size: :infinity,
  eviction_policy: :ttl_only,
  stampede_protection: true,
  stampede_timeout: 5_000,
  stats_enabled: true,
  telemetry_prefix: [:vela, :cache],
  sweep_interval: 30_000,
  near_cache: false,
  near_cache_l1_ttl: 60_000
```

Options can be overridden at runtime:

```elixir
MyApp.Cache.start_link(default_ttl: :timer.minutes(10))
```

## Multi-Node Setup

Vela reacts to BEAM node connections. Use [libcluster](https://github.com/bitwalker/libcluster) for automatic node discovery in production.

```elixir
# Connect nodes manually:
Node.connect(:"node2@hostname")

# Or use libcluster in your supervision tree
```

Once nodes are connected, Replicated and Partitioned topologies work automatically.

## Benchmarks

Run benchmarks with:

```bash
mix run benchmarks/get_bench.exs
mix run benchmarks/topology_bench.exs
```

## Testing

```bash
# Unit tests
mix test

# Including distributed multi-node tests
elixir --sname primary -S mix test --include distributed
```

## License

MIT