Current section
12 Versions
Jump to
Current section
12 Versions
Compare versions
7
files changed
+348
additions
-92
deletions
| @@ -369,16 +369,50 @@ def index(conn, params) do | |
| 369 369 | |
| 370 370 | ## Caching |
| 371 371 | |
| 372 | - Carve includes request-scoped caching enabled by default, using [cachex](https://github.com/whitfin/cachex) under the hood. Each linked resource is stored in the cache for 100ms by default. |
| 372 | + ### Request-scoped cache |
| 373 373 | |
| 374 | - To disable caching: |
| 374 | + Carve deduplicates linked resource lookups within a single request using [cachex](https://github.com/whitfin/cachex). Each request gets an isolated cache that's cleared when the response completes. TTL (default 500ms) acts as a safety net for slow requests. |
| 375 375 | |
| 376 | - ``` |
| 377 376 | ```elixir |
| 378 377 | config :carve, Carve.Config, |
| 379 | - enable_cache: false |
| 378 | + cache_ttl: 5_000, # milliseconds, default 500 |
| 379 | + enable_cache: false # set to disable caching entirely |
| 380 380 | ``` |
| 381 381 | |
| 382 | + ### Shared computation between links and view |
| 383 | + |
| 384 | + When `links` and `view` both need the same expensive data, use `cache` to compute it once: |
| 385 | + |
| 386 | + ```elixir |
| 387 | + defmodule UserJSON do |
| 388 | + use Carve.View, :user |
| 389 | + |
| 390 | + get fn id -> Users.get!(id) end |
| 391 | + |
| 392 | + cache fn user -> |
| 393 | + %{ |
| 394 | + plan: Plans.get_active_plan(user.id), |
| 395 | + team_plan: Plans.get_team_plan(user.id) |
| 396 | + } |
| 397 | + end |
| 398 | + |
| 399 | + links fn user, cached -> |
| 400 | + %{PlanJSON => cached.plan.id} |
| 401 | + end |
| 402 | + |
| 403 | + view fn user, cached -> |
| 404 | + %{ |
| 405 | + id: hash(user.id), |
| 406 | + plan_id: PlanJSON.hash(cached.plan.id) |
| 407 | + } |
| 408 | + end |
| 409 | + end |
| 410 | + ``` |
| 411 | + |
| 412 | + The `cache` function runs once per entity per request. If the same user is linked from multiple parents (e.g. 10 asset versions by the same user), it still runs only once. |
| 413 | + |
| 414 | + Modules without `cache` are unaffected — single-argument `links` and `view` work as before. |
| 415 | + |
| 382 416 | ## How does it work? |
| 383 417 | |
| 384 418 | * Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })` |
| @@ -31,4 +31,4 @@ | |
| 31 31 | {<<"optional">>,false}, |
| 32 32 | {<<"repository">>,<<"hexpm">>}, |
| 33 33 | {<<"requirement">>,<<"~> 3.6">>}]]}. |
| 34 | - {<<"version">>,<<"0.4.0">>}. |
| 34 | + {<<"version">>,<<"0.5.0">>}. |
| @@ -2,16 +2,17 @@ | |
| 2 2 | defmodule Carve.Cache do |
| 3 3 | @moduledoc """ |
| 4 4 | Request-scoped cache for Carve to prevent redundant data fetching within a single render. |
| 5 | - Each render operation gets its own isolated cache that expires after 100ms. |
| 5 | + Each render operation gets its own isolated cache keyed by a unique ref. |
| 6 | + TTL defaults to 500ms. Increase if requests involve slow queries. |
| 7 | + The cache is cleared at end of each render regardless of TTL. |
| 6 8 | |
| 7 | - Can be disabled via config: |
| 9 | + Configuration: |
| 8 10 | |
| 9 11 | config :carve, Carve.Config, |
| 10 | - enable_cache: false |
| 12 | + enable_cache: false, |
| 13 | + cache_ttl: 100 |
| 11 14 | """ |
| 12 15 | |
| 13 | - @ttl 100 |
| 14 | - |
| 15 16 | @doc """ |
| 16 17 | Creates a new cache context for a render operation. |
| 17 18 | Returns a cache key that should be passed through the render pipeline. |
| @@ -19,7 +20,7 @@ defmodule Carve.Cache do | |
| 19 20 | def new_context do |
| 20 21 | if Carve.Config.caching_enabled?() do |
| 21 22 | cache_key = make_ref() |
| 22 | - Cachex.put(:carve_cache, cache_key, %{}, ttl: @ttl) |
| 23 | + Cachex.put(:carve_cache, cache_key, %{}, ttl: Carve.Config.cache_ttl()) |
| 23 24 | cache_key |
| 24 25 | else |
| 25 26 | nil |
| @@ -34,24 +35,31 @@ defmodule Carve.Cache do | |
| 34 35 | |
| 35 36 | def fetch(cache_key, key, fun) when is_function(fun, 0) do |
| 36 37 | if Carve.Config.caching_enabled?() do |
| 37 | - case Cachex.get(:carve_cache, cache_key) do |
| 38 | - {:ok, nil} -> |
| 39 | - fun.() |
| 38 | + cache_data = |
| 39 | + case Cachex.get(:carve_cache, cache_key) do |
| 40 | + {:ok, nil} -> %{} |
| 41 | + {:ok, data} -> data |
| 42 | + {:error, _} -> %{} |
| 43 | + end |
| 40 44 | |
| 41 | - {:ok, cache_data} -> |
| 42 | - case Map.get(cache_data, key) do |
| 43 | - nil -> |
| 44 | - value = fun.() |
| 45 | - updated_cache = Map.put(cache_data, key, value) |
| 46 | - Cachex.put(:carve_cache, cache_key, updated_cache, ttl: @ttl) |
| 47 | - value |
| 45 | + case Map.get(cache_data, key) do |
| 46 | + nil -> |
| 47 | + value = fun.() |
| 48 48 | |
| 49 | - cached_value -> |
| 50 | - cached_value |
| 51 | - end |
| 49 | + # Re-read cache to pick up writes from nested fetch calls |
| 50 | + current_cache = |
| 51 | + case Cachex.get(:carve_cache, cache_key) do |
| 52 | + {:ok, nil} -> %{} |
| 53 | + {:ok, data} -> data |
| 54 | + {:error, _} -> %{} |
| 55 | + end |
| 52 56 | |
| 53 | - {:error, _} -> |
| 54 | - fun.() |
| 57 | + updated_cache = Map.put(current_cache, key, value) |
| 58 | + Cachex.put(:carve_cache, cache_key, updated_cache, ttl: Carve.Config.cache_ttl()) |
| 59 | + value |
| 60 | + |
| 61 | + cached_value -> |
| 62 | + cached_value |
| 55 63 | end |
| 56 64 | else |
| 57 65 | fun.() |
| @@ -25,4 +25,11 @@ defmodule Carve.Config do | |
| 25 25 | def caching_enabled? do |
| 26 26 | get(:enable_cache, true) |
| 27 27 | end |
| 28 | + |
| 29 | + @doc """ |
| 30 | + Returns the cache TTL in milliseconds. Defaults to 500. |
| 31 | + """ |
| 32 | + def cache_ttl do |
| 33 | + get(:cache_ttl, 500) |
| 34 | + end |
| 28 35 | end |
| @@ -1,93 +1,125 @@ | |
| 1 1 | defmodule Carve.Links do |
| 2 | - # Update all public functions to accept cache_key as last parameter |
| 3 | - def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) |
| 4 | - def get_links_by_id(_module, nil, _visited, _whitelist, _cache_key), do: [] |
| 2 | + # Public API — returns lists (backward compatible) |
| 5 3 | |
| 6 | - def get_links_by_id(module, id, visited, whitelist, cache_key) when not is_list(id) do |
| 4 | + def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do |
| 5 | + {links, _visited} = do_get_links_by_id(module, id, visited, whitelist, cache_key) |
| 6 | + links |
| 7 | + end |
| 8 | + |
| 9 | + def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do |
| 10 | + {links, _visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key) |
| 11 | + links |
| 12 | + end |
| 13 | + |
| 14 | + # Internal API — returns {links, visited} to accumulate visited across items |
| 15 | + |
| 16 | + defp do_get_links_by_id(_module, nil, visited, _whitelist, _cache_key), do: {[], visited} |
| 17 | + |
| 18 | + defp do_get_links_by_id(module, id, visited, whitelist, cache_key) when not is_list(id) do |
| 7 19 | cache_key = cache_key || Carve.Cache.get_or_create_context() |
| 8 20 | |
| 9 21 | case Map.get(visited, {module, id}) do |
| 10 22 | nil -> |
| 11 | - # Use cache for get_by_id calls |
| 12 23 | data = Carve.Cache.fetch(cache_key, {module, :get, id}, fn -> |
| 13 24 | module.get_by_id(id) |
| 14 25 | end) |
| 15 26 | |
| 16 27 | case data do |
| 17 | - nil -> [] |
| 18 | - data -> get_links_by_data(module, data, visited, whitelist, cache_key) |> prepare_result(whitelist) |
| 28 | + nil -> |
| 29 | + {[], visited} |
| 30 | + |
| 31 | + data -> |
| 32 | + {links, visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key) |
| 33 | + {prepare_result(links, whitelist), visited} |
| 19 34 | end |
| 20 35 | |
| 21 36 | _ -> |
| 22 | - [] |
| 37 | + {[], visited} |
| 23 38 | end |
| 24 39 | end |
| 25 40 | |
| 26 | - def get_links_by_id(module, ids, visited, whitelist, cache_key) when is_list(ids) do |
| 41 | + defp do_get_links_by_id(module, ids, visited, whitelist, cache_key) when is_list(ids) do |
| 27 42 | cache_key = cache_key || Carve.Cache.get_or_create_context() |
| 28 43 | |
| 29 | - Enum.flat_map(ids, &get_links_by_id(module, &1, visited, whitelist, cache_key)) |
| 30 | - |> prepare_result(whitelist) |
| 44 | + {links, visited} = |
| 45 | + Enum.reduce(ids, {[], visited}, fn id, {acc, vis} -> |
| 46 | + {new_links, vis} = do_get_links_by_id(module, id, vis, whitelist, cache_key) |
| 47 | + {acc ++ new_links, vis} |
| 48 | + end) |
| 49 | + |
| 50 | + {prepare_result(links, whitelist), visited} |
| 31 51 | end |
| 32 52 | |
| 33 | - def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) |
| 34 | - def get_links_by_data(_module, nil, _visited, _whitelist, _cache_key), do: [] |
| 53 | + defp do_get_links_by_data(_module, nil, visited, _whitelist, _cache_key), do: {[], visited} |
| 35 54 | |
| 36 | - def get_links_by_data(module, data_list, visited, whitelist, cache_key) when is_list(data_list) do |
| 55 | + defp do_get_links_by_data(module, data_list, visited, whitelist, cache_key) |
| 56 | + when is_list(data_list) do |
| 37 57 | cache_key = cache_key || Carve.Cache.get_or_create_context() |
| 38 58 | |
| 39 | - Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist, cache_key)) |
| 40 | - |> prepare_result(whitelist) |
| 59 | + {links, visited} = |
| 60 | + Enum.reduce(data_list, {[], visited}, fn item, {acc, vis} -> |
| 61 | + {new_links, vis} = do_get_links_by_data(module, item, vis, whitelist, cache_key) |
| 62 | + {acc ++ new_links, vis} |
| 63 | + end) |
| 64 | + |
| 65 | + {prepare_result(links, whitelist), visited} |
| 41 66 | end |
| 42 67 | |
| 43 | - def get_links_by_data(_module, data, _visited, _whitelist, _cache_key) when not is_map(data), do: [] |
| 68 | + defp do_get_links_by_data(_module, data, visited, _whitelist, _cache_key) |
| 69 | + when not is_map(data), |
| 70 | + do: {[], visited} |
| 44 71 | |
| 45 | - def get_links_by_data(module, data, visited, whitelist, cache_key) when not is_list(data) do |
| 72 | + defp do_get_links_by_data(module, data, visited, whitelist, cache_key) |
| 73 | + when not is_list(data) do |
| 46 74 | cache_key = cache_key || Carve.Cache.get_or_create_context() |
| 47 75 | |
| 48 76 | case fetch_id(data) do |
| 49 77 | {:ok, id} -> |
| 50 78 | if Map.get(visited, {module, id}) do |
| 51 | - [] |
| 79 | + {[], visited} |
| 52 80 | else |
| 53 81 | visited = Map.put(visited, {module, id}, true) |
| 54 82 | |
| 83 | + cached = fetch_cached(module, data, id, cache_key) |
| 84 | + |
| 55 85 | links = |
| 56 | - module.declare_links(data) |
| 86 | + module.declare_links(data, cached) |
| 57 87 | |> filter_and_evaluate_links(whitelist) |
| 58 88 | |
| 59 | - links = |
| 60 | - links |
| 61 | - |> Enum.flat_map(fn {link_module, link_data_or_ids} -> |
| 89 | + {links, visited} = |
| 90 | + Enum.reduce(links, {[], visited}, fn {link_module, link_data_or_ids}, {acc, vis} -> |
| 62 91 | link_data_or_ids |
| 63 92 | |> normalize_link_ids() |
| 64 | - |> Enum.map(fn link_id_or_data -> |
| 65 | - link = process_single_link(link_module, link_id_or_data, visited, cache_key) |
| 93 | + |> Enum.reduce({acc, vis}, fn link_id_or_data, {acc, vis} -> |
| 94 | + link = process_single_link(link_module, link_id_or_data, vis, cache_key) |
| 66 95 | |
| 67 | - children = |
| 68 | - if Map.get(visited, {link_module, extract_id(link_id_or_data)}) do |
| 69 | - [] |
| 96 | + # let do_get_links_by_id/data mark visited internally |
| 97 | + {children, vis} = |
| 98 | + if is_map(link_id_or_data) do |
| 99 | + do_get_links_by_data(link_module, link_id_or_data, vis, whitelist, cache_key) |
| 70 100 | else |
| 71 | - if is_map(link_id_or_data) do |
| 72 | - get_links_by_data(link_module, link_id_or_data, visited, whitelist, cache_key) |
| 73 | - else |
| 74 | - get_links_by_id(link_module, link_id_or_data, visited, whitelist, cache_key) |
| 75 | - end |
| 101 | + do_get_links_by_id(link_module, link_id_or_data, vis, whitelist, cache_key) |
| 76 102 | end |
| 77 103 | |
| 78 | - [link | children] |
| 104 | + {acc ++ [link | children], vis} |
| 79 105 | end) |
| 80 106 | end) |
| 81 107 | |
| 82 | - prepare_result(links, whitelist) |
| 108 | + {prepare_result(links, whitelist), visited} |
| 83 109 | end |
| 84 110 | |
| 85 111 | :error -> |
| 86 | - [] |
| 112 | + {[], visited} |
| 87 113 | end |
| 88 114 | end |
| 89 115 | |
| 90 | - # Update process_single_link to use cache |
| 116 | + # Fetch cached data for a module, using the Cachex cache |
| 117 | + defp fetch_cached(module, data, id, cache_key) do |
| 118 | + Carve.Cache.fetch(cache_key, {module, :cache, id}, fn -> |
| 119 | + module.__cache__(data) |
| 120 | + end) |
| 121 | + end |
| 122 | + |
| 91 123 | defp process_single_link(module, id, visited, cache_key) when is_number(id) or is_binary(id) do |
| 92 124 | case Map.get(visited, {module, id}) do |
| 93 125 | nil -> |
| @@ -97,7 +129,9 @@ defmodule Carve.Links do | |
| 97 129 | |
| 98 130 | case data do |
| 99 131 | nil -> nil |
| 100 | - data -> module.prepare_for_view(data) |
| 132 | + data -> |
| 133 | + cached = fetch_cached(module, data, id, cache_key) |
| 134 | + module.prepare_for_view(data, cached) |
| 101 135 | end |
| 102 136 | |
| 103 137 | _ -> |
| @@ -105,12 +139,16 @@ defmodule Carve.Links do | |
| 105 139 | end |
| 106 140 | end |
| 107 141 | |
| 108 | - defp process_single_link(module, data, visited, _cache_key) do |
| 142 | + defp process_single_link(module, data, visited, cache_key) do |
| 109 143 | case fetch_id(data) do |
| 110 144 | {:ok, id} -> |
| 111 145 | case Map.get(visited, {module, id}) do |
| 112 | - nil -> module.prepare_for_view(data) |
| 113 | - _ -> nil |
| 146 | + nil -> |
| 147 | + cached = fetch_cached(module, data, id, cache_key) |
| 148 | + module.prepare_for_view(data, cached) |
| 149 | + |
| 150 | + _ -> |
| 151 | + nil |
| 114 152 | end |
| 115 153 | |
| 116 154 | :error -> |
| @@ -118,7 +156,6 @@ defmodule Carve.Links do | |
| 118 156 | end |
| 119 157 | end |
| 120 158 | |
| 121 | - # Keep all other private functions unchanged |
| 122 159 | defp filter_and_evaluate_links(links, nil) do |
| 123 160 | links |
| 124 161 | |> Enum.filter(fn {_, value} -> not is_function(value) end) |
| @@ -150,7 +187,7 @@ defmodule Carve.Links do | |
| 150 187 | |> filter_result(whitelist) |
| 151 188 | end |
| 152 189 | |
| 153 | - defp filter_result(result, []), do: [] |
| 190 | + defp filter_result(_result, []), do: [] |
| 154 191 | defp filter_result(result, nil), do: result |
| 155 192 | |
| 156 193 | defp filter_result(result, whitelist) do |
Loading more files…