add cache option to disable segment evition

{ttl,   undefined} disables segment expire timeout
{check, undefined} disables segment quota check
It makes cache behave as ets table
This commit is contained in:
Dmitry Kolesnikov 2014-08-20 22:00:32 +03:00
parent 559070765b
commit 0db068cc75
8 changed files with 378 additions and 260 deletions

View File

@ -4,9 +4,7 @@ Cache uses N disposable ETS tables instead of single one. The cache applies evic
policies at segment level. The oldest ETS table is destroyed and new one is created when
quota or TTL criteria are exceeded.
The write operation always uses youngest segment. The read operation lookup key from youngest to oldest table
until it is found same time key is moved to youngest segment to prolong TTL. If none of ETS table contains key
then cache-miss occurs.
The write operation always uses youngest segment. The read operation lookup key from youngest to oldest table until it is found same time key is moved to youngest segment to prolong TTL. If none of ETS table contains key then cache-miss occurs.
The downside is inability to assign precise TTL per single cache entry. TTL is always approximated to nearest segment. (e.g. cache with 60 sec TTL and 10 segments has 6 sec accuracy on TTL)
@ -68,10 +66,8 @@ The local cache instance is accessible for any Erlang nodes in the cluster.
MacBook Pro, Intel Core i5, 2.5GHz, 8GB 1600 MHz DDR3, 256 SSD
LRU Cache, 10 segments, 20 sec ttl (~2 sec per segment)
Local cache (application and cache within same VM)
![Local cache (application and cache within same VM)](local.png)
@ -82,5 +78,3 @@ The local cache instance is accessible for any Erlang nodes in the cluster.
* Jose Luis Navarro https://github.com/artefactop
* Valentin Micic

View File

