mirror of
https://github.com/valitydev/cache.git
synced 2024-11-06 01:45:19 +00:00
new cache design (based on multi-page idea)
This commit is contained in:
parent
20544f1bd1
commit
d324600f4b
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,5 +8,6 @@
|
||||
ebin/
|
||||
deps/
|
||||
.eunit/
|
||||
tests/
|
||||
rebar
|
||||
*.sublime-*
|
||||
|
7
Emakefile
Normal file
7
Emakefile
Normal file
@ -0,0 +1,7 @@
|
||||
{"src/*", [
|
||||
report,
|
||||
verbose,
|
||||
{i, "include"},
|
||||
{outdir, "ebin"},
|
||||
debug_info
|
||||
]}.
|
8
Makefile
8
Makefile
@ -1,5 +1,7 @@
|
||||
.PHONY: deps test
|
||||
|
||||
BB=../basho_bench
|
||||
|
||||
all: rebar deps compile
|
||||
|
||||
compile:
|
||||
@ -31,3 +33,9 @@ rebar:
|
||||
curl -O http://cloud.github.com/downloads/basho/rebar/rebar
|
||||
chmod ugo+x rebar
|
||||
|
||||
benchmark:
|
||||
$(BB)/basho_bench priv/cache.benchmark
|
||||
$(BB)/priv/summary.r -i tests/current
|
||||
open tests/current/summary.png
|
||||
|
||||
|
||||
|
26
priv/cache.benchmark
Normal file
26
priv/cache.benchmark
Normal file
@ -0,0 +1,26 @@
|
||||
{code_paths, ["./ebin"]}.
|
||||
{log_level, info}.
|
||||
{report_interval, 1}.
|
||||
{driver, cache_benchmark}.
|
||||
|
||||
%%
|
||||
%% workload
|
||||
{mode, max}.
|
||||
{duration, 1}.
|
||||
{concurrent, 10}.
|
||||
{key_generator, {uniform_int, 1000000}}.
|
||||
{value_generator, {fixed_bin, 1000}}.
|
||||
|
||||
{operations, [
|
||||
{put, 5}
|
||||
,{get, 5}
|
||||
%,{remove, 1}
|
||||
]}.
|
||||
|
||||
{cache, [
|
||||
{ttl, 20}
|
||||
,{n, 10}
|
||||
,{policy, lru}
|
||||
%,{size, 500000}
|
||||
%,{memory, 400000000}
|
||||
]}.
|
@ -1,7 +1,7 @@
|
||||
{application, cache,
|
||||
[
|
||||
{description, "in-memory cache"},
|
||||
{vsn, "0.1.0"},
|
||||
{vsn, "0.2.0"},
|
||||
{modules, [
|
||||
cache,
|
||||
cache_sup,
|
||||
|
@ -1,49 +1,50 @@
|
||||
-module(cache).
|
||||
-export([start/0]).
|
||||
-export([start_link/2, i/1, put/3, get/2, evict/1, stop/1]).
|
||||
|
||||
|
||||
start() ->
|
||||
AppFile = code:where_is_file(atom_to_list(?MODULE) ++ ".app"),
|
||||
{ok, [{application, _, List}]} = file:consult(AppFile),
|
||||
Apps = proplists:get_value(applications, List, []),
|
||||
lists:foreach(
|
||||
fun(X) ->
|
||||
ok = case application:start(X) of
|
||||
{error, {already_started, X}} -> ok;
|
||||
Ret -> Ret
|
||||
end
|
||||
end,
|
||||
Apps
|
||||
),
|
||||
application:start(?MODULE).
|
||||
|
||||
-export([
|
||||
start_link/2, drop/1, i/1,
|
||||
put/3, put_/3, get/2, has/2, remove/2, remove_/2
|
||||
]).
|
||||
|
||||
%%
|
||||
%%
|
||||
start_link(Name, Opts) ->
|
||||
cache_bucket:start_link(Name, Opts).
|
||||
start_link(Cache, Opts) ->
|
||||
cache_bucket:start_link(Cache, Opts).
|
||||
|
||||
%%
|
||||
%%
|
||||
drop(Cache) ->
|
||||
erlang:exit(whereis(Cache), shutdown).
|
||||
|
||||
%%
|
||||
%%
|
||||
i(Cache) ->
|
||||
cache_bucket:i(Cache).
|
||||
gen_server:call(Cache, i).
|
||||
|
||||
%%
|
||||
%%
|
||||
put(Cache, Key, Val) ->
|
||||
cache_bucket:put(Cache, Key, Val).
|
||||
gen_server:call(Cache, {put, Key, Val}).
|
||||
|
||||
put_(Cache, Key, Val) ->
|
||||
gen_server:cast(Cache, {put, Key, Val}).
|
||||
|
||||
%%
|
||||
%%
|
||||
get(Cache, Key) ->
|
||||
cache_bucket:get(Cache, Key).
|
||||
gen_server:call(Cache, {get, Key}).
|
||||
|
||||
%%
|
||||
%%
|
||||
evict(Cache) ->
|
||||
cache_bucket:evict(Cache).
|
||||
has(Cache, Key) ->
|
||||
gen_server:call(Cache, {has, Key}).
|
||||
|
||||
%%
|
||||
%%
|
||||
stop(Cache) ->
|
||||
cache_bucket:stop(Cache).
|
||||
remove(Cache, Key) ->
|
||||
gen_server:call(Cache, {remove, Key}).
|
||||
|
||||
%%
|
||||
%%
|
||||
remove_(Cache, Key) ->
|
||||
gen_server:cast(Cache, {remove, Key}).
|
||||
|
||||
|
19
src/cache.hrl
Normal file
19
src/cache.hrl
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
%-define(VERBOSE, true).
|
||||
-ifdef(VERBOSE).
|
||||
-define(DEBUG(Str, Args), error_logger:error_msg(Str, Args)).
|
||||
-else.
|
||||
-define(DEBUG(Str, Args), ok).
|
||||
-endif.
|
||||
|
||||
|
||||
%% default cache eviction policy
|
||||
-define(DEF_CACHE_POLICY, lru).
|
||||
|
||||
%% default cache ttl and number of generations
|
||||
-define(DEF_CACHE_TTL, 600).
|
||||
-define(DEF_CACHE_N, 10).
|
||||
|
||||
%% default cache house keeping frequency
|
||||
-define(DEF_CACHE_QUOTA, 5000).
|
66
src/cache_benchmark.erl
Normal file
66
src/cache_benchmark.erl
Normal file
@ -0,0 +1,66 @@
|
||||
%% @description
|
||||
%% basho_bench driver
|
||||
-module(cache_benchmark).
|
||||
|
||||
-export([
|
||||
new/1, run/4
|
||||
]).
|
||||
|
||||
%%
|
||||
%%
|
||||
new(_Id) ->
|
||||
try
|
||||
lager:set_loglevel(lager_console_backend, basho_bench_config:get(log_level, info)),
|
||||
init()
|
||||
catch _:Err ->
|
||||
error_logger:error_msg("cache failed: ~p", [Err]),
|
||||
halt(1)
|
||||
end,
|
||||
{ok, undefined}.
|
||||
|
||||
%%
|
||||
%%
|
||||
run(put, KeyGen, ValGen, S) ->
|
||||
Key = KeyGen(),
|
||||
case (catch cache:put(cache, Key, ValGen())) of
|
||||
ok -> {ok, S};
|
||||
E -> {error, failure(p, Key, E), S}
|
||||
end;
|
||||
|
||||
run(get, KeyGen, _ValueGen, S) ->
|
||||
Key = KeyGen(),
|
||||
case (catch cache:get(cache, Key)) of
|
||||
Val when is_binary(Val) -> {ok, S};
|
||||
undefined -> {ok, S};
|
||||
E -> {error, failure(g, Key, E), S}
|
||||
end;
|
||||
|
||||
run(remove, KeyGen, _ValueGen, S) ->
|
||||
Key = KeyGen(),
|
||||
case (catch cache:remove(cache, Key)) of
|
||||
ok -> {ok, S};
|
||||
E -> {error, failure(r, Key, E), S}
|
||||
end;
|
||||
|
||||
run(dump, _KeyGen, _ValueGen, S) ->
|
||||
error_logger:info_msg("cache: ~p", [cache:i(cache)]),
|
||||
{ok, S}.
|
||||
|
||||
|
||||
%%
|
||||
%%
|
||||
init() ->
|
||||
case application:start(cache) of
|
||||
{error, {already_started, _}} ->
|
||||
ok;
|
||||
ok ->
|
||||
Cache = basho_bench_config:get(cache, 30000),
|
||||
{ok, _} = cache:start_link(cache, Cache)
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
failure(Tag, _Key, E) ->
|
||||
%io:format("----> ~p~n", [process_info(pns:whereis(kv, Key))]),
|
||||
io:format("~s -> ~p~n", [Tag, E]),
|
||||
failed.
|
@ -2,97 +2,73 @@
|
||||
-module(cache_bucket).
|
||||
-behaviour(gen_server).
|
||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||
-include("cache.hrl").
|
||||
|
||||
-export([start_link/2]).
|
||||
-export([i/1, put/3, get/2, evict/1, stop/1]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(DEFAULT_POLICY, lru).
|
||||
-define(DEFAULT_TTL, 60000).
|
||||
-define(DEFAULT_EVICT, 10000).
|
||||
-define(DEFAULT_CHUNK, 100).
|
||||
-export([
|
||||
start_link/2,
|
||||
init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3
|
||||
]).
|
||||
|
||||
%%
|
||||
%%
|
||||
-record(cache, {
|
||||
policy, %%
|
||||
memory, %% memory threshold
|
||||
size, %% size threshold
|
||||
chunk, %% number of evicted elements
|
||||
name :: atom(), %% name of cache bucket
|
||||
heap :: list(), %% heap
|
||||
|
||||
ttl, %% cache element ttl
|
||||
evict, %% house keeping timer to evict cache elements
|
||||
elements,
|
||||
access,
|
||||
n = ?DEF_CACHE_N :: integer(), %% number of cells
|
||||
ttl = ?DEF_CACHE_TTL :: integer(), %% chunk time to live
|
||||
policy = ?DEF_CACHE_POLICY :: integer(), %% eviction policy
|
||||
evict :: integer(), %% age of heap cell (cell eviction frequency)
|
||||
|
||||
hit,
|
||||
miss
|
||||
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
|
||||
}).
|
||||
|
||||
%%
|
||||
%%
|
||||
start_link(Name, Opts) ->
|
||||
gen_server:start_link({local, Name}, ?MODULE, [Opts], []).
|
||||
gen_server:start_link({local, Name}, ?MODULE, [Name, Opts], []).
|
||||
|
||||
init([Opts]) ->
|
||||
init(Opts, #cache{
|
||||
evict = ?DEFAULT_EVICT,
|
||||
policy = ?DEFAULT_POLICY,
|
||||
chunk = ?DEFAULT_CHUNK,
|
||||
ttl = ?DEFAULT_TTL * 1000,
|
||||
hit = 0,
|
||||
miss = 0
|
||||
}).
|
||||
init([Name, Opts]) ->
|
||||
{ok, init(Opts, #cache{name=Name})}.
|
||||
|
||||
init([{policy, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{policy=X});
|
||||
|
||||
init([{memory, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{memory=X div erlang:system_info(wordsize)});
|
||||
init([{memory, X} | Opts], S) ->
|
||||
init(Opts, S#cache{quota_memory=X div erlang:system_info(wordsize)});
|
||||
|
||||
init([{size, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{size=X});
|
||||
init([{size, X} | Opts], S) ->
|
||||
init(Opts, S#cache{quota_size=X});
|
||||
|
||||
init([{chunk, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{chunk=X});
|
||||
init([{n, X} | Opts], S) ->
|
||||
init(Opts, S#cache{n = X});
|
||||
|
||||
init([{ttl, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{ttl=X * 1000});
|
||||
init([{ttl, X} | Opts], S) ->
|
||||
init(Opts, S#cache{ttl = X});
|
||||
|
||||
init([{evict, X} | T], #cache{}=S) ->
|
||||
init(T, S#cache{evict=X});
|
||||
init([{quota, X} | Opts], S) ->
|
||||
init(Opts, S#cache{quota=X * 1000});
|
||||
|
||||
init([], #cache{evict=Evict}=S) ->
|
||||
init([_ | Opts], S) ->
|
||||
init(Opts, S);
|
||||
|
||||
init([], S) ->
|
||||
random:seed(erlang:now()),
|
||||
Evict = (S#cache.ttl div S#cache.n) * 1000,
|
||||
erlang:send_after(S#cache.quota, self(), quota),
|
||||
erlang:send_after(Evict, self(), evict),
|
||||
{ok,
|
||||
S#cache{
|
||||
elements = ets:new(undefined, [set, private]),
|
||||
access = ets:new(undefined, [ordered_set, private])
|
||||
}
|
||||
S#cache{
|
||||
heap = cache_heap:new(S#cache.n),
|
||||
evict = Evict
|
||||
}.
|
||||
|
||||
%%%----------------------------------------------------------------------------
|
||||
%%%
|
||||
%%% api
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------------
|
||||
|
||||
%%
|
||||
%%
|
||||
i(Cache) ->
|
||||
gen_server:call(Cache, info).
|
||||
|
||||
put(Cache, Key, Val) ->
|
||||
gen_server:cast(Cache, {put, Key, Val}).
|
||||
|
||||
get(Cache, Key) ->
|
||||
gen_server:call(Cache, {get, Key}, infinity).
|
||||
|
||||
evict(Cache) ->
|
||||
gen_server:cast(Cache, evict).
|
||||
|
||||
stop(Cache) ->
|
||||
gen_server:call(Cache, stop, infinity).
|
||||
%%
|
||||
terminate(_Reason, S) ->
|
||||
cache_heap:free(S#cache.heap),
|
||||
ok.
|
||||
|
||||
%%%----------------------------------------------------------------------------
|
||||
%%%
|
||||
@ -100,72 +76,51 @@ stop(Cache) ->
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------------
|
||||
|
||||
%%
|
||||
%%
|
||||
handle_call(info, _Tx, #cache{elements=E, access=A, hit=Hit, miss=Miss}=S) ->
|
||||
Mem = ets:info(E, memory) + ets:info(A, memory),
|
||||
Size = ets:info(E, size),
|
||||
Info = [
|
||||
{memory, Mem},
|
||||
{size, Size},
|
||||
{hit, Hit},
|
||||
{miss, Miss}
|
||||
],
|
||||
{reply, {ok, Info}, S};
|
||||
handle_call({put, Key, Val}, _, S) ->
|
||||
{reply, ok, insert(Key, Val, S)};
|
||||
|
||||
handle_call({get, Key}, _Tx, #cache{ttl=TTL, elements=E, access=A, hit=Hit, miss=Miss}=S) ->
|
||||
Now = usec(),
|
||||
case ets:lookup(E, Key) of
|
||||
[] ->
|
||||
{reply, none, S#cache{miss=Miss + 1}};
|
||||
[{Key, _Val, Expire0}] when Expire0 =< Now ->
|
||||
{reply, none, S#cache{miss=Miss + 1}};
|
||||
[{Key, Val, Expire0}] ->
|
||||
Expire = Now + TTL,
|
||||
ets:insert(E, {Key, Val, Expire}),
|
||||
ets:delete(A, Expire0),
|
||||
ets:insert(A, {Expire, Key}),
|
||||
{reply, {ok, Val}, S#cache{hit=Hit + 1}}
|
||||
handle_call({get, Key}, _, S) ->
|
||||
{reply, lookup(Key, S), S};
|
||||
|
||||
handle_call({has, Key}, _, S) ->
|
||||
case member(Key, S) of
|
||||
true -> {reply, true, S};
|
||||
_ -> {reply, false, S}
|
||||
end;
|
||||
|
||||
handle_call(stop, _Tx, S) ->
|
||||
{stop, normal, ok, S};
|
||||
handle_call({remove, Key}, _, S) ->
|
||||
{reply, ok, remove(Key, S)};
|
||||
|
||||
handle_call(_Req, _Tx, S) ->
|
||||
{reply, {error, not_implemented}, 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};
|
||||
|
||||
%%
|
||||
%%
|
||||
handle_cast({put, Key, Val}, #cache{ttl=TTL, elements=E, access=A}=S) ->
|
||||
Expire = usec() + TTL,
|
||||
ets:insert(E, {Key, Val, Expire}),
|
||||
ets:insert(A, {Expire, Key}),
|
||||
{noreply, S};
|
||||
|
||||
handle_cast(evict, #cache{elements=E, access=A}=S) ->
|
||||
evict_expired(usec(), E, A),
|
||||
evict_cache(S),
|
||||
{noreply, S};
|
||||
|
||||
handle_cast(_Req, S) ->
|
||||
handle_call(_, _, S) ->
|
||||
{noreply, S}.
|
||||
|
||||
%%
|
||||
%%
|
||||
handle_info(evict, #cache{evict=Evict, elements=E, access=A}=S) ->
|
||||
evict_expired(usec(), E, A),
|
||||
evict_cache(S),
|
||||
erlang:send_after(Evict, self(), evict),
|
||||
{noreply, S};
|
||||
handle_cast({put, Key, Val}, S) ->
|
||||
{noreply, insert(Key, Val, S)};
|
||||
|
||||
handle_cast({remove, Key}, S) ->
|
||||
{noreply, remove(Key, S)};
|
||||
|
||||
handle_info(_Msg, S) ->
|
||||
handle_cast(_, S) ->
|
||||
{noreply, S}.
|
||||
|
||||
%%
|
||||
%%
|
||||
terminate(_Reason, _S) ->
|
||||
ok.
|
||||
handle_info(evict, S) ->
|
||||
erlang:send_after(S#cache.evict, self(), evict),
|
||||
{noreply, evict(S)};
|
||||
|
||||
handle_info(quota, S) ->
|
||||
erlang:send_after(S#cache.quota, self(), quota),
|
||||
{noreply, quota(S)};
|
||||
|
||||
handle_info(_, S) ->
|
||||
{noreply, S}.
|
||||
|
||||
%%
|
||||
%%
|
||||
@ -180,106 +135,143 @@ code_change(_Vsn, S, _Extra) ->
|
||||
%%%----------------------------------------------------------------------------
|
||||
|
||||
%%
|
||||
%%
|
||||
usec() ->
|
||||
{Mega, Sec, USec} = erlang:now(),
|
||||
(Mega * 1000000 + Sec) * 1000000 + USec.
|
||||
%% 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
|
||||
).
|
||||
|
||||
%%
|
||||
%%
|
||||
check_memory(#cache{memory=undefined}) ->
|
||||
true;
|
||||
check_memory(#cache{memory=Mem, elements=E, access=A}) ->
|
||||
Used = ets:info(E, memory) + ets:info(A, memory),
|
||||
if
|
||||
Used > Mem -> false;
|
||||
true -> true
|
||||
insert(Key, Val, #cache{}=S) ->
|
||||
[Head | Tail] = cache_heap:cells(S#cache.heap),
|
||||
true = ets:insert(Head, {Key, Val}),
|
||||
_ = evict_key(Key, Tail),
|
||||
?DEBUG("cache ~p: put ~p to cell ~p~n", [S#cache.name, Key, Head]),
|
||||
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.
|
||||
|
||||
%%
|
||||
%%
|
||||
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
|
||||
% 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))
|
||||
);
|
||||
|
||||
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]),
|
||||
Val
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
check_size(#cache{size=undefined}) ->
|
||||
true;
|
||||
check_size(#cache{size=Size, elements=E}) ->
|
||||
Used = ets:info(E, size),
|
||||
if
|
||||
Used > Size -> false;
|
||||
true -> true
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
check(S) ->
|
||||
case check_memory(S) of
|
||||
true -> check_size(S);
|
||||
false -> false
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
evict_expired(Expire, Element, Access) ->
|
||||
case ets:first(Access) of
|
||||
'$end_of_table' ->
|
||||
ok;
|
||||
Time when Time > Expire ->
|
||||
ok;
|
||||
Time ->
|
||||
[{_, Key}] = ets:lookup(Access, Time),
|
||||
ets:delete(Element, Key),
|
||||
ets:delete(Access, Time),
|
||||
evict_expired(Expire, Element, Access)
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
evict_cache(#cache{policy=lru, chunk=Chunk, elements=E, access=A}=S) ->
|
||||
case check(S) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
N = erlang:max(1, random:uniform(Chunk)),
|
||||
evict_lru(N, E, A),
|
||||
evict_cache(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
|
||||
end;
|
||||
|
||||
evict_cache(#cache{policy=mru, chunk=Chunk, elements=E, access=A}=S) ->
|
||||
case check(S) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
N = erlang:max(1, random:uniform(Chunk)),
|
||||
evict_mru(N, E, A),
|
||||
evict_cache(S)
|
||||
dowhile(_Pred, []) ->
|
||||
undefined.
|
||||
|
||||
%%
|
||||
%%
|
||||
evict(#cache{}=S) ->
|
||||
Cell = cache_heap:last(S#cache.heap),
|
||||
?DEBUG("cache ~p: free cell ~p~n", [S#cache.name, Cell]),
|
||||
S#cache{
|
||||
heap = cache_heap:alloc(cache_heap:free(Cell, S#cache.heap))
|
||||
}.
|
||||
|
||||
drop(#cache{}=S) ->
|
||||
Cell = cache_heap:last(S#cache.heap),
|
||||
?DEBUG("cache ~p: free cell ~p~n", [S#cache.name, Cell]),
|
||||
S#cache{
|
||||
heap = cache_heap:free(Cell, S#cache.heap)
|
||||
}.
|
||||
|
||||
%%
|
||||
%%
|
||||
quota(#cache{}=S) ->
|
||||
maybe_memory_quota(
|
||||
maybe_size_quota(S)
|
||||
).
|
||||
|
||||
|
||||
maybe_size_quota(#cache{quota_size=undefined}=S) ->
|
||||
S;
|
||||
maybe_size_quota(S) ->
|
||||
case lists:sum(cache_heap:size(S#cache.heap)) of
|
||||
Size when Size > S#cache.quota_size ->
|
||||
maybe_size_quota(drop(S));
|
||||
_ ->
|
||||
S#cache{
|
||||
heap = cache_heap:talloc(S#cache.n, S#cache.heap)
|
||||
}
|
||||
end.
|
||||
|
||||
|
||||
%%
|
||||
%%
|
||||
evict_lru(0, _Element, _Access) ->
|
||||
ok;
|
||||
evict_lru(N, Element, Access) ->
|
||||
case ets:first(Access) of
|
||||
'$end_of_table' ->
|
||||
ok;
|
||||
Time ->
|
||||
[{_, Key}] = ets:lookup(Access, Time),
|
||||
ets:delete(Element, Key),
|
||||
ets:delete(Access, Time),
|
||||
evict_lru(N - 1, Element, Access)
|
||||
maybe_memory_quota(#cache{quota_memory=undefined}=S) ->
|
||||
S;
|
||||
maybe_memory_quota(S) ->
|
||||
case lists:sum(cache_heap:memory(S#cache.heap)) of
|
||||
Size when Size > S#cache.quota_memory ->
|
||||
maybe_memory_quota(drop(S));
|
||||
_ ->
|
||||
S#cache{
|
||||
heap = cache_heap:talloc(S#cache.n, S#cache.heap)
|
||||
}
|
||||
end.
|
||||
|
||||
%%
|
||||
%%
|
||||
evict_mru(0, _Element, _Access) ->
|
||||
ok;
|
||||
evict_mru(N, Element, Access) ->
|
||||
case ets:last(Access) of
|
||||
'$end_of_table' ->
|
||||
ok;
|
||||
Time ->
|
||||
[{_, Key}] = ets:lookup(Access, Time),
|
||||
ets:delete(Element, Key),
|
||||
ets:delete(Access, Time),
|
||||
evict_mru(N - 1, Element, Access)
|
||||
end.
|
||||
|
||||
|
97
src/cache_heap.erl
Normal file
97
src/cache_heap.erl
Normal file
@ -0,0 +1,97 @@
|
||||
%%
|
||||
%% 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, talloc/2, 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]
|
||||
}.
|
||||
|
||||
% tail alloc
|
||||
talloc(N, #heap{}=H)
|
||||
when length(H#heap.cells) < N ->
|
||||
talloc(N,
|
||||
H#heap{
|
||||
cells = H#heap.cells ++ [create()]
|
||||
}
|
||||
);
|
||||
talloc(_, #heap{}=H) ->
|
||||
H.
|
||||
|
||||
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,14 +1,15 @@
|
||||
%%
|
||||
-module(cache_sup).
|
||||
-behaviour(supervisor).
|
||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||
|
||||
-export([start_link/0, init/1]).
|
||||
|
||||
%%
|
||||
%%
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
{ok, Sup} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
|
||||
lists:foreach(fun default_cache/1, default()),
|
||||
{ok, Sup}.
|
||||
|
||||
init([]) ->
|
||||
{ok,
|
||||
@ -16,4 +17,19 @@ init([]) ->
|
||||
{one_for_one, 4, 1800},
|
||||
[]
|
||||
}
|
||||
}.
|
||||
}.
|
||||
|
||||
|
||||
%% list of default caches
|
||||
default() ->
|
||||
case application:get_env(cache, default) of
|
||||
{ok, Val} -> Val;
|
||||
undefined -> []
|
||||
end.
|
||||
|
||||
default_cache({Name, Opts}) ->
|
||||
supervisor:start_child(?MODULE, {
|
||||
Name,
|
||||
{cache, start_link, [Name, Opts]},
|
||||
permanent, 900000, worker, dynamic
|
||||
}).
|
@ -2,132 +2,170 @@
|
||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(CACHE, [
|
||||
{ttl, 3}, %% time-to-live 3 sec
|
||||
{n, 3}, %% 3 cells
|
||||
{evict, 1} %% evict 1 sec
|
||||
]).
|
||||
|
||||
lifecyle_1_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{ttl, 10},
|
||||
{evict, 5}
|
||||
]),
|
||||
ok = cache:put(test, key, val),
|
||||
timer:sleep(6),
|
||||
{ok, val} = cache:get(test, key),
|
||||
timer:sleep(6),
|
||||
{ok, val} = cache:get(test, key),
|
||||
timer:sleep(20),
|
||||
none = cache:get(test, key),
|
||||
cache:stop(test).
|
||||
lru_test_() ->
|
||||
{
|
||||
setup,
|
||||
fun cache_init/0,
|
||||
fun cache_free/1,
|
||||
[
|
||||
{"put", fun cache_put/0}
|
||||
,{"has", fun cache_has/0}
|
||||
,{"get", fun cache_get/0}
|
||||
,{"del", fun cache_del/0}
|
||||
,{"lifecycle 1", {timeout, 10000, fun cache_lc1/0}}
|
||||
]
|
||||
}.
|
||||
|
||||
cache_init() ->
|
||||
cache:start_link(test, ?CACHE).
|
||||
|
||||
cache_free({ok, Pid}) ->
|
||||
erlang:unlink(Pid),
|
||||
cache:drop(test).
|
||||
|
||||
cache_put() ->
|
||||
ok = cache:put(test, <<"key">>, <<"val">>).
|
||||
|
||||
cache_has() ->
|
||||
true = cache:has(test, <<"key">>),
|
||||
false = cache:has(test, <<"yek">>).
|
||||
|
||||
cache_get() ->
|
||||
<<"val">> = cache:get(test, <<"key">>),
|
||||
undefined = cache:get(test, <<"yek">>).
|
||||
|
||||
cache_del() ->
|
||||
ok = cache:remove(test, <<"key">>),
|
||||
ok = cache:remove(test, <<"yek">>).
|
||||
|
||||
cache_lc1() ->
|
||||
error_logger:error_msg("~n~n life-cycle #1"),
|
||||
ok = cache:put(test, key, val),
|
||||
timer:sleep(1200),
|
||||
val = cache:get(test, key),
|
||||
timer:sleep(1200),
|
||||
val = cache:get(test, key),
|
||||
timer:sleep(1200),
|
||||
val = cache:get(test, key),
|
||||
timer:sleep(3200),
|
||||
undefined = cache:get(test, key).
|
||||
|
||||
|
||||
lifecyle_2_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{ttl, 10},
|
||||
{evict, 100}
|
||||
]),
|
||||
ok = cache:put(test, key, val),
|
||||
timer:sleep(6),
|
||||
{ok, val} = cache:get(test, key),
|
||||
timer:sleep(6),
|
||||
{ok, val} = cache:get(test, key),
|
||||
timer:sleep(20),
|
||||
none = cache:get(test, key),
|
||||
cache:stop(test).
|
||||
% lifecyle_2_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {ttl, 10},
|
||||
% {evict, 100}
|
||||
% ]),
|
||||
% ok = cache:put(test, key, val),
|
||||
% timer:sleep(6),
|
||||
% {ok, val} = cache:get(test, key),
|
||||
% timer:sleep(6),
|
||||
% {ok, val} = cache:get(test, key),
|
||||
% timer:sleep(20),
|
||||
% none = cache:get(test, key),
|
||||
% cache:stop(test).
|
||||
|
||||
lifecyle_3_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{ttl, 10},
|
||||
{evict, 5}
|
||||
]),
|
||||
ok = cache:put(test, key1, val1),
|
||||
timer:sleep(5),
|
||||
ok = cache:put(test, key2, val2),
|
||||
timer:sleep(5),
|
||||
ok = cache:put(test, key3, val3),
|
||||
timer:sleep(5),
|
||||
ok = cache:put(test, key4, val4),
|
||||
% lifecyle_3_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {ttl, 10},
|
||||
% {evict, 5}
|
||||
% ]),
|
||||
% ok = cache:put(test, key1, val1),
|
||||
% timer:sleep(5),
|
||||
% ok = cache:put(test, key2, val2),
|
||||
% timer:sleep(5),
|
||||
% ok = cache:put(test, key3, val3),
|
||||
% timer:sleep(5),
|
||||
% ok = cache:put(test, key4, val4),
|
||||
|
||||
none = cache:get(test, key1),
|
||||
none = cache:get(test, key2),
|
||||
{ok, val3} = cache:get(test, key3),
|
||||
{ok, val4} = cache:get(test, key4),
|
||||
cache:stop(test).
|
||||
% none = cache:get(test, key1),
|
||||
% none = cache:get(test, key2),
|
||||
% {ok, val3} = cache:get(test, key3),
|
||||
% {ok, val4} = cache:get(test, key4),
|
||||
% cache:stop(test).
|
||||
|
||||
evict_lru_1_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{policy, lru},
|
||||
{ttl, 100},
|
||||
{evict, 5},
|
||||
{size, 10},
|
||||
{chunk, 2}
|
||||
]),
|
||||
lists:foreach(
|
||||
fun(X) -> cache:put(test, X, X) end,
|
||||
lists:seq(1, 10)
|
||||
),
|
||||
timer:sleep(10),
|
||||
{ok, 1} = cache:get(test, 1),
|
||||
cache:put(test, key, val),
|
||||
timer:sleep(10),
|
||||
none = cache:get(test, 2),
|
||||
cache:stop(test).
|
||||
% evict_lru_1_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {policy, lru},
|
||||
% {ttl, 100},
|
||||
% {evict, 5},
|
||||
% {size, 10},
|
||||
% {chunk, 2}
|
||||
% ]),
|
||||
% lists:foreach(
|
||||
% fun(X) -> cache:put(test, X, X) end,
|
||||
% lists:seq(1, 10)
|
||||
% ),
|
||||
% timer:sleep(10),
|
||||
% {ok, 1} = cache:get(test, 1),
|
||||
% cache:put(test, key, val),
|
||||
% timer:sleep(10),
|
||||
% none = cache:get(test, 2),
|
||||
% cache:stop(test).
|
||||
|
||||
evict_lru_2_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{policy, lru},
|
||||
{ttl, 100},
|
||||
{evict, 100},
|
||||
{size, 10},
|
||||
{chunk, 2}
|
||||
]),
|
||||
lists:foreach(
|
||||
fun(X) -> cache:put(test, X, X) end,
|
||||
lists:seq(1, 10)
|
||||
),
|
||||
{ok, 1} = cache:get(test, 1),
|
||||
cache:put(test, key, val),
|
||||
cache:evict(test),
|
||||
none = cache:get(test, 2),
|
||||
cache:stop(test).
|
||||
% evict_lru_2_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {policy, lru},
|
||||
% {ttl, 100},
|
||||
% {evict, 100},
|
||||
% {size, 10},
|
||||
% {chunk, 2}
|
||||
% ]),
|
||||
% lists:foreach(
|
||||
% fun(X) -> cache:put(test, X, X) end,
|
||||
% lists:seq(1, 10)
|
||||
% ),
|
||||
% {ok, 1} = cache:get(test, 1),
|
||||
% cache:put(test, key, val),
|
||||
% cache:evict(test),
|
||||
% none = cache:get(test, 2),
|
||||
% cache:stop(test).
|
||||
|
||||
evict_mru_1_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{policy, mru},
|
||||
{ttl, 100},
|
||||
{evict, 5},
|
||||
{size, 10},
|
||||
{chunk, 2}
|
||||
]),
|
||||
lists:foreach(
|
||||
fun(X) -> cache:put(test, X, X) end,
|
||||
lists:seq(1, 10)
|
||||
),
|
||||
timer:sleep(10),
|
||||
{ok, 1} = cache:get(test, 1),
|
||||
cache:put(test, key, val),
|
||||
timer:sleep(10),
|
||||
none = cache:get(test, key),
|
||||
cache:stop(test).
|
||||
% evict_mru_1_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {policy, mru},
|
||||
% {ttl, 100},
|
||||
% {evict, 5},
|
||||
% {size, 10},
|
||||
% {chunk, 2}
|
||||
% ]),
|
||||
% lists:foreach(
|
||||
% fun(X) -> cache:put(test, X, X) end,
|
||||
% lists:seq(1, 10)
|
||||
% ),
|
||||
% timer:sleep(10),
|
||||
% {ok, 1} = cache:get(test, 1),
|
||||
% cache:put(test, key, val),
|
||||
% timer:sleep(10),
|
||||
% none = cache:get(test, key),
|
||||
% cache:stop(test).
|
||||
|
||||
evict_mru_2_test() ->
|
||||
cache:start(),
|
||||
{ok, _} = cache:start_link(test, [
|
||||
{policy, mru},
|
||||
{ttl, 100},
|
||||
{evict, 100},
|
||||
{size, 10},
|
||||
{chunk, 2}
|
||||
]),
|
||||
lists:foreach(
|
||||
fun(X) -> cache:put(test, X, X) end,
|
||||
lists:seq(1, 10)
|
||||
),
|
||||
{ok, 1} = cache:get(test, 1),
|
||||
cache:put(test, key, val),
|
||||
cache:evict(test),
|
||||
none = cache:get(test, key),
|
||||
cache:stop(test).
|
||||
% evict_mru_2_test() ->
|
||||
% cache:start(),
|
||||
% {ok, _} = cache:start_link(test, [
|
||||
% {policy, mru},
|
||||
% {ttl, 100},
|
||||
% {evict, 100},
|
||||
% {size, 10},
|
||||
% {chunk, 2}
|
||||
% ]),
|
||||
% lists:foreach(
|
||||
% fun(X) -> cache:put(test, X, X) end,
|
||||
% lists:seq(1, 10)
|
||||
% ),
|
||||
% {ok, 1} = cache:get(test, 1),
|
||||
% cache:put(test, key, val),
|
||||
% cache:evict(test),
|
||||
% none = cache:get(test, key),
|
||||
% cache:stop(test).
|
Loading…
Reference in New Issue
Block a user