%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% Created : 12 Apr 2017 by Evgeny Khramtsov %%% %%% %%% Copyright (C) 2002-2021 ProcessOne, SARL. 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. %%% %%%------------------------------------------------------------------- -module(ets_cache). -behaviour(gen_server). %% API -export([start_link/2]). -export([new/1, new/2, delete/1]). -export([delete/2, delete/3, match_delete/2, match_delete/3]). -export([lookup/2, lookup/3]). -export([insert/3, insert/4, insert_new/3, insert_new/4]). -export([update/4, update/5]). -export([incr/3, incr/4]). -export([setopts/2, all/0, info/1, info/2]). -export([clear/1, clear/2, clean/1, clean/2]). -export([filter/2, fold/3]). -export([untag/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Counters: for tests *only*, don't use them outside this module -export([new_counter/0, get_counter/1, incr_counter/1, delete_counter/1, new_counter_nif/0, get_counter_nif/1, incr_counter_nif/1, delete_counter_nif/1]). -compile(no_native). -on_load(load_nif/0). -include_lib("stdlib/include/ms_transform.hrl"). -record(state, {tab :: atom(), clean_timer :: reference(), counter :: counter() | undefined, cache_missed = true :: boolean(), life_time = infinity :: milli_seconds() | infinity, max_size = infinity :: pos_integer() | infinity}). -define(DEFAULT_MAX_CACHE_TABLES, 1024). -define(IS_EXPIRED(Time, LifeTime, CurrTime), (is_integer(LifeTime) andalso (Time + LifeTime =< CurrTime))). -type tag() :: cache | nocache. -type counter() :: non_neg_integer(). -type milli_seconds() :: pos_integer(). -type option() :: {max_size, pos_integer() | infinity} | {life_time, milli_seconds() | infinity} | {cache_missed, boolean()} | {type, set | ordered_set | bag | duplicate_bag}. -type read_fun() :: fun(() -> {ok, any()} | error). -type update_fun() :: fun(() -> ok | {ok, any()} | error). -type filter_fun() :: fun((any(), {ok, any()} | error) -> boolean()). -type fold_fun() :: fun((any(), {ok, any()} | error, any()) -> any()). -type state() :: #state{}. -export_type([tag/0]). -dialyzer({no_match, [handle_info/2, check_size/2, insert/5, do_insert/5, do_filter/6, do_fold/6, delete_if_expired/4, delete_if_expired/2]}). -dialyzer({no_return, terminate/2}). -dialyzer({no_unused, calculate_time/2}). -dialyzer({[no_match, no_return], [delete_counter/1, get_counter/1, incr_counter/1]}). %%%=================================================================== %%% API %%%=================================================================== start_link(Name, Opts) -> gen_server:start_link({local, Name}, ?MODULE, [Name, Opts], []). -spec new(atom()) -> ok. new(Name) -> new(Name, []). -spec new(atom(), [option()]) -> ok. new(Name, Opts) -> Opts1 = check_opts(Opts), case whereis(Name) of undefined -> Spec = {Name, {?MODULE, start_link, [Name, Opts1]}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(cache_tab_sup, Spec) of {ok, _Pid} -> ok; {error, {already_started, _}} -> setopts(Name, Opts1); {error, Err} -> erlang:error(Err) end; _ -> setopts(Name, Opts1) end. -spec setopts(atom(), [option()]) -> ok. setopts(Name, Opts) -> Opts1 = check_opts(Opts), gen_server:cast(Name, {setopts, Opts1}). -spec delete(atom()) -> ok. delete(Name) -> supervisor:terminate_child(cache_tab_sup, Name), supervisor:delete_child(cache_tab_sup, Name), ok. -spec delete(atom(), any()) -> ok. delete(Name, Key) -> delete(Name, Key, [node()]). -spec delete(atom(), any(), [atom()]) -> ok. delete(Name, Key, Nodes) -> lists:foreach( fun(Node) when Node /= node() -> send({Name, Node}, {delete, Key}); (_MyNode) -> ets_delete(Name, Key) end, Nodes). -spec match_delete(atom(), any()) -> ok. match_delete(Name, Match) -> match_delete(Name, Match, [node()]). -spec match_delete(atom(), any(), [atom()]) -> ok. match_delete(Name, Match, Nodes) -> lists:foreach( fun(Node) when Node /= node() -> send({Name, Node}, {match_delete, Match}); (_MyNode) -> ets_match_delete(Name, Match) end, Nodes). -spec insert(atom(), any(), any()) -> boolean(). insert(Name, Key, Val) -> insert(Name, Key, Val, [node()]). -spec insert(atom(), any(), any(), [node()]) -> boolean(). insert(Name, Key, Val, Nodes) -> insert(replace, Name, Key, Val, Nodes). -spec insert_new(atom(), any(), any()) -> boolean(). insert_new(Name, Key, Val) -> insert(Name, Key, Val, [node()]). -spec insert_new(atom(), any(), any(), [node()]) -> boolean(). insert_new(Name, Key, Val, Nodes) -> insert(new, Name, Key, Val, Nodes). -spec lookup(atom(), any()) -> {ok, any()} | any(). lookup(Name, Key) -> lookup(Name, Key, error). -spec lookup(atom(), any(), read_fun() | term()) -> {ok, any()} | any(). lookup(Name, Key, Return) -> try ets:lookup(Name, Key) of [{_, Val, _} = Obj] -> delete_if_expired(Name, Obj), Val; [] when is_function(Return) -> Ver = get_counter(Name), case case Return() of {ok, _} = Ret -> {cache, Ret}; error -> {cache, error}; {error, notfound} -> {cache, error}; {cache, _} = Ret -> Ret; {cache_with_timeout, _, _} = Ret -> Ret; {nocache, _} = Ret -> Ret; Other -> {nocache, Other} end of {nocache, Val} -> Val; {cache_with_timeout, Val, Timeout} -> case get_counter(Name) of Ver -> Time = calculate_time(Name, Timeout), do_insert(new, Name, {Key, Val, Time}); _ -> ok end, Val; {cache, Val} -> case get_counter(Name) of Ver -> do_insert(new, Name, {Key, Val, current_time()}); _ -> ok end, Val end; [] -> Return catch _:badarg when is_function(Return) -> untag(Return()); _:badarg -> Return end. -spec update(atom(), any(), {ok, any()} | error, update_fun()) -> ok | any(). update(Name, Key, Val, UpdateFun) -> update(Name, Key, Val, UpdateFun, [node()]). -spec update(atom(), any(), {ok, any()} | error, update_fun(), [node()]) -> ok | any(). update(Name, Key, Val, UpdateFun, Nodes) -> NeedUpdate = try ets:lookup(Name, Key) of [{_, Val, _} = Obj] -> delete_if_expired(Name, Obj); _ -> true catch _:badarg -> true end, if NeedUpdate -> case case UpdateFun() of ok -> {cache, Val}; {ok, _} = Ret -> {cache, Ret}; error -> {cache, error}; {error, notfound} -> {cache, error}; {cache, _} = Ret -> Ret; {nocache, _} = Ret -> Ret; Other -> {nocache, Other} end of {nocache, NewVal} -> NewVal; {cache, NewVal} -> lists:foreach( fun(Node) when Node /= node() -> send({Name, Node}, {delete, Key}); (_) -> do_insert(replace, Name, {Key, NewVal, current_time()}) end, Nodes), NewVal end; true -> Val end. -spec incr(atom(), any(), non_neg_integer()) -> ok. incr(Name, Key, Incr) -> incr(Name, Key, Incr, [node()]). -spec incr(atom(), any(), non_neg_integer(), [node()]) -> ok. incr(Name, Key, Incr, Nodes) -> Obj = {Key, Incr, current_time()}, lists:foreach( fun(Node) when Node /= node() -> send({Name, Node}, {incr, Obj}); (_) -> do_insert(incr, Name, Obj) end, Nodes). -spec clear(atom()) -> ok. clear(Name) -> clear(Name, [node()]). -spec clear(atom(), [atom()]) -> ok. clear(Name, Nodes) -> lists:foreach( fun(Node) when Node /= node() -> send({Name, Node}, clear); (_MyNode) -> ets_delete_all_objects(Name) end, Nodes). -spec clean(atom()) -> ok. clean(Name) -> clean(Name, [node()]). -spec clean(atom(), [node()]) -> ok. clean(Name, Nodes) -> lists:foreach( fun(Node) -> send({Name, Node}, clean) end, Nodes). -spec filter(atom(), filter_fun()) -> non_neg_integer(). filter(Name, FilterFun) -> case whereis(Name) of undefined -> 0; _ -> {ok, LifeTime} = ets_cache_options:life_time(Name), CurrTime = current_time(), ets_safe_fixtable(Name, true), Num = do_filter(Name, FilterFun, LifeTime, CurrTime, 0, ets_first(Name)), ets_safe_fixtable(Name, false), Num end. -spec fold(fold_fun(), any(), atom()) -> any(). fold(FoldFun, Acc, Name) -> case whereis(Name) of undefined -> Acc; _ -> {ok, LifeTime} = ets_cache_options:life_time(Name), CurrTime = current_time(), ets_safe_fixtable(Name, true), NewAcc = do_fold(Name, FoldFun, LifeTime, CurrTime, Acc, ets_first(Name)), ets_safe_fixtable(Name, false), NewAcc end. -spec all() -> [atom()]. all() -> [Child || {Child, _, _, [?MODULE]} <- supervisor:which_children(cache_tab_sup)]. -spec info(atom()) -> [proplists:property()]. info(Name) -> case ets:info(Name) of undefined -> []; ETSInfo -> {ok, MaxSize} = ets_cache_options:max_size(Name), {ok, CacheMissed} = ets_cache_options:cache_missed(Name), {ok, LifeTime} = ets_cache_options:life_time(Name), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime} |lists:filtermap( fun({memory, V}) -> {true, {memory, V * erlang:system_info(wordsize)}}; ({owner, _}) -> true; ({name, _}) -> true; ({size, _}) -> true; ({type, _}) -> true; (_) -> false end, ETSInfo)] end. -spec info(atom(), atom()) -> any(). info(Name, Option) -> Options = info(Name), case lists:keyfind(Option, 1, Options) of {_, Value} -> Value; _ -> erlang:error(badarg) end. -spec untag(term()) -> term(). untag({cache, Val}) -> Val; untag({nocache, Val}) -> Val; untag(Val) -> Val. load_nif() -> case os:getenv("COVERALLS") of "true" -> ok; _ -> load_nif2() end. load_nif2() -> Path = p1_nif_utils:get_so_path(?MODULE, [?MODULE], atom_to_list(?MODULE)), MaxTables = get_max_tables(), case erlang:load_nif(Path, MaxTables) of ok -> ok; {error, {Reason, Text}} -> error_logger:error_msg("Failed to load NIF ~s: ~s (~p)", [Path, Text, Reason]), erlang:nif_error(Reason) end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Name, Opts]) -> process_flag(trap_exit, true), Type = proplists:get_value(type, Opts, set), ets:new(Name, [named_table, public, {read_concurrency, true}, Type]), Counter = new_counter(), TRef = start_clean_timer(), State = #state{tab = Name, clean_timer = TRef}, {ok, do_setopts(State, [{counter, Counter}|Opts])}. handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. handle_cast({setopts, Opts}, State) -> {noreply, do_setopts(State, Opts)}; handle_cast(_Msg, State) -> {noreply, State}. handle_info({insert, Op, {Key, _, CurrTime} = New}, #state{tab = Tab} = State) -> case ets:lookup(Tab, Key) of [Old] -> case delete_if_expired(Tab, Old, CurrTime) of true -> do_insert(Op, Tab, New); false -> ok end; [] -> do_insert(Op, Tab, New) end, {noreply, State}; handle_info({incr, Obj}, #state{tab = Tab} = State) -> do_insert(incr, Tab, Obj), {noreply, State}; handle_info({delete, Key}, #state{tab = Tab} = State) -> ets_delete(Tab, Key), {noreply, State}; handle_info({match_delete, Match}, #state{tab = Tab} = State) -> ets_match_delete(Tab, Match), {noreply, State}; handle_info(clear, #state{tab = Tab} = State) -> ets_delete_all_objects(Tab), {noreply, State}; handle_info(clean, #state{clean_timer = TRef} = State) -> clean_expired(State), NewTRef = start_clean_timer(TRef), {noreply, State#state{clean_timer = NewTRef}}; handle_info(table_overflow, #state{tab = Tab, max_size = MaxSize} = State) -> case ets:info(Tab, size) of N when N >= MaxSize -> error_logger:warning_msg( "Shrinking cache '~s' (current size = ~B, max size = ~B, " "life time = ~p, memory = ~B bytes); you should increase maximum " "cache size if this message repeats too often", [Tab, N, MaxSize, State#state.life_time, ets:info(Tab, memory) * erlang:system_info(wordsize)]), ets_delete_all_objects(Tab); _ -> ok end, {noreply, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, #state{tab = Tab}) -> delete_counter(Tab). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec check_size(atom(), pos_integer() | infinity) -> ok. check_size(_Tab, infinity) -> ok; check_size(Tab, MaxSize) -> try ets:info(Tab, size) of N when N >= MaxSize -> send({Tab, node()}, table_overflow); _ -> ok catch _:badarg -> ok end. -spec insert(new | replace, atom(), any(), any(), [node()]) -> boolean(). insert(Op, Name, Key, Val, Nodes) -> CurrTime = current_time(), Obj = {Key, {ok, Val}, CurrTime}, lists:foldl( fun(Node, Result) when Node /= node() -> send({Name, Node}, {insert, Op, Obj}), Result; (_, _) -> try ets:lookup(Name, Key) of [Old] -> case delete_if_expired(Name, Old, CurrTime) of true -> do_insert(Op, Name, Obj); false -> false end; [] -> do_insert(Op, Name, Obj) catch _:badarg -> false end end, false, Nodes). do_insert(Op, Name, Obj) -> {ok, CacheMissed} = ets_cache_options:cache_missed(Name), {ok, MaxSize} = ets_cache_options:max_size(Name), do_insert(Op, Name, Obj, CacheMissed, MaxSize). do_insert(_Op, _Name, {_Key, error, _Time}, _CacheMissed = false, _MaxSize) -> false; do_insert(Op, Name, Obj, _CacheMissed, MaxSize) -> check_size(Name, MaxSize), try case Op of new -> ets:insert_new(Name, Obj); replace -> ets:insert(Name, Obj); incr -> incr_counter(Name), {Key, Incr, Time} = Obj, ets:update_counter(Name, Key, {2, Incr}), ets:update_element(Name, Key, {3, Time}) end catch _:badarg -> false end. -spec do_filter(atom(), filter_fun(), pos_integer() | infinity, integer(), non_neg_integer(), any()) -> non_neg_integer(). do_filter(_Name, _FilterFun, _LifeTime, _CurrTime, Num, '$end_of_table') -> Num; do_filter(Name, FilterFun, LifeTime, CurrTime, Num, Key) -> NewNum = try ets:lookup(Name, Key) of [{Key, Val, Time}] -> if ?IS_EXPIRED(Time, LifeTime, CurrTime) -> ets_delete_object(Name, {Key, Val, Time}), Num; true -> case FilterFun(Key, Val) of true -> Num; false -> ets_delete_object(Name, {Key, Val, Time}), Num + 1 end end; _ -> Num catch _:badarg -> Num end, do_filter(Name, FilterFun, LifeTime, CurrTime, NewNum, ets_next(Name, Key)). -spec do_fold(atom(), fold_fun(), pos_integer() | infinity, integer(), any(), any()) -> any(). do_fold(_Name, _FoldFun, _LifeTime, _CurrTime, Acc, '$end_of_table') -> Acc; do_fold(Name, FoldFun, LifeTime, CurrTime, Acc, Key) -> NewAcc = try ets:lookup(Name, Key) of [{Key, Val, Time}] -> if ?IS_EXPIRED(Time, LifeTime, CurrTime) -> ets_delete_object(Name, {Key, Val, Time}), Acc; true -> FoldFun(Key, Val, Acc) end; _ -> Acc catch _:badarg -> Acc end, do_fold(Name, FoldFun, LifeTime, CurrTime, NewAcc, ets_next(Name, Key)). -spec check_opts([option()]) -> [option()]. check_opts(Opts) -> lists:map( fun({Name, Val} = Opt) -> case {Name, Val} of {max_size, Val} when is_integer(Val), Val>0 -> Opt; {max_size, infinity} -> Opt; {life_time, Val} when is_integer(Val), Val>0 -> Opt; {life_time, infinity} -> Opt; {cache_missed, Bool} when is_boolean(Bool) -> Opt; {type, Type} when Type == set; Type == ordered_set; Type == bag; Type == duplicate_bag -> Opt; _ -> erlang:error({bad_option, Opt}) end; (cache_missed) -> {cache_missed, true}; (Opt) -> erlang:error({bad_option, Opt}) end, Opts). -spec do_setopts(state(), [{counter, counter()} | option()]) -> state(). do_setopts(State, Opts) -> MaxSize = proplists:get_value(max_size, Opts, State#state.max_size), LifeTime = proplists:get_value(life_time, Opts, State#state.life_time), CacheMissed = proplists:get_value(cache_missed, Opts, State#state.cache_missed), Counter = proplists:get_value(counter, Opts, State#state.counter), NewState = State#state{max_size = MaxSize, counter = Counter, life_time = LifeTime, cache_missed = CacheMissed}, if State == NewState -> ok; true -> Tab = State#state.tab, p1_options:insert(ets_cache_options, max_size, Tab, MaxSize), p1_options:insert(ets_cache_options, life_time, Tab, LifeTime), p1_options:insert(ets_cache_options, cache_missed, Tab, CacheMissed), p1_options:insert(ets_cache_options, counter, Tab, Counter), p1_options:compile(ets_cache_options) end, NewState. -spec calculate_time(atom(), integer()) -> integer(). calculate_time(Name, Timeout) -> Now = current_time(), case ets_cache_options:life_time(Name) of {ok, infinity} -> Now; {ok, LifeTime} -> Now - LifeTime + Timeout end. -spec current_time() -> integer(). current_time() -> erlang:system_time(millisecond). -spec start_clean_timer() -> reference(). start_clean_timer() -> start_clean_timer(make_ref()). -spec start_clean_timer(reference()) -> reference(). start_clean_timer(TRef) -> case erlang:cancel_timer(TRef) of false -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end, Timeout = timer:minutes(1) + p1_rand:uniform(timer:minutes(4)), erlang:send_after(Timeout, self(), clean). -spec clean_expired(state()) -> non_neg_integer(). clean_expired(#state{tab = Tab, life_time = LifeTime}) -> CurrTime = current_time(), ets_select_delete( Tab, ets:fun2ms( fun({_, _, Time}) -> ?IS_EXPIRED(Time, LifeTime, CurrTime) end)). delete_if_expired(Name, Obj) -> case ets_cache_options:life_time(Name) of {ok, infinity} -> false; {ok, LifeTime} -> delete_if_expired(Name, Obj, current_time(), LifeTime) end. delete_if_expired(Name, Obj, CurrTime) -> {ok, LifeTime} = ets_cache_options:life_time(Name), delete_if_expired(Name, Obj, CurrTime, LifeTime). delete_if_expired(Name, {_, _, Time} = Obj, CurrTime, LifeTime) -> if ?IS_EXPIRED(Time, LifeTime, CurrTime) -> ets_delete_object(Name, Obj); true -> false end. send(Dst, Msg) -> erlang:send(Dst, Msg, [noconnect, nosuspend]). -spec ets_delete(atom(), any()) -> boolean(). ets_delete(Tab, Key) -> try incr_counter(Tab), ets:delete(Tab, Key) catch _:badarg -> false; _:{badmatch, undefined} -> false end. -spec ets_match_delete(atom(), any()) -> boolean(). ets_match_delete(Tab, Match) -> try incr_counter(Tab), ets:match_delete(Tab, {Match, '_', '_'}) catch _:badarg -> false; _:{badmatch, undefined} -> false end. -spec ets_delete_object(atom(), any()) -> boolean(). ets_delete_object(Tab, Obj) -> try incr_counter(Tab), ets:delete_object(Tab, Obj) catch _:badarg -> false; _:{badmatch, undefined} -> false end. -spec ets_delete_all_objects(atom()) -> boolean(). ets_delete_all_objects(Tab) -> try incr_counter(Tab), ets:delete_all_objects(Tab) catch _:badarg -> false; _:{badmatch, undefined} -> false end. -spec ets_select_delete(atom(), ets:match_spec()) -> non_neg_integer(). ets_select_delete(Tab, Fun) -> try incr_counter(Tab), ets:select_delete(Tab, Fun) catch _:badarg -> 0 end. -spec ets_first(atom()) -> any(). ets_first(Tab) -> try ets:first(Tab) catch _:badarg -> '$end_of_table' end. -spec ets_next(atom(), any()) -> any(). ets_next(Tab, Key) -> try ets:next(Tab, Key) catch _:badarg -> '$end_of_table' end. -spec ets_safe_fixtable(atom(), boolean()) -> boolean(). ets_safe_fixtable(Tab, Fix) -> try ets:safe_fixtable(Tab, Fix) catch _:badarg -> false end. get_max_tables() -> case application:get_env(cache_tab, max_cache_tables) of undefined -> ?DEFAULT_MAX_CACHE_TABLES; {ok, I} when is_integer(I), I > 0 -> I; {ok, Val} -> error_logger:error_msg( "Incorrect value of 'max_cache_tables' " "application parameter: ~p", [Val]), ?DEFAULT_MAX_CACHE_TABLES end. %%%=================================================================== %%% Counters %%%=================================================================== -spec new_counter() -> counter(). new_counter() -> case new_counter_nif() of {ok, Counter} -> Counter; {error, {system_limit, MaxTables}} -> error_logger:error_msg( "Maximum number (~B) of ETS cache tables has reached", [MaxTables]), erlang:error(system_limit) end. -spec get_counter(atom()) -> non_neg_integer(). get_counter(Name) -> {ok, Counter} = ets_cache_options:counter(Name), get_counter_nif(Counter). -spec incr_counter(atom()) -> non_neg_integer(). incr_counter(Name) -> {ok, Counter} = ets_cache_options:counter(Name), incr_counter_nif(Counter). -spec delete_counter(counter()) -> ok. delete_counter(Name) -> {ok, Counter} = ets_cache_options:counter(Name), delete_counter_nif(Counter). -spec new_counter_nif() -> {ok, counter()} | {error, {system_limit, pos_integer()}}. new_counter_nif() -> erlang:nif_error({nif_not_loaded, ?MODULE}). -spec get_counter_nif(counter()) -> non_neg_integer(). get_counter_nif(_) -> erlang:nif_error({nif_not_loaded, ?MODULE}). -spec incr_counter_nif(counter()) -> non_neg_integer(). incr_counter_nif(_) -> erlang:nif_error({nif_not_loaded, ?MODULE}). -spec delete_counter_nif(counter()) -> ok. delete_counter_nif(_) -> erlang:nif_error({nif_not_loaded, ?MODULE}).