Packages
:timer.send_after/3 persistable. Allows scheduling message delivery to specified process in miliseconds or at specified Calendar.DateTime. AlarmClock will make sure that messages are delivered using retry mechanism and after it's own crash, since all messages can be persisted.
Current section
Files
Jump to
Current section
Files
lib/alarm_clock.ex
defmodule AlarmClock do
use GenServer
require Logger
@facade_name AlarmClock.Facade
def start_link(settings, request_sup),
do: GenServer.start_link(__MODULE__, {settings, request_sup}, name: @facade_name)
def set_alarm_call(target_pid, msg, opts),
do: GenServer.call @facade_name, {:set_alarm, {:call, target_pid, msg, opts}}
def load_saved_alarms,
do: GenServer.cast @facade_name, :load_saved_alarms
def init({settings, request_sup}) do
Logger.info "AlarmClock.Facade started"
{:ok, %{settings: settings, request_sup: request_sup}}
end
def handle_call(msg, from, state) do
Task.Supervisor.start_child(state.request_sup, fn ->
result = execute msg, state.settings
GenServer.reply from, result
end)
{:noreply, state}
end
def handle_cast(:load_saved_alarms, state) do
load_saved_alarms state.settings.persister
{:noreply, state}
end
def handle_info({{:alarm, type, target_pid, msg, opts, attempt}, alarm_id, on_time?}, state) do
Task.Supervisor.start_child(state.request_sup, fn ->
execute {{:alarm, type, target_pid, msg, opts, attempt}, alarm_id, on_time?}, state.settings
end)
{:noreply, state}
end
def handle_info(_, state),
do: {:noreply, state}
defp execute({:set_alarm, {call_type, target_pid, msg, opts}}, settings) do
at = Keyword.get opts, :at
response = if at do
case Calendar.DateTime.diff(Calendar.DateTime.now_utc, at) do
{:ok, seconds, useconds, :before} ->
ms = abs (seconds * 1000) + round(useconds / 1000)
Logger.debug "There are #{inspect ms} miliseconds left"
{:ok, ms}
error ->
Logger.error "There's error in calculating time diff: #{inspect error}"
{:error, error}
end
else
Keyword.fetch opts, :in
end
case response do
{:ok, ms} ->
Logger.debug "Setting alarm for #{inspect target_pid} in #{inspect ms} miliseconds with message: #{inspect msg}"
message = {:alarm, call_type, target_pid, msg, opts, 1}
{:ok, alarm_id} = settings.persister.save_alarm message
case :timer.send_after ms, @facade_name, {message, alarm_id, :on_time} do
{:ok, _} -> :ok
error -> error
end
error -> error
end
end
defp execute({{:alarm, :call, target_pid, msg, opts, attempt}, alarm_id, on_time?}, settings) do
settings = get_settings opts, settings
case deliver(target_pid, msg, settings, attempt, on_time?) do
{:error, {:timeout, _}} ->
:timer.send_after settings.retry_delay, @facade_name, {{:alarm, :call, target_pid, msg, opts, attempt + 1}, alarm_id, {:expired, :unknown}}
Logger.warn "Target timeouts with response! Alarm scheduled for redelivery"
{:error, {:noproc, _}} ->
:timer.send_after settings.retry_delay, @facade_name, {{:alarm, :call, target_pid, msg, opts, attempt + 1}, alarm_id, {:expired, :unknown}}
Logger.warn "No target process! Alarm scheduled for redelivery"
_other ->
settings.persister.delete_alarm alarm_id
:ok
end
end
defp get_settings(overridden_settings, state_settings) do
client_settings = overridden_settings |> Enum.into(%{})
Map.merge state_settings, client_settings
end
defp deliver(target_pid, msg, %{retries: retries}=settings, attempt, on_time?) when attempt <= retries do
try do
Logger.debug "Calling #{inspect target_pid} with #{inspect msg}"
case GenServer.call(target_pid, {:alarm, on_time?, msg}, settings.timeout) do
:ok ->
Logger.debug "Everything looks ok"
other ->
Logger.warn "Delivery not successful: #{inspect other}"
{:error, other}
end
catch
:exit, reason ->
Logger.warn "Calling server failed with: #{inspect reason}"
{:error, reason}
end
end
defp deliver(target_pid, msg, _settings, attempt, _) do
Logger.error "Delivery of message #{inspect msg} to #{inspect target_pid} didn't succeed in #{attempt - 1} attempts"
end
defp load_saved_alarms(persister) do
ensure_implements persister, AlarmClock.Persister
persister.load_saved_alarms
|> Enum.each(&recover_alarm/1)
end
defp recover_alarm({alarm_id, {_,_,_,_,opts,_}=alarm}) do
Logger.debug "Recovering alarm #{inspect alarm}"
case Keyword.fetch!(opts, :in) do
{:warn, :expired, ms_ago} ->
Logger.warn "Message expired #{inspect ms_ago} mseconds ago! Delivering it now!"
send self(), {alarm, alarm_id, {:expired, ms_ago}}
ms ->
:timer.send_after ms, @facade_name, {alarm, alarm_id, :on_time}
end
end
defp ensure_implements(module, behaviour) do
all = Keyword.take(module.__info__(:attributes), [:behaviour])
unless [behaviour] in Keyword.values(all) do
Mix.raise "Expected #{inspect module} to implement #{inspect behaviour} " <>
"in order to make alarms durable"
end
end
end