Concurrent Programming in Erlang - Week 1 Assignment
%% Based on code from
%% Erlang Programming
%% Francecso Cesarini and Simon Thompson
%% O'Reilly, 2008
%% http://oreilly.com/catalog/9780596518189/
%% http://www.erlangprogramming.org/
%% (c) Francesco Cesarini and Simon Thompson
-module(frequency_sup).
-export([start/0,allocate/0,deallocate/1,stop/0,client/3,start_client/1]).
-export([init/2,super/2]).
% Launches the supervisor process with the specified MFA and registered name
% to which {M, F, A} is to be associated to.
super({M, F, A}, Name) ->
% Add a reference to the supervisor's PID so that the child has a reference
% to its parent, in order to be able to communicate with it to send its
% state, as well as to be able to determine whether the supervisor was
% terminated or one of the clients was terminated.
State = [self() | A],
% Trap exits.
process_flag(trap_exit, true),
io:format("Starting system with {M, F, A} ~p.~n", [{M, F, State}]),
% Spawn and register the MFA used to manage the main process. Again, note that
% we have injected the supervisor PID into the arguments.
register(Name, ChildPid = spawn_link(M, F, State)),
super_loop({M, F, State}, Name, ChildPid).
% The loop which manages the lifetime of the process that is spawned using the
% {M, F, A} specified above. We also maintain the child PID, so that messages
% tagged with {state, ...} coming from the child PID are identified.
super_loop({M, F, State}, Name, ChildPid) ->
receive
{'EXIT', _Pid, _Reason} ->
io:format("Restarting system.~n"),
register(Name, NewChildPid = spawn_link(M, F, State)),
super_loop({M, F, State}, Name, NewChildPid);
{state, ChildPid, NewFrequencies} ->
io:format("Updating frequencies in supervisor ~p.~n", [NewFrequencies]),
super_loop({M, F, [self(), NewFrequencies]}, Name, ChildPid)
end.
%% These are the start functions used to create and
%% initialize the server.
start() ->
% Get the list of hard-coded frequencies to pass to the init function, which
% is presented as a {M, F, A} below. Note that the process is will also be
% registered under the name 'frequency'.
Frequencies = {get_frequencies(), []},
register(super, spawn(frequency_sup, super,
[{frequency_sup, init, [Frequencies]}, frequency])).
% The main function includes also the supervisor PID, in addition to the
% frequencies. This serves two functions:
% 1. The main process will be able to send its state also to the supervisor,
% so that if it crashes, the supervisor knows to start it from the last
% known state.
% 2. This process is set to trap EXITs to deal with client deaths. However,
% since the supervisor spawns_links this process, a death of the supervisor
% would also send a message to this process in the form of KILLED. We need
% to identify between process death messages coming from the supervisor and
% the connected clients, and we do this by pattern-matching the EXIT
% messages using the SuperPid passed to the init/2 function.
init(SuperPid, Frequencies) ->
process_flag(trap_exit, true),
loop(SuperPid, Frequencies).
% Hard Coded
get_frequencies() -> [10,11,12,13,14,15].
%% The Main Loop
% The main frequency server loop. Note that in addition to the frequencies, it
% also takes the supervisor PID. The updated state that is generated after
% processing each allocate/deallocate/client death messages is relayed to the
% supervisor, so that in case of the frequency server's death, this data is can
% be used by the supervisor to restart the frequency server in the last known
% state.
loop(SuperPid, Frequencies) ->
% Print list of frequencies.
io:format("[SERVER] List of frequencies free/assigned ~p.~n", [Frequencies]),
receive
{request, Pid, allocate} ->
{NewFrequencies, Reply} = allocate(Frequencies, Pid),
Pid ! {reply, Reply},
super ! {state, self(), NewFrequencies},
loop(SuperPid, NewFrequencies);
{request, Pid , {deallocate, Freq}} ->
NewFrequencies = deallocate(Frequencies, Freq),
Pid ! {reply, ok},
super ! {state, self(), NewFrequencies},
loop(SuperPid, NewFrequencies);
{request, Pid, stop} ->
Pid ! {reply, stopped},
exit(stop);
{'EXIT', SuperPid, _Reason} ->
exit(stopped_sup);
{'EXIT', Pid, _Reason} ->
NewFrequencies = exited(Frequencies, Pid),
super ! {state, self(), NewFrequencies},
loop(SuperPid, NewFrequencies)
end.
%% Functional interface
allocate() ->
frequency ! {request, self(), allocate},
receive
{reply, Reply} -> Reply
end.
deallocate(Freq) ->
frequency ! {request, self(), {deallocate, Freq}},
receive
{reply, Reply} -> Reply
end.
stop() ->
frequency ! {request, self(), stop},
receive
{reply, Reply} -> Reply
end.
%% The Internal Help Functions used to allocate and
%% deallocate frequencies.
allocate({[], Allocated}, _Pid) ->
{{[], Allocated}, {error, no_frequency}};
allocate({[Freq|Free], Allocated}, Pid) ->
link(Pid), %%% ADDED
io:format("[Server] Allocated new frequency ~p to client ~p.~n", [Freq, Pid]),
{{Free, [{Freq, Pid}|Allocated]}, {ok, Freq}}.
deallocate({Free, Allocated}, Freq) ->
{value,{Freq,Pid}} = lists:keysearch(Freq,1,Allocated), %%% ADDED
unlink(Pid), %%% ADDED
NewAllocated=lists:keydelete(Freq, 1, Allocated),
io:format("[Server] Deallocated existing frequency ~p from client ~p.~n", [Freq, Pid]),
{[Freq|Free], NewAllocated}.
exited({Free, Allocated}, Pid) -> %%% FUNCTION ADDED
case lists:keysearch(Pid,2,Allocated) of
{value,{Freq,Pid}} ->
NewAllocated = lists:keydelete(Freq,1,Allocated),
io:format("[Server] Reclaimed frequency ~p to client ~p.~n", [Freq, Pid]),
{[Freq|Free],NewAllocated};
false ->
{Free,Allocated}
end.
% Starts the client with the specified mode. The client process is set to
% trap EXITs so that when the frequency server process dies, it suspends itself.
% Also, this allows us to handle a stop command to issued to the frequency
% server by gracefully terminating all the clients.
% The client operates in one of two modes:
% die: the client makes one allocation and then dies normally. This triggers the
% frequency server to reclaim the allocated frequency number.
% normal: The client alternates between allocating a new frequency number and
% deallocating it.
start_client(Type) ->
spawn(fun() -> process_flag(trap_exit, true), client(Type, null, whereis(frequency)) end).
% Modes: die, normal
client(Type, AllocatedFreq, ServerPid) ->
% Get PID for logging purposes.
Pid = self(),
receive
{'EXIT', ServerPid, killed} ->
% Frequency server is dead - suspend myself.
io:format("[CLIENT ~p] Frequency server ~p is dead...suspending till wakeup call.~n", [Pid, ServerPid]),
receive
{NewPid, wakeup} -> client(Type, AllocatedFreq, NewPid)
end;
{'EXIT', ServerPid, stop} ->
% Normal stop signal from system.
io:format("[CLIENT ~p] Putting client to sleep.~n", [Pid]),
exit(stop)
after 0 -> ok
end,
case Type of
die ->
% Allocate, wait and die. If we do not match the pattern (i.e. all
% frequencies are depleated, we die abnormally), othewise we terminate
% normally.
{ok, Freq} = allocate(),
io:format("[CLIENT ~p] Allocated ~p and dying normally.~n", [Pid, Freq]),
timer:sleep(3000);
normal ->
% Default behaviour - allocate and hold frequency and then deallocate.
% If we do not match the pattern (i.e. all frequencies are depleated,
% we die abnormally).
case AllocatedFreq of
null ->
% We are not allocated a frequency, so let's acquire one.
{ok, Freq} = allocate(),
io:format("[CLIENT ~p] Allocated ~p and will release it soon.~n", [Pid, Freq]),
timer:sleep(1000 + rand:uniform(5000)),
client(Type, Freq, ServerPid);
_ ->
% We are allocated a frequency, so let's relinquish it.
deallocate(AllocatedFreq),
client(Type, null, ServerPid)
end
end.