falaw.cache#

Content-addressed cache for fal calls.

Why this exists: the directorial workflow (“a single edit, the scene re-renders around it”) only works if unchanged beats don’t re-render. A cache hit is the difference between a 30-second edit-and-preview loop and a 5-minute one.

Design:

  • Key = SHA256 of (model_id, sorted(arguments_json)). Two structurally identical fal calls collapse to the same key.

  • Value = the raw fal response, plus optional locally-downloaded asset paths. We persist both the JSON manifest and the binary downloads.

  • Cache root: $FALAW_CACHE_DIR or $FALAW_DATA_DIR/cache or ~/.config/falaw/cache.

  • The cache is per-process aware via lru_cache for hot lookups; on-disk for persistence across runs.

The key arguments are not always the wire arguments. A chained plan (generate_image → image_to_video) sends fal a URL for the upstream image, but a URL is minted fresh per upload — so keying on it guarantees a miss the moment the upstream is genuinely re-executed, and the miss re-bills the expensive downstream call for unchanged work. cached_call_fal() therefore takes an optional key_arguments: the same call with upstream references resolved to content hashes (sha256:<hex>) instead of URLs. See falaw.content for how those hashes are produced.

Usage:

from falaw.cache import cached_call_fal raw = cached_call_fal(“fal-ai/flux/dev”, {“prompt”: “…”})

The wrapped variant has the same signature as core.call_fal but skips the network when the (model_id, arguments) tuple was seen before.

Module Attributes

ASSETS_DIRNAME

Sub-directory of the falaw cache holding materialize_asset() copies.

Functions

cache_get(application, arguments, *[, ...])

Return the raw fal response if cached, else None.

cache_put(application, arguments, raw, *[, ...])

Persist a fal response.

cache_stats()

Quick summary of the cache: entry count, disk usage, and where it went.

cached_call_fal(application, arguments, *[, ...])

Call a fal model, but reuse the cached response when present.

drop_cache_entry(application, arguments, *)

Delete the cache entry for (application, arguments).

emit_cache_hit(application[, on_event])

Emit the synthetic cache_hit progress event for application.

materialize_asset(url, *[, key_hint, store, ...])

Download a remote asset to the cache and return the local path.

falaw.cache.ASSETS_DIRNAME = 'assets'#

Sub-directory of the falaw cache holding materialize_asset() copies.

Named here rather than in falaw.prune because _asset_path() is what decides the layout; the prune side reads this so the two cannot drift.

falaw.cache.cache_get(application, arguments, *, backend='fal', key_extra=None)[source]#

Return the raw fal response if cached, else None.

Return type:

Optional[dict]

falaw.cache.cache_put(application, arguments, raw, *, note='', wire_arguments=None, backend='fal', key_extra=None)[source]#

Persist a fal response. Returns the entry directory path.

