Common asynchronous communication pattern
Off-loading heavy mathematical operations
Server code
loop(Count) -> receive stop -> true ; {get_count, From, Ref} -> From ! {result, Ref, Count}, loop(Count); {factorial, From, Ref, N} -> Result = factorial(N), From ! {result, Ref, Result}, loop(Count+1) end. % starting server with initial state 0 start() -> spawn(fun() -> loop(0) end).
Client code
compute_factorial(Pid, N) -> Ref = make_ref(), Pid ! {factorial, self(), Ref, N}, receive {result, Ref, Result} -> Result end.
A simple test
> c(math_examples). {ok,math_examples} > math_examples:factorial(10). 3628800 > mserver:start(). <0.40.0> > mserver:compute_factorial(list_to_pid("<0.40.0>"),10). 3628800
What if the server crashes or stop?
> list_to_pid("<0.40.0>") ! stop > mserver:compute_factorial(list_to_pid("<0.40.0>"),10). * 2: syntax error before: mserver > mserver:start(). <0.44.0> > mserver:compute_factorial(list_to_pid("<0.44.0>"),10). 3628800
Processes IDs are dynamic but code is static!
Erlang has a method for publishing a process identifier
BIF register
% starting server with initial state 0 start() -> Pid = spawn(fun() -> loop(0) end), register(server,Pid).
The atom server
can be used instead of a concrete process ID
> mserver2:start(). true > mserver2:compute_factorial(server,10). 3628800 > server ! stop. stop > mserver2:compute_factorial(server,10). ** exception error: bad argument in function mserver2:compute_factorial/2 > mserver2:start(). true > mserver2:compute_factorial(server,10). 3628800
Message passing abstractions extend easily for distributed environments
Erlang nodes
An instance of an Erlang runtime system
Nodes can easily communicate with each other
Creating a node
erl -name 'nodeS@127.0.0.1' -setcookie lecture
Creating two nodes (for simplicity on the same machine)
erl -name 'nodeS@127.0.0.1' -setcookie lecture erl -name 'nodeC@127.0.0.1' -setcookie lecture
Connecting nodes
nodeC@127.0.0.1
(nodeC@127.0.0.1)> net_adm:ping('nodeS@127.0.0.1'). pong (nodeC@127.0.0.1)> nodes(). ['nodeS@127.0.0.1']
Running your code
(nodeC@127.0.0.1)> nl(math_examples). abcast (nodeC@127.0.0.1)> nl(mserver2). abcast
nodeS
node (nodeS@127.0.0.1)> mserver2:start(). true
(nodeC@127.0.0.1)> mserver2:compute_factorial({server, 'nodeS@127.0.0.1'}, 10). 3628800
Use of {registered_name, node@IP}
instead of the process ID
(e.g. <0.47.0>
) or only the registered name (e.g. server
)
The code has not been changed for running in a distributed setting!
The code must expose the following features:
We will achieve all of this with approximately 20 lines of code!
The engine of the server consists of a function F
of the
following type:
F :: (State,Data) -> (Result, NewState)
loop(State, F) -> receive stop -> true ; {update, From, Ref, NewF} -> From ! {ok, Ref}, loop(State, NewF) ; {request, From, Ref, Data} -> {R, NS} = F(State, Data), From ! {result, Ref, R}, loop(NS, F) end.
It is correct, parametrized, and upgradable
Where can the server go wrong?
The evaluation of expressions can fail
Arithmetic error
> 1/0. ** exception error: bad argument in an arithmetic expression in operator '/'/2 called as 1 / 0
Bad pattern matching
[] = [1]. ** exception error: no match of right hand side value [1]
net_adm:ping(1,2). ** exception error: undefined function net_adm:ping/2
catch
catches exceptions and convert them in
a tuple of the form {'EXIT', Reason}
> catch(1/0). {'EXIT',{badarith,[{erlang,'/',[1,0]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,7}, {shell,eval_exprs,7}, {shell,eval_loop,3}]}} > catch([] = [1]). {'EXIT',{{badmatch,[1]},[{erl_eval,expr,3}]}} > catch(net_adm:ping(1,2)). {'EXIT',{undef,[{net_adm,ping,[1,2]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,7}, {shell,eval_exprs,7}, {shell,eval_loop,3}]}} >
loop(State, F) -> receive stop -> true ; {update, From, Ref, NewF} -> From ! {ok, Ref}, loop(State, NewF); {request, From, Ref, Data} -> case catch(F(State, Data)) of {'EXIT', Reason} -> From!{exit, Ref, Reason}, loop(State, F); {R, NewState} -> From!{result, Ref, R}, loop(NewState, F) end end.
request(Pid, Data) -> Ref = make_ref(), Pid!{request, self(), Ref, Data}, receive {result, Ref, Result} -> Result; {exit, Ref, Reason} -> exit(Reason) end.
update(Pid, Fun) -> Ref = make_ref(), Pid!{update, self(), Ref, Fun}, receive {ok, Ref} -> ok end.
The code for a generic server takes care of the communication, faults, and upgrades
Programmers then only focus on writing the engine
No communication primitives
Mathematical server using the generic server
engine(Count, {factorial,N}) -> Result = math_examples:factorial(N), {Result, Count+1} ; engine(Count, get_count) -> {Count, Count}. start() -> genserver:start(server, 0, fun engine/2). compute_factorial(Pid, N) -> genserver:request(Pid, {factorial, N}).
> genservermath:start(). <0.130.0> > genservermath:compute_factorial(server,10). 3628800