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