Where route definitions live. This module is both the behaviour every
store implements and the instance-scoped facade the context calls, the
same split as Ankusa.SourceStore: the facade resolves
config.routes.store's {module, opts} and delegates.
Two stores
Ankusa.Routes.Store.ETS— the default. Definitions are node-local and vanish on restart;config.routes.seedis how a standalone deployment gets a durable starting point, and it is applied on every boot.Ankusa.Routes.Store.Redis(inankusa_redis) — definitions live in Redis and a version counter plus pub/sub invalidates every node. The guard still reads the in-memory snapshot, never Redis.
Definitions, never evictions
A store holds definitions, so insert/3 is capped by
config.routes.max_routes and returns {:error, :too_many_routes} past it —
nothing is silently evicted. An evicted definition would turn into a rejected
webhook, and a rejected webhook is an operator's outage, not a cache miss.
Writes are version-checked
Ankusa.Routes validates a write — a free id, no colliding enabled route —
against the snapshot it just read, so a store may only apply it if that
snapshot is still current. insert/3 and replace/3 therefore take the
version of the snapshot the caller validated against, and answer
{:error, :stale}, having changed nothing, when the table has moved on. Before
answering :stale a store MUST have published the newer snapshot, so the
caller's retry validates against what is really there.
A store shared by several nodes checks that version in the shared store, atomically with the write, not against its own copy: a node whose copy lags would otherwise happily validate against a table another node already changed, and two nodes could both create the same id.
delete/3 and put_ip_rules/2 carry no cross-route validation, so they take
no version: a delete answers {:error, :not_found} from the store's own
authoritative state, and replacing the global rules is a whole-value write.
A delete made on behalf of one tenant passes it as tenant, and the store
answers {:error, :not_found} for a route another tenant owns, checked in
the same step as the delete, so a route that was re-created under another
tenant between the caller's read and the delete is never removed; nil
deletes whoever owns it.
{:error, :store_unavailable} is the transient failure — Redis is down, or the
store process did not answer, which the facade reports the same way. The
in-memory store never returns it itself.
Summary
Callbacks
Called by Ankusa.Instance with [instance: instance]; read the store's own
options from Ankusa.config(instance).routes.store.
Types
Callbacks
@callback insert(atom(), Ankusa.Routes.Route.t(), pos_integer()) :: :ok | {:error, error()}
@callback replace(atom(), Ankusa.Routes.Route.t(), pos_integer()) :: :ok | {:error, error()}
@callback start_link(keyword()) :: GenServer.on_start()
Called by Ankusa.Instance with [instance: instance]; read the store's own
options from Ankusa.config(instance).routes.store.
Functions
@spec insert(atom(), Ankusa.Routes.Route.t(), pos_integer()) :: :ok | {:error, error()}
@spec replace(atom(), Ankusa.Routes.Route.t(), pos_integer()) :: :ok | {:error, error()}