@ -1,13 +1,8 @@
{application, cache,
[
{description, "in-memory cache"},
{vsn, "0.11.1"},
{modules, [
cache,
cache_bucket,
cache_sup,
cache_app
]},
{vsn, "1.0.0"},
{modules, []},
{registered, []},
{applications,[
kernel,

View File

@ -327,7 +327,7 @@ append(Cache, Key, Val) ->
gen_server:call(Cache, {append, Key, Val}, ?DEF_CACHE_TIMEOUT).
%%
%% synchronously add data to existing key after existing data, the operation do not prolong entry ttl
%% asynchronously add data to existing key after existing data, the operation do not prolong entry ttl
-spec(append_/3 :: (cache(), key(), entity()) -> ok).
append_(Cache, Key, Val) ->

View File

@ -14,12 +14,7 @@
%% limitations under the License.
%%
%%
%% define default select limit
-define(CONFIG_SELECT, 1000).
%-define(VERBOSE, true).
% -define(VERBOSE, true).
-ifdef(VERBOSE).
-define(DEBUG(Str, Args), error_logger:error_msg(Str, Args)).
-else.
@ -35,7 +30,7 @@
-define(DEF_CACHE_N, 10).
%% default cache house keeping frequency
-define(DEF_CACHE_QUOTA, 5).
-define(DEF_CACHE_CHECK, 20000).
%% default cache i/o timeout
-define(DEF_CACHE_TIMEOUT, 60000).

View File

@ -34,30 +34,14 @@
%% internal bucket state
-record(cache, {
name = undefined :: atom(), %% name of cache bucket
heap = [] :: list(), %% cache heap segments
n = ?DEF_CACHE_N :: integer(), %% number of segments
type = ?DEF_CACHE_TYPE :: atom(),
ttl = ?DEF_CACHE_TTL :: integer(), %% cache time to live
policy = ?DEF_CACHE_POLICY :: integer(), %% eviction policy
cardinality = undefined :: integer(), %% cache cardinality
memory = undefined :: integer(), %% cache memory limit
quota = ?DEF_CACHE_QUOTA :: integer(), %% quota enforcement timer
evict = undefined %% evict timer
,stats = undefined :: any() %% stats aggregation functor
,heir = undefined :: pid() %% the heir of evicted cache segment
}).
%% cache segment
-record(heap, {
id :: integer(), %% segment heap
expire :: integer(), %% segment expire time
cardinality :: integer(), %% segment cardinality quota
memory :: integer() %% segment memory quota
name = undefined :: atom() %% name of cache bucket
,heap = undefined :: list() %% cache heap segments
,n = ?DEF_CACHE_N :: integer() %% number of segments
,policy = ?DEF_CACHE_POLICY :: integer() %% eviction policy
,check = ?DEF_CACHE_CHECK :: integer() %% status check timeout
,evict = undefined :: integer() %% evict timeout
,stats = undefined :: any() %% stats aggregation functor
,heir = undefined :: pid() %% the heir of evicted cache segment
}).
%%%----------------------------------------------------------------------------
@ -76,57 +60,42 @@ start_link(Name, Opts) ->
gen_server:start_link({local, Name}, ?MODULE, [Name, Opts], []).
init([Name, Opts]) ->
{ok, init(Opts, #cache{name=Name})}.
{ok, init(Opts, Opts, #cache{name=Name})}.
init([{type, X} | T], #cache{}=S) ->
init(T, S#cache{type=X});
init([{policy, X} | T], #cache{}=S) ->
init(T, S#cache{policy=X});
init([{memory, X} | Opts], S) ->
init(Opts, S#cache{memory=X div erlang:system_info(wordsize)});
init([{size, X} | Opts], S) ->
init(Opts, S#cache{cardinality=X});
init([{n, X} | Opts], S) ->
init(Opts, S#cache{n = X});
init([{ttl, X} | Opts], S) ->
init(Opts, S#cache{ttl = X});
init([{quota, X} | Opts], S) ->
init(Opts, S#cache{quota=X});
init([{stats, X} | Opts], S) ->
init(Opts, S#cache{stats=X});
init([{heir, X} | Opts], S) ->
init(Opts, S#cache{heir=X});
init([_ | Opts], S) ->
init(Opts, S);
init([], S) ->
random:seed(os:timestamp()),
Evict = cache_util:mdiv(S#cache.ttl, S#cache.n),
init_heap(
S#cache{
evict = cache_util:timeout(Evict * 1000, evict),
quota = cache_util:timeout(S#cache.quota * 1000, quota)
}
).
init([{policy, X} | Tail], Opts, State) ->
init(Tail, Opts, State#cache{policy=X});
init([{n, X} | Tail], Opts, State) ->
init(Tail, Opts, State#cache{n=X});
init([{check, X} | Tail], Opts, State) ->
init(Tail, Opts, State#cache{check=X * 1000});
init([{stats, X} | Tail], Opts, State) ->
init(Tail, Opts, State#cache{stats=X});
init([{heir, X} | Tail], Opts, State) ->
init(Tail, Opts, State#cache{heir=X});
init([_ | Tail], Opts, State) ->
init(Tail, Opts, State);
init([], Opts, State) ->
Type = proplists:get_value(type, Opts, ?DEF_CACHE_TYPE),
TTL = proplists:get_value(ttl, Opts, ?DEF_CACHE_TTL),
Size = proplists:get_value(size, Opts),
Mem = proplists:get_value(memory, Opts),
Evict= cache_util:mdiv(TTL, State#cache.n),
Heap = cache_heap:new(
Type
,cache_util:mdiv(TTL, State#cache.n)
,cache_util:mdiv(Size, State#cache.n)
,cache_util:mdiv(cache_util:mdiv(Mem, State#cache.n), erlang:system_info(wordsize))
),
(catch erlang:send_after(State#cache.check, self(), check_heap)),
(catch erlang:send_after(Evict, self(), evict_heap)),
State#cache{heap=Heap, evict=Evict}.
%%
%%
terminate(_Reason, S) ->
lists:foreach(
fun(X) ->
destroy_heap(X#heap.id, S#cache.heir)
end,
S#cache.heap
).
terminate(_Reason, State) ->
cache_heap:purge(State#cache.heap, State#cache.heir),
ok.
%%%----------------------------------------------------------------------------
%%%
@ -213,32 +182,27 @@ handle_call({append, Key, Val}, _, S) ->
{reply, ok, cache_put(Key, [X, Val], S)}
end;
handle_call(i, _, S) ->
Heap = [X#heap.id || X <- S#cache.heap],
Expire = [X#heap.expire || X <- S#cache.heap],
Size = [ets:info(X#heap.id, size) || X <- S#cache.heap],
Memory = [ets:info(X#heap.id, memory) || X <- S#cache.heap],
{reply, [{heap, Heap}, {expire, Expire}, {size, Size}, {memory, Memory}], S};
handle_call(i, _, State) ->
Heap = cache_heap:refs(State#cache.heap),
Refs = [X || {_, X} <- Heap],
Expire = [X || {X, _} <- Heap],
Size = [ets:info(X, size) || {_, X} <- Heap],
Memory = [ets:info(X, memory) || {_, X} <- Heap],
{reply, [{heap, Refs}, {expire, Expire}, {size, Size}, {memory, Memory}], State};
handle_call({heap, N}, _, S) ->
handle_call({heap, N}, _, State) ->
try
H = lists:nth(N, S#cache.heap),
{reply, H#heap.id, S}
Ref = lists:nth(N, cache_heap:refs(State#cache.heap)),
{reply, Ref, State}
catch _:_ ->
{reply, badarg, S}
{reply, badarg, State}
end;
handle_call(drop, _, S) ->
{stop, normal, ok, S};
handle_call(drop, _, State) ->
{stop, normal, ok, State};
handle_call(purge, _, S) ->
lists:foreach(
fun(X) ->
destroy_heap(X#heap.id, S#cache.heir)
end,
S#cache.heap
),
{reply, ok, init_heap(S#cache{heap = []})};
handle_call(purge, _, State) ->
{reply, ok, State#cache{heap=cache_heap:purge(State#cache.heap, State#cache.heir)}};
handle_call(_, _, S) ->
{noreply, S}.
@ -317,48 +281,69 @@ handle_cast(_, S) ->
%%
%%
handle_info(evict, S) ->
Now = cache_util:now(),
case lists:last(S#cache.heap) of
H when H#heap.expire =< Now ->
{noreply,
free_heap(
S#cache{evict = cache_util:timeout(S#cache.evict, evict)}
)
};
handle_info(check_heap, #cache{n=N, check=Check}=State) ->
erlang:send_after(Check, self(), check_heap),
Heap = cache_heap:slip(State#cache.heap),
case cache_heap:size(Heap) of
X when X > N ->
{noreply, State#cache{heap=cache_heap:drop(Heap, State#cache.heir)}};
_ ->
{noreply,
init_heap(
S#cache{evict = cache_util:timeout(S#cache.evict, evict)}
)
}
{noreply, State#cache{heap=Heap}}
end;
handle_info(quota, S) ->
case is_heap_out_of_quota(hd(S#cache.heap)) of
true ->
case length(S#cache.heap) of
N when N =:= S#cache.n ->
{noreply,
free_heap(
S#cache{quota = cache_util:timeout(S#cache.quota, quota)}
)
};
_ ->
{noreply,
init_heap(
S#cache{quota = cache_util:timeout(S#cache.quota, quota)}
)
}
end;
false ->
{noreply,
S#cache{
quota = cache_util:timeout(S#cache.quota, quota)
}
}
handle_info(evict_heap, #cache{n=N, evict=Evict}=State) ->
erlang:send_after(Evict, self(), evict_heap),
Heap = cache_heap:slip(State#cache.heap),
case cache_heap:size(Heap) of
X when X > N ->
{noreply, State#cache{heap=cache_heap:drop(Heap, State#cache.heir)}};
_ ->
{noreply, State#cache{heap=Heap}}
end;
% handle_info(evict, S) ->
% Now = cache_util:now(),
% case lists:last(S#cache.heap) of
% H when H#heap.expire =< Now ->
% {noreply,
% free_heap(
% S#cache{evict = cache_util:timeout(S#cache.evict, evict)}
% )
% };
% _ ->
% {noreply,
% init_heap(
% S#cache{evict = cache_util:timeout(S#cache.evict, evict)}
% )
% }
% end;
% handle_info(quota, S) ->
% case is_heap_out_of_quota(hd(S#cache.heap)) of
% true ->
% case length(S#cache.heap) of
% N when N =:= S#cache.n ->
% {noreply,
% free_heap(
% S#cache{quota = cache_util:timeout(S#cache.quota, quota)}
% )
% };
% _ ->
% {noreply,
% init_heap(
% S#cache{quota = cache_util:timeout(S#cache.quota, quota)}
% )
% }
% end;
% false ->
% {noreply,
% S#cache{
% quota = cache_util:timeout(S#cache.quota, quota)
% }
% }
% end;
handle_info(_, S) ->
{noreply, S}.
@ -376,31 +361,32 @@ code_change(_Vsn, S, _Extra) ->
%%
%% insert value to cache
cache_put(Key, Val, #cache{}=S) ->
[Head | Tail] = S#cache.heap,
true = ets:insert(Head#heap.id, {Key, Val}),
cache_put(Key, Val, #cache{heap=Heap}=State) ->
{_, Head} = cache_heap:head(Heap),
true = ets:insert(Head, {Key, Val}),
lists:foreach(
fun(X) -> ets:delete(X#heap.id, Key) end,
Tail
fun({_, X}) -> ets:delete(X, Key) end,
cache_heap:tail(Heap)
),
cache_util:stats(S#cache.stats, {cache, S#cache.name, put}),
?DEBUG("cache ~p: put ~p to heap ~p~n", [S#cache.name, Key, Head#heap.id]),
S.
cache_util:stats(State#cache.stats, {cache, State#cache.name, put}),
?DEBUG("cache ~p: put ~p to heap ~p~n", [State#cache.name, Key, Head]),
State.
cache_put(Key, Val, Expire, #cache{}=S) ->
case lists:splitwith(fun(X) -> X#heap.expire > Expire end, S#cache.heap) of
cache_put(Key, Val, Expire, #cache{}=State) ->
Refs = cache_heap:refs(State#cache.heap),
case lists:splitwith(fun({X, _}) -> X > Expire end, Refs) of
{[], _Tail} ->
cache_put(Key, Val, S);
cache_put(Key, Val, State);
{Head, Tail} ->
[Heap | Rest] = lists:reverse(Head),
true = ets:insert(Heap#heap.id, {Key, Val}),
[{_, Heap} | Rest] = lists:reverse(Head),
true = ets:insert(Heap, {Key, Val}),
lists:foreach(
fun(X) -> ets:delete(X#heap.id, Key) end,
fun({_, X}) -> ets:delete(X, Key) end,
Rest ++ Tail
),
cache_util:stats(S#cache.stats, {cache, S#cache.name, put}),
?DEBUG("cache ~p: put ~p to heap ~p~n", [S#cache.name, Key, Heap#heap.id]),
S
cache_util:stats(State#cache.stats, {cache, State#cache.name, put}),
?DEBUG("cache ~p: put ~p to heap ~p~n", [State#cache.name, Key, Heap]),
State
end.
%%
@ -412,19 +398,19 @@ cache_get(Key, #cache{policy=mru}=S) ->
cache_lookup(Key, S);
cache_get(Key, #cache{}=S) ->
Head = hd(S#cache.heap),
case heap_lookup(Key, S#cache.heap) of
{_, Head} = cache_heap:head(S#cache.heap),
case heap_lookup(Key, cache_heap:refs(S#cache.heap)) of
undefined ->
cache_util:stats(S#cache.stats, {cache, S#cache.name, miss}),
undefined;
{Heap, Val} when Heap#heap.id =:= Head#heap.id ->
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
{Head, Val} ->
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Head]),
cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
Val;
{Heap, Val} ->
true = ets:insert(Head#heap.id, {Key, Val}),
_ = ets:delete(Heap#heap.id, Key),
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
true = ets:insert(Head, {Key, Val}),
_ = ets:delete(Heap, Key),
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap]),
cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
Val
end.
@ -432,12 +418,12 @@ cache_get(Key, #cache{}=S) ->
%%
%% lookup cache value
cache_lookup(Key, #cache{}=S) ->
case heap_lookup(Key, S#cache.heap) of
case heap_lookup(Key, cache_heap:refs(S#cache.heap)) of
undefined ->
cache_util:stats(S#cache.stats, {cache, S#cache.name, miss}),
undefined;
{_Heap, Val} ->
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
{Heap, Val} ->
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap]),
cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
Val
end.
@ -445,30 +431,30 @@ cache_lookup(Key, #cache{}=S) ->
%%
%% check if key exists
cache_has(Key, #cache{}=S) ->
case heap_has(Key, S#cache.heap) of
false ->
case heap_has(Key, cache_heap:refs(S#cache.heap)) of
false ->
false;
_Heap ->
?DEBUG("cache ~p: has ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
Heap ->
?DEBUG("cache ~p: has ~p at cell ~p~n", [S#cache.name, Key, Heap]),
true
end.
%%
%% check key ttl
cache_ttl(Key, #cache{}=S) ->
case heap_has(Key, S#cache.heap) of
false ->
case heap_has(Key, cache_heap:refs(S#cache.heap)) of
false ->
undefined;
Heap ->
Heap#heap.expire - cache_util:now()
{Expire, _} ->
Expire - cache_util:now()
end.
%%
%%
cache_remove(Key, #cache{}=S) ->
lists:foreach(
fun(X) -> ets:delete(X#heap.id, Key) end,
S#cache.heap
fun({_, X}) -> ets:delete(X, Key) end,
cache_heap:refs(S#cache.heap)
),
cache_util:stats(S#cache.stats, {cache, S#cache.name, remove}),
?DEBUG("cache ~p: remove ~p~n", [S#cache.name, Key]),
@ -509,10 +495,10 @@ tuple_acc(List, X) ->
%%
%%
heap_lookup(Key, [H | Tail]) ->
case ets:lookup(H#heap.id, Key) of
heap_lookup(Key, [{_, Heap} | Tail]) ->
case ets:lookup(Heap, Key) of
[] -> heap_lookup(Key, Tail);
[{_, Val}] -> {H, Val}
[{_, Val}] -> {Heap, Val}
end;
heap_lookup(_Key, []) ->
@ -520,73 +506,73 @@ heap_lookup(_Key, []) ->
%%
%%
heap_has(Key, [H | Tail]) ->
case ets:member(H#heap.id, Key) of
heap_has(Key, [{_, Heap}=X | Tail]) ->
case ets:member(Heap, Key) of
false -> heap_has(Key, Tail);
true -> H
true -> X
end;
heap_has(_Key, []) ->
false.
%%
%% init cache heap
init_heap(#cache{}=S) ->
Id = ets:new(undefined, [S#cache.type, protected]),
?DEBUG("cache ~p: init heap ~p~n", [S#cache.name, Id]),
Heap = #heap{
id = Id,
expire = cache_util:madd(S#cache.ttl, cache_util:now()),
cardinality = cache_util:mdiv(S#cache.cardinality, S#cache.n),
memory = cache_util:mdiv(S#cache.memory, S#cache.n)
},
S#cache{
heap = [Heap | S#cache.heap]
}.
% %%
% %% init cache heap
% init_heap(#cache{}=S) ->
% Id = ets:new(undefined, [S#cache.type, protected]),
% ?DEBUG("cache ~p: init heap ~p~n", [S#cache.name, Id]),
% Heap = #heap{
% id = Id,
% expire = cache_util:madd(S#cache.ttl, cache_util:now()),
% cardinality = cache_util:mdiv(S#cache.cardinality, S#cache.n),
% memory = cache_util:mdiv(S#cache.memory, S#cache.n)
% },
% S#cache{
% heap = [Heap | S#cache.heap]
% }.
%%
%%
free_heap(#cache{}=S) ->
[H | Tail] = lists:reverse(S#cache.heap),
Size = ets:info(H#heap.id, size),
cache_util:stats(S#cache.stats, {cache, S#cache.name, evicted}, Size),
destroy_heap(H#heap.id, S#cache.heir),
?DEBUG("cache ~p: free heap ~p~n", [S#cache.name, H#heap.id]),
init_heap(
S#cache{
heap = lists:reverse(Tail)
}
).
% %%
% %%
% free_heap(#cache{}=S) ->
% [H | Tail] = lists:reverse(S#cache.heap),
% Size = ets:info(H#heap.id, size),
% cache_util:stats(S#cache.stats, {cache, S#cache.name, evicted}, Size),
% destroy_heap(H#heap.id, S#cache.heir),
% ?DEBUG("cache ~p: free heap ~p~n", [S#cache.name, H#heap.id]),
% init_heap(
% S#cache{
% heap = lists:reverse(Tail)
% }
% ).
destroy_heap(Id, undefined) ->
ets:delete(Id);
destroy_heap(Id, Heir)
when is_pid(Heir) ->
ets:give_away(Id, Heir, evicted);
destroy_heap(Id, Heir)
when is_atom(Heir) ->
case erlang:whereis(Heir) of
undefined ->
ets:delete(Id);
Pid ->
ets:give_away(Id, Pid, evicted)
end.
% destroy_heap(Id, undefined) ->
% ets:delete(Id);
% destroy_heap(Id, Heir)
% when is_pid(Heir) ->
% ets:give_away(Id, Heir, evicted);
% destroy_heap(Id, Heir)
% when is_atom(Heir) ->
% case erlang:whereis(Heir) of
% undefined ->
% ets:delete(Id);
% Pid ->
% ets:give_away(Id, Pid, evicted)
% end.
%%
%% heap policy check
is_heap_out_of_quota(#heap{}=H) ->
is_out_of_memory(H) orelse is_out_of_capacity(H).
% %%
% %% heap policy check
% is_heap_out_of_quota(#heap{}=H) ->
% is_out_of_memory(H) orelse is_out_of_capacity(H).
is_out_of_capacity(#heap{cardinality=undefined}) ->
false;
is_out_of_capacity(#heap{cardinality=N}=H) ->
ets:info(H#heap.id, size) >= N.
% is_out_of_capacity(#heap{cardinality=undefined}) ->
% false;
% is_out_of_capacity(#heap{cardinality=N}=H) ->
% ets:info(H#heap.id, size) >= N.
is_out_of_memory(#heap{memory=undefined}) ->
false;
is_out_of_memory(#heap{memory=N}=H) ->
ets:info(H#heap.id, memory) >= N.
% is_out_of_memory(#heap{memory=undefined}) ->
% false;
% is_out_of_memory(#heap{memory=N}=H) ->
% ets:info(H#heap.id, memory) >= N.

144
src/cache_heap.erl Normal file
View File

@ -0,0 +1,144 @@
%%
%% Copyright 2012 Dmitry Kolesnikov, 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.
%%
%% @description
%% cache segmented heap
-module(cache_heap).
-export([
new/4
,size/1
,head/1
,tail/1
,refs/1
,slip/1
,drop/2
,purge/2
]).
%%
%% heap
-record(heap, {
type = set :: atom(), %% type of segment
ttl = undefined :: integer(), %% segment expire time
cardinality = undefined :: integer(), %% segment cardinality quota
memory = undefined :: integer(), %% segment memory quota
segments = [] :: [integer()] %% segment references
}).
%%
%% create new empty heap
-spec(new/4 :: (atom(), integer(), integer(), integer()) -> #heap{}).
new(Type, TTL, Cardinality, Memory) ->
init(#heap{
type = Type
,ttl = TTL
,cardinality = Cardinality
,memory = Memory
}).
%%
%% return size of heap (number of segments)
-spec(size/1 :: (#heap{}) -> integer()).
size(#heap{segments=List}) ->
length(List).
%%
%% return head
-spec(head/1 :: (#heap{}) -> {integer(), integer()}).
head(#heap{segments=[Head | _]}) ->
Head.
%%
%% return tail
-spec(tail/1 :: (#heap{}) -> [{integer(), integer()}]).
tail(#heap{segments=[_ | Tail]}) ->
Tail.
%%
%% return tail
-spec(refs/1 :: (#heap{}) -> [{integer(), integer()}]).
refs(#heap{segments=Refs}) ->
Refs.
%%
%% slip heap segments
-spec(slip/1 :: (#heap{}) -> #heap{}).
slip(#heap{}=Heap) ->
case is_expired(cache_util:now(), Heap) of
true ->
init(Heap);
false ->
Heap
end.
is_expired(Time, #heap{cardinality=C, memory=M, segments=[{Expire, Ref}|_]}) ->
Time >= Expire orelse ets:info(Ref, size) >= C orelse ets:info(Ref, memory) >= M.
%%
%% drop last segment
-spec(drop/2 :: (#heap{}, pid()) -> #heap{}).
drop(#heap{segments=Segments}=Heap, Heir) ->
[{_, Ref}|T] = lists:reverse(Segments),
free(Heir, Ref),
Heap#heap{
segments = lists:reverse(T)
}.
%%
%% purge cache segments
-spec(purge/2 :: (#heap{}, pid()) -> #heap{}).
purge(#heap{segments=Segments}=Heap, Heir) ->
lists:foreach(fun({_, Ref}) -> free(Heir, Ref) end, Segments),
init(Heap#heap{segments=[]}).
%%
%% create heap segment
init(#heap{segments=Segments}=Heap) ->
Ref = ets:new(undefined, [Heap#heap.type, protected]),
Expire = cache_util:madd(Heap#heap.ttl, cache_util:now()),
Heap#heap{segments = [{Expire, Ref} | Segments]}.
%%
%% destroy heap segment
free(undefined, Ref) ->
ets:delete(Ref);
free(Heir, Ref)
when is_pid(Heir) ->
case erlang:is_process_alive(Heir) of
true ->
ets:give_away(Ref, Heir, evicted);
false ->
ets:delete(Ref)
end;
free(Heir, Ref)
when is_atom(Heir) ->
case erlang:whereis(Heir) of
undefined ->
ets:delete(Ref);
Pid ->
ets:give_away(Ref, Pid, evicted)
end.

View File

@ -18,7 +18,7 @@
-module(cache_util).
-export([
mdiv/2, madd/2, mmul/2, now/0, stats/2, stats/3, timeout/2
mdiv/2, madd/2, mmul/2, now/0, stats/2, stats/3
]).
%%
@ -69,17 +69,4 @@ stats(Fun, Counter, Val)
when is_function(Fun) ->
Fun(Counter, Val).
%%
%% set / reset timeout
timeout(T, Msg)
when is_integer(T) ->
{clock, T, erlang:send_after(T, self(), Msg)};
timeout({clock, T, Timer}, Msg) ->
erlang:cancel_timer(Timer),
{clock, T, erlang:send_after(T, self(), Msg)};
timeout(X, _) ->
X.

View File

@ -1,3 +1,20 @@
%%
%% Copyright 2012 Dmitry Kolesnikov, 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.
%%
%% @description
%% cache unit test
-module(cache_tests).
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
-include_lib("eunit/include/eunit.hrl").
@ -5,7 +22,7 @@
-define(CACHE, [
{ttl, 3}, %% time-to-live 3 sec
{n, 3}, %% 3 cells
{evict, 1} %% evict 1 sec
{check, 1} %% check eviction status 1 sec
]).
lru_test_() ->