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

The claim gateway's memory of what it has read: pack indexes, pack digests
and whole claim bodies, so a repeat read never reaches the object store.

## What is cached

  * a pack — the store and key it was found under, and its first bytes
    (`Ankusa.ClaimCheck.Pack`'s index), with how many bytes were asked for
    (`read`), so a short prefix (a small pack) is told apart from one that
    was never asked for that far. Every later claim of the pack is located
    without a store read. Once the gateway has checked a claim of the pack
    (`claim_check.integrity_check: :manifest`) the entry also holds the
    sha256 of every claim in it, 32 bytes each, taken from the pack's
    manifest: the rest of the pack's claims are checked without a store
    read. Kept 10 minutes.
  * a claim's whole bytes, when they are at most
    `claim_check.read_cache_max_entry_bytes`; larger claims are served and
    never inserted, and ranged reads are never cached. Kept 2 minutes.

Packs and claims are written once and never rewritten, so an entry is never
wrong while its pack exists. The only staleness is a pack swept by
retention: a cached index answers `HEAD` `200` for up to 10 minutes after
the sweep, a cached claim is served for up to 2, and a `GET` whose bytes
read answers `:not_found` forgets the pack (`Ankusa.ClaimCheck.read_bytes/5`).

## Memory

`claim_check.read_cache_bytes` bounds what is held, counted as each entry's
bytes plus a fixed per-entry overhead. The entries live in two generations,
each an ETS table: inserts go to the newer one, and when it holds half the
budget it becomes the older one and the previous older one is dropped whole.
A hit in the older generation is copied into the newer one (and counted
again), so an entry that keeps being read outlives the generation it was
written in, up to its own TTL. Every 10 minutes the generations turn over
regardless, so an idle cache empties within 20.

An insert reserves its bytes before it lands, and one that finds the newer
generation already full is dropped (the next read goes to the store), so a
generation never holds more than half the budget plus the one entry that
filled it; an entry larger than half the budget on its own is never
inserted. The insert that fills a generation asks this process for the
turnover, and this process also checks every second, in case that insert's
caller died first; inserts dropped until the turnover is handled are what
keeps the bound while it is pending.

`Ankusa.Routes.Cache` uses Nebulex; this cache can't. Nebulex bounds memory
by `:ets.info(tab, :memory)`, which counts a binary longer than 64 bytes as
the few words of its reference, never its bytes (a thousand cached 50 KiB
claims measure about 140 KB), and every claim and pack index here is such a
binary.

Instance-scoped: this process registers under
`Ankusa.via(instance, :claim_cache)` and owns the tables. When it isn't
running (no `:claim_check` role, or `read_cache_bytes: 0`) every lookup
misses and every insert is dropped.

# `pack`

```elixir
@type pack() :: %{
  :store =&gt; non_neg_integer(),
  :key =&gt; String.t(),
  :prefix =&gt; binary(),
  :read =&gt; pos_integer(),
  optional(:digests) =&gt; binary()
}
```

A located pack: the claim store it was found in, its key there, its first
bytes, and how many bytes were asked for (`prefix` is shorter only when the
object is); `digests` once its manifest has been read
(`Ankusa.ClaimCheck.Pack.digests/3`).

# `body`

```elixir
@spec body(atom(), String.t(), String.t(), non_neg_integer()) ::
  {:ok, binary()} | :error
```

The cached whole bytes of the claim at `index` of pack `pack_id`, if any.

# `child_spec`

```elixir
@spec child_spec(keyword()) :: Supervisor.child_spec()
```

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `digests`

```elixir
@spec digests(atom(), String.t(), String.t()) :: {:ok, binary()} | :error
```

The sha256 of every claim of a cached pack (`Ankusa.ClaimCheck.Pack.digests/3`), if known.

# `forget_pack`

```elixir
@spec forget_pack(atom(), String.t(), String.t()) :: :ok
```

Forget a pack (one that turned out to be gone), if it was cached.

# `pack`

```elixir
@spec pack(atom(), String.t(), String.t()) :: {:ok, pack()} | :error
```

The cached pack `pack_id` of `tenant_id`, if any.

# `put_body`

```elixir
@spec put_body(atom(), String.t(), String.t(), non_neg_integer(), binary()) :: :ok
```

Cache a claim's whole bytes, or do nothing if the cache isn't running. The
caller keeps them under `claim_check.read_cache_max_entry_bytes`.

# `put_digests`

```elixir
@spec put_digests(atom(), String.t(), String.t(), binary()) :: :ok
```

Add a pack's digests to its cached entry, counted against the same budget as
the entry itself. Nothing happens when the pack is not cached (the next read
of it reads the index again, and its manifest after it).

# `put_pack`

```elixir
@spec put_pack(atom(), String.t(), String.t(), pack()) :: :ok
```

Cache a located pack, or do nothing if the cache isn't running.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

---

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