extra function calls to lookup data and meta-data

This commit is contained in:
Dmitry Kolesnikov 2013-04-26 23:24:05 +03:00
parent cfdba43817
commit dfcf699149
10 changed files with 508 additions and 282 deletions

View File

@ -17,7 +17,7 @@
%,{remove, 1} %,{remove, 1}
]}. ]}.
{cache, {mycache, 'cache@127.0.0.1'}}. %{cache, {mycache, 'cache@127.0.0.1'}}.
{local, [ {local, [
{ttl, 20} {ttl, 20}
,{n, 10} ,{n, 10}

View File

@ -1,2 +1,3 @@
{cover_enabled, true}. {cover_enabled, true}.
{deps, []}.

View File

@ -1,57 +1,130 @@
%%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
%% segmented in-memory cache, cache memory is split into N segments,
%% each segment is associated with quota and ttl, eviction policy applied
%% at segment level.
-module(cache). -module(cache).
-include("cache.hrl").
-export([ -export([
start_link/2, drop/1, i/1, start_link/2,
put/3, put/4, put_/3, put_/4, drop/1,
get/2, has/2, i/1,
heap/2,
put/3, put/4, put_/3, put_/4,
get/2, lookup/2,
has/2, ttl/2,
remove/2, remove_/2 remove/2, remove_/2
]). ]).
-type(key() :: any()).
-type(entity() :: any()).
%% %%
%% %% start new cache
-spec(start_link/2 :: (atom(), list()) -> {ok, pid()} | {error, any()}).
start_link(Cache, Opts) -> start_link(Cache, Opts) ->
cache_bucket:start_link(Cache, Opts). cache_bucket:start_link(Cache, Opts).
%% %%
%% %% drop cache
-spec(drop/1 :: (atom()) -> ok).
drop(Cache) -> drop(Cache) ->
erlang:exit(whereis(Cache), shutdown). erlang:exit(whereis(Cache), shutdown).
%% %%
%% %% return cache meta data
-spec(i/1 :: (atom()) -> list()).
i(Cache) -> i(Cache) ->
gen_server:call(Cache, i). gen_server:call(Cache, i).
%% %%
%% return nth cache heap
-spec(heap/2 :: (atom(), integer()) -> integer() | badarg).
heap(Cache, N) ->
gen_server:call(Cache, {heap, N}).
%% %%
%% put entity to cache
-spec(put/3 :: (atom(), key(), entity()) -> ok).
-spec(put_/3 :: (atom(), key(), entity()) -> ok).
-spec(put/4 :: (atom(), key(), entity(), integer()) -> ok).
-spec(put_/4 :: (atom(), key(), entity(), integer()) -> ok).
put(Cache, Key, Val) -> put(Cache, Key, Val) ->
gen_server:call(Cache, {put, Key, Val}). gen_server:call(Cache, {put, Key, Val}, ?DEF_IO_TIMEOUT).
put(Cache, Key, Val, TTL) -> put(Cache, Key, Val, TTL) ->
gen_server:call(Cache, {put, Key, Val, TTL}). gen_server:call(Cache, {put, Key, Val, TTL}, ?DEF_IO_TIMEOUT).
put_(Cache, Key, Val) -> put_(Cache, Key, Val) ->
gen_server:cast(Cache, {put, Key, Val}). gen_server:cast(Cache, {put, Key, Val}).
put_(Cache, Key, Val, TTL) -> put_(Cache, Key, Val, TTL) ->
gen_server:cast(Cache, {put, Key, Val, TTL}). gen_server:cast(Cache, {put, Key, Val, TTL}).
%% %%
%% %% get entity from cache, prolong ttl
-spec(get/2 :: (atom(), key()) -> entity() | undefined).
get(Cache, Key) -> get(Cache, Key) ->
gen_server:call(Cache, {get, Key}). gen_server:call(Cache, {get, Key}, ?DEF_IO_TIMEOUT).
%% %%
%% lookup entity at cache do not prolong ttl
-spec(lookup/2 :: (atom(), key()) -> entity() | undefined).
lookup(Cache, Key) ->
gen_server:call(Cache, {lookup, Key}, ?DEF_IO_TIMEOUT).
%% %%
%% check entity at cache
-spec(has/2 :: (atom(), key()) -> true | false).
has(Cache, Key) -> has(Cache, Key) ->
gen_server:call(Cache, {has, Key}). gen_server:call(Cache, {has, Key}, ?DEF_IO_TIMEOUT).
%% %%
%% check entity at cache and return estimated ttl
-spec(ttl/2 :: (atom(), key()) -> true | false).
ttl(Cache, Key) ->
gen_server:call(Cache, {ttl, Key}, ?DEF_IO_TIMEOUT).
%% %%
%% remove entity from cache
-spec(remove/2 :: (atom(), key()) -> ok).
-spec(remove_/2 :: (atom(), key()) -> ok).
remove(Cache, Key) -> remove(Cache, Key) ->
gen_server:call(Cache, {remove, Key}). gen_server:call(Cache, {remove, Key}, ?DEF_IO_TIMEOUT).
%%
%%
remove_(Cache, Key) -> remove_(Cache, Key) ->
gen_server:cast(Cache, {remove, Key}). gen_server:cast(Cache, {remove, Key}).

View File

@ -17,3 +17,5 @@
%% default cache house keeping frequency %% default cache house keeping frequency
-define(DEF_CACHE_QUOTA, 5). -define(DEF_CACHE_QUOTA, 5).
-define(DEF_IO_TIMEOUT,60000).

View File

@ -1,4 +1,22 @@
%% %%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
-module(cache_app). -module(cache_app).
-behaviour(application). -behaviour(application).
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>'). -author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').

View File

@ -1,3 +1,21 @@
%%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description %% @description
%% basho_bench driver %% basho_bench driver
-module(cache_benchmark). -module(cache_benchmark).

View File

@ -1,4 +1,23 @@
%% %%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
%%
-module(cache_bucket). -module(cache_bucket).
-behaviour(gen_server). -behaviour(gen_server).
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>'). -author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
@ -12,18 +31,27 @@
%% %%
%% %%
-record(cache, { -record(cache, {
name :: atom(), %% name of cache bucket name :: atom(), %% name of cache bucket
heap :: list(), %% heap heap = [] :: list(), %% cache heap segments
n = ?DEF_CACHE_N :: integer(), %% number of cells n = ?DEF_CACHE_N :: integer(), %% number of segments
ttl = ?DEF_CACHE_TTL :: integer(), %% chunk time to live ttl = ?DEF_CACHE_TTL :: integer(), %% chunk time to live
policy = ?DEF_CACHE_POLICY :: integer(), %% eviction policy policy = ?DEF_CACHE_POLICY :: integer(), %% eviction policy
quota = ?DEF_CACHE_QUOTA :: integer(), %% frequency of limit enforcement (clean-up) cardinality :: integer(), %% cache cardinality
quota_size :: integer(), %% max number of elements memory :: integer(), %% cache memory limit
quota_memory :: integer(), %% max number of memory in bytes
counter :: any() %% counter functor quota = ?DEF_CACHE_QUOTA :: integer(), %% quota enforcement timer
evict = undefined, %% evict timer
stats :: any() %% stats functor
}).
-record(heap, {
id :: integer(), %% segment heap
expire :: integer(), %% segment expire time
cardinality :: integer(), %% segment cardinality quota
memory :: integer() %% segment memory quota
}). }).
%% %%
@ -41,10 +69,10 @@ init([{policy, X} | T], #cache{}=S) ->
init(T, S#cache{policy=X}); init(T, S#cache{policy=X});
init([{memory, X} | Opts], S) -> init([{memory, X} | Opts], S) ->
init(Opts, S#cache{quota_memory=X}); init(Opts, S#cache{memory=X div erlang:sysinfo(wordsize)});
init([{size, X} | Opts], S) -> init([{size, X} | Opts], S) ->
init(Opts, S#cache{quota_size=X}); init(Opts, S#cache{cardinality=X});
init([{n, X} | Opts], S) -> init([{n, X} | Opts], S) ->
init(Opts, S#cache{n = X}); init(Opts, S#cache{n = X});
@ -55,24 +83,26 @@ init([{ttl, X} | Opts], S) ->
init([{quota, X} | Opts], S) -> init([{quota, X} | Opts], S) ->
init(Opts, S#cache{quota=X}); init(Opts, S#cache{quota=X});
init([{counter, X} | Opts], S) -> init([{stats, X} | Opts], S) ->
init(Opts, S#cache{counter=X}); init(Opts, S#cache{stats=X});
init([_ | Opts], S) -> init([_ | Opts], S) ->
init(Opts, S); init(Opts, S);
init([], S) -> init([], S) ->
random:seed(erlang:now()), random:seed(erlang:now()),
set_quota_timeout(S), Evict = cache_util:mdiv(S#cache.ttl, S#cache.n),
set_evict_timeout(S), init_heap(
S#cache{ S#cache{
heap = cache_heap:new(S#cache.n) evict = cache_util:timeout(Evict * 1000, evict),
}. quota = cache_util:timeout(S#cache.quota * 1000, quota)
}
).
%% %%
%% %%
terminate(_Reason, S) -> terminate(_Reason, _S) ->
cache_heap:free(S#cache.heap), %cache_heap:free(S#cache.heap),
ok. ok.
%%%---------------------------------------------------------------------------- %%%----------------------------------------------------------------------------
@ -82,35 +112,40 @@ terminate(_Reason, S) ->
%%%---------------------------------------------------------------------------- %%%----------------------------------------------------------------------------
handle_call({put, Key, Val}, _, S) -> handle_call({put, Key, Val}, _, S) ->
{reply, ok, insert(Key, Val, S)}; {reply, ok, cache_put(Key, Val, S)};
handle_call({put, Key, Val, _}, _, S) -> handle_call({put, Key, Val, TTL}, _, S) ->
{reply, ok, insert(Key, Val, S)}; {reply, ok, cache_put(Key, Val, cache_util:now() + TTL, S)};
handle_call({get, Key}, _, S) -> handle_call({get, Key}, _, S) ->
case lookup(Key, S) of {reply, cache_get(Key, S), S};
undefined ->
inc_counter(miss, S), handle_call({lookup, Key}, _, S) ->
{reply, undefined, S}; {reply, cache_lookup(Key, S), S};
Val ->
inc_counter(hit, S),
{reply, Val, S}
end;
handle_call({has, Key}, _, S) -> handle_call({has, Key}, _, S) ->
case member(Key, S) of {reply, cache_has(Key, S), S};
true -> {reply, true, S};
_ -> {reply, false, S} handle_call({ttl, Key}, _, S) ->
end; {reply, cache_ttl(Key, S), S};
handle_call({remove, Key}, _, S) -> handle_call({remove, Key}, _, S) ->
{reply, ok, remove(Key, S)}; {reply, ok, cache_remove(Key, S)};
handle_call(i, _, S) -> handle_call(i, _, S) ->
Cells = cache_heap:cells(S#cache.heap), Heap = [X#heap.id || X <- S#cache.heap],
Size = cache_heap:size(S#cache.heap), Expire = [X#heap.expire || X <- S#cache.heap],
Memory = cache_heap:memory(S#cache.heap), Size = [ets:info(X#heap.id, size) || X <- S#cache.heap],
{reply, [{heap, Cells}, {size, Size}, {memory, Memory}], S}; Memory = [ets:info(X#heap.id, memory) || X <- S#cache.heap],
{reply, [{heap, Heap}, {expire, Expire}, {size, Size}, {memory, Memory}], S};
handle_call({heap, N}, _, S) ->
try
H = lists:nth(N, S#cache.heap),
{reply, H#heap.id, S}
catch _:_ ->
{reply, badarg, S}
end;
handle_call(_, _, S) -> handle_call(_, _, S) ->
{noreply, S}. {noreply, S}.
@ -118,24 +153,58 @@ handle_call(_, _, S) ->
%% %%
%% %%
handle_cast({put, Key, Val}, S) -> handle_cast({put, Key, Val}, S) ->
{noreply, insert(Key, Val, S)}; {noreply, cache_put(Key, Val, S)};
handle_cast({put, Key, Val, _}, S) -> handle_cast({put, Key, Val, TTL}, S) ->
{noreply, insert(Key, Val, S)}; {noreply, cache_put(Key, Val, cache_util:now() + TTL, S)};
handle_cast({remove, Key}, S) -> handle_cast({remove, Key}, S) ->
{noreply, remove(Key, S)}; {noreply, cache_remove(Key, S)};
handle_cast(_, S) -> handle_cast(_, S) ->
{noreply, S}. {noreply, S}.
handle_info(evict, S) -> handle_info(evict, S) ->
set_evict_timeout(S), Now = cache_util:now(),
{noreply, evict(S)}; 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) -> handle_info(quota, S) ->
set_quota_timeout(S), case is_heap_out_of_quota(hd(S#cache.heap)) of
{noreply, quota(S)}; true ->
case length(S#cache.heap) of
N when N =:= S#cache.n ->
{noreply,
free_heap(
S#cache{quota = cache_util:timeout(S#cache.evict, quota)}
)
};
_ ->
{noreply,
init_heap(
S#cache{quota = cache_util:timeout(S#cache.evict, quota)}
)
}
end;
false ->
{noreply,
S#cache{
quota = cache_util:timeout(S#cache.quota, quota)
}
}
end;
handle_info(_, S) -> handle_info(_, S) ->
{noreply, S}. {noreply, S}.
@ -152,160 +221,184 @@ code_change(_Vsn, S, _Extra) ->
%%% %%%
%%%---------------------------------------------------------------------------- %%%----------------------------------------------------------------------------
%% set-up cache evict timeout
set_evict_timeout(#cache{ttl=undefined}) ->
ok;
set_evict_timeout(S) ->
T = (S#cache.ttl div S#cache.n) * 1000,
erlang:send_after(T, self(), evict).
set_quota_timeout(#cache{quota=undefined}) ->
ok;
set_quota_timeout(S) ->
T = S#cache.quota * 1000,
erlang:send_after(T, self(), quota).
%% update statistic
inc_counter(_Id, #cache{counter=undefined}) ->
ok;
inc_counter(Id, #cache{counter={M, F}, name=Name}) ->
M:F({cache, Name, Id});
inc_counter(Id, #cache{counter=Fun, name=Name}) ->
Fun({cache, Name, Id}).
%% %%
%% evict key from cells %% insert value to cache
evict_key(Key, List) -> cache_put(Key, Val, #cache{}=S) ->
dowhile( [Head | Tail] = S#cache.heap,
fun(Cell) -> true = ets:insert(Head#heap.id, {Key, Val}),
case ets:member(Cell, Key) of lists:foreach(
true -> fun(X) -> ets:delete(X#heap.id, Key) end,
ets:delete(Cell, Key), Tail
Cell; ),
Result -> cache_util:stats(S#cache.stats, {cache, S#cache.name, put}),
Result ?DEBUG("cache ~p: put ~p to heap ~p~n", [S#cache.name, Key, Head#heap.id]),
end
end,
List
).
%%
%%
insert(Key, Val, #cache{}=S) ->
[Head | Tail] = cache_heap:cells(S#cache.heap),
true = ets:insert(Head, {Key, Val}),
_ = evict_key(Key, Tail),
inc_counter(put, S),
?DEBUG("cache ~p: put ~p to cell ~p~n", [S#cache.name, Key, Head]),
S. S.
%% cache_put(Key, Val, Time, #cache{}=S) ->
%% case lists:splitwith(fun(X) -> X#heap.expire > Time end, S#cache.heap) of
remove(Key, S) -> {[], _Tail} ->
Cell = evict_key(Key, cache_heap:cells(S#cache.heap)), cache_put(Key, Val, S);
?DEBUG("cache ~p: remove ~p in cell ~p~n", [S#cache.name, Key, Cell]), {Head, Tail} ->
S. [Heap | Rest] = lists:reverse(Head),
true = ets:insert(Heap#heap.id, {Key, Val}),
lists:foreach(
fun(X) -> ets:delete(X#heap.id, 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
end.
%% cache_get(Key, #cache{policy=mru}=S) ->
%%
member(Key, S) ->
dowhile(
fun(X) -> ets:member(X, Key) end,
cache_heap:cells(S#cache.heap)
).
%%
%%
lookup(Key, #cache{policy=mru}=S) ->
% cache always evicts last generation % cache always evicts last generation
% MRU caches do not prolong recently used entity % MRU caches do not prolong recently used entity
dowhile( case heap_lookup(Key, S#cache.heap) of
fun(Cell) -> undefined ->
case ets:lookup(Cell, Key) of cache_util:stats(S#cache.stats, {cache, S#cache.name, miss}),
[] -> undefined;
false; {Heap, Val} ->
[{_, Val}] -> ?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Cell]), cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
Val Val
end end;
end,
lists:reverse(cache_heap:cells(S#cache.heap))
);
lookup(Key, S) -> cache_get(Key, #cache{}=S) ->
[Head | Tail] = cache_heap:cells(S#cache.heap), Head = hd(S#cache.heap),
case ets:lookup(Head, Key) of case heap_lookup(Key, S#cache.heap) of
% no value at head chunk, lookup and evict tail undefined ->
[] -> cache_util:stats(S#cache.stats, {cache, S#cache.name, miss}),
dowhile( undefined;
fun(Cell) -> {Heap, Val} when Heap#heap.id =:= Head#heap.id ->
case ets:lookup(Cell, Key) of ?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
[] -> cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
false; Val;
[{_, Val}] -> {Heap, Val} ->
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Cell]), true = ets:insert(Head#heap.id, {Key, Val}),
_ = ets:delete(Cell, Key), _ = ets:delete(Heap#heap.id, Key),
true = ets:insert(Head, {Key, Val}), ?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
Val cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
end
end,
Tail
);
[{_, Val}] ->
?DEBUG("cache ~p: get ~p in cell ~p~n", [S#cache.name, Key, Head]),
Val Val
end. end.
cache_lookup(Key, #cache{}=S) ->
case heap_lookup(Key, 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]),
cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
Val
end.
cache_has(Key, #cache{}=S) ->
case heap_has(Key, S#cache.heap) of
false ->
false;
Heap ->
?DEBUG("cache ~p: has ~p at cell ~p~n", [S#cache.name, Key, Heap#heap.id]),
true
end.
cache_ttl(Key, #cache{}=S) ->
case heap_has(Key, S#cache.heap) of
false ->
undefined;
Heap ->
Heap#heap.expire - cache_util:now()
end.
cache_remove(Key, #cache{}=S) ->
lists:foreach(
fun(X) -> ets:delete(X#heap.id, Key) end,
S#cache.heap
),
cache_util:stats(S#cache.stats, {cache, S#cache.name, remove}),
?DEBUG("cache ~p: remove ~p~n", [S#cache.name, Key]),
S.
%% %%
%% execute predicate while it succeeded %%
dowhile(Pred, [Head | Tail]) -> heap_lookup(Key, [H | Tail]) ->
case Pred(Head) of case ets:lookup(H#heap.id, Key) of
% predicate false, apply predicate to next chunk [] -> heap_lookup(Key, Tail);
false -> dowhile(Pred, Tail); [{_, Val}] -> {H, Val}
Result -> Result
end; end;
dowhile(_Pred, []) -> heap_lookup(_Key, []) ->
undefined. undefined.
%% %%
%% %%
evict(#cache{}=S) -> heap_has(Key, [H | Tail]) ->
Cell = cache_heap:last(S#cache.heap), case ets:member(H#heap.id, Key) of
?DEBUG("cache ~p: free cell ~p~n", [S#cache.name, Cell]), false -> heap_has(Key, Tail);
true -> H
end;
heap_has(_Key, []) ->
false.
% %%
% %%
% remove(Key, S) ->
% Cell = evict_key(Key, cache_heap:cells(S#cache.heap)),
% ?DEBUG("cache ~p: remove ~p in cell ~p~n", [S#cache.name, Key, Cell]),
% S.
% %%
% %%
% member(Key, S) ->
% dowhile(
% fun(X) -> ets:member(X, Key) end,
% cache_heap:cells(S#cache.heap)
% ).
%%
%% init cache heap
init_heap(#cache{}=S) ->
Id = ets:new(undefined, [set, 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{ S#cache{
heap = cache_heap:alloc(cache_heap:free(Cell, S#cache.heap)) heap = [Heap | S#cache.heap]
}. }.
%% %%
%% %%
quota(#cache{}=S) -> free_heap(#cache{}=S) ->
maybe_memory_quota( [H | Tail] = lists:reverse(S#cache.heap),
maybe_size_quota(S) Size = ets:info(H#heap.id, size),
ets:delete(H#heap.id),
cache_util:stats(S#cache.stats, {cache, S#cache.name, evicted}, Size),
?DEBUG("cache ~p: free heap ~p~n", [S#cache.name, H#heap.id]),
init_heap(
S#cache{
heap = lists:reverse(Tail)
}
). ).
%%
%%
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_memory(#heap{memory=undefined}) ->
false;
is_out_of_memory(#heap{memory=N}=H) ->
ets:info(H#heap.id, memory) >= N.
maybe_size_quota(#cache{quota_size=undefined}=S) ->
S;
maybe_size_quota(S) ->
Quota = S#cache.quota_size div S#cache.n,
[Head | _] = cache_heap:cells(S#cache.heap),
case ets:info(Head, size) of
Val when Val >= Quota ->
evict(S);
_ ->
S
end.
maybe_memory_quota(#cache{quota_memory=undefined}=S) ->
S;
maybe_memory_quota(S) ->
Quota = (S#cache.quota_memory div erlang:system_info(wordsize)) div S#cache.n,
[Head | _] = cache_heap:cells(S#cache.heap),
case ets:info(Head, memory) of
Val when Val >= Quota ->
evict(S);
_ ->
S
end.

View File

@ -1,86 +0,0 @@
%%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
%% heap of ETS entities (sorted by age)
-module(cache_heap).
-export([
new/1, alloc/1, free/1, free/2,
cells/1, size/1, memory/1, last/1, first/1
]).
-record(heap, {
cells :: list() %% list of cells
}).
%%
%% create new heap
new(N) ->
lists:foldl(
fun(_, Acc) -> alloc(Acc) end,
#heap{cells=[]},
lists:seq(1, N)
).
%%
%% allocate new cell
alloc(#heap{}=H) ->
H#heap{
cells = [create() | H#heap.cells]
}.
create() ->
ets:new(undefined, [set, protected]).
%%
%% free cells
free(#heap{cells=Cells}) ->
[ets:delete(X) || {_, X} <- Cells],
ok.
free(Cell, #heap{}=H) ->
ets:delete(Cell),
H#heap{
cells = lists:delete(Cell, H#heap.cells)
}.
%%
%%
cells(#heap{}=H) ->
H#heap.cells.
%%
%%
size(#heap{}=H) ->
[ets:info(X, size) || X <- H#heap.cells].
%%
%%
memory(#heap{}=H) ->
[ets:info(X, memory) || X <- H#heap.cells].
%%
%%
last(#heap{}=H) ->
lists:last(H#heap.cells).
%%
%%
first(#heap{}=H) ->
hd(H#heap.cells).

View File

@ -1,4 +1,23 @@
%% %%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
%% root cache supervisor
-module(cache_sup). -module(cache_sup).
-behaviour(supervisor). -behaviour(supervisor).

88
src/cache_util.erl Normal file
View File

@ -0,0 +1,88 @@
%%
%% Copyright (c) 2012, Dmitry Kolesnikov
%% All Rights Reserved.
%%
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License, version 3.0
%% as published by the Free Software Foundation (the "License").
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA or retrieve online http://www.opensource.org/licenses/lgpl-3.0.html
%%
%% @description
%%
-module(cache_util).
-export([
mdiv/2, madd/2, mmul/2, now/0, stats/2, stats/3, timeout/2
]).
%%
%%
mdiv(undefined, _) ->
undefined;
mdiv(X, Y) ->
X div Y.
%%
%%
madd(undefined, _) ->
undefined;
madd(X, Y) ->
X + Y.
%%
%%
mmul(undefined, _) ->
undefined;
mmul(X, Y) ->
X * Y.
%%
%%
now() ->
{Mega, Sec, _} = erlang:now(),
Mega * 1000000 + Sec.
%%
%%
stats(undefined, _) ->
ok;
stats({M, F}, Counter) ->
M:F(Counter);
stats(Fun, Counter)
when is_function(Fun) ->
Fun(Counter).
stats(undefined, _, _) ->
ok;
stats({M, F}, Counter, Val) ->
M:F(Counter, Val);
stats(Fun, Counter, Val)
when is_function(Fun) ->
Fun(Counter, Val).
%%
%%
%%
%% re-set alarm after time
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.