# `Ankusa.Routes.Cache`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/routes/cache.ex#L1)

The decision cache: `{epoch, method, segments}` → matched route id, or the
reason no route matched.

Pattern routes can't be found by one key lookup, so the guard would otherwise
scan the compiled pattern list on every request. Caching the **decision**
turns the common case — the same provider POSTing to the same path over and
over — into one ETS read.

## Only matching is cached

IP decisions are not cached: they are per-client-address, and the rule list is
a short in-memory scan of bitmasks. Caching them would mean a key space
proportional to the number of distinct senders, for a saving smaller than the
cache lookup itself.

## Epoch-keyed, plus a TTL

The key carries the snapshot's `epoch`: a number `Ankusa.Routes.Snapshot`
draws afresh for every snapshot it publishes and never reuses. Publishing a
snapshot therefore makes every earlier entry unreachable at once — no delete
pass, no invalidation broadcast.

The epoch, not the store's `version`, is the key because versions *are*
reused: an in-memory store that restarts starts over at 1, and a Redis that is
flushed starts its counter again, and either would let an entry cached against
the old table answer for the new one. Stale entries age out on their own TTL
and are collected by the adapter's generational sweep; nothing depends on them
expiring, so the TTL only has to bound memory.

A cached rejection uses a shorter TTL than a cached match: a rejection is what
a scanner hitting random paths produces, and there are far more of those than
there are real routes.

## Bounded keys

The key holds the request's own path, which the sender chooses. Only requests
of at most 16 segments and 256 bytes of path are cached
(see `cacheable?/1`); anything longer is matched by a scan and never stored, so
a scanner sending long random paths can't fill the cache with large keys.

## Why Nebulex

`nebulex_local` is a generation-based ETS cache with `max_size`; the
alternative was hand-rolling eviction, TTL expiry, and a sweep timer for the
same thing. It enforces `max_size` in a periodic memory check
(`gc_memory_check_interval`, one second here) rather than on every write, so
the size is approximate between checks — and an evicted entry only costs a
re-scan, never a wrong decision. Core carries the dependency because every
deployment that turns routes on wants it — the Redis *store* is the part that
stays in an adapter package, since only multi-node deployments need it.

Instance-scoped: the cache process registers under
`Ankusa.via(instance, :routes_cache)`, and the Nebulex API is addressed with
that process's pid (Nebulex resolves a cache by atom or pid, and a
`Registry`-based name is neither).

# `decision`

```elixir
@type decision() :: {:match, String.t()} | {:reject, :no_route | :method}
```

# `cacheable?`

```elixir
@spec cacheable?([String.t()]) :: boolean()
```

Is this request path short enough to be a cache key? The limits are on the
segment count and the summed segment bytes, walked without allocating.

# `count_all`

# `count_all`

# `count_all!`

# `count_all!`

# `decr`

# `decr`

# `decr!`

# `decr!`

# `delete`

# `delete`

# `delete!`

# `delete!`

# `delete_all`

# `delete_all`

# `delete_all!`

# `delete_all!`

# `expire`

# `expire`

# `expire!`

# `expire!`

# `fetch`

# `fetch`

# `fetch!`

# `fetch!`

# `fetch_or_store`

# `fetch_or_store`

# `fetch_or_store!`

# `fetch_or_store!`

# `generations`

A convenience function for retrieving the current generations.

# `get`

# `get`

# `get!`

# `get!`

# `get_all`

# `get_all`

# `get_all!`

# `get_all!`

# `get_and_update`

# `get_and_update`

# `get_and_update!`

# `get_and_update!`

# `get_or_store`

# `get_or_store`

# `get_or_store!`

# `get_or_store!`

# `has_key?`

# `has_key?`

# `in_transaction?`

# `in_transaction?`

# `incr`

# `incr`

# `incr!`

# `incr!`

# `info`

# `info`

# `info!`

# `info!`

# `lookup`

```elixir
@spec lookup(atom(), pos_integer(), String.t(), [String.t()]) ::
  {:ok, decision()} | :error
```

Look up the cached decision for this exact request shape.

# `new_generation`

A convenience function for creating new generations.

# `newer_generation`

A convenience function for retrieving the newer generation.

# `put`

# `put`

# `put!`

# `put!`

# `put_all`

# `put_all`

# `put_all!`

# `put_all!`

# `put_new`

# `put_new`

# `put_new!`

# `put_new!`

# `put_new_all`

# `put_new_all`

# `put_new_all!`

# `put_new_all!`

# `register_event_listener`

# `register_event_listener`

# `register_event_listener!`

# `register_event_listener!`

# `replace`

# `replace`

# `replace!`

# `replace!`

# `reset_gc_interval`

A convenience function for reset the GC interval.

# `store`

```elixir
@spec store(
  atom(),
  pos_integer(),
  String.t(),
  [String.t()],
  decision(),
  pos_integer()
) :: :ok
```

Cache a decision for `ttl_ms`, or do nothing if the cache isn't running.

# `stream`

# `stream`

# `stream!`

# `stream!`

# `take`

# `take`

# `take!`

# `take!`

# `touch`

# `touch`

# `touch!`

# `touch!`

# `transaction`

# `transaction`

# `ttl`

# `ttl`

# `ttl!`

# `ttl!`

# `unregister_event_listener`

# `unregister_event_listener`

# `unregister_event_listener!`

# `unregister_event_listener!`

# `update`

# `update`

# `update!`

# `update!`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
