diff --git a/src/fd_chat.erl b/src/fd_chat.erl new file mode 100644 index 0000000..3788677 --- /dev/null +++ b/src/fd_chat.erl @@ -0,0 +1,229 @@ +% @doc +% controller for chat +-module(fd_chat). +-vsn("0.1.0"). +-behavior(gen_server). +-author("Peter Harpending "). +-copyright("Peter Harpending "). +-license("BSD-2-Clause-FreeBSD"). + +-export([ + join/1, + nick_available/1 +]). +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + + +-record(o, {pid :: pid(), + nick :: string()}). +-type orator() :: #o{}. + +-record(s, {orators = [] :: [orator()]}). + +-type state() :: #s{}. + + + +%%% Service Interface + +-spec join(Nick) -> Result + when Nick :: string(), + Result :: ok + | {error, Reason :: any()}. + +join(Nick) -> + gen_server:call(?MODULE, {join, Nick}). + + + +-spec nick_available(Nick) -> Result + when Nick :: string(), + Result :: boolean(). + +nick_available(Nick) -> + gen_server:call(?MODULE, {nick_available, Nick}). + + +%%% Startup Functions + + +-spec start_link() -> Result + when Result :: {ok, pid()} + | {error, Reason :: term()}. +%% @private +%% This should only ever be called by fd_chat_orators (the service-level supervisor). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, none, []). + + +-spec init(none) -> {ok, state()}. +%% @private +%% Called by the supervisor process to give the process a chance to perform any +%% preparatory work necessary for proper function. + +init(none) -> + ok = io:format("Starting.~n"), + State = #s{}, + {ok, State}. + + + +%%% gen_server Message Handling Callbacks + + +-spec handle_call(Message, From, State) -> Result + when Message :: term(), + From :: {pid(), reference()}, + State :: state(), + Result :: {reply, Response, NewState} + | {noreply, State}, + Response :: term(), + NewState :: state(). +%% @private +%% The gen_server:handle_call/3 callback. +%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3 + +handle_call({join, Nick}, {Pid, _}, State) -> + {Reply, NewState} = do_join(Pid, Nick, State), + {reply, Reply, NewState}. +handle_call({nick_available, Nick}, _, State = #s{orators = Orators}) -> + Reply = is_nick_available(Nick, Orators), + {reply, Reply, State}. +handle_call(Unexpected, From, State) -> + ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]), + {noreply, State}. + + +-spec handle_cast(Message, State) -> {noreply, NewState} + when Message :: term(), + State :: state(), + NewState :: state(). +%% @private +%% The gen_server:handle_cast/2 callback. +%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2 + +handle_cast(Unexpected, State) -> + ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]), + {noreply, State}. + + +-spec handle_info(Message, State) -> {noreply, NewState} + when Message :: term(), + State :: state(), + NewState :: state(). +%% @private +%% The gen_server:handle_info/2 callback. +%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2 + +handle_info(Msg = {'DOWN', Mon, process, Pid, Reason}, State) -> + NewState = handle_down(Msg, State), + {noreply, NewState}; +handle_info(Unexpected, State) -> + ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]), + {noreply, State}. + + + +%%% OTP Service Functions + +-spec code_change(OldVersion, State, Extra) -> Result + when OldVersion :: {down, Version} | Version, + Version :: term(), + State :: state(), + Extra :: term(), + Result :: {ok, NewState} + | {error, Reason :: term()}, + NewState :: state(). +%% @private +%% The gen_server:code_change/3 callback. +%% See: http://erlang.org/doc/man/gen_server.html#Module:code_change-3 + +code_change(_, State, _) -> + {ok, State}. + + +-spec terminate(Reason, State) -> no_return() + when Reason :: normal + | shutdown + | {shutdown, term()} + | term(), + State :: state(). +%% @private +%% The gen_server:terminate/2 callback. +%% See: http://erlang.org/doc/man/gen_server.html#Module:terminate-2 + +terminate(_, _) -> + ok. + + +%%% internals + +-spec do_join(Pid, Nick, State) -> {Reply, NewState} + when Pid :: pid(), + Nick :: string(), + Reply :: ok | {error, Reason :: any()}, + NewState :: State. + +do_join(Pid, Nick, State = #s{orators = Orators}) -> + case ensure_can_join(Pid, Nick, Orators) of + ok -> do_join2(Pid, Nick, State); + Error -> {Error, State} + end. + + +do_join2(Pid, Nick, State = #s{orators = Orators}) -> + _Monitor = erlang:monitor(process, Pid), + NewOrator = #o{pid = Pid, nick = Nick}, + NewOrators = [NewOrator | NewOrators], + NewState = State#s{orators = NewOrators}, + {ok, NewState}. + + +-spec ensure_can_join(Pid, Nick, Orators) -> Result. + when Pid :: pid(), + Nick :: string(), + Orators :: [orator()] + Result :: ok + | {error, Reason}, + Reason :: any(). +% @private +% ensures both Pid and Nick are unique + +ensure_can_join(Pid, _ , [#o{pid = Pid} | _ ]) -> {error, already_joined}; +ensure_can_join(_ , Nick, [#o{nick = Nick} | _ ]) -> {error, {nick_taken, Nick}}; +ensure_can_join(Pid, Nick, [_ | Rest]) -> ensure_can_join(Pid, Nick, Rest); +ensure_can_join(Pid, _, _, [] ) -> ok. + + +-spec is_nick_available(Nick, Orators) -> boolean() + when Nick :: string(), + Orators :: [orator()]. + +is_nick_available(Nick, [#o{nick = Nick} | _ ]) -> false; +is_nick_available(Nick, [_ | Rest]) -> is_nick_available(Nick, Rest); +is_nick_available(Nick, [] ) -> true. + + + +-spec handle_down(Msg, State) -> NewState + when Msg :: {'DOWN', Mon, process, Pid, Reason}, + Mon :: monitor(), + Pid :: pid(), + Reason :: any(), + State :: state(), + NewState :: State. + +handle_down(Msg = {'DOWN', _, process, Pid, _}, State = #s{orators = Orators}) -> + NewOrators = hdn(Msg, Pid, Orators, []), + NewState = State#s{orators = NewOrators}, + NewState. + +% encountered item, removing +hdn(_, Pid, [#o{pid = Pid} | Rest], Acc) -> Rest ++ Acc; +hdn(Msg, Pid, [Skip | Rest], Acc) -> hdn(Msg, Pid, Rest, [Skip | Acc]); +hdn(Msg, Pid, [] , Acc) -> + log("~tp: Unexpected message: ~tp", [?MODULE, Msg]), + Acc. diff --git a/src/fd_sup.erl b/src/fd_sup.erl index e5d753a..9c267f0 100644 --- a/src/fd_sup.erl +++ b/src/fd_sup.erl @@ -42,11 +42,17 @@ init([]) -> 5000, supervisor, [fd_clients]}, + Chat = {fd_chat, + {fd_chat, start_link, []}, + permanent, + 5000, + worker, + [fd_chat]}, Cache = {fd_cache, {fd_cache, start_link, []}, permanent, 5000, worker, [fd_cache]}, - Children = [Clients, Cache], + Children = [Clients, Chat, Cache], {ok, {RestartStrategy, Children}}.