Skip to content

Instantly share code, notes, and snippets.

@allyourcode
Last active August 29, 2015 14:15
Show Gist options
  • Select an option

  • Save allyourcode/d042dd5af7db77377739 to your computer and use it in GitHub Desktop.

Select an option

Save allyourcode/d042dd5af7db77377739 to your computer and use it in GitHub Desktop.
-module(click_server).
-behavior(gen_server).
% For gen_server behavior.
-export([
% life cycle functions
init/1, code_change/3, terminate/2,
% handle_* service provider functions
handle_call/3, handle_cast/2, handle_info/2]).
% For users of this module.
-export([
% "constructors"
start/0, start_link/0,
% "methods"
click/2, persist/1]).
% This is an over-glorified bag of counters, keyed by URL (any string, actually).
% The reason this is interesting is that this trades risk of data loss
% for speed. In that spirit, the API is all "fire and forget"
% (i.e. based on gen_server:cast).
%
% But that's not where the speed (and risk) really comes from. Instead,
% counters are NOT persisted on every cast (= public function call).
% Data is made persistent only every once in a while, when the server feels
% like it. You can also request that a persistence process be kicked off,
% but this should not be necessary (only provided as a security blanket).
% Counters are also persisted during termination.
% Public
start() ->
Options = [],
gen_server:start(?MODULE, no_extra, Options)
.
start_link() ->
Options = [],
gen_server:start_link(?MODULE, no_extra, Options)
.
click(Server, Url) ->
gen_server:cast(Server, {click, Url})
.
persist(Server) ->
gen_server:cast(Server, persist)
.
% Implement gen_server behavior.
% private
-record(state, {nagger=spawn_nagger(2 * 1000),
counters=orddict:new(),
persisters=sets:new()}).
% gen_server handle_* service provider functions.
handle_call(no_calls_all_casts, _From, _State) ->
throw(not_supoprted)
.
handle_cast({click, Url}, State) ->
{noreply, increment(Url, State)}
;
handle_cast(persist, State) ->
{noreply, spawn_persister(State)}
.
handle_info({'EXIT', Pid, Reason}, State) ->
case sets:is_element(Pid, State#state.persisters) of
true ->
{noreply, remove_persister(Pid, State)};
false ->
io:format("Received exit from non-persister (therefore, self will be "
"terminate).\nPid: ~w\nReason: ~p\n",
[Pid, Reason]),
{stop, Reason, State}
end
.
% gen_server life cycle functions.
init(_) ->
process_flag(trap_exit, true),
{ok, #state{}}
.
code_change(_OldVersion, State, _Extra) ->
{ok, State}
.
terminate(Reason, State) ->
case is_clean_exit(Reason) of
true ->
NewState = spawn_persister(State),
DrainTimeout = 60 * 1000,
drain_linked_processes(NewState#state.persisters, DrainTimeout);
false ->
io:format("Abnormal exit. See gen_server logs for details.\n")
end
.
% Private
spawn_persister(State=#state{}) ->
NewPersisters = sets:add_element(
spawn_link(fun() -> server_persist(State#state.counters) end),
State#state.persisters
),
% Aside: Since this clobbers all fields, this is equivalent to creating
% a whole new record, but update instead, in case fields are added
% in future.
State#state{counters=orddict:new(),
persisters=NewPersisters}
.
% TODO: Implement. Need to learn how to write to disk...
server_persist(Counters) ->
% In later versions of Erlang, there is an orddict:is_empty function.
IsEmpty = orddict:size(Counters) == 0,
if
IsEmpty ->
io:format("Nothing to persist.\n");
true ->
io:format("Persisting counters:\n~p\n", [Counters]),
timer:sleep(1000)
end % if
.
increment(Url, State=#state{counters=Counters}) ->
NewCounters = orddict:update(Url,
fun(Count) -> Count + 1 end,
0, Counters),
NewState = State#state{counters=NewCounters},
% Yay, if expression restrictions...
TooLarge = too_large(NewState),
if TooLarge ->
spawn_persister(NewState);
true ->
NewState
end
.
% TODO: Implement.
too_large(_State=#state{}) ->
false
.
remove_persister(Pid, State) ->
State#state{persisters=sets:del_element(Pid, State#state.persisters)}
.
spawn_nagger(Period) ->
Self = self(),
spawn_link(fun() -> nag(Self, Period) end)
.
% In newer versions of Erlang, we'd be able to define this within spawn_nag
% itself. IMHO, that would be preferrable, because we don't want this to be
% called from anywhere else.
nag(Parent, Period) ->
receive
reset -> ok
after Period ->
persist(Parent)
end,
nag(Parent, Period)
.
% Actually, this belongs in a general purpose library.
% Perhaps, one already exists?
%
% TODO: Use Timeout.
% TODO: Maybe return some kind of status?
drain_linked_processes(Processes, _Timeout) ->
Empty = sets:size(Processes) == 0,
if Empty ->
ok;
true ->
receive
{'EXIT', Pid, Reason} ->
io:format(
case sets:is_element(Pid, Processes) of
true ->
"Waitee ~w exited.\nReason: ~p\n";
false ->
"Yikes! Non-Waitee process ~w exited.\nReason: ~p\n"
end,
[Pid, Reason]),
drain_linked_processes(sets:del_element(Pid, Processes), _Timeout)
end % receive
end % if
.
is_clean_exit(normal) -> true;
is_clean_exit(shutdown) -> true;
is_clean_exit({shutdown, _}) -> true;
is_clean_exit(_Reason) -> false.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment