fix ETS cache resilience, skip heavy columns from queries
All checks were successful
deploy / deploy (push) Successful in 1m27s

- settings cache: create ETS table in application.ex so it survives
  GenServer crashes (same pattern as redirects cache)
- redirects: remove DB fallback on cache miss — cache is warmed on
  startup and kept in sync, so a miss means no redirect exists
- product listing: exclude provider_data (up to 72KB JSON) and
  description from listing queries via listing_select/1
- logo/header: select only rendering fields, skip BLOB data column

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-02 21:44:32 +00:00
parent 2f3b7e7b21
commit e041f5d8f0
5 changed files with 40 additions and 22 deletions

View File

@ -7,9 +7,10 @@ defmodule Berrypod.Application do
@impl true
def start(_type, _args) do
# Create ETS table here so the supervisor process owns it (lives forever).
# The Task below only warms it with data from the DB.
# Create ETS tables here so the supervisor process owns them (live forever).
# The GenServers/Tasks below only warm them with data from the DB.
Berrypod.Redirects.create_table()
Berrypod.Settings.SettingsCache.create_table()
Berrypod.ActivityLog.ObanTelemetryHandler.attach()
children = [

View File

@ -135,7 +135,8 @@ defmodule Berrypod.Media do
from i in ImageSchema,
where: i.image_type == "logo",
order_by: [desc: i.inserted_at],
limit: 1
limit: 1,
select: struct(i, [:id, :image_type, :is_svg, :svg_content, :source_width])
)
SettingsCache.put_cached(:logo, logo)
@ -165,7 +166,8 @@ defmodule Berrypod.Media do
from i in ImageSchema,
where: i.image_type == "header",
order_by: [desc: i.inserted_at],
limit: 1
limit: 1,
select: struct(i, [:id, :image_type, :source_width])
)
SettingsCache.put_cached(:header, header)

View File

@ -146,6 +146,14 @@ defmodule Berrypod.Products do
[images: {pi_query, image: image_preload_query()}]
end
# Skip provider_data (up to 72KB JSON) and description from listing queries —
# neither is used on product cards. Same idea as image_preload_query().
@listing_fields Product.__schema__(:fields) -- [:provider_data, :description]
defp listing_select(query) do
from(p in query, select: struct(p, ^@listing_fields))
end
@doc """
Gets a single visible, active product by slug with full preloads (for detail page).
"""
@ -179,6 +187,7 @@ defmodule Berrypod.Products do
|> apply_sort(opts[:sort])
|> maybe_limit(opts[:limit])
|> maybe_exclude(opts[:exclude])
|> listing_select()
|> Repo.all()
|> Repo.preload(listing_preloads())
end
@ -195,6 +204,7 @@ defmodule Berrypod.Products do
|> apply_visible_filters(opts)
|> apply_sort(opts[:sort])
|> maybe_exclude(opts[:exclude])
|> listing_select()
|> Berrypod.Pagination.paginate(page: opts[:page], per_page: opts[:per_page] || 24)
%{pagination | items: Repo.preload(pagination.items, listing_preloads())}

View File

@ -67,16 +67,9 @@ defmodule Berrypod.Redirects do
{:ok, %{to_path: to_path, status_code: status_code, id: id}}
[] ->
case Repo.one(from r in Redirect, where: r.from_path == ^path) do
nil ->
:not_found
redirect ->
put_cache(redirect.from_path, redirect.to_path, redirect.status_code, redirect.id)
{:ok,
%{to_path: redirect.to_path, status_code: redirect.status_code, id: redirect.id}}
end
# Cache is warmed on startup with all redirects and kept in sync on
# create/update/delete, so a miss here means no redirect exists.
:not_found
end
end

View File

@ -21,6 +21,24 @@ defmodule Berrypod.Settings.SettingsCache do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Creates the ETS table. Called from application.ex so the supervisor
process owns the table it survives GenServer crashes.
"""
def create_table do
if :ets.whereis(@table_name) == :undefined do
:ets.new(@table_name, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: false
])
end
@table_name
end
@doc """
Gets a cached setting by key.
@ -131,14 +149,8 @@ defmodule Berrypod.Settings.SettingsCache do
@impl true
def init(_opts) do
:ets.new(@table_name, [
:set,
:public,
:named_table,
read_concurrency: true,
write_concurrency: false
])
# Table is created in application.ex so it survives GenServer restarts
create_table()
{:ok, %{}, {:continue, :warm}}
end