From cac467487acb6c9cff8554f0ca14e5b9162b4c0e Mon Sep 17 00:00:00 2001 From: Daniel Calderon Date: Fri, 13 Mar 2026 21:21:34 -0500 Subject: [PATCH] feat: make cache adapter configurable via application config (#1) --- lib/fun_with_flags/config.ex | 5 ++ lib/fun_with_flags/store.ex | 9 ++-- lib/fun_with_flags/store/cache.ex | 2 + lib/fun_with_flags/store/cache/behaviour.ex | 55 +++++++++++++++++++++ lib/fun_with_flags/supervisor.ex | 2 +- 5 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 lib/fun_with_flags/store/cache/behaviour.ex diff --git a/lib/fun_with_flags/config.ex b/lib/fun_with_flags/config.ex index 02667be2..262224a7 100644 --- a/lib/fun_with_flags/config.ex +++ b/lib/fun_with_flags/config.ex @@ -55,6 +55,11 @@ defmodule FunWithFlags.Config do end + def cache_adapter do + Keyword.get(ets_cache_config(), :adapter, FunWithFlags.Store.Cache) + end + + def ets_cache_config do Keyword.merge( @default_cache_config, diff --git a/lib/fun_with_flags/store.ex b/lib/fun_with_flags/store.ex index 1eb1991b..069c34c7 100644 --- a/lib/fun_with_flags/store.ex +++ b/lib/fun_with_flags/store.ex @@ -2,21 +2,20 @@ defmodule FunWithFlags.Store do @moduledoc false require Logger - alias FunWithFlags.Store.Cache alias FunWithFlags.{Config, Flag, Telemetry} - import FunWithFlags.Config, only: [persistence_adapter: 0] + import FunWithFlags.Config, only: [persistence_adapter: 0, cache_adapter: 0] @spec lookup(atom) :: {:ok, FunWithFlags.Flag.t} def lookup(flag_name) do - case Cache.get(flag_name) do + case cache_adapter().get(flag_name) do {:ok, flag} -> {:ok, flag} {:miss, reason, stale_value_or_nil} -> case persistence_adapter().get(flag_name) do {:ok, flag} -> Telemetry.emit_persistence_event({:ok, nil}, :read, flag_name, nil) - Cache.put(flag) + cache_adapter().put(flag) {:ok, flag} err = {:error, _reason} -> Telemetry.emit_persistence_event(err, :read, flag_name, nil) @@ -89,7 +88,7 @@ defmodule FunWithFlags.Store do end defp cache_persistence_result(result = {:ok, flag}) do - Cache.put(flag) + cache_adapter().put(flag) result end diff --git a/lib/fun_with_flags/store/cache.ex b/lib/fun_with_flags/store/cache.ex index ffb82452..e7cc4f29 100644 --- a/lib/fun_with_flags/store/cache.ex +++ b/lib/fun_with_flags/store/cache.ex @@ -9,6 +9,8 @@ defmodule FunWithFlags.Store.Cache do @type ttl :: integer @type cached_at :: integer + @behaviour FunWithFlags.Store.Cache.Behaviour + @doc false use GenServer diff --git a/lib/fun_with_flags/store/cache/behaviour.ex b/lib/fun_with_flags/store/cache/behaviour.ex new file mode 100644 index 00000000..4623c47a --- /dev/null +++ b/lib/fun_with_flags/store/cache/behaviour.ex @@ -0,0 +1,55 @@ +defmodule FunWithFlags.Store.Cache.Behaviour do + @moduledoc """ + A behaviour module for implementing cache adapters. + + The package ships with a default ETS-based cache adapter (`FunWithFlags.Store.Cache`), + but you can provide your own adapter by adopting this behaviour. + + This is useful, for example, to partition cache keys by application in an umbrella + project where multiple OTP apps share the same `fun_with_flags` instance. + + ## Configuration + + config :fun_with_flags, :cache, + enabled: true, + ttl: 900, + adapter: MyApp.CustomCache + """ + + @doc """ + Returns either a child specification if the cache adapter needs a process + to be started and supervised, or `nil` if it does not. + """ + @callback worker_spec() :: Supervisor.child_spec() | nil + + @doc """ + Looks up a flag by name in the cache. + + Should return: + - `{:ok, flag}` if the flag is found and not expired. + - `{:miss, :not_found, nil}` if the flag is not in the cache. + - `{:miss, :expired, stale_flag}` if the flag is in the cache but expired. + - `{:miss, :invalid, nil}` if the cached entry is invalid. + """ + @callback get(flag_name :: atom) :: + {:ok, FunWithFlags.Flag.t()} + | {:miss, :not_found, nil} + | {:miss, :expired, FunWithFlags.Flag.t()} + | {:miss, :invalid, nil} + + @doc """ + Stores a flag in the cache. + """ + @callback put(flag :: FunWithFlags.Flag.t()) :: + {:ok, FunWithFlags.Flag.t()} + + @doc """ + Clears all entries from the cache. + """ + @callback flush() :: true + + @doc """ + Returns the contents of the cache for inspection and debugging. + """ + @callback dump() :: list() +end diff --git a/lib/fun_with_flags/supervisor.ex b/lib/fun_with_flags/supervisor.ex index 85c5a0aa..9f7425b2 100644 --- a/lib/fun_with_flags/supervisor.ex +++ b/lib/fun_with_flags/supervisor.ex @@ -43,7 +43,7 @@ defmodule FunWithFlags.Supervisor do defp children do [ - FunWithFlags.Store.Cache.worker_spec(), + Config.cache_adapter().worker_spec(), persistence_spec(), notifications_spec(), ]