mirror of
https://github.com/valitydev/cache.git
synced 2024-11-06 01:45:19 +00:00
extra function calls to lookup data and meta-data
This commit is contained in:
parent
cfdba43817
commit
dfcf699149
@ -17,7 +17,7 @@
|
||||
%,{remove, 1}
|
||||
]}.
|
||||
|
||||
{cache, {mycache, 'cache@127.0.0.1'}}.
|
||||
%{cache, {mycache, 'cache@127.0.0.1'}}.
|
||||
{local, [
|
||||
{ttl, 20}
|
||||
,{n, 10}
|
||||
|
@ -1,2 +1,3 @@
|
||||
{cover_enabled, true}.
|
||||
|
||||
{deps, []}.
|
||||
|
103
src/cache.erl
103
src/cache.erl
@ -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).
|
||||
-include("cache.hrl").
|
||||
|
||||
-export([
|
||||
start_link/2, drop/1, i/1,
|
||||
put/3, put/4, put_/3, put_/4,
|
||||
get/2, has/2,
|
||||
start_link/2,
|
||||
drop/1,
|
||||
i/1,
|
||||
heap/2,
|
||||
put/3, put/4, put_/3, put_/4,
|
||||
get/2, lookup/2,
|
||||
has/2, ttl/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) ->
|
||||
cache_bucket:start_link(Cache, Opts).
|
||||
|
||||
%%
|
||||
%%
|
||||
%% drop cache
|
||||
-spec(drop/1 :: (atom()) -> ok).
|
||||
|
||||
drop(Cache) ->
|
||||
erlang:exit(whereis(Cache), shutdown).
|
||||
|
||||
%%
|
||||
%%
|
||||
%% return cache meta data
|
||||
-spec(i/1 :: (atom()) -> list()).
|
||||
|
||||
i(Cache) ->
|
||||
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) ->
|
||||
gen_server:call(Cache, {put, Key, Val}).
|
||||
gen_server:call(Cache, {put, Key, Val}, ?DEF_IO_TIMEOUT).
|
||||
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) ->
|
||||
gen_server:cast(Cache, {put, Key, Val}).
|
||||
|
||||
put_(Cache, 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) ->
|
||||
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) ->
|
||||
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) ->
|
||||
gen_server:call(Cache, {remove, Key}).
|
||||
gen_server:call(Cache, {remove, Key}, ?DEF_IO_TIMEOUT).
|
||||
|
||||
%%
|
||||
%%
|
||||
remove_(Cache, Key) ->
|
||||
gen_server:cast(Cache, {remove, Key}).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -17,3 +17,5 @@
|
||||
|
||||
%% default cache house keeping frequency
|
||||
-define(DEF_CACHE_QUOTA, 5).
|
||||
|
||||
-define(DEF_IO_TIMEOUT,60000).
|
@ -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).
|
||||
-behaviour(application).
|
||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||
|
@ -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
|
||||
%% basho_bench driver
|
||||
-module(cache_benchmark).
|
||||
|
@ -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).
|
||||
-behaviour(gen_server).
|
||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||
@ -12,18 +31,27 @@
|
||||
%%
|
||||
%%
|
||||
-record(cache, {
|
||||
name :: atom(), %% name of cache bucket
|
||||
heap :: list(), %% heap
|
||||
name :: atom(), %% name of cache bucket
|
||||
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
|
||||
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)
|
||||
quota_size :: integer(), %% max number of elements
|
||||
quota_memory :: integer(), %% max number of memory in bytes
|
||||
cardinality :: integer(), %% cache cardinality
|
||||
memory :: integer(), %% cache memory limit
|
||||
|
||||
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([{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(Opts, S#cache{quota_size=X});
|
||||
init([{size, X} | Opts], S) ->
|
||||
init(Opts, S#cache{cardinality=X});
|
||||
|
||||
init([{n, X} | Opts], S) ->
|
||||
init(Opts, S#cache{n = X});
|
||||
@ -55,24 +83,26 @@ init([{ttl, X} | Opts], S) ->
|
||||
init([{quota, X} | Opts], S) ->
|
||||
init(Opts, S#cache{quota=X});
|
||||
|
||||
init([{counter, X} | Opts], S) ->
|
||||
init(Opts, S#cache{counter=X});
|
||||
init([{stats, X} | Opts], S) ->
|
||||
init(Opts, S#cache{stats=X});
|
||||
|
||||
init([_ | Opts], S) ->
|
||||
init(Opts, S);
|
||||
|
||||
init([], S) ->
|
||||
random:seed(erlang:now()),
|
||||
set_quota_timeout(S),
|
||||
set_evict_timeout(S),
|
||||
S#cache{
|
||||
heap = cache_heap:new(S#cache.n)
|
||||
}.
|
||||
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)
|
||||
}
|
||||
).
|
||||
|
||||
%%
|
||||
%%
|
||||
terminate(_Reason, S) ->
|
||||
cache_heap:free(S#cache.heap),
|
||||
terminate(_Reason, _S) ->
|
||||
%cache_heap:free(S#cache.heap),
|
||||
ok.
|
||||
|
||||
%%%----------------------------------------------------------------------------
|
||||
@ -82,35 +112,40 @@ terminate(_Reason, 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) ->
|
||||
{reply, ok, insert(Key, Val, S)};
|
||||
handle_call({put, Key, Val, TTL}, _, S) ->
|
||||
{reply, ok, cache_put(Key, Val, cache_util:now() + TTL, S)};
|
||||
|
||||
handle_call({get, Key}, _, S) ->
|
||||
case lookup(Key, S) of
|
||||
undefined ->
|
||||
inc_counter(miss, S),
|
||||
{reply, undefined, S};
|
||||
Val ->
|
||||
inc_counter(hit, S),
|
||||
{reply, Val, S}
|
||||
end;
|
||||
{reply, cache_get(Key, S), S};
|
||||
|
||||
handle_call({lookup, Key}, _, S) ->
|
||||
{reply, cache_lookup(Key, S), S};
|
||||
|
||||
handle_call({has, Key}, _, S) ->
|
||||
case member(Key, S) of
|
||||
true -> {reply, true, S};
|
||||
_ -> {reply, false, S}
|
||||
end;
|
||||
{reply, cache_has(Key, S), S};
|
||||
|
||||
handle_call({ttl, Key}, _, S) ->
|
||||
{reply, cache_ttl(Key, S), S};
|
||||
|
||||
handle_call({remove, Key}, _, S) ->
|
||||
{reply, ok, remove(Key, S)};
|
||||
{reply, ok, cache_remove(Key, S)};
|
||||
|
||||
handle_call(i, _, S) ->
|
||||
Cells = cache_heap:cells(S#cache.heap),
|
||||
Size = cache_heap:size(S#cache.heap),
|
||||
Memory = cache_heap:memory(S#cache.heap),
|
||||
{reply, [{heap, Cells}, {size, Size}, {memory, Memory}], 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({heap, N}, _, S) ->
|
||||
try
|
||||
H = lists:nth(N, S#cache.heap),
|
||||
{reply, H#heap.id, S}
|
||||
catch _:_ ->
|
||||
{reply, badarg, S}
|
||||
end;
|
||||
|
||||
handle_call(_, _, S) ->
|
||||
{noreply, S}.
|
||||
@ -118,24 +153,58 @@ handle_call(_, _, S) ->
|
||||
%%
|
||||
%%
|
||||
handle_cast({put, Key, Val}, S) ->
|
||||
{noreply, insert(Key, Val, S)};
|
||||
{noreply, cache_put(Key, Val, S)};
|
||||
|
||||
handle_cast({put, Key, Val, _}, S) ->
|
||||
{noreply, insert(Key, Val, S)};
|
||||
handle_cast({put, Key, Val, TTL}, S) ->
|
||||
{noreply, cache_put(Key, Val, cache_util:now() + TTL, S)};
|
||||
|
||||
handle_cast({remove, Key}, S) ->
|
||||
{noreply, remove(Key, S)};
|
||||
{noreply, cache_remove(Key, S)};
|
||||
|
||||
handle_cast(_, S) ->
|
||||
{noreply, S}.
|
||||
|
||||
handle_info(evict, S) ->
|
||||
set_evict_timeout(S),
|
||||
{noreply, 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) ->
|
||||
set_quota_timeout(S),
|
||||
{noreply, 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.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) ->
|
||||
{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
|
||||
evict_key(Key, List) ->
|
||||
dowhile(
|
||||
fun(Cell) ->
|
||||
case ets:member(Cell, Key) of
|
||||
true ->
|
||||
ets:delete(Cell, Key),
|
||||
Cell;
|
||||
Result ->
|
||||
Result
|
||||
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]),
|
||||
%% insert value to cache
|
||||
cache_put(Key, Val, #cache{}=S) ->
|
||||
[Head | Tail] = S#cache.heap,
|
||||
true = ets:insert(Head#heap.id, {Key, Val}),
|
||||
lists:foreach(
|
||||
fun(X) -> ets:delete(X#heap.id, Key) end,
|
||||
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, Head#heap.id]),
|
||||
S.
|
||||
|
||||
%%
|
||||
%%
|
||||
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.
|
||||
cache_put(Key, Val, Time, #cache{}=S) ->
|
||||
case lists:splitwith(fun(X) -> X#heap.expire > Time end, S#cache.heap) of
|
||||
{[], _Tail} ->
|
||||
cache_put(Key, Val, S);
|
||||
{Head, Tail} ->
|
||||
[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.
|
||||
|
||||
%%
|
||||
%%
|
||||
member(Key, S) ->
|
||||
dowhile(
|
||||
fun(X) -> ets:member(X, Key) end,
|
||||
cache_heap:cells(S#cache.heap)
|
||||
).
|
||||
|
||||
%%
|
||||
%%
|
||||
lookup(Key, #cache{policy=mru}=S) ->
|
||||
cache_get(Key, #cache{policy=mru}=S) ->
|
||||
% cache always evicts last generation
|
||||
% MRU caches do not prolong recently used entity
|
||||
dowhile(
|
||||
fun(Cell) ->
|
||||
case ets:lookup(Cell, Key) of
|
||||
[] ->
|
||||
false;
|
||||
[{_, Val}] ->
|
||||
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Cell]),
|
||||
Val
|
||||
end
|
||||
end,
|
||||
lists:reverse(cache_heap:cells(S#cache.heap))
|
||||
);
|
||||
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;
|
||||
|
||||
lookup(Key, S) ->
|
||||
[Head | Tail] = cache_heap:cells(S#cache.heap),
|
||||
case ets:lookup(Head, Key) of
|
||||
% no value at head chunk, lookup and evict tail
|
||||
[] ->
|
||||
dowhile(
|
||||
fun(Cell) ->
|
||||
case ets:lookup(Cell, Key) of
|
||||
[] ->
|
||||
false;
|
||||
[{_, Val}] ->
|
||||
?DEBUG("cache ~p: get ~p at cell ~p~n", [S#cache.name, Key, Cell]),
|
||||
_ = ets:delete(Cell, Key),
|
||||
true = ets:insert(Head, {Key, Val}),
|
||||
Val
|
||||
end
|
||||
end,
|
||||
Tail
|
||||
);
|
||||
[{_, Val}] ->
|
||||
?DEBUG("cache ~p: get ~p in cell ~p~n", [S#cache.name, Key, Head]),
|
||||
cache_get(Key, #cache{}=S) ->
|
||||
Head = hd(S#cache.heap),
|
||||
case heap_lookup(Key, 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]),
|
||||
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]),
|
||||
cache_util:stats(S#cache.stats, {cache, S#cache.name, hit}),
|
||||
Val
|
||||
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]) ->
|
||||
case Pred(Head) of
|
||||
% predicate false, apply predicate to next chunk
|
||||
false -> dowhile(Pred, Tail);
|
||||
Result -> Result
|
||||
%%
|
||||
heap_lookup(Key, [H | Tail]) ->
|
||||
case ets:lookup(H#heap.id, Key) of
|
||||
[] -> heap_lookup(Key, Tail);
|
||||
[{_, Val}] -> {H, Val}
|
||||
end;
|
||||
|
||||
dowhile(_Pred, []) ->
|
||||
heap_lookup(_Key, []) ->
|
||||
undefined.
|
||||
|
||||
%%
|
||||
%%
|
||||
evict(#cache{}=S) ->
|
||||
Cell = cache_heap:last(S#cache.heap),
|
||||
?DEBUG("cache ~p: free cell ~p~n", [S#cache.name, Cell]),
|
||||
heap_has(Key, [H | Tail]) ->
|
||||
case ets:member(H#heap.id, Key) of
|
||||
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{
|
||||
heap = cache_heap:alloc(cache_heap:free(Cell, S#cache.heap))
|
||||
heap = [Heap | S#cache.heap]
|
||||
}.
|
||||
|
||||
%%
|
||||
%%
|
||||
quota(#cache{}=S) ->
|
||||
maybe_memory_quota(
|
||||
maybe_size_quota(S)
|
||||
free_heap(#cache{}=S) ->
|
||||
[H | Tail] = lists:reverse(S#cache.heap),
|
||||
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.
|
||||
|
@ -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).
|
@ -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).
|
||||
-behaviour(supervisor).
|
||||
|
||||
|
88
src/cache_util.erl
Normal file
88
src/cache_util.erl
Normal 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.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user