Caches publishing group listing metadata in :persistent_term for sub-millisecond reads.
Instead of querying the database on every request, the listing page reads from an in-memory cache populated from the database.
How It Works
- When a post is created/updated/published,
regenerate/1is called - This queries the database and stores post metadata in :persistent_term
render_group_listingreads from the in-memory cache- Cache includes: title, slug, date, status, languages, versions (no content)
Performance
- Cache miss: ~20ms (DB query + store in :persistent_term)
- Cache hit: ~0.1μs (direct memory access, no variance)
Cache Invalidation
Cache is regenerated when:
- Post is created
- Post is updated (metadata or content)
- Post status changes (draft/published/archived)
- Translation is added
- Version is created
In-Memory Caching with :persistent_term
For sub-millisecond performance, parsed cache data is stored in :persistent_term.
- First read after restart: queries DB, stores in :persistent_term (~20ms)
- Subsequent reads: direct memory access (~0.1μs, no variance)
- On regenerate: updates :persistent_term from DB
- On invalidate: clears :persistent_term entry (next read triggers regeneration)
Summary
Functions
Returns the timestamp of when the cache was last generated from the database.
Returns the :persistent_term key for tracking when the cache was last generated.
Checks if a cache exists for a group in :persistent_term.
Finds a post by a previous URL slug for 301 redirects.
Finds a post by URL slug for a specific language.
Finds a post by slug in the cache.
Finds a cached post by mode — uses date/time lookup for timestamp mode, slug for others.
Finds a post by path pattern in the cache (for timestamp mode).
Invalidates (clears) the cache for a group.
Loads the cache from the database into :persistent_term.
Returns the :persistent_term key for tracking when the memory cache was loaded.
Returns whether memory caching (:persistent_term) is enabled. Uses cached settings to avoid database queries on every call.
Returns when the memory cache was loaded (ISO 8601 string), or nil if not loaded.
Returns the :persistent_term key for a publishing group's cache.
Reads the cached listing for a publishing group.
Regenerates the listing cache for a group.
Regenerates the cache if no other process is already regenerating it.
Functions
Returns the timestamp of when the cache was last generated from the database.
Returns the :persistent_term key for tracking when the cache was last generated.
Checks if a cache exists for a group in :persistent_term.
@spec find_by_previous_url_slug(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, :not_found | :cache_miss}
Finds a post by a previous URL slug for 301 redirects.
When a URL slug changes, the old slug is stored in previous_url_slugs.
This function finds posts that previously used the given URL slug.
Returns
{:ok, cached_post}- Found post that previously used this slug{:error, :not_found}- No post with this previous slug{:error, :cache_miss}- Cache not available
@spec find_by_url_slug(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, :not_found | :cache_miss}
Finds a post by URL slug for a specific language.
This enables O(1) lookup from URL slug to internal identifier, supporting per-language URL slugs for SEO-friendly localized URLs.
Parameters
group_slug- The publishing grouplanguage- The language code to search inurl_slug- The URL slug to find
Returns
{:ok, cached_post}- Found post (includes internalslugfor DB lookup){:error, :not_found}- No post with this URL slug for this language{:error, :cache_miss}- Cache not available
Finds a post by slug in the cache.
This is useful for single post views where we need metadata (language_statuses, version_statuses, allow_version_access) without a separate DB query.
Returns {:ok, cached_post} if found, {:error, :not_found} otherwise.
Finds a cached post by mode — uses date/time lookup for timestamp mode, slug for others.
@spec find_post_by_path(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, :not_found | :cache_miss}
Finds a post by path pattern in the cache (for timestamp mode).
Matches posts where the path contains the date/time pattern.
Returns {:ok, cached_post} if found, {:error, :not_found} otherwise.
@spec invalidate(String.t()) :: :ok
Invalidates (clears) the cache for a group.
Clears the :persistent_term entries. The next read will trigger a regeneration from the database.
Loads the cache from the database into :persistent_term.
Returns :ok if successful or {:error, reason} on failure.
Returns the :persistent_term key for tracking when the memory cache was loaded.
@spec memory_cache_enabled?() :: boolean()
Returns whether memory caching (:persistent_term) is enabled. Uses cached settings to avoid database queries on every call.
Returns when the memory cache was loaded (ISO 8601 string), or nil if not loaded.
Returns the :persistent_term key for a publishing group's cache.
Reads the cached listing for a publishing group.
Returns {:ok, posts} if cache exists and is valid.
Returns {:error, :cache_miss} if cache doesn't exist or caching is disabled.
Respects the publishing_memory_cache_enabled setting.
Regenerates the listing cache for a group.
Queries the database for all posts and stores the metadata in :persistent_term.
This should be called after any post operation that changes the listing:
- create_post
- update_post
- add_language_to_post
- create_new_version
Returns :ok on success or {:error, reason} on failure.
Regenerates the cache if no other process is already regenerating it.
This prevents the "thundering herd" problem where multiple concurrent requests all trigger cache regeneration simultaneously after a server restart.
Uses ETS with insert_new/2 for atomic lock acquisition - only one process
can acquire the lock at a time. The lock includes a timestamp and will be
considered stale after 30000ms to prevent permanent lockout
if a process dies mid-regeneration.
Returns:
:okif regeneration was performed successfully:already_in_progressif another process is currently regenerating{:error, reason}if regeneration failed
Usage
On cache miss in read paths, use this instead of regenerate/1:
case ListingCache.regenerate_if_not_in_progress(group_slug) do
:ok -> # Cache is ready, read from it
:already_in_progress -> # Another process is regenerating, try again later
{:error, _} -> # Regeneration failed, query DB directly
end