Cache.RefreshAhead (elixir_cache v0.4.6)

View Source

Refresh-ahead caching strategy that proactively refreshes values before they expire.

Values are stored with a timestamp. On get, if the value is within the refresh window (i.e. now - inserted_at >= ttl - refresh_before), the current value is returned immediately and an async Task is spawned to refresh it.

Only keys that are actively read get refreshed — unread keys naturally expire from the underlying adapter.

Usage

defmodule MyApp.Cache do
  use Cache,
    adapter: {Cache.RefreshAhead, Cache.Redis},
    name: :my_cache,
    opts: [
      uri: "redis://localhost:6379",
      refresh_before: :timer.seconds(30)
    ]

  def refresh(key) do
    {:ok, fetch_fresh_value(key)}
  end
end

Options

  • :refresh_before (pos_integer/0) - Required. Milliseconds before TTL expiry at which to trigger a background refresh.

  • :on_refresh - Optional refresh callback. If not provided, the cache module must define refresh/1.

  • :lock_node_whitelist - Optional node whitelist for distributed refresh locks. Defaults to all connected nodes.

How It Works

  1. put/5 wraps the value as {value, inserted_at_ms, ttl_ms} before delegating.
  2. get/4 unwraps the tuple. If now - inserted_at >= ttl - refresh_before, a background Task is spawned to call the refresh callback with the key.
  3. A per-cache ETS deduplication table (:<name>_refresh_tracker) prevents multiple concurrent refresh tasks for the same key.
  4. On successful refresh, put/5 is called with the new value and the same TTL, resetting the inserted_at timestamp.

Note: When sandbox?: true, values are stored unwrapped. Refresh logic is bypassed entirely.