Packages
unleash_fresha
3.0.1
5.1.4
5.1.3
5.1.2
5.1.1
5.1.0
5.0.0
4.0.0
4.0.0-git-bda3
3.0.1
3.0.0
2.1.0
2.0.1
2.0.0-git-75d8
2.0.0-git-67b2
2.0.0-git-4b61
2.0.0-git-49f4
1.16.0
1.15.0
1.14.1-git-e754
1.14.0
1.14.0-git-71e9
1.13.0
1.13.0-git-4ba6
1.12.0
1.11.0
1.11.0-git-1b62
1.10.2
1.10.2-git-b835
1.10.1
1.10.0
1.9.3-alpha0
1.9.2
1.9.1
1.9.0
An Unleash Feature Flag client for Elixir, forked from [unleash](https://gitlab.com/afontaine/unleash_ex)
Current section
Files
Jump to
Current section
Files
lib/unleash/runtime.ex
defmodule Unleash.Runtime do
@moduledoc """
Main Runtime implementation for the Unleash SDK.
Like all the official SDKs, this one keeps and serves feature from a local cache
(in our case, based on an `:ets` table), including evaluating activation strategies locally.
The features are initialised from a bootstrap file and updated periodically
from the Unleash API in the background, independently of how many feature checks are invoked.
This allows applications using this SDK to get the status of a feature instantaneously and locally,
without overloading the Unleash Server with API calls; but it also means that the features might be stale,
up to the polling interval or even longer in case of errors contacting the Unleash APIs.
This Runtime is an Elixir Application that comes with its own Supervision Tree,
which starts two GenServer worker processes:
* `Unleash.Repo`, responsible for periodically refreshing the features from the Unleash API Server
and maintaining the local cache updated.
* `Unleash.Metrics`, responsible for periodically
[sending metrics](https://docs.getunleash.io/client-specification#metrics)
to the Unleash API Server.
The latter can be disabled with `disable_metrics: true`; while setting `disable_client: true`
will disable both of them and the SDK will start in "disconnected mode",
where no features are defined and feature flags values are returned only from
local overrides (see `Unleash.Propagation`) and fallback values.
Note that:
* Currently, in "disconnected mode", the SDK won't even load features from the local bootstrap file.
That might change in the future, which would allow the SDK to work with a static set of
local features initialised from a local file.
* In the test environment, the SDK won't automatically start polling from the API, even if the client is enabled.
You'll have to start it explicitly with `Unleash.Repo.start_polling/0`.
"""
@behaviour Unleash
use Application
alias Unleash.Config
alias Unleash.Feature
alias Unleash.Metrics
alias Unleash.Propagation
alias Unleash.Repo
alias Unleash.Variant
# Internal equivalent of `[t:Unleash.enabled_opt/0]`, post normalization and defaults.
@typep enabled_params :: %{
required(:allow_overrides) => boolean(),
required(:context) => Unleash.context(),
required(:fallback) => boolean() | nil
}
@enabled_default_params %{
allow_overrides: false,
context: %{},
fallback: false
}
@supported_ff_opts [:allow_overrides, :context, :fallback]
# Internal equivalent of `[t:Unleash.get_variant_opt/0]`, post normalization and defaults.
@typep get_variant_params :: %{
required(:context) => Unleash.context(),
required(:fallback) => Variant.result() | nil
}
@get_variant_default_params %{
context: %{},
fallback: Variant.disabled()
}
@supported_variant_opts [:context, :fallback]
@doc """
Checks if the given feature is enabled.
Takes a feature name and a keyword list of options,
which are used to customise the behaviour of the feature check.
See `t:Unleash.enabled_opt/0` for the supported options and their defaults.
Returns a boolean indicating whether the feature flag is enabled.
Can return `nil` only if `:fallback` was set to `nil` and the feature was not found locally.
NOTE: Prefer using `Unleash.Macros.enabled?/2` whenever possible. See `Unleash.Macros`.
"""
@impl Unleash
def enabled?(feature_name, opts) do
params = Map.merge(@enabled_default_params, opts |> Keyword.take(@supported_ff_opts) |> Map.new())
start_metadata = Unleash.Client.telemetry_metadata(%{feature: feature_name})
enabled? =
:telemetry.span(
[:unleash, :feature, :enabled?],
start_metadata,
fn -> do_enabled(feature_name, params, start_metadata) end
)
Propagation.record_impression(feature_name, enabled?)
enabled?
end
@spec do_enabled(Unleash.feature_name(), enabled_params(), :telemetry.event_metadata()) ::
{boolean() | nil, :telemetry.event_metadata()}
defp do_enabled(name, params, start_metadata) do
{result, metadata} =
with {_, false} <- {:disable, Config.disable_client()},
{_, nil} <- {:override, get_propagated_override(name, params.allow_overrides)},
{_, feature} when not is_nil(feature) <- {:feature, Repo.get_feature(name)} do
context = enrich_context(params.context, feature.name)
{result, strategy_evaluations} = Feature.enabled?(feature, context)
Metrics.add_metric({feature, result})
metadata = %{
reason: :strategy_evaluations,
strategy_evaluations: strategy_evaluations,
feature_enabled: feature.enabled
}
{result, metadata}
else
{:disable, _} -> {params.fallback, %{reason: :disabled_client}}
{:override, val} when val == :on or is_binary(val) -> {true, %{reason: :override}}
{:override, :off} -> {false, %{reason: :propagated_override}}
{:feature, nil} -> {params.fallback, %{reason: :feature_not_found}}
end
final_metadata =
start_metadata
|> Map.put(:result, result)
|> Map.put(:evaluation_pid, self())
|> Map.merge(metadata)
{result, final_metadata}
end
@doc """
Returns the variant for the given feature flag.
Strategy variants are prioritised. If none is available then it will fallback to using feature
variants.
For more info on strategy variants see https://docs.getunleash.io/reference/strategy-variants.
Feature variants are deprecated, but are still supported for backwards compatibility and you can
find more info about them here: https://docs.getunleash.io/reference/feature-toggle-variants
It takes a feature name and a keyword list of options, which are used to customise the behaviour
of the feature check. See `t:Unleash.get_variant_opt/0` for the supported options and their
defaults.
Returns a `t:Unleash.Variant.t/0` representing the selected variant.
A special variant `%{enabled: false, name: "disabled"}` will be returned in
any of these scenarios:
* the feature flag is disabled
* the feature flag does not define any strategy variants in the active strategy
* the feature flag does not define any feature variants
* the feature flag does not exist and no fallback has been supplied
NOTE: Prefer using `Unleash.Macros.get_variant/2` whenever possible. See `Unleash.Macros`.
"""
@impl Unleash
def get_variant(feature_name, opts) do
params = Map.merge(@get_variant_default_params, opts |> Keyword.take(@supported_variant_opts) |> Map.new())
start_metadata = Unleash.Client.telemetry_metadata(%{feature: feature_name})
result =
:telemetry.span(
[:unleash, :variant, :get],
start_metadata,
fn -> do_get_variant(feature_name, params, start_metadata) end
)
Propagation.record_impression(feature_name, result[:enabled])
result
end
@spec do_get_variant(Unleash.feature_name(), get_variant_params(), :telemetry.event_metadata()) ::
{Variant.result(), :telemetry.event_metadata()}
defp do_get_variant(name, params, start_metadata) do
with {_, false} <- {:disable, Config.disable_client()},
{_, feature} when not is_nil(feature) <- {:feature, Repo.get_feature(name)},
context = enrich_context(params.context, name),
{_, {true, strategy_evaluations}} <- {:enabled, Feature.enabled?(feature, context)} do
{result, metadata} =
case find_strategy_variants(strategy_evaluations) do
nil -> Variant.select_feature_variant(feature, context)
variants -> Variant.select_strategy_variant(feature, variants, context)
end
{result, start_metadata |> Map.put(:result, result) |> Map.merge(metadata)}
else
{:disable, _} ->
{params.fallback, Map.put(start_metadata, :reason, :disabled_client)}
{:feature, nil} ->
{params.fallback, Map.put(start_metadata, :reason, :feature_not_found)}
{:enabled, _} ->
{Variant.disabled(), Map.put(start_metadata, :reason, :feature_disabled)}
end
end
# Extracts strategy variants information out of the strategy evaluations value as returned by
# `&Feature.enabled?/2`, if any.
defp find_strategy_variants(strategy_evaluations) do
Enum.find_value(strategy_evaluations, fn
{_name, {true, %{variants: variants}}} when is_list(variants) and variants != [] -> variants
_ -> nil
end)
end
@spec enrich_context(Unleash.context(), Unleash.feature_name()) :: Unleash.context()
defp enrich_context(local_context, feature_name) do
# We want the local context to override any field in the propagated context,
# except for `:properties`, which we want to "deep" merge.
{local_properties, local_context} = Map.pop(local_context, :properties, %{})
# Start with context passed from client (it might be empty). See `Unleash.Propagation`
propagated_context()
# Adds namespace from "UNLEASH_NAMESPACE" as a custom context property.
# Fresha-specific behaviour. Should be made generic by moving it to a config-based mechanism
|> Map.update(:properties, %{}, & &1)
|> put_in([:properties, "namespace"], System.get_env("UNLEASH_NAMESPACE"))
# Overwrite values that are defined in local context (minus `:properties`)
|> Map.merge(local_context)
# Special context value required by the flexible_rollout strategy
|> Map.put(:feature_toggle, feature_name)
# "Deep" merge client properties and local ones.
# Because properties is supposed to be a flat map, a normal merge should be enough.
|> Map.update(:properties, local_properties, &Map.merge(&1, local_properties))
end
defp get_propagated_override(_feature_name, false), do: nil
defp get_propagated_override(feature_name, true) do
case Unleash.Propagation.get_overrides() do
nil -> nil
overrides -> Map.get(overrides, feature_name, nil)
end
end
@spec propagated_context() :: Unleash.context()
defp propagated_context do
case Unleash.Propagation.get_context() do
nil -> %{}
context when is_map(context) -> context
end
end
@doc """
Starts the Unleash SDK Runtime.
See `Unleash.Runtime` for details.
"""
@impl Application
def start(_type, _args) do
children =
[
{Repo, Config.disable_client()},
{{Metrics, name: Metrics}, Config.disable_client() or Config.disable_metrics()}
]
|> Enum.filter(fn {_m, not_enabled} -> not not_enabled end)
|> Enum.map(fn {module, _e} -> module end)
unless children == [] do
Config.client().register_client()
end
Supervisor.start_link(children, strategy: :one_for_one)
end
end