Skip to content

Cache consistency

Enable raw-template caching only when repeated source reads justify another stateful dependency. With no CACHE section, resolution never initializes a Django cache backend.

The backend must provide atomic add() and coherent reads for generation barriers. Django's FileBasedCache and its subclasses are rejected with dj_hyperview.E018: their separate existence check and write can resurrect an invalidated template. Direct TemplateCache use raises SourceUnavailable with reason unsupported backend. FAILURE_MODE="bypass" does not override this configuration error.

Do not point CACHE.ALIAS at Django's DummyCache. That backend intentionally stores nothing and cannot satisfy generation or invalidation guarantees. Omit the CACHE section when caching should be disabled.

Opt in

Configure a normal Django cache alias and bind Hyperview to it:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "hyperview-docs",
    },
}

HYPERVIEW = {
    "TEMPLATE_DIRS": [BASE_DIR / "hyperview"],
    "SOURCES": [{"BACKEND": "dj_hyperview.sources.FileSystemSource"}],
    "CACHE": {
        "ALIAS": "default",
        "NAMESPACE": "my-service-v1",
        "TTL": 300,
        "NEGATIVE_TTL": 15,
        "FAILURE_MODE": "bypass",
    },
}

The cache stores raw content and source misses, including valid empty content. It never stores compiled templates; the loader keeps those in process, as template engine behavior describes. Namespace, source identity, canonical name, revision, and generation keep entries isolated.

Choose which sources are cached

Caching is an explicit opt-in per source. The resolver caches a source only when its cacheable attribute is exactly True. A source without the attribute, or with any other value, is read on every lookup and creates no cache entries.

Source Cached
FileSystemSource Yes: it reports cacheable = True.
DatabaseSource with an explicit using alias Yes.
DatabaseSource without using No: Django routing can select a different database per request.
A subclass of either that overrides resolve() Only when it declares cacheable = True itself.
A custom source Only when it declares cacheable = True.

A custom source opts in with a class attribute, an instance attribute, or a property that returns True:

from dj_hyperview import ResolvedTemplate


class ReleaseBucketSource:
    # The content depends only on the template name and the constructor options.
    cacheable = True

    def __init__(self, *, bucket: str) -> None:
        self.bucket = bucket

    def resolve(self, name: str) -> ResolvedTemplate | None: ...

Declare cacheable = True only when the content returned for a name depends solely on that name and the constructor options. A cache entry is keyed by the namespace, the source's position, class and constructor options, the canonical name, the revision, and the generation. Nothing about the request, user, tenant, language, or any other ambient state is part of the key. A source that reads such state, for example a ContextVar that selects the current tenant, thread-local data, or the active translation, must not opt in: the first caller's content would be served to every other caller until the entry expires or is invalidated.

Subclasses of your own sources inherit the attribute, so a subclass whose resolve() reads more than its parent does must set cacheable = False. The built-in sources are stricter. FileSystemSource and DatabaseSource report cacheable as True only while resolve() is their own implementation, so a subclass that overrides resolve(), for example to select a tenant's directory, is not cached until it declares cacheable = True itself. Do that only when the rule above still holds. Both built-in sources accept the class attribute and the instance attribute (self.cacheable = False in __init__).

Earlier betas cached every custom source that lacked a private marker. Custom sources are no longer cached until they declare cacheable = True. Add the declaration only after checking the rule above; otherwise the only effect of the change is that the source is read on every lookup.

Select a Redis database

CACHE.ALIAS can point to a compatible stateful cache configured in Django. Redis host, credentials, logical database, and transport belong in CACHES; dj-hyperview only resolves the alias. LocMemCache provides atomic operations inside one process, but a multi-process deployment needs a shared backend such as Redis so every worker observes invalidations.

For example, dedicate Redis logical database 3 to Hyperview templates:

CACHES = {
    "hyperview": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/3",
    },
}

HYPERVIEW = {
    "CACHE": {
        "ALIAS": "hyperview",
        "NAMESPACE": "my-service-v1",
        "TTL": 300,
        "NEGATIVE_TTL": 15,
        "FAILURE_MODE": "bypass",
    },
}

Change the final URL segment to select another logical database supported by the configured Redis deployment. A separate alias may also point to another Redis server or cluster. Backend-specific restrictions still apply; the package does not bypass limitations imposed by Django's backend or Redis.

When upgrading from FileBasedCache, either remove the Hyperview CACHE section or configure a compatible backend before deployment. Use a fresh Hyperview namespace rather than copying old cache entries; do not clear a shared Django cache to migrate this package's entries.

bypass returns authoritative source data when ordinary cache reads or writes fail. raise reports SourceUnavailable instead. Neither mode hides a real source failure.

A cache that bypass silently skips would never be noticed, so each swallowed failure logs one WARNING on the dj_hyperview.cache logger: template_cache_bypassed operation=<operation> reason=<reason>. The operation is initialize, generation, read or write, and the reason is one of the fixed descriptions SourceUnavailable carries, such as backend failure, invalid payload, invalid alias or unsupported backend. The message never contains a template name, a cache key, a source identity or the backend's own error. A cache that works logs nothing. A source that declares cacheable = True but has constructor options the cache cannot identify, such as an arbitrary object, is never cached; building the resolver logs one INFO record, template_source_uncacheable source_index=<n> reason=unsupported_options.

A source must return None or a ResolvedTemplate named exactly as requested, with str fields. Anything else raises SourceUnavailable with the reason invalid resolved template in both failure modes, because the cache could not read the entry back.

An explicitly bound DatabaseSource never publishes a source read while its database connection is inside a transaction. Existing cache hits remain available, but an older transaction snapshot cannot pin stale content under a newer generation. Applications using ATOMIC_REQUESTS therefore fall back to database reads for cache misses without sharing those snapshot-local results.

Invalidate after publication

Database services and model signals schedule invalidation after commit. For a custom publisher, rotate the generation only after its write commits:

from django.db import transaction

from dj_hyperview import invalidate_templates

transaction.on_commit(
    lambda: invalidate_templates("screens/home.xml"),
    using="default",
)

Invalidation is a shared fail-closed barrier even when reads use bypass. A multi-name call validates all names first, but rotations are not atomic; retry the whole requested set after a reported failure.

Unknown template misses create no cache metadata. The first successful lookup through a cacheable source creates one non-expiring generation key; later invalidations rotate that same key with a new random token. Raw content and negative entries remain bounded by their configured TTLs. If the generation key is externally evicted, the next successful source lookup establishes a fresh generation and old raw entries remain unreachable.

Invalidate after a filesystem deploy

A filesystem deploy does not invalidate already cached source content. In-place file changes and atomic root symlink swaps remain stale until their TTL expires. After every filesystem deploy, call invalidate_templates for the affected canonical names or rotate CACHE.NAMESPACE for the whole release. Namespace rotation is simpler for immutable deployments; targeted invalidation avoids a cold cache when only a few screens changed.