Current section
Files
Jump to
Current section
Files
src/erl_cache_server.erl
-module(erl_cache_server).
-behaviour(gen_server).
-include("erl_cache.hrl").
-include("logging.hrl").
%% ==================================================================
%% API Function Exports
%% ==================================================================
-export([
start_link/1,
get/3,
is_valid_name/1,
set/9,
evict/3,
check_mem_usage/1,
get_stats/1,
evict_all/2
]).
%% ==================================================================
%% gen_server Function Exports
%% ==================================================================
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(stats, {
hit = 0 :: non_neg_integer(),
miss = 0 :: non_neg_integer(),
overdue = 0 :: non_neg_integer(),
evict = 0 :: non_neg_integer(),
set = 0 :: non_neg_integer()
}).
-type stats() :: #stats{}.
-record(state, {
name :: erl_cache:name(), %% The name of this cache instance
cache :: ets:tid(), %% Holds cache
stats :: stats() %% Statistics about cache hits
}).
-record(cache_entry, {
key::erl_cache:key(),
value::erl_cache:value(),
created::pos_integer(),
validity::pos_integer(),
evict::pos_integer(),
validity_delta::erl_cache:validity(),
error_validity_delta::erl_cache:error_validity(),
evict_delta::erl_cache:evict(),
refresh_callback::erl_cache:refresh_callback(),
is_error_callback::erl_cache:is_error_callback()
}).
%% ==================================================================
%% API Function Definitions
%% ==================================================================
-spec start_link(erl_cache:name()) -> {ok, pid()}.
start_link(Name) ->
gen_server:start_link({local, Name}, ?MODULE, Name, []).
-spec get(erl_cache:name(), erl_cache:key(), erl_cache:wait_for_refresh()) ->
{ok, erl_cache:value()} | {error, not_found}.
get(Name, Key, WaitForRefresh) ->
Now = now_ms(),
case ets:lookup(get_table_name(Name), Key) of
[#cache_entry{validity=Validity, value=Value}] when Now < Validity ->
gen_server:cast(Name, {increase_stat, hit}),
{ok, Value};
[#cache_entry{evict=Evict, value=Value, refresh_callback=undefined}] when Now < Evict ->
gen_server:cast(Name, {increase_stat, overdue}),
{ok, Value};
[#cache_entry{evict=Evict, refresh_callback=Cb}=Entry] when Now < Evict, Cb /=undefined ->
?DEBUG("Refreshing overdue key ~p", [Key]),
gen_server:cast(Name, {increase_stat, overdue}),
{ok, NewVal} = refresh(Name, Entry, WaitForRefresh),
{ok, NewVal};
[#cache_entry{value=_ExpiredValue}] ->
{error, not_found};
[] ->
gen_server:cast(Name, {increase_stat, miss}),
{error, not_found}
end.
-spec set(erl_cache:name(), erl_cache:key(), erl_cache:value(), pos_integer(), non_neg_integer(),
erl_cache:refresh_callback(), erl_cache:wait_until_done(), erl_cache:error_validity(),
erl_cache:is_error_callback()) -> ok.
set(Name, Key, Value, ValidityDelta, EvictDelta,
RefreshCb, WaitTillSet, ErrorValidityDelta, IsErrorCb) ->
Now = now_ms(),
{Validity, Evict} = case is_error_value(IsErrorCb, Value) of
false -> {Now + ValidityDelta, Now + ValidityDelta + EvictDelta};
true -> {Now + ErrorValidityDelta, Now + ErrorValidityDelta}
end,
Entry = #cache_entry{
key = Key,
value = Value,
created = Now,
validity = Validity,
error_validity_delta = ErrorValidityDelta,
evict = Evict,
validity_delta = ValidityDelta,
evict_delta = EvictDelta,
refresh_callback = RefreshCb,
is_error_callback = IsErrorCb
},
operate_cache(Name, fun do_set/2, [Name, Entry], set, WaitTillSet).
-spec evict(erl_cache:name(), erl_cache:key(), erl_cache:wait_until_done()) -> ok.
evict(Name, Key, WaitUntilDone) ->
operate_cache(Name, fun do_evict/2, [Name, Key], evict, WaitUntilDone).
-spec get_stats(erl_cache:name()) -> erl_cache:cache_stats().
get_stats(Name) ->
Info = ets:info(get_table_name(Name)),
Memory = proplists:get_value(memory, Info, 0),
Entries = proplists:get_value(size, Info, 0),
ServerStats = gen_server:call(Name, get_stats),
[{entries, Entries}, {memory, Memory}] ++ ServerStats.
-spec is_valid_name(erl_cache:name()) -> boolean().
is_valid_name(Name) ->
not lists:member(get_table_name(Name), ets:all()).
-spec evict_all(erl_cache:name(), boolean()) -> ok.
evict_all(Name, WaitUntilDone) ->
Args = [Name, get_table_name(Name)],
operate_cache(fun do_evict_all/2, Args, WaitUntilDone).
%% ==================================================================
%% gen_server Function Definitions
%% ==================================================================
%% @private
-spec init(erl_cache:name()) -> {ok, #state{}}.
init(Name) ->
CacheTid = ets:new(get_table_name(Name), [set, public, named_table, {keypos,2},
{read_concurrency, true},
{write_concurrency, true}]),
EvictInterval = erl_cache:get_cache_option(Name, evict_interval),
{ok, _} = timer:send_after(EvictInterval, Name, purge_cache),
MemCheckInterval = erl_cache:get_cache_option(Name, mem_check_interval),
{ok, _} = timer:apply_after(MemCheckInterval, ?MODULE, check_mem_usage, [Name]),
{ok, #state{name=Name, cache=CacheTid, stats=#stats{}}}.
%% @private
-spec handle_call(term(), term(), #state{}) ->
{reply, Data::any(), #state{}}.
handle_call(get_stats, _From, #state{stats=Stats} = State) ->
{reply, stats_to_list(Stats), State};
handle_call(_Request, _From, State) ->
{reply, ok, State}.
%% @private
-spec handle_cast(any(), #state{}) -> {noreply, #state{}}.
handle_cast({increase_stat, Stat}, #state{stats=Stats} = State) ->
{noreply, State#state{stats=update_stats(Stat, Stats)}};
handle_cast({increase_stat, Stat, N}, #state{stats=Stats} = State) ->
{noreply, State#state{stats=update_stats(Stat, N, Stats)}};
handle_cast(_Msg, State) ->
{noreply, State}.
%% @private
-spec handle_info(any(), #state{}) -> {noreply, #state{}}.
handle_info(purge_cache, #state{name=Name}=State) ->
purge_cache(Name),
EvictInterval = erl_cache:get_cache_option(Name, evict_interval),
{ok, _} = timer:send_after(EvictInterval, Name, purge_cache),
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
%% @private
-spec terminate(any(), #state{}) -> any().
terminate(_Reason, _State) ->
ok.
%% @private
-spec code_change(any(), #state{}, any()) -> {ok, #state{}}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% ====================================================================
%% Internal Function Definitions
%% ====================================================================
%% @private
-spec operate_cache(erl_cache:name(), function(), list(), atom(), boolean()) -> ok.
operate_cache(Name, Function, Input, Stat, Sync) ->
operate_cache(Function, Input, Sync),
gen_server:cast(Name, {increase_stat, Stat}).
operate_cache(Function, Input, Sync) ->
case Sync of
true -> apply(Function, Input);
false -> spawn_link(erlang, apply, [Function, Input])
end,
ok.
%% @private
-spec do_set(erl_cache:name(), #cache_entry{}) -> ok.
do_set(Name, Entry) ->
true = ets:insert(get_table_name(Name), Entry),
ok.
%% @private
-spec do_evict(erl_cache:name(), erl_cache:key()) -> ok.
do_evict(Name, Key) ->
true = ets:delete(get_table_name(Name), Key),
ok.
do_evict_all(Name, TableName) ->
Deleted = ets:select_delete(TableName, [{'_', [], [true]}]),
gen_server:cast(Name, {increase_stat, evict, Deleted}).
%% @private
-spec purge_cache(erl_cache:name()) -> ok.
purge_cache(Name) ->
Now = now_ms(),
TableName = get_table_name(Name),
%% make sure the table has not disappeared out from under us
case ets:info(TableName, type) of
undefined -> ok;
_ -> purge_cache( Name, TableName, Now )
end.
purge_cache( Name, TableName, Now ) ->
{_Time, Deleted} =
timer:tc( ets, select_delete,
[TableName, [{#cache_entry{evict='$1', _='_'},
[{'<', '$1', Now}], [true]}]] ),
?DEBUG("~p cache purged in ~bms", [Name, _Time]),
gen_server:cast(Name, {increase_stat, evict, Deleted}),
ok.
%% @private
-spec refresh(erl_cache:name(), #cache_entry{}, erl_cache:wait_for_refresh()) ->
{ok, erl_cache:value()}.
refresh(Name, #cache_entry{refresh_callback=Callback}=Entry, true) when Callback/=undefined ->
NewVal = do_refresh(Name, Entry, true),
{ok, NewVal};
refresh(Name, #cache_entry{value=Value, refresh_callback=Callback}=Entry, false)
when Callback/=undefined ->
F = fun () -> do_refresh(Name, Entry, false) end,
_ = spawn(F),
{ok, Value}.
%% @private
-spec do_refresh(erl_cache:name(), #cache_entry{}, erl_cache:wait_for_refresh()) ->
erl_cache:value().
do_refresh(Name, #cache_entry{key=Key, validity_delta=ValidityDelta, evict_delta=EvictDelta,
refresh_callback=Callback, is_error_callback=IsErrorCb}=Entry,
WaitForRefresh) ->
NewVal = do_apply(Callback),
Now = now_ms(),
RefreshedEntry = case is_error_value(IsErrorCb, NewVal) of
false ->
Entry#cache_entry{value=NewVal, validity=Now+ValidityDelta,
evict=Now+ValidityDelta+EvictDelta};
true ->
?NOTICE("Error refreshing ~p at ~p: ~p. Disabling auto refresh...",
[Key, Name, NewVal]),
Entry#cache_entry{refresh_callback=undefined}
end,
ok = operate_cache(Name, fun do_set/2, [Name, RefreshedEntry], set, WaitForRefresh),
NewVal.
%% @private
-spec check_mem_usage(erl_cache:name()) -> ok.
check_mem_usage(Name) ->
TableName = get_table_name(Name),
%% make sure the table has not disappeared out from under us
case ets:info(TableName, memory) of
undefined -> ok;
CurrentWords -> check_mem_usage( Name, CurrentWords )
end.
check_mem_usage( Name, CurrentWords ) ->
MaxMB = erl_cache:get_cache_option(Name, max_cache_size),
CurrentMB = (CurrentWords * erlang:system_info(wordsize)) div (1024 * 1024),
case MaxMB /= undefined andalso CurrentMB > MaxMB of
true ->
?WARNING("~p exceeded memory limit of ~pMB: ~pMB in use! Forcing eviction...",
[Name, MaxMB, CurrentMB]),
purge_cache(Name);
false -> ok
end,
MemCheckInterval = erl_cache:get_cache_option(Name, mem_check_interval),
{ok, _} = timer:apply_after(MemCheckInterval, ?MODULE, check_mem_usage, [Name]),
ok.
%% @private
-spec do_apply(function() | mfa() | {function(), [any()]}) -> term().
do_apply({M, F, A}) when is_atom(M), is_atom(F), is_list(A) ->
apply(M, F, A);
do_apply({F, A}) when is_function(F, length(A)) ->
apply(F, A);
do_apply(F) when is_function(F) ->
F().
%% @private
-spec is_error_value(erl_cache:is_error_callback(), erl_cache:value()) -> boolean().
is_error_value({M, F, A}, Value) ->
apply(M, F, [Value|A]);
is_error_value({F, A}, Value) ->
apply(F, [Value|A]);
is_error_value(F, Value) when is_function(F) ->
F(Value).
%% @private
-spec update_stats(hit|miss|overdue|evict|set, stats()) -> stats().
update_stats(Stat, Stats) ->
update_stats(Stat, 1, Stats).
%% @private
-spec update_stats(hit|miss|overdue|evict|set, pos_integer(), stats()) -> stats().
update_stats(hit, N, S) -> S#stats{hit = S#stats.hit + N};
update_stats(miss, N, S) -> S#stats{miss = S#stats.miss + N};
update_stats(overdue, N, S) -> S#stats{overdue = S#stats.overdue + N};
update_stats(evict, N, S) -> S#stats{evict = S#stats.evict + N};
update_stats(set, N, S) -> S#stats{set = S#stats.set + N}.
stats_to_list(#stats{hit = Hit,
miss = Miss,
overdue = Overdue,
evict = Evict,
set = Set}) ->
[{total_ops, Hit + Miss + Overdue + Evict + Set},
{hit, Hit},
{miss, Miss},
{overdue, Overdue},
{evict, Evict},
{set, Set}].
%% @private
-spec now_ms() -> pos_integer().
now_ms() ->
{Mega, Sec, Micro} = os:timestamp(),
Mega * 1000000000 + Sec * 1000 + Micro div 1000.
%% @private
-spec get_table_name(erl_cache:name()) -> atom().
get_table_name(Name) ->
to_atom(atom_to_list(Name) ++ "_ets").
%% @private
-spec to_atom(string()) -> atom().
to_atom(Str) ->
try list_to_existing_atom(Str) catch error:badarg -> list_to_atom(Str) end.