defmodule Lockstep.Process do
@moduledoc """
Controller-aware wrappers for `Process.whereis/1`, `Process.register/2`,
`Process.unregister/1`, and `Process.registered/0`.
These rewrite the per-name registry to be **per-node** rather than
BEAM-global, so atom names like `:counter` can refer to different
pids on different simulated cluster nodes. Without this, every name
lives in a single BEAM-wide table and you get spurious
`:already_registered` collisions when the same registered name
legitimately exists on multiple nodes.
Falls through to BEAM's vanilla `Process.*` calls when no Lockstep
controller is in scope.
## Rewriter integration
`Lockstep.Rewriter` rewrites bare `Process.whereis/1`,
`Process.register/2`, `Process.unregister/1`, and `Process.registered/0`
to the corresponding `Lockstep.Process.*` calls.
"""
@doc """
Look up a process registered under `name` on the calling process's
notional cluster node. Returns the pid or `nil`. Mirrors
`Process.whereis/1`.
"""
@spec whereis(atom()) :: pid() | nil
def whereis(name) when is_atom(name) do
case Process.get(:lockstep_controller) do
nil ->
Process.whereis(name)
ctl when is_pid(ctl) ->
case Lockstep.Controller.cluster_whereis_name(ctl, self(), name) do
:undefined -> nil
pid -> pid
end
end
end
@doc """
Register `pid_or_name` under `name` on the registered process's
notional node. Mirrors `Process.register/2`.
Returns `true` on success. Raises `ArgumentError` if the name is
already registered on that node, or if the pid is already
registered under another name.
"""
@spec register(pid() | atom(), atom()) :: true
def register(pid_or_name, name) when is_atom(name) do
pid = resolve_pid(pid_or_name)
case Process.get(:lockstep_controller) do
nil ->
Process.register(pid, name)
ctl when is_pid(ctl) ->
case Lockstep.Controller.cluster_register_name(ctl, pid, name) do
:yes ->
true
{:already, _existing} ->
raise ArgumentError,
"name #{inspect(name)} already registered on node " <>
inspect(Lockstep.Controller.cluster_node_of(ctl, pid))
end
end
end
@doc """
Unregister `name` on the calling process's node. Mirrors
`Process.unregister/1`.
Returns `true` on success. Raises `ArgumentError` if no such name.
"""
@spec unregister(atom()) :: true
def unregister(name) when is_atom(name) do
case Process.get(:lockstep_controller) do
nil ->
Process.unregister(name)
ctl when is_pid(ctl) ->
case Lockstep.Controller.cluster_whereis_name(ctl, self(), name) do
:undefined ->
raise ArgumentError, "no such name registered: #{inspect(name)}"
_pid ->
:ok = Lockstep.Controller.cluster_unregister_name(ctl, self(), name)
true
end
end
end
@doc """
List atom names registered on the calling process's node. Mirrors
`Process.registered/0`.
"""
@spec registered() :: [atom()]
def registered do
case Process.get(:lockstep_controller) do
nil ->
Process.registered()
ctl when is_pid(ctl) ->
Lockstep.Controller.cluster_registered_names(ctl, self())
end
end
@doc """
Set a debug label on the current process. Mirrors OTP 27+'s
`set_label/1` from the `Process` module but works on older OTPs by
no-op'ing -- the label is purely cosmetic for `:observer` / debug
tooling, so it's safe to ignore.
"""
@spec set_label(any()) :: :ok
def set_label(label) do
if function_exported?(Process, :set_label, 1) do
apply(Process, :set_label, [label])
end
:ok
end
defp resolve_pid(pid) when is_pid(pid), do: pid
defp resolve_pid(name) when is_atom(name) do
case whereis(name) do
nil -> raise ArgumentError, "no such name registered: #{inspect(name)}"
pid -> pid
end
end
end