Current section

Files

Jump to
split_client lib boundary treatments.ex
Raw

lib/boundary/treatments.ex

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