defmodule Unleash.Repo do
@moduledoc """
This genserver polls the unleash service each time the given interval has
elapsed, refreshing both our local ETS cache and the backup state file if the
flag state has diverged.
The following configuration values are used:
Config.features_period(): polling interval - default 15 seconds
Config.retries(): number of time the call to refresh values is allowed to retry - default -1 (0)
"""
use GenServer
alias Unleash.Cache
alias Unleash.Config
alias Unleash.Features
def init(%Features{} = features) do
Cache.init(features.features)
{:ok, []}
end
def init(_) do
Cache.init()
{:ok, []}
end
def start_link(state) do
{:ok, pid} = GenServer.start_link(__MODULE__, state, name: Unleash.Repo)
unless Config.test?() do
initialize()
end
{:ok, pid}
end
def get_feature(name) do
Cache.get_feature(name)
end
def get_all_feature_names do
Cache.get_all_feature_names()
end
def handle_info({:initialize, etag, retries}, state) do
telemetry_metadata = Unleash.Client.telemetry_metadata(%{retries: retries, etag: etag})
if retries > 0 or retries <= -1 do
cached_features = %Features{features: Cache.get_features()}
{source, remote_features} =
case Unleash.Config.client().features(etag) do
:cached ->
{:cache, schedule_features(cached_features, etag)}
{nil, _error} ->
{source, features} = read_file_state(cached_features)
{source, schedule_features(features, etag, retries - 1)}
{etag, features} ->
{:remote, schedule_features(features, etag)}
end
:telemetry.execute(
[:unleash, :repo, :features_update],
%{},
Map.put(telemetry_metadata, :source, source)
)
if remote_features === cached_features do
{:noreply, state}
else
Cache.upsert_features(remote_features.features)
write_file_state(remote_features)
{:noreply, state}
end
else
:telemetry.execute([:unleash, :repo, :disable_polling], telemetry_metadata)
{:noreply, state}
end
end
# https://github.com/appcues/mojito/issues/57
# Work around for messages received from Mojito after we've passed over the timeout
# threshold.
def handle_info({:mojito_response, _ref, _message}, state) do
{:noreply, state}
end
defp read_file_state(%Features{features: []} = cached_features) do
if File.exists?(Config.backup_file()) do
{:backup_file,
Config.backup_file()
|> File.read!()
|> Jason.decode!()
|> Features.from_map!()}
else
{:cache, cached_features}
end
end
defp read_file_state(cached_features), do: {:cache, cached_features}
defp write_file_state(features) do
:ok = File.mkdir_p(Config.backup_dir())
content = Jason.encode_to_iodata!(features)
Config.backup_file()
|> File.write!(content)
:telemetry.execute(
[:unleash, :repo, :backup_file_update],
%{},
Unleash.Client.telemetry_metadata(%{
content: content,
filename: Config.backup_file()
})
)
end
defp initialize do
Process.send(Unleash.Repo, {:initialize, nil, Config.retries()}, [])
end
defp schedule_features(features, etag, retries \\ Config.retries()) do
:telemetry.execute(
[:unleash, :repo, :schedule],
%{},
Unleash.Client.telemetry_metadata(%{
retries: retries,
etag: etag,
interval: Config.features_period()
})
)
Process.send_after(self(), {:initialize, etag, retries}, Config.features_period())
features
end
end