Ankusa.ClaimCheck.Cache (ankusa v0.7.0)

Copy Markdown View Source

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.

Summary

Types

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).

Functions

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

Returns a specification to start this module under a supervisor.

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

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

The cached pack pack_id of tenant_id, if any.

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.

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).

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

Types

pack()

@type pack() :: %{
  :store => non_neg_integer(),
  :key => String.t(),
  :prefix => binary(),
  :read => pos_integer(),
  optional(:digests) => 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).

Functions

body(instance, tenant_id, pack_id, index)

@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(init_arg)

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

Returns a specification to start this module under a supervisor.

See Supervisor.

digests(instance, tenant_id, pack_id)

@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(instance, tenant_id, pack_id)

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

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

pack(instance, tenant_id, pack_id)

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

The cached pack pack_id of tenant_id, if any.

put_body(instance, tenant_id, pack_id, index, bin)

@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(instance, tenant_id, pack_id, digests)

@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(instance, tenant_id, pack_id, pack)

@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(opts)

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