defmodule Gtimer do
@moduledoc """
A small library which provides a global timer facility in Elixir (or Erlang).
## Example:
iex> timer_ref = Gtimer.new_timer(timeout,timeout_action \\ nil)
Starts a new timer running for `timeout` millseconds, returns a "timer reference".
iex> Gtimer.cancel_timer(timer_ref)
Cancels a running timer.
If a running timer is not cancelled when the timer expires the `timeout_action` function
will be called with the process identifier of the process that created the timer
as a single argument. If the timeout_action is not a function of arity 1, then the
default action of terminating the process that created the timer,
and displaying an informative message, will be taken.
Timer management is done in a separate process. Inside the process
timers are stored in a priority queue, and using a map, which
affords logarithmic worst-case time complexity for both function
calls (in terms of the number of timers running).
## Example:
iex> timer_ref = Gtimer.new_timer(1000,fn pid -> Process.exit(pid,:because) end)
This will start a new timer which when it expires kills the process which invoked
the call to `new_timer`.
"""
use GenServer
@doc false
def init(_) do
{:ok,pqueue} = :epqueue.new()
{:ok, %{pqueue: pqueue, map: %{}, counter: 0}}
end
def handle_call({:new_timer, timeout, pid, timeout_action}, _from, state) do
timer_ref = state[:counter]
timeout_time = :os.system_time(:millisecond)+timeout
queue_item = %{timer_ref: timer_ref, timeout: timeout_time, pid: pid, timeout_action: timeout_action}
{:ok, item_ref} = :epqueue.insert(state[:pqueue], queue_item, timeout_time)
map_item = Map.put(queue_item,:pqueue_ref,item_ref)
new_state =
%{ state |
map: Map.put(state[:map],timer_ref,map_item),
counter: state[:counter]+1 }
case calculate_timeout(new_state) do
{:ok, next_time} ->
{:reply, timer_ref, new_state, next_time}
_ ->
{:reply, timer_ref, new_state}
end
end
def handle_call({:cancel_timer, timer_ref}, _from, state) do
new_state =
case Map.get(state[:map],timer_ref,:undefined) do
:undefined ->
state
map_item ->
pqueue_ref = map_item[:pqueue_ref]
:epqueue.remove(state[:pqueue],pqueue_ref)
%{ state | map: Map.drop(state[:map],[timer_ref]) }
end
case calculate_timeout(new_state) do
{:ok, next_time} ->
{:reply, :ok, new_state, next_time}
_ ->
{:reply, :ok, new_state}
end
end
def handle_info(:timeout, state) do
{:ok, queue_item, _} = :epqueue.pop(state[:pqueue])
# We should do a user defined action here...
pid = queue_item[:pid]
timeout_action = queue_item[:timeout_action]
if is_function(timeout_action,1) do
try do
timeout_action.(pid)
rescue
_ -> IO.puts("*** ERROR: Gtimer: timeout_function raised an exception")
end
else
if Process.alive?(pid) do
IO.puts("Pid #{inspect pid} timed out; terminating...")
Process.exit(pid,:timed_out)
end
end
new_state = %{ state | map: Map.drop(state[:map],[queue_item[:timer_ref]])}
case calculate_timeout(new_state) do
{:ok, next_time} ->
{:noreply, new_state, next_time}
_ ->
{:noreply, new_state}
end
end
def handle_info(other, state) do
IO.puts("*** WARNING: Gtimer: unexpected message #{inspect other} received")
{:noreply, state}
end
defp calculate_timeout(state) do
case :epqueue.size(state[:pqueue]) do
n when n>0 ->
{:ok, _, timeout_time} = :epqueue.peek(state[:pqueue])
current_time = :os.system_time(:millisecond)
next_timeout = max(0,timeout_time-current_time)
{:ok, next_timeout}
_ ->
nil
end
end
@doc """
Starts a new timer running for `timeout` millseconds, returns a "timer reference".
If a running timer is not cancelled when the timer expires the timeout_action function
will be called with the process identifier of the process that created the timer as
a single argument. If the timeout_action is not a function of arity 1, then
the default action of terminating the process that created the timer,
and displaying an informative message, will be taken.
"""
def new_timer(timeout,timeout_action \\ nil) do
case Process.whereis(:gtimer) do
nil ->
# Do not link to the calling process; we are likely to kill it!
GenServer.start(Gtimer, [], [{:name, :gtimer}])
new_timer(timeout)
_pid ->
GenServer.call(:gtimer,{:new_timer, timeout, self(), timeout_action})
end
end
@doc """
Cancels a running timer.
"""
def cancel_timer(time_ref) do
case Process.whereis(:gtimer) do
nil ->
# Do not link to the calling process; we are likely to kill it!
GenServer.start(Gtimer, [], [{:name, :gtimer}])
cancel_timer(time_ref)
_pid ->
GenServer.call(:gtimer,{:cancel_timer, time_ref})
end
end
end