From d324600f4b9dc13e2073fa3c0c462999e7700ab5 Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Tue, 16 Apr 2013 12:15:14 +0300 Subject: [PATCH] new cache design (based on multi-page idea) --- .gitignore | 1 + Emakefile | 7 + Makefile | 8 + priv/cache.benchmark | 26 +++ src/cache.app.src | 2 +- src/cache.erl | 55 +++--- src/cache.hrl | 19 ++ src/cache_benchmark.erl | 66 +++++++ src/cache_bucket.erl | 412 ++++++++++++++++++++-------------------- src/cache_heap.erl | 97 ++++++++++ src/cache_sup.erl | 22 ++- test/cache_tests.erl | 278 +++++++++++++++------------ 12 files changed, 632 insertions(+), 361 deletions(-) create mode 100644 Emakefile create mode 100644 priv/cache.benchmark create mode 100644 src/cache.hrl create mode 100644 src/cache_benchmark.erl create mode 100644 src/cache_heap.erl diff --git a/.gitignore b/.gitignore index e3ce4a9..3e54b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ ebin/ deps/ .eunit/ +tests/ rebar *.sublime-* diff --git a/Emakefile b/Emakefile new file mode 100644 index 0000000..65e3fde --- /dev/null +++ b/Emakefile @@ -0,0 +1,7 @@ +{"src/*", [ + report, + verbose, + {i, "include"}, + {outdir, "ebin"}, + debug_info +]}. diff --git a/Makefile b/Makefile index 24acf8e..defcef9 100644 --- a/Makefile +++ b/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 + + diff --git a/priv/cache.benchmark b/priv/cache.benchmark new file mode 100644 index 0000000..10cc58d --- /dev/null +++ b/priv/cache.benchmark @@ -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} +]}. diff --git a/src/cache.app.src b/src/cache.app.src index ba8b5a5..8bb83a1 100644 --- a/src/cache.app.src +++ b/src/cache.app.src @@ -1,7 +1,7 @@ {application, cache, [ {description, "in-memory cache"}, - {vsn, "0.1.0"}, + {vsn, "0.2.0"}, {modules, [ cache, cache_sup, diff --git a/src/cache.erl b/src/cache.erl index 9175351..606cae5 100644 --- a/src/cache.erl +++ b/src/cache.erl @@ -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}). diff --git a/src/cache.hrl b/src/cache.hrl new file mode 100644 index 0000000..e14374d --- /dev/null +++ b/src/cache.hrl @@ -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). diff --git a/src/cache_benchmark.erl b/src/cache_benchmark.erl new file mode 100644 index 0000000..e72f55f --- /dev/null +++ b/src/cache_benchmark.erl @@ -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. \ No newline at end of file diff --git a/src/cache_bucket.erl b/src/cache_bucket.erl index 338097a..cd3f099 100644 --- a/src/cache_bucket.erl +++ b/src/cache_bucket.erl @@ -2,97 +2,73 @@ -module(cache_bucket). -behaviour(gen_server). -author('Dmitry Kolesnikov '). +-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. - diff --git a/src/cache_heap.erl b/src/cache_heap.erl new file mode 100644 index 0000000..7b32198 --- /dev/null +++ b/src/cache_heap.erl @@ -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). diff --git a/src/cache_sup.erl b/src/cache_sup.erl index 9f51785..9ad695d 100644 --- a/src/cache_sup.erl +++ b/src/cache_sup.erl @@ -1,14 +1,15 @@ %% -module(cache_sup). -behaviour(supervisor). --author('Dmitry Kolesnikov '). -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}, [] } - }. \ No newline at end of file + }. + + +%% 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 + }). \ No newline at end of file diff --git a/test/cache_tests.erl b/test/cache_tests.erl index bc8c0b9..265bce8 100644 --- a/test/cache_tests.erl +++ b/test/cache_tests.erl @@ -2,132 +2,170 @@ -author('Dmitry Kolesnikov '). -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). \ No newline at end of file +% 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). \ No newline at end of file