Current section
Files
Jump to
Current section
Files
src/jc_eviction_manager.erl
%%% ----------------------------------------------------------------------------
%%% @author Jim Rosenblum
%%% @copyright (C) 2011-2015, Jim Rosenblum
%%% @doc jc_eviction_manager
%%% 1: Implements item-level ttl by spying on the cache table, creating and
%%% deleting TTL timers as indicated by the ttl attribute of the cache entry.
%%% A unique reference is assocateid with a cached item, and a TTL timer
%%% has this unique reference as part of its sate. When the timer goes off,
%%% it evicts using this cache-item reference. This ensures that the timer never
%%% evicts anything but the correct'version' of an item. If an item is updated
%%% (a 'Put' with a Map and Key of an existing value), it will get a new
%%% referenceso the old TTL will not evict the updated version even if it goes
%%% off before it can be cancelled.
%%% 2: Implements a map-based ttl (max_ttl) by using a timer to periodically
%%% evict any cache items whose create time is older than the configured value
%%% for a given map.
%%% @end
%%% Created : 25 Oct 2011 by Jim Rosenblum
%% -----------------------------------------------------------------------------
-module(jc_eviction_manager).
-behaviour(gen_server).
%% API
-export([start_link/0]).
% Api to set and retrieve Map-level max TTLs.
-export([set_max_ttl/2,
get_max_ttls/0]).
% gen_server callbacks.
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
% Internal function used in spawn.
-export([handle_max_ttl/1]).
% Table and record definitions.
-include("../include/records.hrl").
-define(SERVER, ?MODULE).
-define(INFINITY, 0).
-define(MAX_TTL_JOB_SECS, 120).
-record(jc_evict_mngr_state, {}).
%%% ============================================================================
%%% Module API
%%% ============================================================================
%% -----------------------------------------------------------------------------
%% @doc Create a TTL timer for the cache entry with the supplied reference. A
%% 0 TTL is ignored as it is the equivalent of infinity.
%%
-spec add_timer(ttl(), rec_ref()) -> ok.
add_timer(?INFINITY, _KVRecRef) ->
ok;
add_timer(TTLSecs, KVRecRef) ->
lager:debug("~p: adding TTL for the record with reference ~p.",
[?MODULE, KVRecRef]),
TTLMsecs = TTLSecs * 1000,
Ref = erlang:send_after(TTLMsecs, ?MODULE, {evict, KVRecRef}),
mnesia:dirty_write(#ttl{key = KVRecRef,
timer_ref = Ref,
ttl_secs = TTLSecs}),
ok.
%% -----------------------------------------------------------------------------
%% @doc Cancel the timer for the cache entry with the supplied reference.
%% Returns the time left before it would have fired, in milliseconds or false
%% if the timer no longer exists.
%%
-spec cancel_timer(RecRef::rec_ref()) -> Time::non_neg_integer() | false.
cancel_timer(RecRef) ->
case mnesia:dirty_read({ttl, RecRef}) of
[#ttl{timer_ref = Timer} = Rec] ->
lager:debug("~p: cancelling TTL for record ~p and timer ~p.",
[?MODULE, RecRef, Timer]),
Result = erlang:cancel_timer(Timer),
mnesia:dirty_delete_object(Rec),
Result;
[] ->
false
end.
%% -----------------------------------------------------------------------------
%% @doc Create, change or delete the max TTL associated with a map. 0 seconds
%% will remove the max_ttl for the map alltogether. Existing values are
%% overwritten.
%%
-spec set_max_ttl(Map::map_name(), Secs::seconds()) -> ok | {error, badarg}.
set_max_ttl(Map, ?INFINITY) ->
lager:debug("~p: removing max ttl from map ~p.", [?MODULE, Map]),
mnesia:dirty_delete({max_ttl, Map}),
ok;
set_max_ttl(Map, Secs) when is_integer(Secs) andalso Secs > 0->
lager:debug("~p: updating max ttl for map ~p to ~p.", [?MODULE, Map, Secs]),
mnesia:dirty_write(#max_ttl{map = Map, ttl_secs = Secs}),
ok;
set_max_ttl(_Map, Bad) ->
lager:warning("~p: bad value sent to set_max_ttl: ~p.", [?MODULE, Bad]),
{error, badarg}.
%% -----------------------------------------------------------------------------
%% @doc Return [{Map, TTLSecs}].
%%
-spec get_max_ttls() -> [{Map::map_name(), Secs::seconds()}].
get_max_ttls()->
Trans = fun() ->
mnesia:foldl(fun(#max_ttl{map = M, ttl_secs = S}, Acc) ->
[{M, S}| Acc]
end,
[],
max_ttl)
end,
trans_execute(Trans).
%%% ============================================================================
%%% Module API
%%% ============================================================================
%% -----------------------------------------------------------------------------
%% @doc Starts the server and links the caller to it.
-spec start_link() -> {ok, pid()} | ignore | {error, any()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%% ============================================================================
%%% gen_server callbacks
%%% ============================================================================
%% -----------------------------------------------------------------------------
%% @private Initialize the server by subscribing to key_to_value table changes.
%% and seeting up the timer for max_ttl evicts.
%%
-spec init([]) -> {ok, #jc_evict_mngr_state{}}.
init([]) ->
get_default_max_ttls(),
create_max_ttl_timer(),
mnesia:subscribe({table, key_to_value, detailed}),
lager:info("~p: up.", [?SERVER]),
{ok, #jc_evict_mngr_state{}}.
get_default_max_ttls() ->
MaxTTLMaps = application:get_env(jc, max_ttl_maps, []),
[set_max_ttl(Map, Secs)|| {Map, Secs} <- MaxTTLMaps].
create_max_ttl_timer() ->
JobSecs = application:get_env(jc, max_ttl_job_secs, ?MAX_TTL_JOB_SECS),
create_max_ttl_job(JobSecs).
%% -----------------------------------------------------------------------------
%% @private Hande call messages.
%%
-spec handle_call(term(), {pid(), _}, #jc_evict_mngr_state{}) ->
{reply, ok, #jc_evict_mngr_state{}}.
handle_call(Request, _From, State) ->
lager:warning("~p: unexpected call request: ~p.",[?SERVER, Request]),
{reply, ok, State}.
%% -----------------------------------------------------------------------------
%% @private Handle cast messages.
%%
-spec handle_cast(any(), #jc_evict_mngr_state{}) ->
{noreply, #jc_evict_mngr_state{}}.
handle_cast(Msg, State) ->
lager:warning("~p: unexpected cast message: ~p.",[?SERVER, Msg]),
{noreply, State}.
%% -----------------------------------------------------------------------------
%% @private Handle info messages: eviction and timer create/deteles due to
%% key_to_value table changes.
%%
-spec handle_info(any(), #jc_evict_mngr_state{}) ->
{noreply, #jc_evict_mngr_state{}}.
handle_info({mnesia_table_event, {delete, key_to_value, _What,
[Rec], _Trx}}, State) ->
#key_to_value{map = M, key = K, ref = RecRef, ttl_secs = TTL} = Rec,
case TTL of
?INFINITY ->
ok;
_TTL ->
lager:debug("~p: cancelling timer for {~p, ~p, ~p}.",
[?SERVER, M, K, RecRef]),
cancel_timer(RecRef)
end,
{noreply, State};
handle_info({mnesia_table_event, {delete, key_to_value, _What,
[], _Trx}}, State) ->
{noreply, State};
handle_info({mnesia_table_event, {write, key_to_value,
Rec,
OldRecs, _Trx}}, State) ->
#key_to_value{map = M, key = K, ref = RecRef, ttl_secs = TTL} = Rec,
case TTL of
?INFINITY ->
ok;
_TTL ->
lager:debug("~p: adding timer for {~p, ~p, ~p}.",
[?SERVER, M, K, RecRef]),
add_timer(TTL, RecRef)
end,
case OldRecs of
[#key_to_value{ref=OldRef, ttl_secs=OldTTL}] when OldTTL /= ?INFINITY ->
lager:debug("~p: cancelling timer, record replaced {~p, ~p, ~p}.",
[?SERVER, M, K, RecRef]),
cancel_timer(OldRef);
_ ->
ok
end,
{noreply, State};
% flush delete's the table form the scheema
handle_info({mnesia_table_event, {delete, schema, {schema, _}, _What, _Trx}}, State) ->
{noreply, State};
% flush adds the table to the sceema
handle_info({mnesia_table_event, {write, schema, {schema, _, _What}, _Rec, _Trx}}, State) ->
{noreply, State};
handle_info({max_ttl_evict, Secs}, State) ->
lager:debug("~p: initiating max_ttl evictions.",[?SERVER]),
_ = spawn(fun()-> jc_eviction_manager:handle_max_ttl(Secs) end),
{noreply, State};
handle_info({evict, RecRef}, State) ->
lager:debug("~p: eviction request for record with reference ~p.",
[?SERVER, RecRef]),
handle_evict(RecRef),
{noreply, State};
handle_info(Msg, State) ->
lager:warning("~p: unexpected info message: ~p.",[?SERVER, Msg]),
{noreply, State}.
%% -----------------------------------------------------------------------------
%% @private Terminate server.
%%
-spec terminate(any(), #jc_evict_mngr_state{}) -> any().
terminate(_Reason, _State) ->
ok.
%% -----------------------------------------------------------------------------
%% @private Convert process state when code is changed.
%%
-spec code_change(term(), #jc_evict_mngr_state{}, any()) ->
{ok, #jc_evict_mngr_state{}}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%% ============================================================================
%%% Internal functions
%%% ============================================================================
%% -----------------------------------------------------------------------------
%% @private For any map that is set up with a max ttl, delete all members of
%% the Map whose create date is older than the defined age in seconds.
%% So that handle_info can return right away, it spawns a process to run this
%% function.
%%
-spec handle_max_ttl(Secs::seconds()) -> no_return().
handle_max_ttl(Secs) ->
CullFn = fun(#max_ttl{map = Map, ttl_secs = Max_Secs}, Acc) ->
lager:debug("~p: max_ttl evicting from map ~p, older than ~p.",
[?MODULE, Map, Secs]),
jc:evict_map_since(Map, Max_Secs),
Acc
end,
Trans = fun() -> mnesia:foldl(CullFn, [], max_ttl) end,
trans_execute(Trans),
create_max_ttl_job(Secs).
% Set a timer to call this gen_server back with the max_ttl_evict message.
create_max_ttl_job(JobSecs)->
erlang:send_after(JobSecs*1000, ?MODULE, {max_ttl_evict, JobSecs}).
%% -----------------------------------------------------------------------------
%% @private Evict key_to-value record - item-level TTL.
%%
handle_evict(RecRef) ->
jc:delete_record_by_ref(RecRef),
mnesia:dirty_delete({ttl, RecRef}).
%% -----------------------------------------------------------------------------
%% Execute F in the context of a transaction, if F is not already
%% executing in the context of a transaction -- Avoids nested transactions
%%
-spec trans_execute(fun(() -> any())) -> any().
trans_execute(F) ->
case mnesia:is_transaction() of
true -> F();
false -> mnesia:async_dirty(F)
end.