defmodule SplitClient.Boundary.Treatments do # A GenServer for requesting and caching all Split.io # treatments for an application @moduledoc false require Logger alias SplitClient.Boundary.Evaluator alias SplitClient.Treatment import Cachex.Spec @ttl :timer.minutes(1) @cache_name :split_treatments_cache @doc """ Child spec for the cache for starting up in a supervision tree """ def cache_child_spec do %{ id: @cache_name, start: {Cachex, :start_link, [ [ name: @cache_name, expiration: expiration(default: cache_ttl()) ] ]} } end @spec get_treatment(key :: String.t(), split_name :: String.t(), options :: keyword()) :: Treatment.t() def get_treatment(key, split_name, opts \\ []) do {use_cache?, opts} = Keyword.pop(opts, :cache, true) get_treatment(key, split_name, {:cache, use_cache?}, opts) end def get_treatment(key, split_name, {:cache, true}, opts) do case Cachex.get(get_cache(opts), {split_name, key}) do {:ok, nil} -> fetch_and_cache_treatment(key, split_name, opts) {:ok, treatment} -> treatment {:error, reason} -> log_cache_failure(reason) control_split(split_name) end end def get_treatment(key, split_name, {:cache, false}, opts) do fetch_and_cache_treatment(key, split_name, opts) end defp fetch_and_cache_treatment(key, split_name, opts) do treatment = fetch_treatment(key, split_name, opts) Cachex.put(get_cache(opts), {split_name, key}, treatment) treatment end defp fetch_treatment(key, split_name, opts) do case data_source(opts).get_treatment(key, split_name, opts) do {:ok, treatment} -> treatment {:error, message} -> Logger.error(message) control_split(split_name) end end @spec get_treatments(key :: String.t(), split_names :: [String.t()], options :: keyword()) :: map() def get_treatments(key, split_names, opts \\ []) do {use_cache?, opts} = Keyword.pop(opts, :cache, true) get_treatments(key, split_names, {:cache, use_cache?}, opts) end def get_treatments(key, split_names, {:cache, true}, opts) do result = Cachex.execute(get_cache(opts), fn cache -> cached_splits = lookup_cached_splits(split_names, key, cache) misses = filter_misses(split_names, cached_splits) fetched_splits = fetch_treatments(key, misses, opts) cache_splits(key, fetched_splits, cache) Map.merge(cached_splits, fetched_splits) end) case result do {:ok, treatments} -> treatments {:error, reason} -> log_cache_failure(reason) all_control_treatments(split_names) end end def get_treatments(key, split_names, {:cache, false}, opts) do fetched_splits = fetch_treatments(key, split_names, opts) cache_splits(key, fetched_splits, get_cache(opts)) fetched_splits end defp lookup_cached_splits(split_names, key, cache) do Enum.reduce(split_names, %{}, fn split_name, acc -> case Cachex.get(cache, {split_name, key}) do {:ok, nil} -> acc {:ok, treatment} -> Map.put(acc, split_name, treatment) {:error, reason} -> log_cache_failure(reason) Map.put(acc, split_name, control_split(split_name)) end end) end defp filter_misses(split_names, cached_splits) do found = Map.keys(cached_splits) |> MapSet.new() MapSet.new(split_names) |> MapSet.difference(found) |> Enum.to_list() end defp fetch_treatments(_key, [], _opts), do: %{} defp fetch_treatments(key, split_names, opts) do case data_source(opts).get_treatments(key, split_names, opts) do {:ok, treatments} -> treatments {:error, message} -> Logger.error(message) all_control_treatments(split_names) end end defp all_control_treatments(split_names) do Enum.reduce(split_names, %{}, fn split_name, acc -> Map.put(acc, split_name, control_split(split_name)) end) end @spec get_all_treatments(keys :: [map()], options :: keyword()) :: map() def get_all_treatments(keys, opts \\ []) do fetch_results = fetch_all_treatments(keys, opts) Cachex.execute(get_cache(opts), fn cache -> Enum.each(keys, fn %{matching_key: key, traffic_type: traffic_type} -> splits = fetch_results[traffic_type] cache_splits(key, splits, cache) end) end) fetch_results end defp fetch_all_treatments(keys, opts) do case data_source(opts).get_all_treatments(keys, opts) do {:ok, treatments} -> treatments {:error, message} -> Logger.error(message) %{} end end defp cache_splits(_key, nil, _cache), do: :ok defp cache_splits(key, splits, cache) do Enum.each(splits, fn {split_name, treatment} -> Cachex.put(cache, {split_name, key}, treatment) end) end defp get_cache(opts) do Keyword.get(opts, :cache_pid, @cache_name) end defp data_source(opts) do Keyword.get(opts, :data_source, Application.get_env(:split_client, :data_source, Evaluator)) end defp control_split(split_name) do Treatment.new(split_name: split_name, treatment: "control") end defp log_cache_failure(reason) do Logger.error("Cache Failure, #{inspect(reason)}") end defp cache_ttl do Application.get_env(:split_client, :cache_ttl, @ttl) end end