-module(ow_beacon). -behaviour(gen_server). -define(SERVER, ?MODULE). % Public API -export([start/0, stop/0, last/0, get_by_id/1, dump/0]). % genserver callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). % milliseconds -define(HEARTBEAT, 1000). % The number of beacons to hold in memory -define(WINDOW_SIZE, 25). -rpc_encoder(#{app => overworld, lib => overworld_pb, interface => ow_msg}). -rpc_client([session_beacon]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % API %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec start() -> gen_server:start_ret(). start() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). -spec stop() -> ok. stop() -> gen_server:stop(?SERVER), ok. -spec last() -> integer(). last() -> gen_server:call(?MODULE, last). -spec get_by_id(integer()) -> integer(). get_by_id(ID) -> gen_server:call(?MODULE, {get_by_id, ID}). -spec dump() -> [integer()]. dump() -> gen_server:call(?MODULE, dump). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % genserver callbacks %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% init([]) -> {ok, new_state()}. handle_call({get_by_id, ID}, _From, State = {Window, _Timer}) -> case lists:keyfind(ID, 1, Window) of false -> logger:notice("Don't have this ID: ~p~n", [ID]), logger:notice("Window is : ~p~n", [Window]), {reply, 0, State}; {ID, Time} -> {reply, Time, State} end; handle_call(last, _From, {[Now | _Rest], Timer}) -> {reply, Now, {Now, Timer}}; handle_call(dump, _From, {Window, Timer}) -> {reply, Window, {Window, Timer}}; handle_call(_Call, _From, State) -> {reply, State, State}. handle_cast(_Req, State) -> {noreply, State}. handle_info(tick, {Window, OldTimer}) -> %[H|_] = Window, %logger:notice("Beacon tick: ~p", [H]), % Stop the old timer erlang:cancel_timer(OldTimer), % Create a new unique integer as the beacon ID ID = erlang:unique_integer([positive]), % Push the beacon ID + current time to the stack Beacon = {ID, erlang:monotonic_time()}, % Encode the beacon and broadcast it to all clients. % n.b., There's a very small race here. A client could conceivably respond % faster than the server has updated the list but I've never observed this % even on LAN Msg = {self(), client_msg, {session_beacon, #{id => ID}}}, % use gproc to send ALL registered processes! gproc:send({p, l, client_session}, Msg), NewTimer = erlang:send_after(?HEARTBEAT, self(), tick), {noreply, {push(Beacon, Window), NewTimer}}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % internal functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% new_state() -> ID = erlang:unique_integer(), Now = erlang:monotonic_time(), NewTimer = erlang:send_after(?HEARTBEAT, self(), tick), {[{ID, Now}], NewTimer}. % Push the latest beacon onto a fixed length list of the last ?WINDOW_SIZE % number of beacons. push(Beacon, List) -> L1 = lists:sublist(List, ?WINDOW_SIZE - 1), [Beacon | L1].