Current section
Files
Jump to
Current section
Files
src/ttl_cache.erl
-module(ttl_cache).
-behaviour(gen_server).
%% API
-export([
start_link/0,
start_link/2
]).
-export([
put/2,
put/3,
put/4,
get/1,
get/2,
get_ttl/1,
get_ttl/2,
set_ttl/2,
set_ttl/3
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-define(MAX_TIMEOUT, 1000 * 60).
-define(CALL_TIMEOUT, 5000).
-define(DEFAULT_TTL, 1000 * 60 * 10).
-record(state, {
name,
cache_ets, % {Timeout,Ref} -> Key
timeout_ets, % Key -> {Value,{Timeout,Ref}}
ttl
}).
%%%===================================================================
%%% API
%%%===================================================================
start_link() ->
start_link(?MODULE, []).
start_link(Name, Opt) ->
gen_server:start_link({local, Name}, ?MODULE, [Name, Opt], []).
put(K, V) ->
gen_server:call(?MODULE, {put, K, V}, ?CALL_TIMEOUT).
put(K, V, TTL) ->
gen_server:call(?MODULE, {put, K, V, TTL}, ?CALL_TIMEOUT).
put(Name, K, V, TTL) ->
gen_server:call(Name, {put, K, V, TTL}, ?CALL_TIMEOUT).
get(K) ->
?MODULE:get(?MODULE, K).
get(Name, K) ->
CacheETS = cache_ets_name(Name),
case ets:lookup(CacheETS, K) of
[{K, {V, _}}] -> {ok, V};
[] -> {error, not_found}
end.
get_ttl(K) ->
get_ttl(?MODULE, K).
get_ttl(Name, K) ->
CacheETS = cache_ets_name(Name),
case ets:lookup(CacheETS, K) of
[{K, {_, {TTL, _}}}] -> {ok, TTL};
[] -> {error, not_found}
end.
set_ttl(K, TTL) ->
set_ttl(?MODULE, K, TTL).
set_ttl(Name, K, TTL) ->
CacheETS = cache_ets_name(Name),
case ets:lookup(CacheETS, K) of
[_] ->
gen_server:call(Name, {set_ttl, K, TTL}, ?CALL_TIMEOUT);
[] ->
{error, not_found}
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init([Name, Opt]) ->
CacheETS = cache_ets_name(Name),
TimeoutETS = timeout_ets_name(Name),
ets:new(CacheETS, [named_table, protected]),
ets:new(TimeoutETS, [named_table, ordered_set, protected]),
TTL = proplists:get_value(ttl, Opt, ?DEFAULT_TTL),
{ok, #state{name = Name, cache_ets = CacheETS, timeout_ets = TimeoutETS, ttl = TTL}}.
handle_call({put, K, V}, _From, #state{ttl = TTL} = State) ->
do_put(K, V, TTL, State);
handle_call({put, K, V, TTL}, _From, State) ->
do_put(K, V, TTL, State);
handle_call({set_ttl, K, TTL}, _From, State) ->
do_set_ttl(K, TTL, State);
handle_call(_Request, _From, State) ->
{reply, ok, State}.
handle_cast(_Request, State) ->
{noreply, State}.
handle_info(timeout, State) ->
Now = timenow_microsecond(),
Timeout = timeout(Now, State),
{noreply, State, Timeout};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
cache_ets_name(Name) ->
erlang:list_to_atom(lists:concat([Name, "_cache_ets"])).
timeout_ets_name(Name) ->
erlang:list_to_atom(lists:concat([Name, "_timeout_ets"])).
timenow_microsecond() ->
os:perf_counter(microsecond).
do_put(K, V, TTL, #state{cache_ets = CacheETS, timeout_ets = TimeoutETS} = S) ->
Now = timenow_microsecond(),
% delete exist record
case ets:lookup(CacheETS, K) of
[{K, {_, TK}}] ->
ets:delete(TimeoutETS, TK);
[] ->
pass
end,
TimeoutKey = {Now + 1000 * TTL, erlang:make_ref()},
ets:insert(TimeoutETS, {TimeoutKey, K}),
ets:insert(CacheETS, {K, {V, TimeoutKey}}),
Timeout = timeout(Now, S),
{reply, ok, S, Timeout}.
do_set_ttl(K, TTL, #state{cache_ets = CacheETS, timeout_ets = TimeoutETS} = S) ->
Now = timenow_microsecond(),
case ets:lookup(CacheETS, K) of
[{_, {V, TK}}] ->
ets:delete(TimeoutETS, TK),
TimeoutKey = {Now + 1000 * TTL, erlang:make_ref()},
ets:insert(TimeoutETS, {TimeoutKey, K}),
ets:insert(CacheETS, {K, {V, TimeoutKey}});
[] ->
pass
end,
Timeout = timeout(Now, S),
{reply, ok, S, Timeout}.
timeout(Now, #state{cache_ets = CacheETS, timeout_ets = TimeoutETS} = State) ->
case ets:first(TimeoutETS) of
'$end_of_table' ->
infinity;
{Time, _Ref} when Time > Now ->
erlang:min(Time - Now, ?MAX_TIMEOUT);
TimeoutKey ->
case ets:lookup(TimeoutETS, TimeoutKey) of
[{_, K}] -> ets:delete(CacheETS, K);
[] -> pass
end,
ets:delete(TimeoutETS, TimeoutKey),
timeout(Now, State)
end.