src/support/z_proc.erl

%% @author Maas-Maarten Zeeman <mmzeeman@xs4all.nl>
%% @copyright 2014-2025 Maas-Maarten Zeeman
%% @doc Process registry interface for a site and proc_lib routines
%% to spawn new processes whilst keeping the logger metadata.
%% @end

%% Copyright 2014-2025 Maas-Maarten Zeeman
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(z_proc).
-author("Maas-Maarten Zeeman <mmzeeman@xs4all.nl").

%% API
-export([
    spawn_link_md/1,
    spawn_md/1,
    spawn_md_opt/2,

    whereis/2,
    register/3,
    unregister/2
]).

%% Behaviour exports
-export([
    register_name/2,
    unregister_name/1,
    whereis_name/1,
    send/2
]).


-include("zotonic.hrl").


%% ===============================================================================
%% API
%% ===============================================================================

%% @doc Spawn a new process using proc_lib and copy the logger metadata
%% from the calling process to the new process.
-spec spawn_link_md( Fun ) -> Pid when
    Fun :: function(),
    Pid :: pid().
spawn_link_md(Fun) ->
    MD = logger:get_process_metadata(),
    Memo = z_memo:get_status(),
    proc_lib:spawn_link(
        fun() ->
            set_process_metadata(MD),
            z_memo:set_status(Memo),
            Fun()
        end).

%% @doc Spawn a new process using proc_lib and copy the logger metadata
%% from the calling process to the new process.
-spec spawn_md( Fun ) -> Pid when
    Fun :: function(),
    Pid :: pid().
spawn_md(Fun) ->
    MD = logger:get_process_metadata(),
    Memo = z_memo:get_status(),
    proc_lib:spawn(
        fun() ->
            set_process_metadata(MD),
            z_memo:set_status(Memo),
            Fun()
        end).

%% @doc Spawn a new process using proc_lib and copy the logger metadata
%% from the calling process to the new process. Pass options to proc_lib
%% for spawning the process.
-spec spawn_md_opt( Fun, SpawnOpts ) -> Pid | {Pid, Reference} when
    Fun :: function(),
    SpawnOpts :: [ proc_lib:spawn_option() ],
    Pid :: pid(),
    Reference :: reference().
spawn_md_opt(Fun, SpawnOpts) ->
    MD = logger:get_process_metadata(),
    Memo = z_memo:get_status(),
    proc_lib:spawn_opt(
        fun() ->
            set_process_metadata(MD),
            z_memo:set_status(Memo),
            Fun()
        end,
        SpawnOpts).

set_process_metadata(undefined) -> ok;
set_process_metadata(MD) -> logger:set_process_metadata(MD).

%% @doc Lookup the pid of the process.
-spec whereis(Name, Site) -> undefined | pid() when
    Name ::  term(),
    Site :: z:context() | atom().
whereis(Name, Site) when is_atom(Site) ->
    whereis_name(name(Name, Site));
whereis(Name, Context) ->
    whereis_name({Name, Context}).


%% @doc Register a Pid under Name. Name can be any erlang term.
-spec register(Name, Pid, Site) -> ok | {error, duplicate} when
    Name :: term(),
    Pid :: pid(),
    Site :: z:context() | atom().
register(Name, Pid, Site) when is_atom(Site) ->
    case register_name(name(Name, Site), Pid) of
        yes -> ok;
        no -> {error, duplicate}
    end;
register(Name, Pid, Context) ->
    case register_name({Name, Context}, Pid) of
        yes -> ok;
        no -> {error, duplicate}
    end.


%% @doc Unregister the current process.
-spec unregister(Name, Site) -> ok | {error, enoent} when
    Name :: term(),
    Site :: z:context() | atom().
unregister(Name, Site) when is_atom(Site) ->
    try
        unregister_name(name(Name, Site)),
        ok
    catch
        error:badarg ->
            {error, enoent}
    end;
unregister(Name, Context) ->
    try
        unregister_name({Name, Context}),
        ok
    catch
        error:badarg ->
            {error, enoent}
    end.


%% ===============================================================================
%% Behaviour callbacks needed to act as a process registry which works
%% with {via, z_proc, Name} or {via, z_proc, {Name, Context}}
%% ===============================================================================

register_name({Name, #context{} = Context}, Pid) ->
    register_name(name(Name, Context), Pid);
register_name(Name, Pid) ->
    gproc:register_name({n, l, Name}, Pid).

unregister_name({Name, #context{} = Context}) ->
    unregister_name(name(Name, Context));
unregister_name(Name) ->
    gproc:unregister_name({n, l, Name}).

whereis_name({Name, #context{} = Context}) ->
    whereis_name(name(Name, Context));
whereis_name(Name) ->
    gproc:whereis_name({n, l, Name}).

send({Name, #context{} = Context}, Msg) ->
    send(name(Name, Context), Msg);
send(Name, Msg) ->
    gproc:send({n, l, Name}, Msg).


%% ===============================================================================
%% Helpers
%% ===============================================================================

name(Name, #context{} = Context) ->
    name(Name, z_context:site(Context));
name(Name, Site) when is_atom(Site) ->
    {Name, Site}.