Cache.MultiLayer (elixir_cache v0.5.0)

Copy Markdown View Source

Multi-layer caching strategy that cascades through multiple cache layers.

Keys are read from fastest to slowest, with automatic backfill on cache hits from slower layers. Writes go slowest-first to avoid polluting fast layers with data that failed to persist in slow ones.

Usage

Pass a list of layers as the strategy config. Each element can be:

  • A module that implements Cache (already running, not supervised by this adapter)
  • An adapter module (e.g. Cache.ETS) — will be auto-started and supervised
  • A tuple {AdapterModule, opts} — adapter with inline opts
defmodule MyApp.LayeredCache do
  use Cache,
    adapter: {Cache.MultiLayer, [Cache.ETS, MyApp.RedisCache]},
    name: :layered_cache,
    opts: []
end

__MODULE__ in Layers

You may include __MODULE__ in the layer list to position the current module's own underlying cache within the chain. If __MODULE__ is omitted, no local cache is created for the defining module—it acts as a pure facade.

defmodule MyApp.LayeredCache do
  use Cache,
    adapter: {Cache.MultiLayer, [Cache.ETS, __MODULE__, MyApp.RedisCache]},
    name: :layered_cache,
    opts: [uri: "redis://localhost"]
end

Read Behaviour

Layers are iterated fastest → slowest (list order). On a hit from layer N, the value is backfilled into layers 1..N-1.

Write Behaviour

Layers are written slowest → fastest (reverse list order). If a slow write fails, the write stops and an error is returned — preventing polluting faster layers with potentially-unsaved data.

Fetch Callback (Optional)

If all layers miss, an optional fetch callback can supply the value. The fetched value is then backfilled into all layers.

Define it as a module callback or pass it via opts:

defmodule MyApp.LayeredCache do
  use Cache,
    adapter: {Cache.MultiLayer, [Cache.ETS, MyApp.RedisCache]},
    name: :layered_cache,
    opts: [on_fetch: &__MODULE__.fetch/1]

  def fetch(key) do
    {:ok, "value_for_#{key}"}
  end
end

Cross-Node Coherence (Optional)

Node-local fast layers (e.g. Cache.ETS) go stale on every node except the writer. Setting broadcast_mode keeps them coherent: after a successful put/delete, every other node running this cache (tracked via :pg — see Cache.MultiLayer.Coordinator) is notified and applies the change to its own broadcast_layers.

defmodule MyApp.LayeredCache do
  use Cache,
    adapter: {Cache.MultiLayer, [MyApp.EtsLayer, MyApp.RedisCache]},
    name: :layered_cache,
    opts: [
      backfill_ttl: :timer.seconds(30),
      broadcast_mode: :invalidate,
      broadcast_layers: [MyApp.EtsLayer]
    ]
end

:invalidate sends only the key — remote nodes drop their local entry and lazily re-read through the shared layer. :replicate ships the value so remote local layers are updated immediately; use it only for small values. Delivery is best-effort: always keep backfill_ttl (and layer TTLs) as the correctness floor for members that miss a message.

Options

  • :on_fetch - Optional fetch callback invoked on total cache miss. Receives the key, returns {:ok, value} or {:error, reason}.

  • :backfill_ttl - TTL in milliseconds to use when backfilling layers on a hit from a slower layer. Defaults to nil (no expiry).

  • :broadcast_mode - Cross-node coherence for writes: :invalidate deletes the key from other nodes' broadcast_layers; :replicate pushes the written value to them. Best-effort delivery.

  • :broadcast_layers (list of atom/0) - Node-local layer modules the broadcast applies to on other nodes. Required with broadcast_mode; must not include the shared (slowest) layer.