AudioProxy.VariantCache (audio_proxy v0.7.0)

Copy Markdown View Source

Serving a variant that is already in the store — everything a client can observe about a cache HIT.

AudioProxy.VariantStore owns where the bytes are; this module owns what happens when they turn out to be there. The split is the same one the API doc draws: a store is a backend detail, a HIT is a contract.

Two lines, and only two

lookup/1 is the HIT check — a AudioProxy.VariantStore.head/1 behind the "is there a store at all" question, answering :miss for both "unconfigured" and "not stored", because the render path does the same thing either way. serve/3 turns an entry into a response.

serve/3 can still answer :miss: an entry can be evicted between the head and the read, and nothing has been sent at that point, so the request falls through to a render rather than failing.

Everything after the response head is committed, and there the only signal left is an abnormal close — the same one, for the same reason, as a render that fails after its 200 (§5). Two things reach it. A store read that raises propagates and the adapter tears the connection down. A store read that ends short would otherwise complete normally, so write/3 counts the bytes and exits when they fall short of the Content-Length already promised; see the comment there for why a well-formed short body is the worse outcome of the two.

Proxy mode declares a length

A stored variant has a known size, and declaring it is the entire point of caching one: Content-Length plus Accept-Ranges: bytes is what makes seeking, resumption and progress reporting work, none of which a chunked MISS can offer. Bandit streams a length-delimited body when the content-length header is set before Plug.Conn.send_chunked/2, so the bytes still leave as they are read — declared length and progressive delivery are not a trade here.

Range handling is single-range only. A multipart/byteranges response would buy nothing a media client asks for, and RFC 9110 §14.2 permits ignoring a Range header outright, which is what a multi-range or malformed one gets. A syntactically valid range that no byte of the variant can satisfy is a 416 — the one status this module produces that is not a HIT's body.

The redirect is not the variant

Redirect mode answers 302 with a short-lived presigned URL — no longer lived than the requesting URL itself, per AudioProxy.Expiry — and Cache-Control: no-store. The no-store is load-bearing rather than cautious: the redirect's Location is a credential with an expiry, and a cached 302 hands out URLs that have already expired. The immutable cache-control belongs to the variant bytes, which the store serves under its own headers — the ones the write-back stored, so a followed redirect delivers the same Content-Type and Cache-Control a proxied HIT would have sent.

Backends without :presign never take this path. AudioProxy.Config refuses that combination at boot, so the runtime check here is a belt on top of a brace — and a presign that fails despite it proxies instead, because the bytes are readable and the client asked for audio, not for a URL.

What the client contract does not depend on

Not the backend, and not the serve mode. Both modes deliver the same bytes under the same Content-Type, Cache-Control and ETag, and both are range-capable — proxy mode because this module implements it, redirect mode because the store does. What does change the observable response is the cache state: the same URL is a chunked, non-seekable 200 while it is being rendered and a length-declared, range-capable one afterwards. That is stated in §5 as a contract clients must not assume their way around.

Summary

Types

What lookup/1 found, if anything.

Functions

Reports whether the variant named by key is in the store, whole.

Serves a stored variant, per AP_SERVE_MODE.

Types

hit()

@type hit() :: {:ok, AudioProxy.VariantStore.entry()} | :miss

What lookup/1 found, if anything.

Functions

lookup(key)

@spec lookup(AudioProxy.VariantStore.key()) :: hit()

Reports whether the variant named by key is in the store, whole.

:miss covers every reason a request must render: no store configured, nothing stored under the key, an entry whose metadata never landed.

serve(conn, key, entry)

Serves a stored variant, per AP_SERVE_MODE.

:miss means the entry disappeared before a byte was sent and the caller should render — see the moduledoc. Any returned conn is sent and halted.