AudioProxy.VariantStore behaviour (audio_proxy v0.4.0)

Copy Markdown View Source

The contract a variant store backend implements, and the dispatch to the configured one.

A completed render is written back under its cache key so the next request for the same variant is served from storage instead of re-rendered. Where that storage is — a local directory, an S3 bucket — is a backend behind this behaviour, selected by the scheme of AP_VARIANT_STORE:

scheme     module                          slice
file://    AudioProxy.VariantStore.Local   add-variant-store
s3://      AudioProxy.VariantStore.S3      add-s3-variant-store

The shape deliberately mirrors AudioProxy.Source.Type: one module per scheme, five callbacks, and a new backend is a registration rather than an edit to the render path. Cache behaviour never depends on where source audio comes from — a file:// store caches renders of s3:// sources and vice versa.

The callbacks, in the order a request meets them

  • head/1 — is this variant stored, and what would serving it need to know? The HIT check.
  • get_stream/2 — the bytes, or a Range slice of them, as a lazy stream. Proxy-mode serving.
  • presign/2 — a URL the client can be redirected to. Redirect-mode serving, and only for backends that advertise it (see below).
  • put_stream/3 — the write-back: bytes plus the response metadata they must be served with.
  • capabilities/0 — what this backend can do beyond the mandatory four.

Metadata travels with the bytes

put_stream/3 takes a metadata/0 because a store that keeps only bytes cannot serve a redirected fetch correctly: a player handed application/octet-stream may refuse to decode it. Every backend persists the metadata; a backend that cannot attach it to a direct fetch (the redirect case) simply does not advertise :presign, and AudioProxy.Config refuses AP_SERVE_MODE=redirect against it at boot.

Atomic or absent

A put_stream/3 that does not run to completion — the render failed, the write errored — must leave nothing readable under the key. Each backend implements that with whatever its storage offers: the local backend stages into a temp file inside the store and File.rename/2s on completion; the S3 backend gets it from the protocol, since an object does not exist until its upload completes and a failed multipart is aborted. head/1 answering {:ok, _} therefore always means the whole variant, with its metadata.

Summary

Types

What a backend can do beyond the mandatory callbacks.

The parsed AP_VARIANT_STORE value, as AudioProxy.Config holds it.

What head/1 reports about a stored variant.

The cache key a variant is stored under — see AudioProxy.CacheKey.

The response headers a stored variant must be served with: its content type, the immutable cache-control the render endpoint would have sent, and the ETag (the quoted cache key). Stored alongside the bytes so a store-direct fetch serves correctly without the proxy in the path.

An inclusive byte range ({first, last}, RFC 9110 §14), or nil for the whole variant. A last past the end is truncated, as Range semantics ask; a first past the end is :invalid_range.

Callbacks

The optional abilities this backend has. See capability/0.

Streams the stored variant's bytes, or the Range slice of them.

Reports whether the variant named by key is stored, whole, with its metadata.

Produces a URL a client can fetch the stored variant from directly.

Writes a variant: consumes chunks (an enumerable of binaries) to completion, then commits bytes and metadata under key — atomically, so a reader sees the whole variant or nothing.

Functions

The configured backend module. Callers must check configured?/0 first.

The backend module for a parsed store value.

capabilities/0 of the configured backend.

Reports whether AP_VARIANT_STORE is configured at all.

get_stream/2 on the configured backend.

head/1 on the configured backend.

presign/2 on the configured backend.

put_stream/3 on the configured backend.

Types

capability()

@type capability() :: :presign

What a backend can do beyond the mandatory callbacks.

  • :presignpresign/2 produces working URLs, which is what redirect serving needs. A backend without it is proxy-mode only.

config()

@type config() :: {:file, Path.t()} | {:s3, String.t()} | {:module, module()}

The parsed AP_VARIANT_STORE value, as AudioProxy.Config holds it.

{:module, backend} is the test seam described at backend_for/1, not a value any environment can produce.

entry()

@type entry() :: %{size: non_neg_integer(), metadata: metadata()}

What head/1 reports about a stored variant.

key()

@type key() :: String.t()

The cache key a variant is stored under — see AudioProxy.CacheKey.

metadata()

@type metadata() :: %{
  content_type: String.t(),
  cache_control: String.t(),
  etag: String.t()
}

The response headers a stored variant must be served with: its content type, the immutable cache-control the render endpoint would have sent, and the ETag (the quoted cache key). Stored alongside the bytes so a store-direct fetch serves correctly without the proxy in the path.

range()

@type range() :: {non_neg_integer(), non_neg_integer()} | nil

An inclusive byte range ({first, last}, RFC 9110 §14), or nil for the whole variant. A last past the end is truncated, as Range semantics ask; a first past the end is :invalid_range.

Callbacks

capabilities()

@callback capabilities() :: [capability()]

The optional abilities this backend has. See capability/0.

get_stream(key, range)

@callback get_stream(key(), range()) ::
  {:ok, Enumerable.t()} | {:error, :not_found | :invalid_range}

Streams the stored variant's bytes, or the Range slice of them.

The stream is lazy and bounded-memory: chunks, never the whole variant at once.

head(key)

@callback head(key()) :: {:ok, entry()} | {:error, :not_found}

Reports whether the variant named by key is stored, whole, with its metadata.

presign(key, keyword)

@callback presign(
  key(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

Produces a URL a client can fetch the stored variant from directly.

{:error, :unsupported} from a backend without the :presign capability.

put_stream(key, t, metadata)

@callback put_stream(key(), Enumerable.t(), metadata()) :: :ok | {:error, term()}

Writes a variant: consumes chunks (an enumerable of binaries) to completion, then commits bytes and metadata under key — atomically, so a reader sees the whole variant or nothing.

If chunks raises — which is how the write-back tee signals a render that failed or was cancelled mid-stream — the write is aborted, anything staged is discarded, and the exception comes back as {:error, exception}.

Functions

backend()

@spec backend() :: module()

The configured backend module. Callers must check configured?/0 first.

backend_for(arg)

@spec backend_for(config()) :: module()

The backend module for a parsed store value.

Used by AudioProxy.Config to validate the serve mode against the backend's capabilities at boot, before anything is stored.

capabilities()

@spec capabilities() :: [capability()]

capabilities/0 of the configured backend.

configured?()

@spec configured?() :: boolean()

Reports whether AP_VARIANT_STORE is configured at all.

get_stream(key, range)

@spec get_stream(key(), range()) ::
  {:ok, Enumerable.t()} | {:error, :not_found | :invalid_range}

get_stream/2 on the configured backend.

head(key)

@spec head(key()) :: {:ok, entry()} | {:error, :not_found}

head/1 on the configured backend.

presign(key, opts \\ [])

@spec presign(
  key(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

presign/2 on the configured backend.

put_stream(key, chunks, metadata)

@spec put_stream(key(), Enumerable.t(), metadata()) :: :ok | {:error, term()}

put_stream/3 on the configured backend.