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/
|
ebin/
|
||||||
deps/
|
deps/
|
||||||
.eunit/
|
.eunit/
|
||||||
|
tests/
|
||||||
rebar
|
rebar
|
||||||
*.sublime-*
|
*.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
|
.PHONY: deps test
|
||||||
|
|
||||||
|
BB=../basho_bench
|
||||||
|
|
||||||
all: rebar deps compile
|
all: rebar deps compile
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
@ -31,3 +33,9 @@ rebar:
|
|||||||
curl -O http://cloud.github.com/downloads/basho/rebar/rebar
|
curl -O http://cloud.github.com/downloads/basho/rebar/rebar
|
||||||
chmod ugo+x 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,
|
{application, cache,
|
||||||
[
|
[
|
||||||
{description, "in-memory cache"},
|
{description, "in-memory cache"},
|
||||||
{vsn, "0.1.0"},
|
{vsn, "0.2.0"},
|
||||||
{modules, [
|
{modules, [
|
||||||
cache,
|
cache,
|
||||||
cache_sup,
|
cache_sup,
|
||||||
|
@ -1,49 +1,50 @@
|
|||||||
-module(cache).
|
-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) ->
|
start_link(Cache, Opts) ->
|
||||||
cache_bucket:start_link(Name, Opts).
|
cache_bucket:start_link(Cache, Opts).
|
||||||
|
|
||||||
|
%%
|
||||||
|
%%
|
||||||
|
drop(Cache) ->
|
||||||
|
erlang:exit(whereis(Cache), shutdown).
|
||||||
|
|
||||||
|
%%
|
||||||
|
%%
|
||||||
i(Cache) ->
|
i(Cache) ->
|
||||||
cache_bucket:i(Cache).
|
gen_server:call(Cache, i).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
put(Cache, Key, Val) ->
|
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) ->
|
get(Cache, Key) ->
|
||||||
cache_bucket:get(Cache, Key).
|
gen_server:call(Cache, {get, Key}).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
evict(Cache) ->
|
has(Cache, Key) ->
|
||||||
cache_bucket:evict(Cache).
|
gen_server:call(Cache, {has, Key}).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
stop(Cache) ->
|
remove(Cache, Key) ->
|
||||||
cache_bucket:stop(Cache).
|
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).
|
-module(cache_bucket).
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||||
|
-include("cache.hrl").
|
||||||
|
|
||||||
-export([start_link/2]).
|
-export([
|
||||||
-export([i/1, put/3, get/2, evict/1, stop/1]).
|
start_link/2,
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
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).
|
|
||||||
|
|
||||||
|
%%
|
||||||
%%
|
%%
|
||||||
-record(cache, {
|
-record(cache, {
|
||||||
policy, %%
|
name :: atom(), %% name of cache bucket
|
||||||
memory, %% memory threshold
|
heap :: list(), %% heap
|
||||||
size, %% size threshold
|
|
||||||
chunk, %% number of evicted elements
|
|
||||||
|
|
||||||
ttl, %% cache element ttl
|
n = ?DEF_CACHE_N :: integer(), %% number of cells
|
||||||
evict, %% house keeping timer to evict cache elements
|
ttl = ?DEF_CACHE_TTL :: integer(), %% chunk time to live
|
||||||
elements,
|
policy = ?DEF_CACHE_POLICY :: integer(), %% eviction policy
|
||||||
access,
|
evict :: integer(), %% age of heap cell (cell eviction frequency)
|
||||||
|
|
||||||
hit,
|
quota = ?DEF_CACHE_QUOTA :: integer(), %% frequency of limit enforcement (clean-up)
|
||||||
miss
|
quota_size :: integer(), %% max number of elements
|
||||||
|
quota_memory :: integer() %% max number of memory in bytes
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
start_link(Name, Opts) ->
|
start_link(Name, Opts) ->
|
||||||
gen_server:start_link({local, Name}, ?MODULE, [Opts], []).
|
gen_server:start_link({local, Name}, ?MODULE, [Name, Opts], []).
|
||||||
|
|
||||||
init([Opts]) ->
|
init([Name, Opts]) ->
|
||||||
init(Opts, #cache{
|
{ok, init(Opts, #cache{name=Name})}.
|
||||||
evict = ?DEFAULT_EVICT,
|
|
||||||
policy = ?DEFAULT_POLICY,
|
|
||||||
chunk = ?DEFAULT_CHUNK,
|
|
||||||
ttl = ?DEFAULT_TTL * 1000,
|
|
||||||
hit = 0,
|
|
||||||
miss = 0
|
|
||||||
}).
|
|
||||||
|
|
||||||
init([{policy, X} | T], #cache{}=S) ->
|
init([{policy, X} | T], #cache{}=S) ->
|
||||||
init(T, S#cache{policy=X});
|
init(T, S#cache{policy=X});
|
||||||
|
|
||||||
init([{memory, X} | T], #cache{}=S) ->
|
init([{memory, X} | Opts], S) ->
|
||||||
init(T, S#cache{memory=X div erlang:system_info(wordsize)});
|
init(Opts, S#cache{quota_memory=X div erlang:system_info(wordsize)});
|
||||||
|
|
||||||
init([{size, X} | T], #cache{}=S) ->
|
init([{size, X} | Opts], S) ->
|
||||||
init(T, S#cache{size=X});
|
init(Opts, S#cache{quota_size=X});
|
||||||
|
|
||||||
init([{chunk, X} | T], #cache{}=S) ->
|
init([{n, X} | Opts], S) ->
|
||||||
init(T, S#cache{chunk=X});
|
init(Opts, S#cache{n = X});
|
||||||
|
|
||||||
init([{ttl, X} | T], #cache{}=S) ->
|
init([{ttl, X} | Opts], S) ->
|
||||||
init(T, S#cache{ttl=X * 1000});
|
init(Opts, S#cache{ttl = X});
|
||||||
|
|
||||||
init([{evict, X} | T], #cache{}=S) ->
|
init([{quota, X} | Opts], S) ->
|
||||||
init(T, S#cache{evict=X});
|
init(Opts, S#cache{quota=X * 1000});
|
||||||
|
|
||||||
init([], #cache{evict=Evict}=S) ->
|
init([_ | Opts], S) ->
|
||||||
|
init(Opts, S);
|
||||||
|
|
||||||
|
init([], S) ->
|
||||||
random:seed(erlang:now()),
|
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),
|
erlang:send_after(Evict, self(), evict),
|
||||||
{ok,
|
S#cache{
|
||||||
S#cache{
|
heap = cache_heap:new(S#cache.n),
|
||||||
elements = ets:new(undefined, [set, private]),
|
evict = Evict
|
||||||
access = ets:new(undefined, [ordered_set, private])
|
|
||||||
}
|
|
||||||
}.
|
}.
|
||||||
|
|
||||||
%%%----------------------------------------------------------------------------
|
|
||||||
%%%
|
|
||||||
%%% api
|
|
||||||
%%%
|
|
||||||
%%%----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
i(Cache) ->
|
terminate(_Reason, S) ->
|
||||||
gen_server:call(Cache, info).
|
cache_heap:free(S#cache.heap),
|
||||||
|
ok.
|
||||||
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).
|
|
||||||
|
|
||||||
%%%----------------------------------------------------------------------------
|
%%%----------------------------------------------------------------------------
|
||||||
%%%
|
%%%
|
||||||
@ -100,72 +76,51 @@ stop(Cache) ->
|
|||||||
%%%
|
%%%
|
||||||
%%%----------------------------------------------------------------------------
|
%%%----------------------------------------------------------------------------
|
||||||
|
|
||||||
%%
|
handle_call({put, Key, Val}, _, S) ->
|
||||||
%%
|
{reply, ok, insert(Key, Val, S)};
|
||||||
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({get, Key}, _Tx, #cache{ttl=TTL, elements=E, access=A, hit=Hit, miss=Miss}=S) ->
|
handle_call({get, Key}, _, S) ->
|
||||||
Now = usec(),
|
{reply, lookup(Key, S), S};
|
||||||
case ets:lookup(E, Key) of
|
|
||||||
[] ->
|
handle_call({has, Key}, _, S) ->
|
||||||
{reply, none, S#cache{miss=Miss + 1}};
|
case member(Key, S) of
|
||||||
[{Key, _Val, Expire0}] when Expire0 =< Now ->
|
true -> {reply, true, S};
|
||||||
{reply, none, S#cache{miss=Miss + 1}};
|
_ -> {reply, false, S}
|
||||||
[{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}}
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
handle_call(stop, _Tx, S) ->
|
handle_call({remove, Key}, _, S) ->
|
||||||
{stop, normal, ok, S};
|
{reply, ok, remove(Key, S)};
|
||||||
|
|
||||||
handle_call(_Req, _Tx, S) ->
|
handle_call(i, _, S) ->
|
||||||
{reply, {error, not_implemented}, 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_call(_, _, 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) ->
|
|
||||||
{noreply, S}.
|
{noreply, S}.
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
handle_info(evict, #cache{evict=Evict, elements=E, access=A}=S) ->
|
handle_cast({put, Key, Val}, S) ->
|
||||||
evict_expired(usec(), E, A),
|
{noreply, insert(Key, Val, S)};
|
||||||
evict_cache(S),
|
|
||||||
erlang:send_after(Evict, self(), evict),
|
|
||||||
{noreply, S};
|
|
||||||
|
|
||||||
|
handle_cast({remove, Key}, S) ->
|
||||||
|
{noreply, remove(Key, S)};
|
||||||
|
|
||||||
handle_info(_Msg, S) ->
|
handle_cast(_, S) ->
|
||||||
{noreply, S}.
|
{noreply, S}.
|
||||||
|
|
||||||
%%
|
handle_info(evict, S) ->
|
||||||
%%
|
erlang:send_after(S#cache.evict, self(), evict),
|
||||||
terminate(_Reason, _S) ->
|
{noreply, evict(S)};
|
||||||
ok.
|
|
||||||
|
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) ->
|
|||||||
%%%----------------------------------------------------------------------------
|
%%%----------------------------------------------------------------------------
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%% evict key from cells
|
||||||
usec() ->
|
evict_key(Key, List) ->
|
||||||
{Mega, Sec, USec} = erlang:now(),
|
dowhile(
|
||||||
(Mega * 1000000 + Sec) * 1000000 + USec.
|
fun(Cell) ->
|
||||||
|
case ets:member(Cell, Key) of
|
||||||
|
true ->
|
||||||
|
ets:delete(Cell, Key),
|
||||||
|
Cell;
|
||||||
|
Result ->
|
||||||
|
Result
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
List
|
||||||
|
).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
check_memory(#cache{memory=undefined}) ->
|
insert(Key, Val, #cache{}=S) ->
|
||||||
true;
|
[Head | Tail] = cache_heap:cells(S#cache.heap),
|
||||||
check_memory(#cache{memory=Mem, elements=E, access=A}) ->
|
true = ets:insert(Head, {Key, Val}),
|
||||||
Used = ets:info(E, memory) + ets:info(A, memory),
|
_ = evict_key(Key, Tail),
|
||||||
if
|
?DEBUG("cache ~p: put ~p to cell ~p~n", [S#cache.name, Key, Head]),
|
||||||
Used > Mem -> false;
|
S.
|
||||||
true -> true
|
|
||||||
|
%%
|
||||||
|
%%
|
||||||
|
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.
|
end.
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%% execute predicate while it succeeded
|
||||||
check_size(#cache{size=undefined}) ->
|
dowhile(Pred, [Head | Tail]) ->
|
||||||
true;
|
case Pred(Head) of
|
||||||
check_size(#cache{size=Size, elements=E}) ->
|
% predicate false, apply predicate to next chunk
|
||||||
Used = ets:info(E, size),
|
false -> dowhile(Pred, Tail);
|
||||||
if
|
Result -> Result
|
||||||
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)
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
evict_cache(#cache{policy=mru, chunk=Chunk, elements=E, access=A}=S) ->
|
dowhile(_Pred, []) ->
|
||||||
case check(S) of
|
undefined.
|
||||||
true ->
|
|
||||||
ok;
|
%%
|
||||||
false ->
|
%%
|
||||||
N = erlang:max(1, random:uniform(Chunk)),
|
evict(#cache{}=S) ->
|
||||||
evict_mru(N, E, A),
|
Cell = cache_heap:last(S#cache.heap),
|
||||||
evict_cache(S)
|
?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.
|
end.
|
||||||
|
|
||||||
|
maybe_memory_quota(#cache{quota_memory=undefined}=S) ->
|
||||||
%%
|
S;
|
||||||
%%
|
maybe_memory_quota(S) ->
|
||||||
evict_lru(0, _Element, _Access) ->
|
case lists:sum(cache_heap:memory(S#cache.heap)) of
|
||||||
ok;
|
Size when Size > S#cache.quota_memory ->
|
||||||
evict_lru(N, Element, Access) ->
|
maybe_memory_quota(drop(S));
|
||||||
case ets:first(Access) of
|
_ ->
|
||||||
'$end_of_table' ->
|
S#cache{
|
||||||
ok;
|
heap = cache_heap:talloc(S#cache.n, S#cache.heap)
|
||||||
Time ->
|
}
|
||||||
[{_, Key}] = ets:lookup(Access, Time),
|
|
||||||
ets:delete(Element, Key),
|
|
||||||
ets:delete(Access, Time),
|
|
||||||
evict_lru(N - 1, Element, Access)
|
|
||||||
end.
|
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).
|
-module(cache_sup).
|
||||||
-behaviour(supervisor).
|
-behaviour(supervisor).
|
||||||
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
|
||||||
|
|
||||||
-export([start_link/0, init/1]).
|
-export([start_link/0, init/1]).
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%%
|
%%
|
||||||
start_link() ->
|
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([]) ->
|
init([]) ->
|
||||||
{ok,
|
{ok,
|
||||||
@ -16,4 +17,19 @@ init([]) ->
|
|||||||
{one_for_one, 4, 1800},
|
{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>').
|
-author('Dmitry Kolesnikov <dmkolesnikov@gmail.com>').
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-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() ->
|
lru_test_() ->
|
||||||
cache:start(),
|
{
|
||||||
{ok, _} = cache:start_link(test, [
|
setup,
|
||||||
{ttl, 10},
|
fun cache_init/0,
|
||||||
{evict, 5}
|
fun cache_free/1,
|
||||||
]),
|
[
|
||||||
ok = cache:put(test, key, val),
|
{"put", fun cache_put/0}
|
||||||
timer:sleep(6),
|
,{"has", fun cache_has/0}
|
||||||
{ok, val} = cache:get(test, key),
|
,{"get", fun cache_get/0}
|
||||||
timer:sleep(6),
|
,{"del", fun cache_del/0}
|
||||||
{ok, val} = cache:get(test, key),
|
,{"lifecycle 1", {timeout, 10000, fun cache_lc1/0}}
|
||||||
timer:sleep(20),
|
]
|
||||||
none = cache:get(test, key),
|
}.
|
||||||
cache:stop(test).
|
|
||||||
|
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() ->
|
% lifecyle_2_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{ttl, 10},
|
% {ttl, 10},
|
||||||
{evict, 100}
|
% {evict, 100}
|
||||||
]),
|
% ]),
|
||||||
ok = cache:put(test, key, val),
|
% ok = cache:put(test, key, val),
|
||||||
timer:sleep(6),
|
% timer:sleep(6),
|
||||||
{ok, val} = cache:get(test, key),
|
% {ok, val} = cache:get(test, key),
|
||||||
timer:sleep(6),
|
% timer:sleep(6),
|
||||||
{ok, val} = cache:get(test, key),
|
% {ok, val} = cache:get(test, key),
|
||||||
timer:sleep(20),
|
% timer:sleep(20),
|
||||||
none = cache:get(test, key),
|
% none = cache:get(test, key),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
||||||
|
|
||||||
lifecyle_3_test() ->
|
% lifecyle_3_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{ttl, 10},
|
% {ttl, 10},
|
||||||
{evict, 5}
|
% {evict, 5}
|
||||||
]),
|
% ]),
|
||||||
ok = cache:put(test, key1, val1),
|
% ok = cache:put(test, key1, val1),
|
||||||
timer:sleep(5),
|
% timer:sleep(5),
|
||||||
ok = cache:put(test, key2, val2),
|
% ok = cache:put(test, key2, val2),
|
||||||
timer:sleep(5),
|
% timer:sleep(5),
|
||||||
ok = cache:put(test, key3, val3),
|
% ok = cache:put(test, key3, val3),
|
||||||
timer:sleep(5),
|
% timer:sleep(5),
|
||||||
ok = cache:put(test, key4, val4),
|
% ok = cache:put(test, key4, val4),
|
||||||
|
|
||||||
none = cache:get(test, key1),
|
% none = cache:get(test, key1),
|
||||||
none = cache:get(test, key2),
|
% none = cache:get(test, key2),
|
||||||
{ok, val3} = cache:get(test, key3),
|
% {ok, val3} = cache:get(test, key3),
|
||||||
{ok, val4} = cache:get(test, key4),
|
% {ok, val4} = cache:get(test, key4),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
||||||
|
|
||||||
evict_lru_1_test() ->
|
% evict_lru_1_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{policy, lru},
|
% {policy, lru},
|
||||||
{ttl, 100},
|
% {ttl, 100},
|
||||||
{evict, 5},
|
% {evict, 5},
|
||||||
{size, 10},
|
% {size, 10},
|
||||||
{chunk, 2}
|
% {chunk, 2}
|
||||||
]),
|
% ]),
|
||||||
lists:foreach(
|
% lists:foreach(
|
||||||
fun(X) -> cache:put(test, X, X) end,
|
% fun(X) -> cache:put(test, X, X) end,
|
||||||
lists:seq(1, 10)
|
% lists:seq(1, 10)
|
||||||
),
|
% ),
|
||||||
timer:sleep(10),
|
% timer:sleep(10),
|
||||||
{ok, 1} = cache:get(test, 1),
|
% {ok, 1} = cache:get(test, 1),
|
||||||
cache:put(test, key, val),
|
% cache:put(test, key, val),
|
||||||
timer:sleep(10),
|
% timer:sleep(10),
|
||||||
none = cache:get(test, 2),
|
% none = cache:get(test, 2),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
||||||
|
|
||||||
evict_lru_2_test() ->
|
% evict_lru_2_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{policy, lru},
|
% {policy, lru},
|
||||||
{ttl, 100},
|
% {ttl, 100},
|
||||||
{evict, 100},
|
% {evict, 100},
|
||||||
{size, 10},
|
% {size, 10},
|
||||||
{chunk, 2}
|
% {chunk, 2}
|
||||||
]),
|
% ]),
|
||||||
lists:foreach(
|
% lists:foreach(
|
||||||
fun(X) -> cache:put(test, X, X) end,
|
% fun(X) -> cache:put(test, X, X) end,
|
||||||
lists:seq(1, 10)
|
% lists:seq(1, 10)
|
||||||
),
|
% ),
|
||||||
{ok, 1} = cache:get(test, 1),
|
% {ok, 1} = cache:get(test, 1),
|
||||||
cache:put(test, key, val),
|
% cache:put(test, key, val),
|
||||||
cache:evict(test),
|
% cache:evict(test),
|
||||||
none = cache:get(test, 2),
|
% none = cache:get(test, 2),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
||||||
|
|
||||||
evict_mru_1_test() ->
|
% evict_mru_1_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{policy, mru},
|
% {policy, mru},
|
||||||
{ttl, 100},
|
% {ttl, 100},
|
||||||
{evict, 5},
|
% {evict, 5},
|
||||||
{size, 10},
|
% {size, 10},
|
||||||
{chunk, 2}
|
% {chunk, 2}
|
||||||
]),
|
% ]),
|
||||||
lists:foreach(
|
% lists:foreach(
|
||||||
fun(X) -> cache:put(test, X, X) end,
|
% fun(X) -> cache:put(test, X, X) end,
|
||||||
lists:seq(1, 10)
|
% lists:seq(1, 10)
|
||||||
),
|
% ),
|
||||||
timer:sleep(10),
|
% timer:sleep(10),
|
||||||
{ok, 1} = cache:get(test, 1),
|
% {ok, 1} = cache:get(test, 1),
|
||||||
cache:put(test, key, val),
|
% cache:put(test, key, val),
|
||||||
timer:sleep(10),
|
% timer:sleep(10),
|
||||||
none = cache:get(test, key),
|
% none = cache:get(test, key),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
||||||
|
|
||||||
evict_mru_2_test() ->
|
% evict_mru_2_test() ->
|
||||||
cache:start(),
|
% cache:start(),
|
||||||
{ok, _} = cache:start_link(test, [
|
% {ok, _} = cache:start_link(test, [
|
||||||
{policy, mru},
|
% {policy, mru},
|
||||||
{ttl, 100},
|
% {ttl, 100},
|
||||||
{evict, 100},
|
% {evict, 100},
|
||||||
{size, 10},
|
% {size, 10},
|
||||||
{chunk, 2}
|
% {chunk, 2}
|
||||||
]),
|
% ]),
|
||||||
lists:foreach(
|
% lists:foreach(
|
||||||
fun(X) -> cache:put(test, X, X) end,
|
% fun(X) -> cache:put(test, X, X) end,
|
||||||
lists:seq(1, 10)
|
% lists:seq(1, 10)
|
||||||
),
|
% ),
|
||||||
{ok, 1} = cache:get(test, 1),
|
% {ok, 1} = cache:get(test, 1),
|
||||||
cache:put(test, key, val),
|
% cache:put(test, key, val),
|
||||||
cache:evict(test),
|
% cache:evict(test),
|
||||||
none = cache:get(test, key),
|
% none = cache:get(test, key),
|
||||||
cache:stop(test).
|
% cache:stop(test).
|
Loading…
Reference in New Issue
Block a user