Current section

Files

Jump to
split_client lib boundary treatment_cache.ex
Raw

lib/boundary/treatment_cache.ex

defmodule SplitClient.Boundary.TreatmentCache do
# A module to organize functionality around caching
# Treatments for splits. Each cache_key is a three-item tuple
# that looks like this `{lookup_key, split_name, options}`.
# Having the cache_key be a tuple like this will allow us to
# reuse the information to poll for Treatment updates in the
# background using the original query paramaters
@moduledoc false
alias SplitClient.Boundary.OrderedAttributes
alias SplitClient.Treatment
@type key_data() :: {key :: String.t(), split_name :: String.t(), opts :: keyword()}
@type t() :: %__MODULE__{treatments: map()}
defstruct treatments: %{}
def new(treatments \\ %{}), do: %__MODULE__{treatments: treatments}
@doc """
Access a cached Treatment
"""
@spec get_treatment(t(), key_data()) :: Treatment.t() | nil
def get_treatment(cache, key_data) do
Map.get(cache.treatments, cache_key(key_data))
end
@doc """
Get multiple treatments out of the cache at once. Returned
as a map of %{split_name => %Treatment{}}
"""
@spec get_treatments(t(), String.t(), list(), keyword()) :: map()
def get_treatments(cache, key, split_names, opts) do
Enum.reduce(split_names, %{}, fn split_name, acc ->
case get_treatment(cache, {key, split_name, opts}) do
nil -> acc
treatment -> Map.put(acc, treatment.split_name, treatment)
end
end)
end
@doc """
Cache a single treatment
"""
@spec cache_treatment(t(), Treatment.t(), key_data()) :: t()
def cache_treatment(cache, treatment, key_data) do
put_in(cache, [Access.key!(:treatments), cache_key(key_data)], treatment)
end
@doc """
Cache treatments that have been grouped by traffic_type
```elixir
%{
"customer" => %{
"shiny-feature" => %Treatment{treatment: "on"},
},
"user" => %{
"my-experiment" => %Treatment{treatment: "off}
}
}
```
"""
@spec cache_all_treatments(t(), map(), [map()], keyword()) :: t()
def cache_all_treatments(cache, treatments, _keys, _opts) when map_size(treatments) == 0,
do: cache
def cache_all_treatments(cache, treatments, keys, opts) do
Enum.reduce(keys, cache, fn key, acc ->
splits = treatments[key.traffic_type]
opts = add_bucketing_key_to_opts(opts, Map.get(key, :bucketing_key))
cache_treatments(acc, splits, key.matching_key, opts)
end)
end
defp add_bucketing_key_to_opts(opts, nil), do: opts
defp add_bucketing_key_to_opts(opts, bucketing_key),
do: Keyword.put(opts, :bucketing_key, bucketing_key)
@doc """
Cache multiple treatments. Pass in treatments in a map with each
key being the split_name.
```elixir
%{
"shiny-feature" => %Treatment{treatment: "on"},
"my-experiment" => %Treatment{treatment: "off}
}
```
"""
@spec cache_treatments(t(), map(), String.t(), keyword()) :: t()
def cache_treatments(cache, treatments, key, opts) do
Enum.reduce(treatments, cache, fn {split_name, treatment}, acc ->
cache_treatment(acc, treatment, {key, split_name, opts})
end)
end
defp cache_key({key, split_name, opts}) do
{key, split_name, ordered_opts(opts)}
end
defp ordered_opts(opts) do
Enum.sort_by(opts, &elem(&1, 0))
|> Enum.map(&sort_option/1)
end
defp sort_option({key, value}) when is_map(value) do
{key, OrderedAttributes.new(value).value}
end
defp sort_option(option), do: option
@doc """
Get the split_names grouped by those that have the same key
and options. This will make it possible to make a single
query for multiple splits that were queried with the same
key and options
"""
@spec group_split_names_by_key_and_options(t()) :: map()
def group_split_names_by_key_and_options(cache) do
group_by_key_and_options(cache)
|> gather_split_names()
end
defp group_by_key_and_options(cache) do
Enum.group_by(
cache.treatments,
fn {{key, _split_name, opts}, _treatment} ->
{key, opts}
end,
fn {_query, treatment} -> treatment end
)
end
defp gather_split_names(data) do
Enum.reduce(data, %{}, fn {query, treatments}, acc ->
split_names = Enum.map(treatments, & &1.split_name)
Map.put(acc, query, split_names)
end)
end
end