Packages
ConfigCat SDK for Elixir. Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages.
Current section
Files
Jump to
Current section
Files
lib/config_cat/cache_policy/auto.ex
defmodule ConfigCat.CachePolicy.Auto do
@moduledoc false
use ConfigCat.CachePolicy.Behaviour
use GenServer
use TypedStruct
alias ConfigCat.CachePolicy
alias ConfigCat.CachePolicy.Helpers
alias ConfigCat.CachePolicy.Helpers.State
alias ConfigCat.ConfigEntry
alias ConfigCat.FetchTime
require ConfigCat.ConfigCatLogger, as: ConfigCatLogger
defmodule LocalState do
@moduledoc false
use TypedStruct
typedstruct enforce: true do
field :callers, [GenServer.from()], default: []
field :initialized?, boolean(), default: false
end
@spec add_caller(t(), GenServer.from()) :: t()
def add_caller(%__MODULE__{} = state, caller) do
%{state | callers: [caller | state.callers]}
end
@spec be_initialized(t()) :: t()
def be_initialized(%__MODULE__{} = state) do
%{state | callers: [], initialized?: true}
end
end
@default_max_init_wait_time_seconds 5
@default_poll_interval_seconds 60
typedstruct enforce: true do
field :max_init_wait_time_ms, non_neg_integer(), default: @default_max_init_wait_time_seconds * 1000
field :mode, String.t(), default: "a"
field :poll_interval_ms, pos_integer(), default: @default_poll_interval_seconds * 1000
end
@type on_changed_callback :: CachePolicy.on_changed_callback()
@type options :: keyword() | map()
@spec new(options()) :: t()
def new(options \\ []) do
{max_init_wait_time_seconds, options} =
Keyword.pop(options, :max_init_wait_time_seconds, @default_max_init_wait_time_seconds)
{poll_interval_seconds, options} =
Keyword.pop(options, :poll_interval_seconds, @default_poll_interval_seconds)
options =
Keyword.merge(
[
max_init_wait_time_ms: rounded_ms(max_init_wait_time_seconds, 0),
poll_interval_ms: rounded_ms(poll_interval_seconds, 1)
],
options
)
struct(__MODULE__, options)
end
defp rounded_ms(seconds, min_value) do
seconds
|> max(min_value)
|> Kernel.*(1000)
|> round()
end
@impl GenServer
def init(%State{} = state) do
Logger.metadata(instance_id: state.instance_id)
state = Map.put(state, :policy_state, %LocalState{})
Process.send_after(self(), :init_timeout, state.policy_options.max_init_wait_time_ms)
{:ok, state, {:continue, :start_polling}}
end
defguardp initialized?(state) when state.policy_state.initialized?
@impl GenServer
def handle_continue(:start_polling, %State{} = state) do
new_state =
if state.offline do
be_initialized(state)
else
schedule_initial_refresh(state)
end
{:noreply, new_state}
end
@impl GenServer
def handle_info(:be_initialized, %State{} = state) do
new_state = be_initialized(state)
{:noreply, new_state}
end
@impl GenServer
def handle_info(:init_timeout, %State{} = state) do
seconds = state.policy_options.max_init_wait_time_ms / 1000
ConfigCatLogger.warning(
"`max_init_wait_time_seconds` for the very first fetch reached (#{seconds}). Returning cached config.",
event_id: 4200
)
new_state = be_initialized(state)
{:noreply, new_state}
end
@impl GenServer
def handle_info(:polled_refresh, %State{} = state) do
pid = self()
unless state.offline do
Task.start_link(fn ->
Logger.metadata(instance_id: state.instance_id)
refresh(state)
schedule_next_refresh(state, pid)
send(pid, :be_initialized)
end)
end
{:noreply, state}
end
@impl GenServer
def handle_call(:get, from, %State{} = state) when not initialized?(state) do
new_state = State.update_policy_state(state, &LocalState.add_caller(&1, from))
{:noreply, new_state}
end
@impl GenServer
def handle_call(:get, _from, %State{} = state) do
{:reply, Helpers.cached_config(state), state}
end
@impl GenServer
def handle_call(:offline?, _from, %State{} = state) do
{:reply, state.offline, state}
end
@impl GenServer
def handle_call(:set_offline, _from, %State{} = state) do
{:reply, :ok, State.set_offline(state)}
end
@impl GenServer
def handle_call(:set_online, _from, %State{} = state) do
new_state =
state
|> State.set_online()
|> schedule_initial_refresh()
{:reply, :ok, new_state}
end
@impl GenServer
def handle_call(:force_refresh, _from, %State{} = state) do
if state.offline do
message = ConfigCatLogger.warn_offline()
{:reply, {:error, message}, state}
else
case refresh(state) do
:ok ->
{:reply, :ok, state}
error ->
{:reply, error, state}
end
end
end
defp schedule_initial_refresh(%State{} = state) do
interval_ms = state.policy_options.poll_interval_ms
delay_ms =
case Helpers.cached_entry(state) do
{:ok, %ConfigEntry{} = entry} ->
next_fetch_ms = entry.fetch_time_ms + interval_ms
max(0, next_fetch_ms - FetchTime.now_ms())
_ ->
0
end
if delay_ms == 0 do
send(self(), :polled_refresh)
state
else
Process.send_after(self(), :polled_refresh, delay_ms)
be_initialized(state)
end
end
defp be_initialized(%State{} = state) when initialized?(state), do: state
defp be_initialized(%State{} = state) do
config = Helpers.cached_config(state)
for caller <- state.policy_state.callers do
GenServer.reply(caller, config)
end
Helpers.on_client_ready(state)
State.update_policy_state(state, &LocalState.be_initialized/1)
end
defp schedule_next_refresh(%State{} = state, pid) do
interval_ms = state.policy_options.poll_interval_ms
Process.send_after(pid, :polled_refresh, interval_ms)
end
defp refresh(%State{} = state) do
if state.offline do
ConfigCatLogger.warn_offline()
:ok
else
Helpers.refresh_config(state)
end
end
end