Current section
Files
Jump to
Current section
Files
lib/core.ex
defmodule Core do
@moduledoc """
Functions for handling process initiation, communication and termination.
This module provides the basic features required for building OTP compliant
processes. Many functions in this module are only intended for use in
processes initiated by functions in this module.
## Examples
defmodule Core.HelloWorld do
use Core
@spec start_link() :: { :ok, pid }
def start_link(), do: Core.start_link(__MODULE__, nil)
def init(_parent, _debug, nil) do
Core.init_ack()
IO.puts("Hello World!")
end
end
defmodule Core.Fn do
use Core
@spec start_link((() -> any)) :: { :ok, pid }
def start_link(fun), do: Core.start_link(__MODULE__, fun)
@spec spawn_link((() -> any)) :: pid
def spawn_link(fun), do: Core.spawn_link(__MODULE__, fun)
def init(parent, debug, fun) when is_function(fun, 0) do
Core.init_ack()
try do
fun.()
rescue
exception ->
reason = { exception, System.stacktrace() }
Core.stop(__MODULE__, fun, parent, debug, reason)
catch
:throw, value ->
exception = Core.UncaughtThrowError[actual: value]
reason = { exception, System.stacktrace() }
Core.stop(__MODULE__, fun, parent, debug, reason)
end
end
end
defmodule Core.PingPong do
use Core
@spec ping(Core.t) :: :pong
def ping(process), do: Core.call(process, __MODULE__, :ping, 5000)
@spec count(Core.t) :: non_neg_integer
def count(process), do: Core.call(process, __MODULE__, :count, 5000)
@spec close(Core.t) :: :ok
def close(process), do: Core.call(process, __MODULE__, :close, 5000)
@spec start_link() :: { :ok, pid }
def start_link(), do: Core.start_link(__MODULE__, nil)
def init(_parent, _debug, nil) do
Core.init_ack()
loop(0)
end
defp loop(count) do
receive do
{ __MODULE__, from, :ping } ->
Core.reply(from, :pong)
loop(count + 1)
{ __MODULE__, from, :count } ->
Core.reply(from, count)
loop(count)
{ __MODULE__, from, :close } ->
Core.reply(from, :ok)
terminate(count)
end
end
defp terminate(_count) do
exit(:normal)
end
end
`Core.Debug` and `Core.Sys` contain more advanced versions of the last
example.
"""
use Behaviour
defcallback init(parent, Core.Debug.t, args) :: no_return
defmacro __using__(_options) do
quote location: :keep do
@behaviour Core
require Core.Debug
@doc false
def init(_parent, _debug, _args) do
Core.init_ignore()
end
defoverridable [init: 3]
end
end
## start/spawn types
@type local_name :: atom()
@type global_name :: { :global, any }
@type via_name :: { :via, module, any }
@type name :: nil | local_name | global_name | via_name
@typep args :: any
@type option :: { :timeout, timeout } | { :debug, [Core.Debug.option] } |
{ :spawn_opt, [Process.spawn_opt] }
@type start_return :: { :ok, pid } | { :ok, pid, any } | :ignore |
{ :error, any }
## communication types
@type process :: pid | { local_name, node }
@type t :: name | process
@type label :: any
@typep request :: any
@opaque tag :: reference
@type from :: { pid, tag }
@typep response :: any
## general types
@typep extra :: any
@typep reason :: Exception.t | { Exception.t, Exception.stacktrace } | atom |
{ atom, any }
@typep state :: any
@type parent :: pid
## start/spawn api
@doc """
Start a parentless Core process without a link.
This function will block until the created processes sends an acknowledgement,
exits or the timeout is reached. If the timeout is reached the created process
is killed and `{ :error, :timeout }` will be returned.
A process created with this function will automatically send error reports to
the error logger if an exception is raised (and not rescued). A crash report
is also sent when the process exits with an abnormal reason.
To atomically spawn and link to the process include the `:spawn_opt` option
`:link`.
"""
@spec start(module, args, [option]) :: start_return
def start(mod, args, opts \\ []) do
name = reg_name(opts)
spawn_opts = Keyword.get(opts, :spawn_opt, [])
timeout = Keyword.get(opts, :timeout, :infinity)
init_args = [mod, name, nil, args, self(), opts]
:proc_lib.start(__MODULE__, :init, init_args, timeout, spawn_opts)
end
@doc """
Start a Core process with a link.
This function will block until the created processes sends an acknowledgement,
exits or the timeout is reached. If the timeout is reached the created process
is killed and `{ :error, :timeout }` will be returned.
A process created with this function will automatically send error reports to
the error logger if an exception is raised (and not rescued). A crash report
is also sent when the process exits with an abnormal reason.
A processes created with this function should exit when its parent does and
with the same reason.
"""
@spec start_link(module, args, [option]) :: start_return
def start_link(mod, args, opts \\ []) do
name = reg_name(opts)
spawn_opts = Keyword.get(opts, :spawn_opt, [])
init_args = [mod, name, self(), args, self(), opts]
timeout = Keyword.get(opts, :timeout, :infinity)
:proc_lib.start_link(__MODULE__, :init, init_args, timeout, spawn_opts)
end
@doc """
Spawn a parentless Core process without a link.
A process created with this function will automatically send error reports to
the error logger if an exception is raised (and not rescued). A crash report
is also sent when the process exits with an abnormal reason.
To atomically spawn and link to the process include the `:spawn_opt` option
`:link`.
"""
@spec spawn(module, args, [option]) :: pid
def spawn(mod, args, opts \\ []) do
name = reg_name(opts)
spawn_opts = Keyword.get(opts, :spawn_opt, [])
init_args = [mod, name, nil, args, nil, opts]
:proc_lib.spawn_opt(__MODULE__, :init, init_args, spawn_opts)
end
@doc """
Spawn a Core process with a link.
A process created with this function will automatically send error reports to
the error logger if an exception is raised (and not rescued). A crash report
is also sent when the process exits with an abnormal reason.
A processes created with this function should exit when its parent does and
with the same reason.
"""
@spec spawn_link(module, any, [option]) :: pid
def spawn_link(mod, args, opts \\ []) do
name = reg_name(opts)
spawn_opts = Keyword.get(opts, :spawn_opt, [])
init_args = [mod, name, self(), args, nil, opts]
:proc_lib.spawn_opt(__MODULE__, :init, init_args, [:link | spawn_opts])
end
@doc """
Sends an acknowledgment to the starter process.
The starter process will block until it receives the acknowledgement. The
start function will return `{ :ok, pid }`.
If the process was created using a spawn function no acknowledgment is sent.
This function is only intended for use by processes created by this module.
"""
@spec init_ack() :: :ok
def init_ack() do
case get_starter() do
starter when starter === self() -> :ok
starter -> :proc_lib.init_ack(starter, { :ok, self() })
end
end
@doc """
Sends an acknowledgment to the starter process with extra information.
The starter process will block until it receives the acknowledgement. The
start function will return `{ :ok, pid, extra }`.
If the process was created using a spawn function no acknowledgment is sent.
This function is only intended for use by processes created by this module.
"""
@spec init_ack(extra) :: :ok
def init_ack(extra) do
case get_starter() do
starter when starter === self() -> :ok
starter -> :proc_lib.init_ack(starter, { :ok, self(), extra })
end
end
@doc """
Sends an acknowledgment to the starter process to ignore the current process.
This function is an alternative to `init_ack/0` and should be used be signal
that nothing will occur in the created process.
Before sending the acknowledgment the process will unregister any name that
was associated with the process during initiation.
After sending the acknowledgment the process will exit with reason `:normal`
so this function should only be used as a tail call.
The starter process will block until it receives the acknowledgement. The
start function will return `:ignore`.
If the process was created using a spawn function no acknowledgment is sent.
This function is only intended for use by processes created by this module.
"""
@spec init_ignore :: no_return
def init_ignore() do
unregister()
starter = get_starter()
if starter !== self(), do: :proc_lib.init_ack(starter, :ignore)
exit(:normal)
end
@doc """
Sends an acknowledgment to the starter process that the current process failed
to initiate.
This function is an alternative to `init_ack/0` and should be used be signal
the rason why initiation failed.
Before sending the acknowledgment the process may print debug information and
log an error with the error logger. If the reason is of the form:
`{ exception, stacktrace }`, the error will note that an exception was raised
format the exception and stacktrace. Also any name that was associated with
the process during initiation will be unregistered.
After sending the acknowledgment the process will exit with the reason passed.
As this function exits it should only be used as a tail call.
The starter process will block until it receives the acknowledgement. The
start function will return `{ :error, reason }`, where reason is the same
reason as exiting.
If the process was created using a spawn function no acknowledgment is sent.
This function is only intended for use by processes created by this module.
"""
@spec init_stop(module, parent, Core.Debug.t, args, reason,
Core.Debug.event) :: no_return
def init_stop(mod, parent, debug, args, reason, event \\ nil)
def init_stop(mod, parent, debug, args, reason, event) do
type = exit_type(reason)
starter = get_starter()
if starter === self() and type === :abnormal do
report_init_stop(mod, parent, args, reason, event)
end
if type === :abnormal, do: Core.Debug.print(debug)
unregister()
if starter !== self(), do: :proc_lib.init_ack(starter, { :error, reason })
exit(reason)
end
@doc """
Stops a Core process.
Before exiting the process may print debug information and will send an error
report to the error logger when the reason is not `:normal`, `:shutdown` or of
the form `{ :shutdown, any }`. If the reason is of the form:
`{ exception, stacktrace }`, the error will note that an exception was raised
format the exception and stacktrace. A crash report with additional
information will also be sent to the error logger.
The process will exit with the reason passed. As this function exits it should
only be used as a tail call.
This function is only intended for use by processes created by this module.
"""
@spec stop(module, state, parent, Core.Debug.t, reason,
Core.Debug.event) :: no_return
def stop(mod, state, parent, debug, reason, event \\ nil)
def stop(mod, state, parent, debug, reason, event) do
type = exit_type(reason)
if type === :abnormal do
report_stop(mod, state, parent, reason, event)
Core.Debug.print(debug)
end
exit(reason)
end
## communication api
@doc """
Returns the pid or `{ local_name, node }` of the Core process associated with
the name or process.
Returns `nil` if no process is associated with the name.
The returned process may not be alive.
"""
@spec whereis(t) :: process | nil
def whereis(pid) when is_pid(pid), do: pid
def whereis(name) when is_atom(name), do: Process.whereis(name)
def whereis({ :global, name }) do
case :global.whereis_name(name) do
:undefined ->
nil
pid ->
pid
end
end
def whereis({ name, node_name })
when is_atom(name) and node_name === node() do
Process.whereis(name)
end
def whereis({ name, node_name } = process)
when is_atom(name) and is_atom(node_name) do
process
end
def whereis({ :via, mod, name }) when is_atom(mod) do
case mod.whereis_name(name) do
:undefined ->
nil
pid ->
pid
end
end
@doc """
Calls the Core process associated the with name or process and returns the
reponse.
The message is sent in the form `{ label, from, request }`. A response is sent
by calling `reply(from, response)`.
Catching an exit from this function may result in unexpected messages
arriving in the calling processes mailbox. It is recommended to terminate soon
after an exit is caught.
"""
@spec call(t, label, request, timeout) :: response
def call(target, label, request, timeout) do
case whereis(target) do
pid when is_pid(pid) ->
case safe_call(pid, label, request, timeout) do
{:ok, response} ->
response
{:error, :noconnection} ->
reason = {:nodedown, node(pid)}
exit({reason,
{__MODULE__, :call, [target, label, request, timeout]}})
{:error, reason} ->
exit({reason,
{__MODULE__, :call, [target, label, request, timeout]}})
end
{ local_name, node_name } = process
when is_atom(local_name) and is_atom(node_name) ->
case safe_call(process, label, request, timeout) do
{:ok, response} ->
response
{:error, :noconnection} ->
reason = {:nodedown, node_name}
exit({reason,
{__MODULE__, :call, [target, label, request, timeout]}})
{:error, reason} ->
exit({reason,
{__MODULE__, :call, [target, label, request, timeout]}})
end
nil ->
exit({:noproc, {__MODULE__, :call, [target, label, request, timeout]}})
end
end
@doc """
Sends a response to a call message.
The first argument is the `from` term from a call message of the form:
`{ label, from, request }`.
"""
@spec reply(from, response) :: response
def reply({ to, tag }, response) do
try do
Kernel.send(to, { tag, response })
catch
ArgumentError ->
response
end
end
@doc """
Sends a message to the Core process associated with the name or process.
This function will not raise an exception if there is not a process associated
with the name.
This function will not block to attempt an connection to a disconnected node.
Instead it will spawn a process to make the attempt and return `:ok`
immediately. Therefore messages to processes on other nodes may not arrive
out of order, if they are received. Messages to processes on the same node
will arrive in order, if they are received.
"""
@spec cast(t, label, request) :: :ok
def cast(target, label, request) do
case whereis(target) do
nil ->
:ok
process ->
msg = { label, request }
cast(process, msg)
end
end
@doc """
Sends a message to the Core process associated with the name or process.
This function will raise an ArgumentError if a name is provided and no process
is associated with the name - unless the name is for a locally registered name
on another node. Similar to the behaviour of `Kernel.send/2`.
This function will block to attempt a connection to a disconnected node, and
so messages sent by this function will arrive in order, if they are received.
"""
@spec send(t, request) :: request
def send(target, msg) do
case whereis(target) do
nil ->
raise ArgumentError,
message: "no process associated with #{format(target)}"
process ->
Kernel.send(process, msg)
end
end
## hibernation api
@doc """
Hibernates the Core process. Must be used to hibernate Core processes to
ensure exceptions (and exits) are handled correctly.
This function throws away the stack and should only be used as a tail call.
When the process leaves hibernation the following will be called:
`apply(mod, fun, [state, parent, debug] ++ args)`
This function is only intended for use by processes created by this module.
"""
@spec hibernate(module, atom, state, parent, Core.Debug.t, [any]) :: no_return
def hibernate(mod, fun, state, parent, debug, args \\ []) do
:proc_lib.hibernate(__MODULE__, :continue,
[mod, fun, state, parent, debug, args])
end
## :proc_lib api
@doc false
@spec init( nil | pid, nil | pid, name, module, args, [option]) ::
no_return
def init(mod, nil, parent, args, starter, opts) do
init(mod, self(), parent, args, starter, opts)
end
def init(mod, name, nil, args, starter, opts) do
init(mod, name, self(), args, starter, opts)
end
def init(mod, name, parent, args, nil, opts) do
init(mod, name, parent, args, self(), opts)
end
def init(mod, name, parent, args, starter, opts) do
try do
do_init(mod, name, parent, args, starter, opts)
else
_ ->
# Explicitly exit.
exit(:normal)
rescue
exception ->
base_stop(mod, parent, { exception, System.stacktrace() })
catch
# Turn throw into the error it would be.
:throw, value ->
error = {:nocatch, value}
base_stop(mod, parent, { error, System.stacktrace() })
# Exits are not caught as they are an explicit intention to exit.
end
end
@doc false
@spec continue(module, atom, state, parent, Core.Debug.t, args) :: no_return
def continue(mod, fun, state, parent, debug, args) do
try do
apply(mod, fun, [state, parent, debug] ++ args)
else
_ ->
# Explicitly exit.
exit(:normal)
rescue
exception ->
base_stop(mod, parent, { exception, System.stacktrace() })
catch
# Turn throw into the error it would be.
:throw, value ->
error = {:nocatch, value}
base_stop(mod, parent, { error, System.stacktrace() })
# Exits are not caught as they are an explicit intention to exit.
end
end
## utils
@doc false
@spec get_name() :: name
def get_name(), do: Process.get(:"$name", self())
@doc false
@spec format(t) :: String.t
def format(proc \\ get_name())
def format(pid) when is_pid(pid) and node(pid) == node() do
inspect(pid)
end
def format(pid) when is_pid(pid) do
"#{inspect(pid)} on #{node(pid)}"
end
def format(name) when is_atom(name) do
to_string(name)
end
def format({ :global, name }) do
"#{inspect(name)} (global)"
end
def format({ name, node_name }) when node_name === node() do
to_string(name)
end
def format({ name, node_name }) do
"#{to_string(name)} on #{to_string(node_name)}"
end
def format({ :via, mod, name }) do
"#{inspect(name)} (#{mod})"
end
## internal
## init
defp reg_name([{ :local, name } | _opts]) when is_atom(name), do: name
defp reg_name([{ :global, _global_name } = name | _opts]), do: name
defp reg_name([{ :via, { mod, via_name } } | _opts]) when is_atom(mod) do
{ :via, mod, via_name }
end
defp reg_name([{ key, _ } = name | _opts])
when key in [:local, :global, :via] do
raise ArgumentError, message: "invalid name: #{inspect(name)}"
end
defp reg_name([_opt | opts]), do: reg_name(opts)
defp reg_name([]), do: nil
defp format_name(name \\ get_name())
defp format_name(pid) when is_pid(pid), do: pid
defp format_name(name) when is_atom(name), do: name
defp format_name({:global, name}), do: name
defp format_name({:via, _mod, name}), do: name
defp do_init(mod, name, parent, args, starter, opts) do
case register(name) do
:yes ->
put_starter(starter)
put_name(name)
debug = new_debug(opts)
mod.init(parent, debug, args)
:no when starter === self() ->
exit(:normal)
:no ->
reason = { :already_started, whereis(name) }
:proc_lib.init_ack(starter, { :error, reason })
exit(:normal)
end
end
defp register(pid) when is_pid(pid), do: :yes
defp register(name) when is_atom(name) do
try do
Process.register(self(), name)
else
:true ->
:yes
rescue
ArgumentError ->
:no
end
end
defp register({ :global, name }) do
:global.register_name(name, self())
end
defp register({ :via, mod, name }) when is_atom(mod) do
mod.register_name(name, self())
end
defp put_name(name), do: Process.put(:"$name", name)
defp put_starter(starter), do: Process.put(:"$starter", starter)
defp get_starter(), do: Process.get(:"$starter", self())
defp new_debug(opts) do
case Keyword.get(opts, :debug, nil) do
nil ->
Core.Debug.new()
debug_opts ->
Core.Debug.new(debug_opts)
end
end
## stopping
defp exit_type(:normal), do: :normal
defp exit_type(:shutdown), do: :normal
defp exit_type({ :shutdown, _reason }), do: :nornal
defp exit_type(_reason), do: :abnormal
defp unregister(name \\ get_name())
defp unregister(pid) when is_pid(pid), do: nil
defp unregister(name) when is_atom(name), do: Process.unregister(name)
defp unregister({ :global, name }), do: :global.unregister_name(name)
defp unregister({ :via, mod, name }), do: mod.unregister_name(name)
defp base_stop(mod, parent, reason) do
report_base_stop(mod, parent, reason)
exit(reason)
end
defp report_base_stop(mod, parent, reason) do
erl_format = '~i** Core ~p is terminating~n** Module == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n'
args = [{__MODULE__, :stop}, format_name(), mod, self(), parent, reason]
report(erl_format, args)
end
defp report_init_stop(mod, parent, args, reason, event) do
erl_format = '~i** ~p ~p is terminating~n** Last event was ~p~n** Arguments == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n'
args = [{__MODULE__, mod, :init_stop}, mod, format_name(), event, args, self(), parent,
reason]
report(erl_format, args)
end
defp report_stop(mod, state, parent, reason, event) do
erl_format = '~i** ~p ~p is terminating~n** Last event was ~p~n** State == ~p~n** Process == ~p~n** Parent == ~p~n** Reason for termination == ~n** ~p~n'
args = [{__MODULE__, mod, :stop}, mod, format_name(), event, state, self(), parent,
reason]
report(erl_format, args)
end
defp report(erl_format, args), do: :error_logger.error_msg(erl_format, args)
## communication
defp safe_call(process, label, request, timeout) do
try do
Process.monitor(process)
rescue
ArgumentError ->
case node() do
# Can't connect to other nodes
:"nonode@nohost" ->
{:error, :noconnection}
# Target node is feature weak
_other ->
{:error, :nomonitor}
end
else
tag ->
Process.send(process, { label, { self(), tag }, request }, [:noconnect])
receive do
{ ^tag, response } ->
Process.demonitor(tag, [:flush])
{:ok, response}
{ :DOWN, ^tag, _, _, reason } ->
{:error, reason}
after
timeout ->
Process.demonitor(tag, [:flush])
{:error, :timeout}
end
end
end
defp cast(process, msg) do
try do
Process.send(process, msg, [:noconnect])
else
:noconnect ->
Kernel.spawn(Process, :send, [process, msg])
:ok
:ok ->
:ok
rescue
_exception ->
:ok
end
end
end