%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2016. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(snmp_note_store).
-behaviour(gen_server).
-include_lib("snmp/src/app/snmp_internal.hrl").
-include("snmp_debug.hrl").
-include("snmp_verbosity.hrl").
%% External exports
-export([start_link/3, stop/1,
get_note/2,
set_note/3, set_note/4,
info/1, verbosity/2]).
%% Internal exports
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-export([timer/3]).
-define(timeout, 30000). % Perform gc twice in a minute.
-ifndef(default_verbosity).
-define(default_verbosity,silence).
-endif.
-ifdef(snmp_debug).
-define(GS_START_LINK(Args),
gen_server:start_link(?MODULE, Args, [{debug,[trace]}])).
-else.
-define(GS_START_LINK(Args),
gen_server:start_link(?MODULE, Args, [])).
-endif.
-record(state, {mod, notes, timer, timeout, active = false}).
%%%-----------------------------------------------------------------
%%% Implements a database for notes with a lifetime. Once in a
%%% while, the database will be gc:ed, to get rid of old notes.
%%% This database will not contain much data.
%%% Options is a list of Option, where Option is
%%% {verbosity, silence|log|debug|trace} % undocumented feature
%%%-----------------------------------------------------------------
start_link(Prio, Mod, Opts) ->
?d("start_link -> entry with"
"~n Prio: ~p"
"~n Mod: ~p"
"~n Opts: ~p", [Prio, Mod, Opts]),
?GS_START_LINK([Prio, Mod, Opts]).
%%-----------------------------------------------------------------
%% Interface functions.
%%-----------------------------------------------------------------
stop(Pid) ->
call(Pid, stop).
get_note(Pid, Key) ->
call(Pid, {get_note, Key}).
%% Lifetime is in 1/10 sec or infinity
set_note(Pid, Key, Value) ->
set_note(Pid, infinity, Key, Value).
set_note(Pid, Lifetime, Key, Value) ->
call(Pid, {set_note, Lifetime, Key, Value}).
info(Pid) ->
call(Pid, info).
verbosity(Pid, Verbosity) ->
cast(Pid, {verbosity, Verbosity}).
init([Prio, Mod, Opts]) ->
?d("init -> entry with"
"~n Prio: ~p"
"~n Mod: ~p"
"~n Opts: ~p", [Prio, Mod, Opts]),
case (catch do_init(Prio, Mod, Opts)) of
{ok, State} ->
{ok, State};
E ->
error_msg("failed starting note-store: ~n~p", [E]),
{stop, E}
end.
do_init(Prio, Mod, Opts) ->
process_flag(trap_exit, true),
process_flag(priority, Prio),
put(sname, get_sname(Opts)),
put(verbosity, get_verbosity(Opts)),
put(snmp_component, get_component(Mod)),
?vlog("starting",[]),
Notes = ets:new(snmp_note_store, [set, protected]),
Timeout = get_timeout(Opts),
State = #state{mod = Mod,
notes = Notes,
timeout = Timeout,
timer = start_timer(Timeout)},
?vdebug("started",[]),
{ok, State}.
%%-----------------------------------------------------------------
%% A note is any internal information that has to be
%% stored for some time (the Lifetime).
%% A note is stored in ets as {Key, {BestBefore, Value}},
%% where BestBefore is currentTime + Lifetime.
%% A GC-op can destroy any notes with CurTime > BestBore.
%% Lifetime is in centiseconds or infinity, in which case
%% the note is eternal.
%%-----------------------------------------------------------------
handle_call({set_note, Lifetime, Key, Value}, _From,
#state{mod = Mod, notes = Notes} = State)
when is_integer(Lifetime) ->
?vlog("set note <~p,~p> with life time ~p", [Key,Value,Lifetime]),
case (catch Mod:system_start_time()) of
SysStartTime when is_integer(SysStartTime) ->
?vtrace("handle_call(set_note) -> SysStartTime: ~p",
[SysStartTime]),
Now = snmp_misc:now(cs),
?vtrace("handle_call(set_note) -> Now: ~p", [Now]),
RealUpTime = Now - SysStartTime,
?vtrace("handle_call(set_note) -> RealUpTime: ~p", [RealUpTime]),
BestBefore = RealUpTime + Lifetime,
?vtrace("handle_call(set_note) -> BestBefore: ~p", [BestBefore]),
Val = ets:insert(Notes, {Key, {BestBefore, Value}}),
NState = activate_timer(State),
{reply, Val, NState};
_Crap ->
?vinfo("handle_call(set_note) -> "
"failed retreiving system start time from ~w: "
"~n ~p", [Mod, _Crap]),
{reply, {error, failed_retreive_system_start_time}, State}
end;
handle_call({set_note, infinity, Key, Value}, _From,
#state{notes = Notes} = State) ->
?vlog("set note <~p,~p>",[Key,Value]),
Val = ets:insert(Notes, {Key, {infinity, Value}}),
?vdebug("set note; old value: ~p",[Val]),
{reply, Val, State};
handle_call({get_note, Key}, _From,
#state{mod = Mod, notes = Notes} = State) ->
?vlog("get note ~p",[Key]),
Val = handle_get_note(Notes, Mod, Key),
?vdebug("get note: ~p",[Val]),
{reply, Val, State};
handle_call(info, _From, #state{timer = Pid, notes = Notes} = State) ->
?vlog("info",[]),
Info = get_info(Pid, Notes),
{reply, Info, State};
handle_call(stop, _From, State) ->
?vlog("stop",[]),
{stop, normal, ok, State};
handle_call(Req, From, State) ->
warning_msg("received unexpected request from ~p: ~n~p",[From, Req]),
{reply, {error, {unknown_request, Req}}, State}.
handle_cast({verbosity,Verbosity}, State) ->
?vlog("verbosity: ~p -> ~p",[get(verbosity),Verbosity]),
put(verbosity,snmp_verbosity:validate(Verbosity)),
{noreply, State};
handle_cast(Msg, State) ->
warning_msg("received unexpected message: ~n~p",[Msg]),
{noreply, State}.
%%-----------------------------------------------------------------
%% If there are no possible garbage left, we don't
%% have to wait for timeout, and perform another
%% gc, because we won't do anything. So
%% we switch the timeout off in that case.
%% It will be switched on as soon as we get some
%% other message.
%%-----------------------------------------------------------------
handle_info(timeout, State) ->
?vdebug("timeout",[]),
case gc(State) of
nothing_left ->
NState = deactivate_timer(State),
{noreply, NState};
work_to_do ->
NState = activate_timer(State),
{noreply, NState}
end;
handle_info({'EXIT', Pid, Reason},
#state{timer = Pid, timeout = Timeout} = State) ->
?vinfo("exit message from the timer process ~p for reason ~p",
[Pid, Reason]),
set_state(State#state{timer = start_timer(Timeout)});
handle_info({'EXIT',Pid,Reason}, State) ->
?vlog("exit message from ~p for reason ~p",[Pid,Reason]),
{noreply, State};
handle_info(Info, State) ->
warning_msg("received unexpected info: ~n~p",[Info]),
{noreply, State}.
set_state(S) ->
case gc(S) of
nothing_left ->
NState = deactivate_timer(S),
{noreply, NState};
work_to_do ->
NState = activate_timer(S),
{noreply, NState}
end.
terminate(Reason, _State) ->
?vdebug("terminate: ~p",[Reason]),
ok.
%%----------------------------------------------------------
%% Code change
%%----------------------------------------------------------
% downgrade
code_change({down, _Vsn}, State, _Extra) ->
NState = activate_timer(deactivate_timer(State)),
{ok, NState};
% upgrade
code_change(_Vsn, State0, _Extra) ->
process_flag(trap_exit, true),
State1 =
case State0 of
#state{timeout = false} ->
State0#state{timeout = ?timeout};
_ ->
State0
end,
State2 = restart_timer(State1),
{ok, State2}.
%%----------------------------------------------------------
%% Timer
%%----------------------------------------------------------
activate_timer(#state{timer = Pid, active = false} = State) ->
Pid ! activate,
receive
activated -> ok
end,
State#state{active = true};
activate_timer(State) ->
State.
deactivate_timer(#state{timer = Pid, active = true} = State) ->
Pid ! deactivate,
receive
deactivated -> ok
end,
State#state{active = false};
deactivate_timer(State) ->
State.
start_timer(Timeout) ->
spawn_link(?MODULE, timer, [self(), passive, Timeout]).
%% Kill, restart and activate timer.
restart_timer(#state{timer = Pid, timeout = Timeout} = State) ->
?d("restart_timer -> kill current timer process ~p",[Pid]),
exit(Pid, kill),
?d("restart_timer -> await acknowledgement",[]),
receive
{'EXIT', Pid, _Reason} ->
ok
end,
?d("restart_timer -> start a new timer process",[]),
activate_timer(State#state{timer = start_timer(Timeout), active = false}).
timer(Pid, passive, Timeout) ->
receive
deactivate ->
?d("timer(passive) -> deactivate request, just send ack",[]),
Pid ! deactivated,
?MODULE:timer(Pid, passive, Timeout);
activate ->
?d("timer(deactive) -> activate request, send ack",[]),
Pid ! activated,
?d("timer(deactive) -> activate",[]),
?MODULE:timer(Pid, active, Timeout) % code replacement
after
Timeout ->
?d("timer(deactive) -> timeout",[]),
?MODULE:timer(Pid, passive, Timeout)
end;
timer(Pid, active, Timeout) ->
receive
activate ->
?d("timer(active) -> activate request, just send ack",[]),
Pid ! activated,
?MODULE:timer(Pid, active, Timeout);
deactivate ->
?d("timer(active) -> deactivate request, send ack",[]),
Pid ! deactivated,
?d("timer(active) -> deactivate",[]),
?MODULE:timer(Pid, passive, Timeout)
after
Timeout ->
?d("timer(active) -> timeout",[]),
Pid ! timeout,
?MODULE:timer(Pid, active, Timeout)
end.
handle_get_note(Notes, Mod, Key) ->
case ets:lookup(Notes, Key) of
[{Key, {infinity, Val}}] ->
Val;
[{Key, {BestBefore, Val}}] ->
?vtrace("get note -> BestBefore: ~w", [BestBefore]),
StartTime = Mod:system_start_time(),
?vtrace("get note -> StartTime: ~w", [StartTime]),
Now = snmp_misc:now(cs),
?vtrace("get note -> Now: ~w", [Now]),
case (Now - StartTime) of
Diff when BestBefore >= Diff ->
?vtrace("get note -> Diff: ~w", [Diff]),
Val;
OldDiff ->
?vtrace("get note -> note to old [~w] - delete", [OldDiff]),
ets:delete(Notes, Key),
undefined
end;
[] ->
undefined
end.
%%-----------------------------------------------------------------
%% Clean up all old notes in the database.
%%-----------------------------------------------------------------
gc(#state{mod = Mod, notes = Notes}) ->
RealUpTime = snmp_misc:now(cs) - Mod:system_start_time(),
gc(nothing_left, ets:tab2list(Notes), Notes, RealUpTime).
gc(Flag, [{_Key, {infinity, _}} | T], Tab, Now) -> gc(Flag, T, Tab, Now);
gc(Flag, [{Key, {BestBefore, _}} | T], Tab, Now)
when is_integer(BestBefore) andalso (BestBefore < Now) ->
ets:delete(Tab, Key),
gc(Flag, T, Tab, Now);
gc(_Flag, [_ | T], Tab, Now) -> gc(work_to_do, T, Tab, Now);
gc(Flag, [], _Tab, _Now) -> Flag.
%%-----------------------------------------------------------------
get_info(Tmr, Notes) ->
ProcSize = proc_mem(self()),
TMRSz = proc_mem(Tmr),
NotesSz = tab_size(Notes),
[{process_memory, [{notes, ProcSize}, {timer, TMRSz}]},
{db_memory, [{notes, NotesSz}]}].
proc_mem(P) when is_pid(P) ->
case (catch erlang:process_info(P, memory)) of
{memory, Sz} ->
Sz;
_ ->
undefined
end;
proc_mem(_) ->
undefined.
tab_size(T) ->
case (catch ets:info(T, memory)) of
Sz when is_integer(Sz) ->
Sz;
_ ->
undefined
end.
%%-----------------------------------------------------------------
call(Pid, Req) ->
call(Pid, Req, infinity).
call(Pid, Req, Timeout) ->
gen_server:call(Pid, Req, Timeout).
cast(Pid, Msg) ->
gen_server:cast(Pid, Msg).
%%-----------------------------------------------------------------
%% info_msg(F, A) ->
%% ?snmp_info(get(snmp_component), "Note store server " ++ F, A).
warning_msg(F, A) ->
?snmp_warning(get(snmp_component), "Note store server " ++ F, A).
error_msg(F, A) ->
?snmp_error(get(snmp_component), "Note store server " ++ F, A).
%%-----------------------------------------------------------------
get_verbosity(Opts) ->
snmp_misc:get_option(verbosity, Opts, ?default_verbosity).
get_sname(Opts) ->
snmp_misc:get_option(sname, Opts, ns).
get_timeout(Opts) ->
snmp_misc:get_option(timeout, Opts, ?timeout).
get_component(snmpm) ->
"manager";
get_component(snmpa) ->
"agent";
get_component(_) ->
"".