Parameters:
  • application (str) – fal model id.

  • arguments (Mapping[str, Any]) – the arguments the entry is keyed on.

  • raw (dict) – the fal response to store.

  • note (str) – free-form label recorded in the manifest.

  • wire_arguments (Optional[Mapping[str, Any]]) – the arguments actually sent to fal, when they differ from the key arguments (chained calls send URLs but are keyed on content hashes). Recorded for debugging only — it never affects the key.

  • backend (str) – which execution backend produced raw (falaw#15). Joins the cache key only when non-default, same rule as falaw.canonical.cache_key_payload(); recorded in the manifest under the same condition, for debugging.

Return type:

str

The manifest is written to a temporary file and moved into place with os.replace(), so a reader never sees a half-written entry. That is not hypothetical since execute_plan(concurrency=N): two calls of one Plan that are structurally identical land on the same key, and an interleaved json.dump would leave a permanently unparseable entry — a cache that poisons itself under exactly the fan-out it exists to make cheap.

falaw.cache.cache_stats()[source]#

Quick summary of the cache: entry count, disk usage, and where it went.

areas is the part worth reading. Since falaw#14 the cache holds the bytes of every generated asset, so a total on its own cannot distinguish gigabytes of irreplaceable blobs from gigabytes of assets/ copies that cost nothing to regenerate — and only the second is safe to reclaim without thinking. Each area’s economics, and the primitives that reclaim it, are in falaw.prune.

size_bytes is every byte under the cache root, unchanged in meaning from before the breakdown existed: the other area absorbs whatever the named areas do not claim, so the areas always sum to the whole.

(+SKIPed — it reads the caller’s real cache, a multi-gigabyte walk on the production box. tests.test_prune pins this against a throwaway cache instead.)

Return type:

dict

>>> stats = cache_stats()
>>> sorted(stats["areas"])
['assets', 'content', 'manifests', 'other', 'scenes', 'url_index']
falaw.cache.cached_call_fal(application, arguments, *, key_arguments=None, refresh=False, on_event=None, backend='fal', key_extra=None)[source]#

Call a fal model, but reuse the cached response when present.

Parameters:
  • application (str) – fal model id.

  • arguments (Mapping[str, Any]) – model input dict — what is sent on the wire.

  • key_arguments (Optional[Mapping[str, Any]]) – what the cache entry is keyed on, when that differs from what goes on the wire. Defaults to arguments. The split exists because a chained call must send fal an expiring URL for its upstream input while being keyed on that input’s content hash, so a byte-identical upstream regeneration hits instead of re-billing.

  • refresh (bool) – if True, bypass the cache and overwrite it with a fresh result.

  • on_event – Per-call subscriber for falaw.events.ProgressEvent. On a cache hit, a synthetic cache_hit event is emitted so UIs can show “skipped” instead of “running”.

  • backend (str) – which execution backend serves this call (falaw#15) — resolved via falaw.backends. Also joins the cache key (non-default only), so two backends never share an entry. Despite the name, this function is no longer fal-specific; the name is kept because “fal” is still the only backend and every existing call site already spells it this way.

Return type:

dict

Returns:

Raw response (whether from cache or network).

falaw.cache.drop_cache_entry(application, arguments, *, backend='fal', key_extra=None)[source]#

Delete the cache entry for (application, arguments). Returns whether one existed.

The counterpart to cache_put(), and the mechanism that keeps a cache from becoming a trap. An entry whose response can no longer be turned into a usable artifact — fal deleted the URL and the bytes are not in the content store — must be a miss, not a permanent failure: without this, escaping one dead call means re-running the whole plan. falaw.plan.execute() calls it.

The plan-level escapes are execute_plan(plan, refresh=True), which re-runs every call and keeps what it pays for, and execute_plan(plan, use_cache=False), which re-runs and keeps nothing — so the second one bills again on the next run (falaw#49). Neither is a substitute for this function: both re-bill every call in the plan, and this drops the one entry that actually went bad.

Only the manifest is removed. Blobs in the content store are shared by content hash across entries and are never dropped from here.

Return type:

bool

falaw.cache.emit_cache_hit(application, on_event=None)[source]#

Emit the synthetic cache_hit progress event for application.

Shared by cached_call_fal() and falaw.plan.execute() (which drives the cache directly, because it must decide whether a hit is usable before committing to it) so a UI sees one event shape either way.

Return type:

None

falaw.cache.materialize_asset(url, *, key_hint='', store=None, fetcher=None, refresh=False)[source]#

Download a remote asset to the cache and return the local path.

The local filename is content-addressed by the asset’s bytes, so two URLs serving identical bytes resolve to one file. The extension is a presentational hint for ffmpeg/PIL; the SHA-256 is the address.

Repeat calls are cheap, in three widening circles — this is what makes it safe to call from a loop over 200 shots:

  1. the file is already on disk here (no store lookup, no network) — immutable URLs only, see below;

  2. the bytes are in the content store (no network, or one validating round-trip) — so it still works after fal has expired the URL;

  3. otherwise, one download.

Circle 1 matters on its own: the content store is prunable, so an asset can survive as a materialized file after its blob is gone.

Circle 1 is taken only when the URL cannot change — that is, falaw.content.is_immutable_url() — because reaching it requires trusting the url -> hash index to name the current bytes, and for an arbitrary caller-supplied URL it does not (thorwhalen/falaw#23). A mutable URL goes to circle 2, where falaw.content.content_ref_for_url() revalidates before reusing anything; when the bytes really are unchanged that costs one conditional request and still no download, and when they have changed you get the new file instead of silently getting the old one.

Parameters:
  • url (str) – the remote asset URL. file:// is supported and is used deliberately by downstream packages for locally-rendered media.

  • key_hint (str) – optional human-readable filename prefix.

  • store – injected lacing.ArtifactStore; defaults to falaw.content.default_content_store().

  • fetcher – injected byte source (url -> Iterable[bytes]); defaults to the urllib-based one. The seam for custom transport (auth headers, retries) and for a hermetic test suite.

  • refresh (bool) – re-fetch unconditionally, skipping every circle. Rarely needed now: a mutable URL revalidates on its own (falaw#23), so this is for an origin that lies about its validators.

Raises:

FalAssetFetchError – the bytes could not be retrieved. Unlike a generated-media artifact (which degrades to URL-only — see falaw.plan.execute()), there is nothing to degrade to here: the caller asked for a local file.

Return type:

str