Last active
August 29, 2015 14:15
-
-
Save allyourcode/d042dd5af7db77377739 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -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