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
use GenServer
require Logger
alias SplitClient.Boundary.Evaluator
alias SplitClient.Boundary.TreatmentCache, as: Cache
alias SplitClient.Treatment
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, opts)
end
@spec get_treatment(key :: String.t(), split_name :: String.t(), options :: keyword()) ::
Treatment.t()
def get_treatment(key, split_name, opts \\ []) do
GenServer.call(server_name(opts), {:get_treatment, key, split_name, opts})
end
@spec get_treatments(key :: String.t(), split_names :: [String.t()], options :: keyword()) ::
map()
def get_treatments(key, split_names, opts \\ []) do
GenServer.call(server_name(opts), {:get_treatments, key, split_names, opts})
end
@spec get_all_treatments(keys :: [map()], options :: keyword()) :: map()
def get_all_treatments(keys, opts \\ []) do
GenServer.call(server_name(opts), {:get_all_treatments, keys, opts})
end
@spec check_for_updates(options :: keyword()) :: nil
def check_for_updates(opts \\ []) do
GenServer.call(server_name(opts), :check_for_updates)
end
defp server_name(opts) do
Keyword.get(opts, :treatments_pid, __MODULE__)
end
@impl true
def init(opts) do
poll_rate = poll_rate(opts)
if poll_rate do
send(self(), {:poll, poll_rate})
end
{:ok, Cache.new(%{})}
end
defp poll_rate(opts) do
Keyword.get(
opts,
:poll_rate,
Application.get_env(:split_client, :poll_rate, :timer.minutes(1))
)
end
@impl true
def handle_call({:get_treatment, key, split_name, opts}, _from, cache) do
treatment =
case Cache.get_treatment(cache, {key, split_name, opts}) do
nil -> fetch_get_treatment(key, split_name, opts)
treatment -> treatment
end
{:reply, treatment, Cache.cache_treatment(cache, treatment, {key, split_name, opts})}
end
@impl true
def handle_call({:get_treatments, key, split_names, opts}, _from, cache) do
cached_treatments = Cache.get_treatments(cache, key, split_names, opts)
treatments =
if map_size(cached_treatments) == Enum.count(split_names) do
cached_treatments
else
fetch_get_treatments(key, split_names, opts)
end
cache = Cache.cache_treatments(cache, treatments, key, opts)
{:reply, treatments, cache}
end
@impl true
def handle_call({:get_all_treatments, keys, opts}, _from, cache) do
treatments = fetch_get_all_treatments(keys, opts)
cache = Cache.cache_all_treatments(cache, treatments, keys, opts)
{:reply, treatments, cache}
end
@impl true
def handle_call(:check_for_updates, _from, cache) do
cache = update_cache(cache)
{:reply, cache, cache}
end
@impl true
def handle_info({:poll, rate}, cache) do
cache = update_cache(cache)
Process.send_after(self(), {:poll, rate}, rate)
{:noreply, cache}
end
defp update_cache(cache) do
Cache.group_split_names_by_key_and_options(cache)
|> Enum.reduce(cache, fn {{key, opts}, split_names}, acc ->
split_names = Enum.sort(split_names)
opts = prepare_options_for_query(opts)
updates = fetch_get_treatments(key, split_names, opts)
Cache.cache_treatments(acc, updates, key, opts)
end)
end
defp fetch_get_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
defp fetch_get_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)
Enum.reduce(split_names, %{}, fn split_name, acc ->
Map.put(acc, split_name, control_split(split_name))
end)
end
end
defp fetch_get_all_treatments(keys, opts) when is_list(keys) do
case data_source(opts).get_all_treatments(keys, opts) do
{:ok, treatments} ->
treatments
{:error, message} ->
Logger.error(message)
%{}
end
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 prepare_options_for_query(opts) do
Enum.map(opts, &prepare_option_for_query/1)
end
defp prepare_option_for_query({:attributes, value}) when is_list(value) do
{:attributes, Enum.into(value, %{})}
end
defp prepare_option_for_query(option), do: option
end