%% @author Arjan Scherpenisse
%% @copyright 2009-2020 Marc Worrell, Arjan Scherpenisse
%% @doc
%%
%% == depcache API ==
%% {@link flush/1}, {@link flush/2}, {@link get/2}, {@link get/3}, {@link get_subkey/3},
%% {@link get_wait/2}, {@link set/3}, {@link set/4}, {@link set/5}, {@link size/1}
%%
%% {@link memo/2}, {@link memo/3}, {@link memo/4}, {@link memo/5}
%%
%% {@link flush_process_dict/0}, {@link in_process/1}, {@link in_process_server/1}
%%
%% === Internal ===
%% {@link cleanup/1}, {@link cleanup/5}
%%
%% @end
%% Copyright 2009-2020 Marc Worrell, Arjan Scherpenisse
%%
%% 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.
%%
-module(depcache).
-author("Marc Worrell ").
-behaviour(gen_server).
-include_lib("depcache/include/depcache.hrl").
%% gen_server API
-export([start_link/1, start_link/2]).
%% depcache API
-export([set/3, set/4, set/5, get/2, get_wait/2, get/3, get_subkey/3, flush/2, flush/1, size/1]).
-export([memo/2, memo/3, memo/4, memo/5]).
-export([in_process_server/1, in_process/1, flush_process_dict/0]).
%% gen_server exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
%% internal export
-export([cleanup/1, cleanup/5]).
-ifdef(fun_stacktrace).
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(),).
-else.
-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
-endif.
-type memo_fun() :: function() | mfa() | {module(), atom()}.
-type depcache_server() :: pid() | atom().
-type sec() :: non_neg_integer().
-type max_age_secs() :: sec().
-type key() :: any().
-type dependencies() :: list( key() ).
-type proplist() :: proplists:proplist().
-export_type([
memo_fun/0,
depcache_server/0,
key/0,
max_age_secs/0
]).
-record(tables, {
meta_table :: ets:tab(),
deps_table :: ets:tab(),
data_table :: ets:tab()
}).
-record(state, {now :: sec(), serial :: non_neg_integer(), tables :: tables(), wait_pids :: dict:dict()}).
-record(meta, {key :: key(), expire :: sec(), serial :: non_neg_integer(), depend :: dependencies()}).
-record(depend, {key :: key(), serial :: non_neg_integer()}).
-record(cleanup_state, {
pid :: pid(),
tables :: #tables{},
name :: atom(),
memory_max :: non_neg_integer(),
callback :: mfa() | undefined
}).
-type tables() :: #tables{meta_table :: ets:tab(), deps_table :: ets:tab(), data_table :: ets:tab()}.
-type state() :: #state{now :: sec(), serial :: non_neg_integer(), tables :: tables(), wait_pids :: dict:dict()}.
-type depend() :: #depend{key :: key(), serial :: non_neg_integer()}.
-type cleanup_state() :: #cleanup_state{pid :: pid(), tables :: tables(), name :: atom(), memory_max :: non_neg_integer(), callback :: mfa() | undefined}.
-type meta() :: #meta{key :: key(), expire :: sec(), serial :: non_neg_integer(), depend :: dependencies()}.
%% @doc Start a depcache process.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#start_link-3 gen_server:start_link/3].
%%
%% For Config, you can pass:
%% `{callback, {Module, Function, Arguments}}': depcache event callback
%% `{memory_max, MaxMemoryInMB}': number of MB to limit depcache size at.
%% @param Config configuration options
%% @returns Result of starting gen_server item.
-spec start_link(Config) -> Result when
Config :: proplist(),
Result :: {ok, pid()} | ignore | {error, term()}.
start_link(Config) ->
gen_server:start_link(?MODULE, Config, []).
%% @doc Start a named depcache process.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#start_link-4 gen_server:start_link/4].
%%
%% For Config, you can pass:
%% `{callback, {Module, Function, Arguments}}': depcache event callback
%% `{memory_max, MaxMemoryInMB}': number of MB to limit depcache size at.
%% @param Name of process
%% @param Config configuration options
%% @returns Result of starting gen_server item.
-spec start_link(Name, Config) -> Result when
Name :: atom(),
Config :: proplist(),
Result :: {ok, pid()} | ignore | {error, term()}.
start_link(Name, Config) ->
gen_server:start_link({local, Name}, ?MODULE, [{name,Name}|Config], []).
-define(META_TABLE_PREFIX, $m).
-define(DEPS_TABLE_PREFIX, $p).
-define(DATA_TABLE_PREFIX, $d).
-define(NAMED_TABLE(Name,Prefix), list_to_atom([Prefix,$:|atom_to_list(Name)])).
%% One hour, in seconds.
-define(HOUR, 3600).
%% Default max size, in Mbs, of the stored data in the depcache before the gc kicks in.
-define(MEMORY_MAX, 100).
% Maximum time to wait for a get_wait/2 call before a timout failure (in secs).
-define(MAX_GET_WAIT, 30).
% Number of slots visited for each gc iteration.
-define(CLEANUP_BATCH, 100).
% Number of entries we keep in the local process dictionary for fast lookups.
-define(PROCESS_DICT_THRESHOLD, 10000).
%% @doc Cache the result of the function for an hour.
%% @param Fun a funciton for producing a value
%% @returns cached value
-spec memo( Fun, Server ) -> Result when
Fun :: memo_fun(),
Server :: depcache_server(),
Result :: any().
memo(Fun, Server) ->
memo(Fun, undefined, ?HOUR, [], Server).
%% @doc If Fun is a function then cache for an hour given the key. If
%% Fun is a {M,F,A} tuple then derive the key from the tuple and
%% cache for `MaxAge' seconds.
%%
%% @param Fun a funciton for producing a value1
%% @param MaxAge a caching time
%% @param Key a cache item key
%% @returns cached value
-spec memo( Fun, MaxAge_Key, Server ) -> Result when
Fun :: memo_fun(),
MaxAge_Key :: MaxAge | Key,
MaxAge :: max_age_secs(),
Key :: key(),
Server :: depcache_server(),
Result :: any().
memo(Fun, MaxAge, Server) when is_tuple(Fun) ->
memo(Fun, undefined, MaxAge, [], Server);
memo(Fun, Key, Server) when is_function(Fun) ->
memo(Fun, Key, ?HOUR, [], Server).
%% @doc Cache the result of the function as Key for `MaxAge' seconds.
%% @returns cached value
%% @equiv memo(Fun, Key, MaxAge, [], Server)
-spec memo( Fun, Key, MaxAge, Server ) -> Result when
Fun :: memo_fun(),
Key :: key(),
MaxAge :: max_age_secs(),
Server :: depcache_server(),
Result :: any().
memo(Fun, Key, MaxAge, Server) ->
memo(Fun, Key, MaxAge, [], Server).
%% @doc Cache the result of the function as Key for `MaxAge' seconds, flush
%% the cached result if any of the dependencies is changed.
%% @returns cached value
-spec memo( Fun, Key, MaxAge, Dep, Server ) -> Result when
Fun :: memo_fun(),
Key :: undefined | key(),
MaxAge :: max_age_secs(),
Dep :: dependencies(),
Server :: depcache_server(),
Result :: any().
memo(Fun, Key, MaxAge, Dep, Server) ->
Key1 = case Key of
undefined -> memo_key(Fun);
_ -> Key
end,
case ?MODULE:get_wait(Key1, Server) of
{ok, Value} ->
Value;
{throw, R} ->
throw(R);
undefined ->
memo_key(Fun, Key, MaxAge, Dep, Server)
end.
%% @private
%% @param MaxAge maximum lifetime of an element in the cache
%% @param Dep list of subkeys
%% @returns cached value
%% @see memo/5
-spec memo_key( Fun, Key, MaxAge, Dep, Server ) -> Result when
Fun :: memo_fun(),
Key :: key(),
MaxAge :: max_age_secs(),
Dep :: dependencies(),
Server :: depcache_server(),
Result :: any().
memo_key(Fun, Key, MaxAge, Dep, Server) ->
try
Value =
case Fun of
{M,F,A} -> erlang:apply(M,F,A);
{M,F} -> M:F();
_ when is_function(Fun) -> Fun()
end,
{Value1, MaxAge1, Dep1} =
case Value of
#memo{value=V, max_age=MA, deps=D} ->
MA1 = case is_integer(MA) of true -> MA; false -> MaxAge end,
{V, MA1, Dep++D};
_ ->
{Value, MaxAge, Dep}
end,
case MaxAge of
0 -> memo_send_replies(Key, Value1, Server);
_ -> set(Key, Value1, MaxAge1, Dep1, Server)
end,
Value1
catch
?WITH_STACKTRACE(Class, R, S)
memo_send_errors(Key, {throw, R}, Server),
erlang:raise(Class, R, S)
end.
%% @private
%% @doc Calculate the key used for memo functions.
%% Returns cached value.
-spec memo_key( MFA ) -> Result when
MFA :: {Module, Function, Arity},
Module :: module(),
Function :: atom(),
Arity :: arity(),
Result :: tuple();
( MF ) -> Result when
MF :: {Module, Function},
Module :: module(),
Function :: atom(),
Result :: tuple().
memo_key({M,F,A}) ->
WithoutContext = lists:filter(fun(T) when is_tuple(T) andalso element(1, T) =:= context -> false; (_) -> true end, A),
{M,F,WithoutContext};
memo_key({M,F}) ->
{M,F}.
%% @private
%% @doc Send the calculated value to the processes waiting for the result.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#reply-2 gen_server:reply/2].
%%
-spec memo_send_replies( Key, Value, Server ) -> Result when
Key :: key(),
Value :: any(),
Server :: depcache_server(),
Result :: ok.
memo_send_replies(Key, Value, Server) ->
Pids = get_waiting_pids(Key, Server),
[ catch gen_server:reply(Pid, {ok, Value}) || Pid <- Pids ],
ok.
%% @private
%% @doc Send an error to the processes waiting for the result.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#reply-2 gen_server:reply/2].
%%
-spec memo_send_errors( Key, Exception, Server ) -> Result when
Key :: key(),
Exception :: {throw, any()},
Server :: depcache_server(),
Result :: list().
memo_send_errors(Key, Exception, Server) ->
Pids = get_waiting_pids(Key, Server),
[ catch gen_server:reply(Pid, Exception) || Pid <- Pids ].
%% @doc Add the key to the depcache, hold it for `3600' seconds and no dependencies.
%% @equiv set(Key, Data, 3600, [], Server)
-spec set( Key, Data, Server ) -> Result when
Key :: key(),
Data :: any(),
Server :: depcache_server(),
Result :: ok.
set(Key, Data, Server) ->
set(Key, Data, ?HOUR, [], Server).
%% @doc Add the key to the depcache, hold it for `MaxAge' seconds and no dependencies.
%% @equiv set(Key, Data, MaxAge, [], Server)
-spec set( Key, Data, MaxAge, Server ) -> Result when
Key :: key(),
Data :: any(),
MaxAge :: max_age_secs(),
Server :: depcache_server(),
Result :: ok.
set(Key, Data, MaxAge, Server) ->
set(Key, Data, MaxAge, [], Server).
%% @doc Add the key to the depcache, hold it for `MaxAge' seconds and check the dependencies.
%% @param MaxAge maximum lifetime of an element in the cache
%% @param Depend list of subkeys
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec set( Key, Data, MaxAge, Depend, Server ) -> Result when
Key :: key(),
Data :: any(),
MaxAge :: max_age_secs(),
Depend :: dependencies(),
Server :: depcache_server(),
Result :: ok.
set(Key, Data, MaxAge, Depend, Server) ->
flush_process_dict(),
gen_server:call(Server, {set, Key, Data, MaxAge, Depend}).
%% @doc Fetch the key from the cache, when the key does not exist then lock the entry and let
%% the calling process insert the value. All other processes requesting the key will wait till
%% the key is updated and receive the key's new value.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-3 gen_server:call/3].
%%
-spec get_wait( Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: {ok, any()} | undefined | {throw, term()}.
get_wait(Key, Server) ->
case get_process_dict(Key, Server) of
NoValue when NoValue =:= undefined orelse NoValue =:= depcache_disabled ->
gen_server:call(Server, {get_wait, Key}, ?MAX_GET_WAIT*1000);
Other ->
Other
end.
%% @private
%% @doc Fetch the queue of pids that are waiting for a `get_wait/2'. This flushes the queue and
%% the key from the depcache.
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-3 gen_server:call/3].
%%
%% @see get_wait/2
-spec get_waiting_pids( Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: [{pid(), Tag}],
Tag :: atom().
get_waiting_pids(Key, Server) ->
gen_server:call(Server, {get_waiting_pids, Key}, ?MAX_GET_WAIT*1000).
%% @doc Fetch the key from the cache, return the data or an `undefined' if not found (or not valid).
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec get( Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: {ok, any()} | undefined.
get(Key, Server) ->
case get_process_dict(Key, Server) of
depcache_disabled -> gen_server:call(Server, {get, Key});
Value -> Value
end.
%% @doc Fetch the key from the cache, return the data or an `undefined' if not found (or not valid)
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec get_subkey( Key, SubKey, Server ) -> Result when
Key :: key(),
SubKey :: key(),
Server :: depcache_server(),
Result :: {ok, any()} | undefined.
get_subkey(Key, SubKey, Server) ->
case in_process_server(Server) of
true ->
case erlang:get({depcache, {subkey, Key, SubKey}}) of
{memo, Value} ->
Value;
undefined ->
Value = gen_server:call(Server, {get, Key, SubKey}),
erlang:put({depcache, {subkey, Key, SubKey}}, {memo, Value}),
Value
end;
false ->
gen_server:call(Server, {get, Key, SubKey})
end.
%% @doc Fetch the key from the cache, return the data or an `undefined' if not found (or not valid)
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec get( Key, SubKey, Server ) -> Result when
Key :: key(),
SubKey :: key(),
Server :: depcache_server(),
Result :: {ok, any()} | undefined.
get(Key, SubKey, Server) ->
case get_process_dict(Key, Server) of
undefined ->
undefined;
depcache_disabled ->
gen_server:call(Server, {get, Key, SubKey});
{ok, Value} ->
{ok, find_value(SubKey, Value)}
end.
%% @doc Flush the key and all keys depending on that key
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec flush( Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: ok.
flush(Key, Server) ->
gen_server:call(Server, {flush, Key}),
flush_process_dict().
%% @doc Flush all keys from the caches
%%
%% See also:
%% [http://erlang.org/doc/man/gen_server.html#call-2 gen_server:call/2].
%%
-spec flush( Server ) -> Result when
Server :: depcache_server(),
Result :: ok.
flush(Server) ->
gen_server:call(Server, flush),
flush_process_dict().
%% @doc Return the total memory size of all stored terms
-spec size( Server ) -> Result when
Server :: depcache_server(),
Result :: non_neg_integer() | undefined.
size(Server) ->
{_Meta, _Deps, Data} = get_tables(Server),
ets:info(Data, memory).
%% @private
%% @doc Fetch the depcache tables.
-spec get_tables( Server ) -> Result when
Server :: depcache_server(),
Result :: tuple().
get_tables(Server) ->
case erlang:get(depcache_tables) of
{ok, Server, Tables} ->
Tables;
{ok, _OtherServer, _Tables} ->
flush_process_dict(),
get_tables1(Server);
undefined->
get_tables1(Server)
end.
%% @private
%% @doc Get or init ets-tables.
%% @see get_tables/1
-spec get_tables1( Server ) -> Result when
Server :: depcache_server(),
Result :: tuple().
get_tables1(Server) when is_pid(Server) ->
{ok, Tables} = gen_server:call(Server, get_tables),
erlang:put(depcache_tables, {ok, Server, Tables}),
Tables;
get_tables1(Server) when is_atom(Server) ->
Tables = {
?NAMED_TABLE(Server, ?META_TABLE_PREFIX),
?NAMED_TABLE(Server, ?DEPS_TABLE_PREFIX),
?NAMED_TABLE(Server, ?DATA_TABLE_PREFIX)
},
erlang:put(depcache_tables, {ok, Server, Tables}),
Tables.
%% @private
%% @doc Fetch a value from the dependency cache, using the in-process cached tables.
-spec get_process_dict(Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: tuple() | depcache_disabled | undefined.
get_process_dict(Key, Server) ->
case in_process_server(Server) of
true ->
case erlang:get({depcache, Key}) of
{memo, Value} ->
Value;
undefined ->
% Prevent the process dict memo from blowing up the process size
case now_sec() > erlang:get(depcache_now)
orelse erlang:get(depcache_count) > ?PROCESS_DICT_THRESHOLD of
true -> flush_process_dict();
false -> nop
end,
Value = get_ets(Key, Server),
erlang:put({depcache, Key}, {memo, Value}),
erlang:put(depcache_count, incr(erlang:get(depcache_count))),
Value
end;
false when is_atom(Server) ->
get_ets(Key, Server);
false ->
depcache_disabled
end.
%% @private
%% @doc Get cached value by key.
-spec get_ets(Key, Server ) -> Result when
Key :: key(),
Server :: depcache_server(),
Result :: undefined | {ok, any()}.
get_ets(Key, Server) ->
{MetaTable, DepsTable, DataTable} = get_tables(Server),
case get_concurrent(Key, get_now(), MetaTable, DepsTable, DataTable) of
flush ->
flush(Key, Server),
undefined;
undefined ->
undefined;
{ok, _Value} = Found ->
Found
end.
%% @doc Check if we use a local process dict cache.
-spec in_process_server( Server ) -> Result when
Server :: depcache_server(),
Result :: boolean().
in_process_server(Server) ->
case erlang:get(depcache_in_process) of
true ->
_ = get_tables(Server),
true;
_ ->
false
end.
%% @doc Enable or disable the in-process caching using the process dictionary
-spec in_process( IsChaching ) -> Result when
IsChaching :: undefined | boolean(),
Result :: undefined | boolean().
in_process(true) ->
erlang:put(depcache_in_process, true);
in_process(false) ->
Old = erlang:erase(depache_in_process),
flush_process_dict(),
Old;
in_process(undefined) ->
in_process(false).
%% @doc Flush all items memoized in the process dictionary.
-spec flush_process_dict() -> Result when
Result :: ok.
flush_process_dict() ->
[ erlang:erase({depcache, Key}) || {{depcache, Key},_Value} <- erlang:get() ],
erlang:erase(depache_now),
erlang:put(depcache_count, 0),
ok.
%% @private
%% @doc Get the current system time in seconds
-spec get_now() -> Result when
Result :: sec().
get_now() ->
case erlang:get(depcache_now) of
undefined ->
Now = now_sec(),
erlang:put(depache_now, Now),
Now;
Now ->
Now
end.
%% gen_server callbacks
%% @doc Initialize the depcache. Creates ets tables for the deps, meta and data. Spawns garbage collector.
-spec init(Config) -> Result when
Config :: proplist(),
State :: state(),
Result :: {ok, State}.
init(Config) ->
MemoryMaxMbs = case proplists:get_value(memory_max, Config) of undefined -> ?MEMORY_MAX; Mbs -> Mbs end,
MemoryMaxWords = 1024 * 1024 * MemoryMaxMbs div erlang:system_info(wordsize),
Tables = case proplists:lookup(name, Config) of
none ->
#tables{
meta_table=ets:new(meta_table, [set, {keypos, 2}, protected, {read_concurrency, true}]),
deps_table=ets:new(deps_table, [set, {keypos, 2}, protected, {read_concurrency, true}]),
data_table=ets:new(data_table, [set, {keypos, 1}, protected, {read_concurrency, true}])
};
{name, Name} when is_atom(Name) ->
#tables{
meta_table=ets:new(?NAMED_TABLE(Name, ?META_TABLE_PREFIX),
[set, named_table, {keypos, 2}, protected, {read_concurrency, true}]),
deps_table=ets:new(?NAMED_TABLE(Name, ?DEPS_TABLE_PREFIX),
[set, named_table, {keypos, 2}, protected, {read_concurrency, true}]),
data_table=ets:new(?NAMED_TABLE(Name, ?DATA_TABLE_PREFIX),
[set, named_table, {keypos, 1}, protected, {read_concurrency, true}])
}
end,
State = #state{
tables = Tables,
now=now_sec(),
serial=0,
wait_pids=dict:new()
},
timer:send_interval(1000, tick),
spawn_link(?MODULE,
cleanup,
[#cleanup_state{
pid = self(),
tables = Tables,
name = proplists:get_value(name, Config),
memory_max = MemoryMaxWords,
callback = proplists:get_value(callback, Config)
}]),
{ok, State}.
%% @doc Handling call messages
-spec handle_call(Request, From, State) -> Result when
Request :: get_tables,
From :: {pid(), atom()},
State :: state(),
Meta_table :: ets:tab(),
Deps_table :: ets:tab(),
Data_table :: ets:tab(),
Result :: {reply, {ok, {Meta_table, Deps_table, Data_table}}, State};
(Request, From, State) -> Result when
Request :: get_wait,
From :: {pid(), atom()},
State :: state(),
Result :: {reply, Reply, state()} | {noreply, state()},
Reply :: undefined | {ok, term()};
(Request, From, State) -> Result when
Request :: {get_waiting_pids, Key},
Key :: key(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, [{pid(), Tag}], state()},
Tag :: atom();
(Request, From, State) -> Result when
Request :: {get, Key},
Key :: key(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, Reply, State},
Reply :: undefined | {ok, term()};
(Request, From, State) -> Result when
Request :: {get, Key, SubKey},
Key :: key(),
SubKey :: key(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, Reply, State},
Reply :: undefined | {ok, term()};
(Request, From, State) -> Result when
Request :: {set, Key, Data, MaxAge, Depend},
Key :: key(),
Data :: any(),
MaxAge :: max_age_secs(),
Depend :: dependencies(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, Reply, State},
Reply :: ok;
(Request, From, State) -> Result when
Request :: {flush, Key},
Key :: key(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, ok, State};
(Request, From, State) -> Result when
Request :: flush,
From :: {pid(), atom()},
State :: state(),
Result :: {reply, ok, State}.
handle_call(get_tables, _From, State) ->
handle_call_get_tables(State);
handle_call({get_wait, Key}, From, State) ->
handle_call_get_wait(Key, From, State);
handle_call({get_waiting_pids, Key}, _From, State) ->
handle_call_get_waiting_pids(Key, State);
handle_call({get, Key}, _From, State) ->
handle_call_get(Key, State);
handle_call({get, Key, SubKey}, _From, State) ->
handle_call_get_sub_key({Key, SubKey}, State);
handle_call({set, Key, Data, MaxAge, Depend}, _From, State) ->
handle_call_set({Key, Data, MaxAge, Depend}, State);
handle_call({flush, Key}, _From, State) ->
handle_call_flush(Key, State);
handle_call(flush, _From, State) ->
handle_call_flush_all(State).
%% @doc Handling cast messages
-spec handle_cast(Request, State) -> Result when
Request :: {flush, Key} | flush | any(),
Key :: key(),
State :: state(),
Result :: {noreply, State}.
handle_cast({flush, Key}, State) ->
flush_key(Key, State),
{noreply, State};
handle_cast(flush, #state{tables = Tables} = State) ->
ets:delete_all_objects(Tables#tables.data_table),
ets:delete_all_objects(Tables#tables.meta_table),
ets:delete_all_objects(Tables#tables.deps_table),
erase_process_dict(),
{noreply, State};
handle_cast(_Msg, State) ->
{noreply, State}.
%% @doc This function is called by a `gen_server' process when when it receives `tick' or
%% any other message than a synchronous or asynchronous request (or a system message).
%% @see gen_server:handle_info/2
-spec handle_info(Info, State) -> Result when
Info :: tick | any(),
State :: state(),
Result :: {noreply, State}.
handle_info(tick, State) ->
erase_process_dict(),
flush_message(tick),
{noreply, State#state{now=now_sec()}};
handle_info(_Msg, State) ->
{noreply, State}.
%% @doc This function is called by a `gen_server' process when it is about to terminate.
%% @see gen_server:terminate/2
-spec terminate(Reason, State) -> Result when
Reason :: normal | shutdown | {shutdown, term()} | term(),
State :: state(),
Result :: ok.
terminate(_Reason, _State) -> ok.
%% @doc This function is called by a gen_server process when it is to update
%% its internal state during a release upgrade/downgrade.
%% @see gen_server:code_change/3
-spec code_change(OldVersion, State, Extra) -> Result when
OldVersion :: (term() | {down, term()}),
State :: state(),
Extra :: term(),
Result :: {ok, State}.
code_change(_OldVersion, State, _Extra) -> {ok, State}.
%% @private
%% @doc Return the ets-tables used by the cache
%% @see handle_call/3
%%
-spec handle_call_get_tables(State) -> Result when
State :: state(),
Meta_table :: ets:tab(),
Deps_table :: ets:tab(),
Data_table :: ets:tab(),
Result :: {reply, {ok, {Meta_table, Deps_table, Data_table}}, State}.
handle_call_get_tables(#state{tables = Tables} = State) ->
{reply, {ok, {Tables#tables.meta_table, Tables#tables.deps_table, Tables#tables.data_table}}, State}.
%% @private
%% @doc Fetch a key from the cache. When the key is not available then let processes wait till the
%% key is available.
%% @see handle_call/3
%%
-spec handle_call_get_wait(Key, From, State) -> Result when
Key :: key(),
From :: {pid(), atom()},
State :: state(),
Result :: {reply, Reply, state()} | {noreply, state()},
Reply :: undefined | {ok, term()}.
handle_call_get_wait(Key, From, #state{tables = Tables} = State) ->
case get_concurrent(Key, State#state.now, Tables#tables.meta_table, Tables#tables.deps_table, Tables#tables.data_table) of
NotFound when NotFound =:= flush; NotFound =:= undefined ->
case NotFound of
flush -> flush_key(Key, State);
undefined -> State
end,
case dict:find(Key, State#state.wait_pids) of
{ok, {MaxAge,List}} when State#state.now < MaxAge ->
%% Another process is already calculating the value, let the caller wait.
WaitPids = dict:store(Key, {MaxAge, [From|List]}, State#state.wait_pids),
{noreply, State#state{wait_pids=WaitPids}};
_ ->
%% Nobody waiting or we hit a timeout, let next requestors wait for this caller.
WaitPids = dict:store(Key, {State#state.now+?MAX_GET_WAIT, []}, State#state.wait_pids),
{reply, undefined, State#state{wait_pids=WaitPids}}
end;
{ok, _Value} = Found ->
{reply, Found, State}
end.
%% @private
%% @doc Return the list of processes waiting for the rendering of a key. Flush the queue and the key.
%% @see handle_call/3
%%
-spec handle_call_get_waiting_pids(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: {reply, [{pid(), Tag}], state()},
Tag :: atom().
handle_call_get_waiting_pids(Key, State) ->
{State1, Pids} = case dict:find(Key, State#state.wait_pids) of
{ok, {_MaxAge, List}} ->
WaitPids = dict:erase(Key, State#state.wait_pids),
{State#state{wait_pids=WaitPids}, List};
error ->
{State, []}
end,
flush_key(Key, State1),
{reply, Pids, State1}.
%% @private
%% @doc Fetch a key from the cache, returns `undefined' when not found.
%% @see handle_call/3
%%
-spec handle_call_get(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: {reply, Reply, State},
Reply :: undefined | {ok, term()}.
handle_call_get(Key, State) ->
{reply, get_in_depcache(Key, State), State}.
%% @private
%% @doc Fetch a subkey from a key from the cache, returns `undefined' when not found.
%% This is useful when the cached data is very large and the fetched data is small in comparison.
%% @see handle_call/3
%%
-spec handle_call_get_sub_key(Request, State) -> Result when
Request :: {Key, SubKey},
Key :: key(),
SubKey :: key(),
State :: state(),
Result :: {reply, Reply, State},
Reply :: undefined | {ok, term()}.
handle_call_get_sub_key({Key, SubKey}, State) ->
case get_in_depcache(Key, State) of
undefined -> {reply, undefined, State};
{ok, Value} -> {reply, {ok, find_value(SubKey, Value)}, State}
end.
%% @private
%% @doc Add an entry to the cache table.
%% @see handle_call/3
%%
-spec handle_call_set(Request, State) -> Result when
Request :: {Key, Data, MaxAge, Depend},
Key :: key(),
Data :: any(),
MaxAge :: max_age_secs(),
Depend :: dependencies(),
State :: state(),
Result :: {reply, Reply, State},
Reply :: ok.
handle_call_set({Key, Data, MaxAge, Depend}, #state{tables = Tables} = State) ->
erase_process_dict(),
State1 = State#state{serial=State#state.serial+1},
case MaxAge of
0 ->
ets:delete(Tables#tables.meta_table, Key),
ets:delete(Tables#tables.data_table, Key);
_ ->
ets:insert(Tables#tables.data_table, {Key, Data}),
ets:insert(Tables#tables.meta_table, #meta{key=Key, expire=State1#state.now+MaxAge, serial=State1#state.serial, depend=Depend})
end,
%% Make sure all dependency keys are available in the deps table
AddDepend = fun(D) ->
ets:insert_new(Tables#tables.deps_table, #depend{key=D, serial=State1#state.serial})
end,
lists:foreach(AddDepend, Depend),
%% Flush the key itself in the dependency table - this will invalidate depending items
case is_simple_key(Key) of
true -> ok;
false -> ets:insert(Tables#tables.deps_table, #depend{key=Key, serial=State#state.serial})
end,
%% Check if other processes are waiting for this key, send them the data
case dict:find(Key, State1#state.wait_pids) of
{ok, {_MaxAge, List}} ->
[ catch gen_server:reply(From, {ok, Data}) || From <- List ],
WaitFroms = dict:erase(Key, State1#state.wait_pids),
{reply, ok, State1#state{wait_pids=WaitFroms}};
error ->
{reply, ok, State1}
end.
%% @private
%% @doc Flush cached data by key.
%% @see handle_call/3
%%
-spec handle_call_flush(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: {reply, ok, State}.
handle_call_flush(Key, State) ->
flush_key(Key, State),
{reply, ok, State}.
%% @private
%% @doc Flush all cached data.
%% @see handle_call/3
%%
-spec handle_call_flush_all(State) -> Result when
State :: state(),
Result :: {reply, ok, State}.
handle_call_flush_all(#state{tables = Tables} = State) ->
ets:delete_all_objects(Tables#tables.data_table),
ets:delete_all_objects(Tables#tables.meta_table),
ets:delete_all_objects(Tables#tables.deps_table),
erase_process_dict(),
{reply, ok, State}.
%% @private
%% @doc Fetch a value, from within the depcache process. Cache the value in the process dictionary of the depcache.
-spec get_in_depcache(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: term().
get_in_depcache(Key, State) ->
case erlang:get({depcache, Key}) of
{memo, Value} ->
Value;
undefined ->
% Prevent the process dict memo from blowing up the process size
case erlang:get(depcache_count) > ?PROCESS_DICT_THRESHOLD of
true -> erase_process_dict();
false -> nop
end,
Value = get_in_depcache_ets(Key, State),
erlang:put({depcache, Key}, {memo, Value}),
erlang:put(depcache_count, incr(erlang:get(depcache_count))),
Value
end.
%% @private
%% @doc Increment input value.
-spec incr(Value) -> Result when
Value :: undefined,
Result :: 1;
(Value) -> Result when
Value :: integer(),
Result :: integer().
incr(undefined) -> 1;
incr(N) -> N+1.
%% @private
%% @doc Get decache value.
-spec get_in_depcache_ets(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: undefined | {ok, Value},
Value :: term().
get_in_depcache_ets(Key, #state{tables = Tables} = State) ->
case get_concurrent(Key, State#state.now, Tables#tables.meta_table, Tables#tables.deps_table, Tables#tables.data_table) of
flush ->
flush_key(Key, State),
undefined;
undefined ->
undefined;
{ok, Value} ->
{ok, Value}
end.
%% @private
%% @doc Get a value from the depache. Called by the depcache and other processes.
-spec get_concurrent(Key, Now, MetaTable, DepsTable, DataTable) -> Result when
Key :: key(),
Now :: sec(),
MetaTable :: ets:tab(),
DepsTable :: ets:tab(),
DataTable :: ets:tab(),
Result :: flush | undefined | {ok, term()}.
get_concurrent(Key, Now, MetaTable, DepsTable, DataTable) ->
case ets:lookup(MetaTable, Key) of
[] ->
undefined;
[#meta{serial=Serial, expire=Expire, depend=Depend}] ->
get_concurrent(Key, Now, DepsTable, DataTable, Serial, Expire, Depend)
end.
%% @hidden
-spec get_concurrent(Key, Now, DepsTable, DataTable, Serial, Expire, Depend) -> Result when
Key :: key(),
Now :: sec(),
DepsTable :: ets:tab(),
DataTable :: ets:tab(),
Serial :: non_neg_integer(),
Expire :: sec(),
Depend :: dependencies(),
Result :: flush | undefined | {ok, term()}.
get_concurrent(Key, Now, DepsTable, DataTable, Serial, Expire, Depend) ->
%% Check expiration
case Expire >= Now of
false ->
flush;
true ->
get_concurrent_check_depend(Key, DepsTable, DataTable, Serial, Depend)
end.
%% @hidden
-spec get_concurrent_check_depend(Key, DepsTable, DataTable, Serial, Depend) -> Result when
Key :: key(),
DepsTable :: ets:tab(),
DataTable :: ets:tab(),
Serial :: non_neg_integer(),
Depend :: dependencies(),
Result :: flush | undefined | {ok, term()}.
get_concurrent_check_depend(Key, DepsTable, DataTable, Serial, Depend) ->
%% Check dependencies
case check_depend(Serial, Depend, DepsTable) of
false ->
flush;
true ->
case ets:lookup(DataTable, Key) of
[] -> undefined;
[{_Key,Data}] -> {ok, Data}
end
end.
%% @private
%% @doc Check if a key is usable as dependency key. That is a string, atom, integer etc, but not a list of lists.
-spec is_simple_key(List) -> Result when
List :: list(),
Result :: true | false.
is_simple_key([]) ->
true;
is_simple_key([H|_]) ->
not is_list(H);
is_simple_key(_Key) ->
true.
%% @private
%% @doc Flush a key from the cache, reset the in-process cache as well (we don't know if any cached value had a dependency).
-spec flush_key(Key, State) -> Result when
Key :: key(),
State :: state(),
Result :: ok.
flush_key(Key, #state{tables = Tables}) ->
ets:delete(Tables#tables.data_table, Key),
ets:delete(Tables#tables.deps_table, Key),
ets:delete(Tables#tables.meta_table, Key),
erase_process_dict().
%% @private
%% @doc Check if all dependencies are still valid, that is they have a serial before or equal to the serial of the entry.
-spec check_depend(Serial, Depend, DepsTable) -> Result when
Serial :: non_neg_integer(),
Depend :: [depend()],
DepsTable :: ets:tab(),
Result :: true | false.
check_depend(_Serial, [], _DepsTable) ->
true;
check_depend(Serial, Depend, DepsTable) ->
CheckDepend = fun
(_Dep,false) -> false;
(Dep,true) ->
case ets:lookup(DepsTable, Dep) of
[#depend{serial=DepSerial}] -> DepSerial =< Serial;
_ -> false
end
end,
lists:foldl(CheckDepend, true, Depend).
%% @private
%% @doc Search by value in some set of data.
-spec find_value(Key, Data) -> Result when
Key :: key() | integer(),
Data :: map() | List | Rsc_list | tuple() | any(),
List :: list() | proplist(),
Rsc_list :: {rsc_list, List},
Result :: undefined | any().
find_value(Key, M) when is_map(M) ->
maps:get(Key, M, undefined);
find_value(Key, L) when is_integer(Key) andalso is_list(L) ->
%% Index of list with an integer like "a[2]"
try
lists:nth(Key, L)
catch
_:_ -> undefined
end;
find_value(Key, {GBSize, GBData}) when is_integer(GBSize) ->
Tree = gb_trees:from_orddict([{GBSize, GBData}]),
case gb_trees:lookup(Key, Tree) of
{value, Val} ->
Val;
_ ->
undefined
end;
find_value(Key, L) when is_list(L) ->
%% Regular proplist lookup
proplists:get_value(Key, L);
find_value(Key, {rsc_list, L}) when is_integer(Key) ->
%% Resource list handling, special lookups when skipping the index
try
lists:nth(Key, L)
catch
_:_ -> undefined
end;
find_value(Key, {rsc_list, [H|_T]}) ->
find_value(Key, H);
find_value(_Key, {rsc_list, []}) ->
undefined;
find_value(Key, T) when is_integer(Key) andalso is_tuple(T) ->
% Index of tuple with an integer like "a[2]"
try
element(Key, T)
catch
_:_ -> undefined
end;
find_value(Key, Tuple) when is_tuple(Tuple) ->
%% Other cases: context or dict lookup.
Module = element(1, Tuple),
case Module of
dict ->
{Key1, Value} = Tuple,
Dict = dict:append(Key1, Value, dict:new()),
case dict:find(Key, Dict) of
{ok, Val} ->
Val;
_ ->
undefined
end;
Module when is_atom(Key) ->
Exports = Module:module_info(exports),
case proplists:get_value(Key, Exports) of
1 -> Module:Key();
_ -> undefined
end
end;
find_value(_Key, _Data) ->
%% Any subvalue of a non-existant value is empty
undefined.
%% @doc Cleanup process for the depcache. Periodically checks a batch of depcache items for their validity.
%% Asks the depcache server to delete invalidated items. When the load of the data table is too high then
%% This cleanup process starts to delete random entries. By using a random delete we don't need to keep
%% a LRU list, which is a bit expensive.
%% @equiv cleanup(CleanUp_state, SlotNr, Now, Mode, Ct)
-spec cleanup(CleanUp_state) -> Result when
CleanUp_state :: cleanup_state(),
Result :: no_return().
cleanup(#cleanup_state{} = State) ->
?MODULE:cleanup(State, 0, now_sec(), normal, 0).
%% @doc Cleanup process for the depcache. Periodically checks a batch of depcache items for their validity.
%% Asks the depcache server to delete invalidated items. When the load of the data table is too high then
%% This cleanup process starts to delete random entries. By using a random delete we don't need to keep
%% a LRU list, which is a bit expensive.
-spec cleanup(State, SlotNr, Now, Mode, Ct) -> Result when
State :: cleanup_state(),
SlotNr :: '$end_of_table' | non_neg_integer(),
Now :: sec(),
Mode :: normal | cache_full,
Ct :: integer(),
Result :: no_return().
cleanup(#cleanup_state{tables = #tables{meta_table = MetaTable}} = State, '$end_of_table', Now, _Mode, Ct) ->
cleanup_wrap_around_table(State, MetaTable, Now, Ct);
cleanup(#cleanup_state{tables = #tables{meta_table = MetaTable}} = State, SlotNr, Now, normal, 0) ->
cleanup_normal(State, MetaTable, SlotNr, Now);
cleanup(#cleanup_state{} = State, SlotNr, Now, cache_full, 0) ->
cleanup_is_cache_full(State, SlotNr, Now);
cleanup(#cleanup_state{tables = #tables{meta_table = MetaTable}} = State, SlotNr, Now, normal, Ct) ->
cleanup_check_expire_stamp(State, MetaTable, SlotNr, Now, Ct);
cleanup(#cleanup_state{pid = Pid, name = Name, callback = Callback, tables = #tables{meta_table = MetaTable}} = State, SlotNr, Now, cache_full, Ct) ->
cleanup_random(State, MetaTable, SlotNr, Now, Ct, Pid, Name, Callback).
%% @private
%% @doc Wrap around the end of table.
%% @see cleanup/5
-spec cleanup_wrap_around_table(State, MetaTable, Now, Ct) -> Result when
State :: cleanup_state(),
MetaTable :: ets:tab(),
Now :: sec(),
Ct :: integer(),
Result :: no_return().
cleanup_wrap_around_table(State, MetaTable, Now, Ct) ->
case ets:info(MetaTable, size) of
0 -> ?MODULE:cleanup(State, 0, Now, cleanup_mode(State), 0);
_ -> ?MODULE:cleanup(State, 0, Now, cleanup_mode(State), Ct)
end.
%% @private
%% @doc In `normal' cleanup, sleep a second between each batch before continuing our cleanup sweep.
%% @see cleanup/5
-spec cleanup_normal(State, MetaTable, SlotNr, Now) -> Result when
State :: cleanup_state(),
MetaTable :: ets:tab(),
SlotNr :: '$end_of_table' | non_neg_integer(),
Now :: sec(),
Result :: no_return().
cleanup_normal(State, MetaTable, SlotNr, Now) ->
timer:sleep(1000),
case ets:info(MetaTable, size) of
0 -> ?MODULE:cleanup(State, SlotNr, Now, normal, 0);
_ -> ?MODULE:cleanup(State, SlotNr, now_sec(), cleanup_mode(State), ?CLEANUP_BATCH)
end.
%% @private
%% @doc After finishing a batch in `cache_full' mode, check if the cache is still full, if so keep deleting entries.
%% @see cleanup/5
-spec cleanup_is_cache_full(State, SlotNr, Now) -> Result when
State :: cleanup_state(),
SlotNr :: '$end_of_table' | non_neg_integer(),
Now :: sec(),
Result :: no_return().
cleanup_is_cache_full(State, SlotNr, Now) ->
case cleanup_mode(State) of
normal -> ?MODULE:cleanup(State, SlotNr, Now, normal, 0);
cache_full -> ?MODULE:cleanup(State, SlotNr, now_sec(), cache_full, ?CLEANUP_BATCH)
end.
%% @private
%% @doc Normal cleanup behaviour - check expire stamp and dependencies.
%% @see cleanup/5
-spec cleanup_check_expire_stamp(State, MetaTable, SlotNr, Now, Ct) -> Result when
State :: cleanup_state(),
MetaTable :: ets:tab(),
SlotNr :: '$end_of_table' | non_neg_integer(),
Now :: sec(),
Ct :: integer(),
Result :: no_return().
cleanup_check_expire_stamp(State, MetaTable, SlotNr, Now, Ct) ->
Slot = try
ets:slot(MetaTable, SlotNr)
catch
_M:_E -> '$end_of_table'
end,
case Slot of
'$end_of_table' -> ?MODULE:cleanup(State, '$end_of_table', Now, normal, 0);
[] -> ?MODULE:cleanup(State, SlotNr + 1, Now, normal, Ct - 1);
Entries ->
lists:foreach(fun(Meta) -> flush_expired(Meta, Now, State) end, Entries),
?MODULE:cleanup(State, SlotNr + 1, Now, normal, Ct - 1)
end.
%% @private
%% @doc Full cache cleanup mode - randomly delete every 10th entry.
%% @see cleanup/5
-spec cleanup_random(State, MetaTable, SlotNr, Now, Ct, Pid, Name, Callback) -> Result when
State :: cleanup_state(),
MetaTable :: ets:tab(),
SlotNr :: '$end_of_table' | non_neg_integer(),
Now :: sec(),
Ct :: integer(),
Pid :: pid(),
Name :: atom(),
Callback :: undefined | mfa(),
Result :: no_return().
cleanup_random(State, MetaTable, SlotNr, Now, Ct, Pid, Name, Callback) ->
Slot = try
ets:slot(MetaTable, SlotNr)
catch
_M:_E -> '$end_of_table'
end,
case Slot of
'$end_of_table' -> ?MODULE:cleanup(State, '$end_of_table', Now, cache_full, 0);
[] -> ?MODULE:cleanup(State, SlotNr + 1, Now, cache_full, Ct - 1);
Entries ->
FlushExpire = fun (Meta) ->
case flush_expired(Meta, Now, State) of
ok -> {ok, Meta};
flushed -> flushed
end
end,
RandomDelete = fun
({ok, #meta{key=Key}}) ->
case rand_uniform(10) of
1 ->
callback(eviction, Name, Callback),
gen_server:cast(Pid, {flush, Key});
_ -> ok
end;
(flushed) -> flushed
end,
Entries1 = lists:map(FlushExpire, Entries),
lists:foreach(RandomDelete, Entries1),
?MODULE:cleanup(State, SlotNr + 1, Now, cache_full, Ct - 1)
end.
-ifdef(rand_only).
rand_uniform(N) ->
rand:uniform(N).
-else.
rand_uniform(N) ->
crypto:rand_uniform(1,N+1).
-endif.
%% @private
%% @doc Check if an entry is expired, if so delete it.
-spec flush_expired(Meta, Now, State) -> Result when
Meta :: meta(),
Now :: sec(),
State :: cleanup_state(),
Result :: flushed | ok.
flush_expired(
#meta{key=Key, serial=Serial, expire=Expire, depend=Depend},
Now,
#cleanup_state{pid = Pid, tables = #tables{deps_table = DepsTable}}
) ->
Expired = Expire < Now orelse not check_depend(Serial, Depend, DepsTable),
case Expired of
true -> gen_server:cast(Pid, {flush, Key}), flushed;
false -> ok
end.
%% @private
%% @doc When the data table is too large then we start to randomly delete keys. It also signals the cleanup process
%% that it needs to be more aggressive, upping its batch size.
%% We use `erts_debug:size()' on the stored terms to calculate the total size of all terms stored. This
%% is better than counting the number of entries. Using the `process_info(Pid,memory)' is not very useful as the
%% garbage collection still needs to be done and then we delete too many entries.
%% @equiv cleanup_mode(DataTable, MemoryMax)
-spec cleanup_mode(State) -> Result when
State :: cleanup_state(),
Result :: cache_full | normal.
cleanup_mode(#cleanup_state{tables = #tables{data_table = DataTable}, memory_max = MemoryMax}) ->
cleanup_mode(DataTable, MemoryMax).
%% @private
%% @doc Returns clear-up mode.
-spec cleanup_mode(DataTable, MemoryMax) -> Result when
DataTable :: ets:tab(),
MemoryMax :: non_neg_integer(),
Result :: cache_full | normal.
cleanup_mode(DataTable, MemoryMax) ->
Memory = ets:info(DataTable, memory),
if
Memory >= MemoryMax -> cache_full;
true -> normal
end.
%% @private
%% @doc Returns the current tick count.
-spec now_sec() -> Result when
Result :: sec().
now_sec() ->
{M,S,_M} = os:timestamp(),
M*1000000 + S.
%% @private
%% @doc Safe erase of process dict, keeps some 'magical' `proc_lib' vars.
-spec erase_process_dict() -> Result when
Result :: ok.
erase_process_dict() ->
Values = [ {K, erlang:get(K)} || K <- ['$initial_call', '$ancestors', '$erl_eval_max_line'] ],
erlang:erase(),
[ erlang:put(K,V) || {K,V} <- Values, V =/= undefined ],
ok.
%% @private
%% @doc Flush all incoming messages, used when receiving timer ticks to prevent multiple ticks.
-spec flush_message(Msg) -> Result when
Msg :: any(),
Result :: ok.
flush_message(Msg) ->
receive
Msg -> flush_message(Msg)
after 0 ->
ok
end.
%% @private
%% @doc Returns the result of applying `Function' in `Module' to `Args'.
-spec callback(Type, Name, MFA) -> Result when
Type :: eviction | atom(),
Name :: atom(),
MFA :: undefined,
Result :: nop | ok | term();
(Type, Name, MFA) -> Result when
Type :: eviction | atom(),
Name :: atom(),
MFA :: {Module, Function, Arity},
Module :: module(),
Function :: atom(),
Arity :: list(),
Result :: nop | ok | term();
(Type, Name, MFA) -> Result when
Type :: eviction | atom(),
Name :: atom(),
MFA :: {Module, Function, Arity},
Module :: module(),
Function :: atom(),
Arity :: arity(),
Result :: nop | ok | term().
callback(_Type, _Name, undefined) ->
ok;
callback(Type, Name, {M, F, A}) when is_list(A) ->
try
erlang:apply(M, F, [{Type, Name} | A])
catch
_:_ ->
%% Don't log errors because of high calling frequency.
nop
end;
callback(Type, Name, {M, F, A}) ->
callback(Type, Name, {M, F, [A]}).