Session scoped gen_server names

Hi,

I am building a business rule setup running in the back-end. The business rule logic will be a network of around 100 gen_server nodes supervised by a single supervisor. When input from user arrives it will change one of the nodes in the network which in turn will trigger other nodes to change etc.

Every session will create an own network setup of nodes based on a common template where every node has a number starting from 1 and also knows what nodes to relate to using the same numbering.

Is there a common or easy way to create named process where the name is only valid and unique within the scope of the one supervisor and the supervised processes?

The ambition is that each network will communicate within the network using the number as name, allowing for simultaneous networks to exist (multiple users, each with it’s own session and network running the same time). When node 7 signals to node 45 it is always within the same network.

I was hoping to be able to use the number as named processes but now realize that local scope is local to the erlang-node, not the supervisor and I need to find another way.

An alternative is to used {SessionId, Number} as name that can be unique within the node (there is as no need to cross nodes in the network at the moment).

You can use pg to group processes via join/2, e.g.:

pg:join({SessionId, Number}, PidOrPids).

To get the Pids, use get_members/1:

pg:get_members({Session, Number}).

You can even create a scope per user via start/1 or start_link/1, group them by using join/3, and, to get the Pids, use get_members/2.

This way, you don’t need to create named gen_servers.

2 Likes

Thanks.

I did not manage to understand how to use pg with gen_server but I did find an gen_server example with gproc.

Each process in the network has an internal number (1-105 or so) and has relations using the same numbering. All processes in the network shares the same session id (whatever number).

The key used with gproc is then simply {sessionNumber, internal_numer}. With this I can forsee the keys to related processes without asking anyone.

The key (or Name in the example) that need to be used is not actually shown in the linked example:

Name = {SessionId, InternalId}, 
gen_server:start_link({via, gproc, {n, l, Name}}, ?MODULE, [BaseState], []).

With this I can simply call or cast to related processes using {MySession, InternalNumber}.

An actual call to 93 via gproc is then:

gen_server:call({via, gproc, {n, l, {SessionId, 93}}}, Message).

Note that the full via triple needs to be used in the call or cast.

This is the solution I will start build on. If it will work is another matter for the future :smile:

Nice, it could work.

Anyway, I’ll give you an idea of how to use pg with a gen_server:

-module(session_worker).

-export([start_link/3]).
-export([call/3]).
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).

% Client

start_link(SessionId, InternalId, BaseState) ->
    gen_server:start_link(
        ?MODULE, {SessionId, InternalId, BaseState}, []
    ).

call(SessionId, InternalId, Message) ->
    Worker = random_worker(pg:get_members({SessionId, InternalId})),
    gen_server:call(Worker, Message).

% Server

% Make sure you have started pg via pg:start/1 or pg:start_link/0,1
init({SessionId, InternalId, BaseState}) ->
    ok = pg:join({SessionId, InternalId}, self()),
    {ok, BaseState}.

handle_call(Message, _From, State) ->
    {reply, Message, State}.

handle_cast(_Message, State) ->
    {noreply, State}.

% Internal

% Use your algorithm to select a Pid.
random_worker([Pid | _]) ->
    Pid;
random_worker([]) ->
    error(no_worker).

In the Erlang shell:

1> pg:start_link().
{ok,<0.424.0>}
2> session_worker:start_link(foo, 93, []).
{ok,<0.426.0>}
3> session_worker:call(foo, 93, bar).
bar

I don’t know the logic behind your implementation, but maybe with pg, you don’t need the InternalId and can just use the SessionId by adjusting the random_worker/1 function.

Note: When a process dies, pg automatically removes it from the member list.

1 Like

Thanks, the number of options evolves :smile:

Note: When a process dies, pg automatically removes it from the member list.

But if the supervisor restarts the gen_server with the same basedata as before a new process will register a new one with same keys in the pg-register?

(Removed part due to me misunderstood your answer)

So some testing done, it works with the following:

If I chose global, I can use any term as name including {SessionId, InternalId}.

Then I need to make sure the SessionId is not reused between nodes in the cluster and that I can do, I already have a multi-node ID table in mnesia. I can also chose to pick a node number that is not reused so that I don’t risk any blocks during session setup.

Thanks @williamthome, this has been very helpful. Now I have to consider my options and decide how to progress.

1 Like

Yes, it will restart normally, and the pg will register it. When the process dies, pg will remove it from the member list, and when the gen_server restarts, the pg:join/2 function in the init/1 callback of the gen_server (in my example) adds it as a member.

Nice! Please share your progress/solution o/