EDA.Cache.Adapter behaviour (EDA v0.5.0-beta.2)

Copy Markdown View Source

Storage backend for EDA's caches.

EDA's caches are split in two: this behaviour owns where entities are stored, while EDA.Cache.Guild and friends own which ones are stored and how many — admission policy (EDA.Cache.Policy), size limits and eviction (EDA.Cache.Evictor), and telemetry.

That split is deliberate. A backend only has to answer "where does this live"; it never reimplements admission filtering or memory bounds, because those sit above it. Every adapter — including one you write yourself — inherits them for free.

Configuration

config :eda, cache_adapter: EDA.Cache.Adapter.ETS   # default
config :eda, cache_adapter: EDA.Cache.Adapter.NoOp  # store nothing

The adapter is resolved once at startup by EDA.Cache.Config and read from :persistent_term, so lookups on the hot path cost nothing.

Key shapes

Caches use one of two key shapes, and an adapter must support both:

  • a flat binary id — guilds and users;
  • a composite {guild_id, id} tuple — channels, members, roles, presences and voice states. match_prefix/2 and delete_prefix/2 exist to serve "everything in this guild" without scanning the whole table.

Channels and roles additionally keep a secondary index table mapping a bare id back to its guild, so they can be looked up without knowing the guild. An index is just another table from the adapter's point of view — put/3 and get/2 serve it.

Writing an adapter

Implement every callback. init/2 is called once per table at startup; the rest must be safe to call from any process, since EDA reads caches directly from the caller rather than through a GenServer.

defmodule MyApp.RedisCache do
  @behaviour EDA.Cache.Adapter
  # ...
end

Summary

Types

A flat id or a {guild_id, id} tuple.

The logical table name, e.g. :eda_guilds.

A cached entity, as a raw string-keyed map, or an index value.

Callbacks

Every value in the table.

How many entries the table holds.

Removes one key. Removing an absent key is not an error.

Removes every entry whose key is {prefix, _}, returning the second element of each key removed.

Fetches one value, or nil.

Prepares a table. Called once per table at startup.

Every value whose key is {prefix, _}.

Stores a value, replacing any existing one.

Functions

The configured adapter module.

Types

key()

@type key() :: term()

A flat id or a {guild_id, id} tuple.

table()

@type table() :: atom()

The logical table name, e.g. :eda_guilds.

value()

@type value() :: term()

A cached entity, as a raw string-keyed map, or an index value.

Callbacks

all(table)

@callback all(table()) :: [value()]

Every value in the table.

count(table)

@callback count(table()) :: non_neg_integer()

How many entries the table holds.

delete(table, key)

@callback delete(table(), key()) :: :ok

Removes one key. Removing an absent key is not an error.

delete_prefix(table, term)

@callback delete_prefix(table(), term()) :: [term()]

Removes every entry whose key is {prefix, _}, returning the second element of each key removed.

The return value lets a cache clean up its secondary index without a second scan.

get(table, key)

@callback get(table(), key()) :: value() | nil

Fetches one value, or nil.

init(table, keyword)

@callback init(
  table(),
  keyword()
) :: :ok

Prepares a table. Called once per table at startup.

match_prefix(table, term)

@callback match_prefix(table(), term()) :: [value()]

Every value whose key is {prefix, _}.

Only meaningful for composite-key tables; a flat-key table may return [].

put(table, key, value)

@callback put(table(), key(), value()) :: :ok

Stores a value, replacing any existing one.

Functions

current()

@spec current() :: module()

The configured adapter module.

Resolved at startup into :persistent_term; falls back to EDA.Cache.Adapter.ETS.