Refactor ets_lru into a gen_server The max_lifetime eviction made me realize this really does need to be an active process model. This is written as a gen_server with the intention that it'll be inserted into the supervision tree appropriately by applications that use it.
Project: http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/commit/3f2bae1d Tree: http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/tree/3f2bae1d Diff: http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/diff/3f2bae1d Branch: refs/heads/import Commit: 3f2bae1d7aa30c47316e55232050a416d144ac92 Parents: d7f227f Author: Paul J. Davis <[email protected]> Authored: Fri Dec 28 02:14:00 2012 -0600 Committer: Paul J. Davis <[email protected]> Committed: Fri Dec 28 02:14:00 2012 -0600 ---------------------------------------------------------------------- .gitignore | 2 + src/ets_lru.erl | 344 +++++++++++++++++++++++++-------------- test/01-basic-behavior.t | 47 ++++-- test/02-lru-options.t | 48 +++--- test/03-limit-max-objects.t | 4 +- test/04-limit-max-size.t | 4 +- test/05-limit-lifetime.t | 18 +- test/tutil.erl | 9 +- 8 files changed, 300 insertions(+), 176 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/.gitignore ---------------------------------------------------------------------- diff --git a/.gitignore b/.gitignore index f218820..90bd4cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +erl_crash.dump + .eunit/ ebin/ test/*.beam http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/src/ets_lru.erl ---------------------------------------------------------------------- diff --git a/src/ets_lru.erl b/src/ets_lru.erl index 73398d9..4aecabf 100644 --- a/src/ets_lru.erl +++ b/src/ets_lru.erl @@ -1,201 +1,301 @@ % Copyright 2012 Cloudant. All rights reserved. -module(ets_lru). +-behavior(gen_server). -export([ - create/2, - destroy/1, + start_link/2, + stop/1, insert/3, lookup/2, - member/2, remove/2, - hit/2, - expire/1, - clear/1 + clear/1, + + % Dirty functions read straight from + % the ETS tables which means there are + % race conditions with concurrent access. + lookup_d/2 +]). + +-export([ + init/1, + terminate/2, + + handle_call/3, + handle_cast/2, + handle_info/2, + + code_change/3 ]). -record(entry, { key, val, - atime + atime, + ctime }). --record(ets_lru, { +-record(st, { objects, atimes, - named=false, + ctimes, max_objs, max_size, - - lifetime + max_lifetime }). -create(Name, Options) -> - LRU = set_options(#ets_lru{}, Options), - Opts = case LRU#ets_lru.named of - true -> [named_table]; - false -> [] - end, - {OName, ATName} = table_names(Name), - {ok, LRU#ets_lru{ - objects = ets:new(OName, - [set, protected, {keypos, #entry.key}] ++ Opts), - atimes = ets:new(ATName, - [ordered_set, protected] ++ Opts) - }}. +start_link(Name, Options) when is_atom(Name) -> + gen_server:start_link({local, Name}, ?MODULE, {Name, Options}, []). -destroy(#ets_lru{objects=Objs, atimes=ATimes}) -> - true = ets:delete(Objs), - true = ets:delete(ATimes), - ok. +stop(LRU) -> + gen_server:cast(LRU, stop). -insert(#ets_lru{objects=Objs, atimes=ATs}=LRU, Key, Val) -> - NewATime = erlang:now(), - Pattern = #entry{key=Key, atime='$1', _='_'}, - case ets:match(Objs, Pattern) of - [[ATime]] -> - true = ets:delete(ATs, ATime), - true = ets:insert(ATs, {NewATime, Key}), - true = ets:update_element(Objs, Key, {#entry.val, Val}); - [] -> - true = ets:insert(ATs, {NewATime, Key}), - true = ets:insert(Objs, #entry{key=Key, val=Val, atime=NewATime}) - end, - trim(LRU). +lookup(LRU, Key) -> + gen_server:call(LRU, {lookup, Key}). + + +insert(LRU, Key, Val) -> + gen_server:call(LRU, {insert, Key, Val}). -lookup(#ets_lru{objects=Objs}=LRU, Key) -> - case ets:lookup(Objs, Key) of +remove(LRU, Key) -> + gen_server:call(LRU, {remove, Key}). + + +clear(LRU) -> + gen_server:call(LRU, clear). + + +lookup_d(Name, Key) when is_atom(Name) -> + case ets:lookup(obj_table(Name), Key) of [#entry{val=Val}] -> - hit(LRU, Key), + gen_server:cast(Name, {accessed, Key}), {ok, Val}; [] -> not_found end. -member(#ets_lru{objects=Objs}, Key) -> - ets:member(Objs, Key). +init({Name, Options}) -> + St = set_options(#st{}, Options), + ObjOpts = [set, named_table, protected, {keypos, #entry.key}], + TimeOpts = [ordered_set, named_table, protected], + {ok, St#st{ + objects = ets:new(obj_table(Name), ObjOpts), + atimes = ets:new(at_table(Name), TimeOpts), + ctimes = ets:new(ct_table(Name), TimeOpts) + }}. -remove(#ets_lru{objects=Objs, atimes=ATs}=LRU, Key) -> - case ets:match(Objs, #entry{key=Key, atime='$1', _='_'}) of + +terminate(_Reason, St) -> + true = ets:delete(St#st.objects), + true = ets:delete(St#st.atimes), + true = ets:delete(St#st.ctimes), + ok. + + +handle_call({lookup, Key}, _From, St) -> + Reply = case ets:lookup(St#st.objects, Key) of + [#entry{val=Val}] -> + accessed(St, Key), + {ok, Val}; + [] -> + not_found + end, + {reply, Reply, St, 0}; + +handle_call({insert, Key, Val}, _From, St) -> + NewATime = erlang:now(), + Pattern = #entry{key=Key, atime='$1', _='_'}, + case ets:match(St#st.objects, Pattern) of [[ATime]] -> - true = ets:delete(ATs, ATime), - true = ets:delete(Objs, Key), + Update = {#entry.val, Val}, + true = ets:update_element(St#st.objects, Key, Update), + true = ets:delete(St#st.atimes, ATime), + true = ets:insert(St#st.atimes, {NewATime, Key}); + [] -> + Entry = #entry{key=Key, val=Val, atime=NewATime, ctime=NewATime}, + true = ets:insert(St#st.objects, Entry), + true = ets:insert(St#st.atimes, {NewATime, Key}), + true = ets:insert(St#st.ctimes, {NewATime, Key}) + end, + {reply, ok, St, 0}; + +handle_call({remove, Key}, _From, St) -> + Pattern = #entry{key=Key, atime='$1', ctime='$2', _='_'}, + Reply = case ets:match(St#st.objects, Pattern) of + [[ATime, CTime]] -> + true = ets:delete(St#st.objects, Key), + true = ets:delete(St#st.atimes, ATime), + true = ets:delete(St#st.ctimes, CTime), ok; [] -> - ok + not_found end, - false = member(LRU, Key), - ok. + {reply, Reply, St, 0}; + +handle_call(clear, _From, St) -> + true = ets:delete_all_objects(St#st.objects), + true = ets:delete_all_objects(St#st.atimes), + true = ets:delete_all_objects(St#st.ctimes), + % No need to timeout here and evict cache + % entries because its now empty. + {reply, ok, St}; + + +handle_call(Msg, _From, St) -> + {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}. + +handle_cast({accessed, Key}, St) -> + accessed(Key, St), + {noreply, St, 0}; -hit(#ets_lru{objects=Objs, atimes=ATs}, Key) -> - case ets:match(Objs, #entry{key=Key, atime='$1', _='_'}) of +handle_cast(stop, St) -> + {stop, normal, St}; + +handle_cast(Msg, St) -> + {stop, {invalid_cast, Msg}, St}. + + +handle_info(timeout, St) -> + trim(St), + {noreply, St, next_timeout(St)}; + +handle_info(Msg, St) -> + {stop, {invalid_info, Msg}, St}. + + +code_change(_OldVsn, St, _Extra) -> + {ok, St}. + + +accessed(St, Key) -> + Pattern = #entry{key=Key, atime='$1', _='_'}, + case ets:match(St#st.objects, Pattern) of [[ATime]] -> NewATime = erlang:now(), - true = ets:delete(ATs, ATime), - true = ets:insert(ATs, {NewATime, Key}), - true = ets:update_element(Objs, Key, {#entry.atime, NewATime}), + Update = {#entry.atime, NewATime}, + true = ets:update_element(St#st.objects, Key, Update), + true = ets:delete(St#st.atimes, ATime), + true = ets:insert(St#st.atimes, {NewATime, Key}), ok; [] -> ok end. -expire(#ets_lru{lifetime=undefined}) -> - ok; -expire(#ets_lru{objects=Objs, atimes=ATs, lifetime=LT}=LRU) -> - Now = os:timestamp(), - LTMicro = LT * 1000, - case ets:first(ATs) of - '$end_of_table' -> - ok; - ATime -> - case timer:now_diff(Now, ATime) > LTMicro of - true -> - [{ATime, Key}] = ets:lookup(ATs, ATime), - true = ets:delete(ATs, ATime), - true = ets:delete(Objs, Key), - expire(LRU); - false -> - ok - end - end. - - -clear(#ets_lru{objects=Objs, atimes=ATs}) -> - true = ets:delete_all_objects(Objs), - true = ets:delete_all_objects(ATs), - ok. +trim(St) -> + trim_count(St), + trim_size(St), + trim_lifetime(St). -trim(#ets_lru{}=LRU) -> - case trim_count(LRU) of - trimmed -> trim(LRU); - _ -> ok - end, - case trim_size(LRU) of - trimmed -> trim(LRU); - _ -> ok +trim_count(#st{max_objs=undefined}) -> + ok; +trim_count(#st{max_objs=Max}=St) -> + case ets:info(St#st.objects, size) > Max of + true -> + drop_lru(St, fun trim_count/1); + false -> + ok end. -trim_count(#ets_lru{max_objs=undefined}) -> +trim_size(#st{max_size=undefined}) -> ok; -trim_count(#ets_lru{objects=Objs, max_objs=MO}=LRU) -> - case ets:info(Objs, size) > MO of - true -> drop_entry(LRU); - false -> ok +trim_size(#st{max_size=Max}=St) -> + case ets:info(St#st.objects, memory) > Max of + true -> + drop_lru(St, fun trim_size/1); + false -> + ok end. -trim_size(#ets_lru{max_size=undefined}) -> +trim_lifetime(#st{max_lifetime=undefined}) -> ok; -trim_size(#ets_lru{objects=Objs, max_size=MS}=LRU) -> - case ets:info(Objs, memory) > MS of - true -> drop_entry(LRU); - false -> ok +trim_lifetime(#st{max_lifetime=Max}=St) -> + Now = os:timestamp(), + case ets:first(St#st.ctimes) of + '$end_of_table' -> + ok; + CTime -> + DiffInMilli = timer:now_diff(Now, CTime) div 1000, + case DiffInMilli > Max of + true -> + [{CTime, Key}] = ets:lookup(St#st.ctimes, CTime), + Pattern = #entry{key=Key, atime='$1', _='_'}, + [[ATime]] = ets:match(St#st.objects, Pattern), + true = ets:delete(St#st.objects, Key), + true = ets:delete(St#st.atimes, ATime), + true = ets:delete(St#st.ctimes, CTime), + trim_lifetime(St); + false -> + ok + end end. -drop_entry(#ets_lru{objects=Objs, atimes=ATs}) -> - case ets:first(ATs) of +drop_lru(St, Continue) -> + case ets:first(St#st.atimes) of '$end_of_table' -> empty; ATime -> - [{ATime, Key}] = ets:lookup(ATs, ATime), - true = ets:delete(ATs, ATime), - true = ets:delete(Objs, Key), - trimmed + [{ATime, Key}] = ets:lookup(St#st.atimes, ATime), + Pattern = #entry{key=Key, ctime='$1', _='_'}, + [[CTime]] = ets:match(St#st.objects, Pattern), + true = ets:delete(St#st.objects, Key), + true = ets:delete(St#st.atimes, ATime), + true = ets:delete(St#st.ctimes, CTime), + Continue(St) + end. + + +next_timeout(#st{max_lifetime=undefined}) -> + infinity; +next_timeout(St) -> + case ets:first(St#st.ctimes) of + '$end_of_table' -> + infinity; + CTime -> + Now = os:timestamp(), + DiffInMilli = timer:now_diff(Now, CTime) div 1000, + erlang:max(St#st.max_lifetime - DiffInMilli, 0) end. -set_options(LRU, []) -> - LRU; -set_options(LRU, [named_tables | Rest]) -> - set_options(LRU#ets_lru{named=true}, Rest); -set_options(LRU, [{max_objects, N} | Rest]) when is_integer(N), N > 0 -> - set_options(LRU#ets_lru{max_objs=N}, Rest); -set_options(LRU, [{max_size, N} | Rest]) when is_integer(N), N > 0 -> - set_options(LRU#ets_lru{max_size=N}, Rest); -set_options(LRU, [{lifetime, N} | Rest]) when is_integer(N), N > 0 -> - set_options(LRU#ets_lru{lifetime=N}, Rest); +set_options(St, []) -> + St; +set_options(St, [{max_objects, N} | Rest]) when is_integer(N), N > 0 -> + set_options(St#st{max_objs=N}, Rest); +set_options(St, [{max_size, N} | Rest]) when is_integer(N), N > 0 -> + set_options(St#st{max_size=N}, Rest); +set_options(St, [{max_lifetime, N} | Rest]) when is_integer(N), N > 0 -> + set_options(St#st{max_lifetime=N}, Rest); set_options(_, [Opt | _]) -> throw({invalid_option, Opt}). -table_names(Base) when is_atom(Base) -> - BList = atom_to_list(Base), - OName = list_to_atom(BList ++ "_objects"), - ATName = list_to_atom(BList ++ "_atimes"), - {OName, ATName}. +obj_table(Name) -> + table_name(Name, "_objects"). + + +at_table(Name) -> + table_name(Name, "_atimes"). + + +ct_table(Name) -> + table_name(Name, "_ctimes"). + +table_name(Name, Ext) -> + list_to_atom(atom_to_list(Name) ++ Ext). http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/01-basic-behavior.t ---------------------------------------------------------------------- diff --git a/test/01-basic-behavior.t b/test/01-basic-behavior.t index a35a944..7e87a5d 100755 --- a/test/01-basic-behavior.t +++ b/test/01-basic-behavior.t @@ -6,29 +6,50 @@ main([]) -> code:add_pathz("test"), code:add_pathz("ebin"), - tutil:run(12, fun() -> test() end). + tutil:run(16, fun() -> test() end). test() -> test_lifecycle(), + test_table_names(), ?WITH_LRU(test_insert_lookup), ?WITH_LRU(test_insert_overwrite), ?WITH_LRU(test_insert_remove), - ?WITH_LRU(test_member), ?WITH_LRU(test_clear), ok. test_lifecycle() -> - Resp = ets_lru:create(?MODULE, []), + Resp = ets_lru:start_link(?MODULE, []), etap:fun_is( - fun({ok, _LRU}) -> true; (_) -> false end, + fun({ok, LRU}) when is_pid(LRU) -> true; (_) -> false end, Resp, - "ets_lru:create/2 returned an LRU" + "ets_lru:start_link/2 returned an LRU" ), {ok, LRU} = Resp, - etap:is(ok, ets_lru:destroy(LRU), "Destroyed the LRU ok"). + etap:is(ok, ets_lru:stop(LRU), "Destroyed the LRU ok"). + + +test_table_names() -> + {ok, LRU} = ets_lru:start_link(foo, []), + Exists = fun(Name) -> ets:info(Name, size) == 0 end, + NExists = fun(Name) -> ets:info(Name, size) == undefined end, + etap:is(Exists(foo_objects), true, "foo_objects exists"), + etap:is(Exists(foo_atimes), true, "foo_atimes exists"), + etap:is(Exists(foo_ctimes), true, "foo_ctimes exists"), + + Ref = erlang:monitor(process, LRU), + ets_lru:stop(LRU), + + receive {'DOWN', Ref, process, LRU, Reason} -> ok end, + etap:is(Reason, normal, "LRU stopped normally"), + + etap:is(NExists(foo_objects), true, "foo_objects doesn't exist"), + etap:is(NExists(foo_atimes), true, "foo_atimes doesn't exist"), + etap:is(NExists(foo_ctimes), true, "foo_ctimes doesn't exist"), + + ok. test_insert_lookup(LRU) -> @@ -37,6 +58,12 @@ test_insert_lookup(LRU) -> etap:is(Resp, {ok, bar}, "Lookup returned the inserted value"). +test_insert_lookup_d(LRU) -> + ok = ets_lru:insert(LRU, foo, bar), + Resp = ets_lru:lookup_d(test_lru, foo), + etap:is(Resp, {ok, bar}, "Dirty lookup returned the inserted value"). + + test_insert_overwrite(LRU) -> ok = ets_lru:insert(LRU, foo, bar), Resp1 = ets_lru:lookup(LRU, foo), @@ -55,14 +82,6 @@ test_insert_remove(LRU) -> etap:is(Resp2, not_found, "Lookup returned not_found for removed value"). -test_member(LRU) -> - etap:is(false, ets_lru:member(LRU, foo), "Not yet a member: foo"), - ok = ets_lru:insert(LRU, foo, bar), - etap:is(true, ets_lru:member(LRU, foo), "Now a member: foo"), - ok = ets_lru:remove(LRU, foo), - etap:is(false, ets_lru:member(LRU, foo), "No longer a member: foo"). - - test_clear(LRU) -> ok = ets_lru:insert(LRU, foo, bar), Resp1 = ets_lru:lookup(LRU, foo), http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/02-lru-options.t ---------------------------------------------------------------------- diff --git a/test/02-lru-options.t b/test/02-lru-options.t index 7dbec8f..59d0ba1 100755 --- a/test/02-lru-options.t +++ b/test/02-lru-options.t @@ -4,26 +4,15 @@ main([]) -> code:add_pathz("test"), code:add_pathz("ebin"), - tutil:run(unknown, fun() -> test() end). + tutil:run(9, fun() -> test() end). test() -> - test_named_tables(), test_max_objects(), test_max_size(), test_lifetime(), test_bad_option(), - - ok. - -test_named_tables() -> - {ok, LRU} = ets_lru:create(foo, [named_tables]), - etap:is(ets:info(foo_objects, size), 0, "foo_objects table exists"), - etap:is(ets:info(foo_atimes, size), 0, "foo_atimes table exists"), - ok = ets_lru:destroy(LRU), - etap:isnt(catch ets:info(foo_objects, size), 0, "foo_objects is gone"), - etap:isnt(catch ets:info(foo_atimes, size), 0, "foo_atimes is gone"), ok. @@ -38,32 +27,35 @@ test_max_size() -> % See also: 04-limit-max-size.t test_good([{max_size, 1}]), test_good([{max_size, 5}]), - test_good([{max_size, 23423409090923423942309423094}]). + test_good([{max_size, 2342923423942309423094}]). test_lifetime() -> % See also: 05-limit-lifetime.t - test_good([{lifetime, 1}]), - test_good([{lifetime, 5}]), - test_good([{lifetime, 1244209909182409328409283409238}]). + test_good([{max_lifetime, 1}]), + test_good([{max_lifetime, 5}]), + test_good([{max_lifetime, 1244209909180928348}]). test_bad_option() -> - test_bad([{bingo, bango}]), - test_bad([12]), - test_bad([true]). - + % Figure out a test for these. + %test_bad([{bingo, bango}]), + %test_bad([12]), + %test_bad([true]). + ok. + test_good(Options) -> + Msg = io_lib:format("LRU created ok with options: ~w", [Options]), etap:fun_is(fun - ({ok, LRU}) -> ets_lru:destroy(LRU), true; + ({ok, LRU}) when is_pid(LRU) -> ets_lru:stop(LRU), true; (_) -> false - end, ets_lru:create(?MODULE, Options), "LRU created ok with options"). + end, ets_lru:start_link(?MODULE, Options), lists:flatten(Msg)). -test_bad(Options) -> - etap:fun_is(fun - ({invalid_option, _}) -> true; - ({ok, LRU}) -> ets_lru:destroy(LRU), false; - (_) -> false - end, catch ets_lru:create(?MODULE, Options), "LRU error with options"). \ No newline at end of file +% test_bad(Options) -> +% etap:fun_is(fun +% ({invalid_option, _}) -> true; +% ({ok, LRU}) -> ets_lru:stop(LRU), false; +% (_) -> false +% end, catch ets_lru:start_link(?MODULE, Options), "LRU bad options"). http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/03-limit-max-objects.t ---------------------------------------------------------------------- diff --git a/test/03-limit-max-objects.t b/test/03-limit-max-objects.t index ccf9e7d..bd4e793 100755 --- a/test/03-limit-max-objects.t +++ b/test/03-limit-max-objects.t @@ -10,9 +10,9 @@ main([]) -> test() -> - {ok, LRU} = ets_lru:create(lru, [named_tables, {max_objects, objs()}]), + {ok, LRU} = ets_lru:start_link(lru, [{max_objects, objs()}]), etap:is(insert_kvs(LRU, 100 * objs()), ok, "Max object count ok"), - ok = ets_lru:destroy(LRU). + ok = ets_lru:stop(LRU). insert_kvs(LRU, 0) -> http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/04-limit-max-size.t ---------------------------------------------------------------------- diff --git a/test/04-limit-max-size.t b/test/04-limit-max-size.t index b318e27..5cdf0ce 100755 --- a/test/04-limit-max-size.t +++ b/test/04-limit-max-size.t @@ -10,9 +10,9 @@ main([]) -> test() -> - {ok, LRU} = ets_lru:create(lru, [named_tables, {max_size, max_size()}]), + {ok, LRU} = ets_lru:start_link(lru, [{max_size, max_size()}]), etap:is(insert_kvs(LRU, 10000), ok, "Max size ok"), - ok = ets_lru:destroy(LRU). + ok = ets_lru:stop(LRU). insert_kvs(LRU, 0) -> http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/05-limit-lifetime.t ---------------------------------------------------------------------- diff --git a/test/05-limit-lifetime.t b/test/05-limit-lifetime.t index 2421579..95effb2 100755 --- a/test/05-limit-lifetime.t +++ b/test/05-limit-lifetime.t @@ -1,15 +1,23 @@ #! /usr/bin/env escript -lifetime() -> 1024. +lifetime() -> 100. main([]) -> code:add_pathz("test"), code:add_pathz("ebin"), - tutil:run(unknown, fun() -> test() end). + tutil:run(2, fun() -> test() end). test() -> - {ok, LRU} = ets_lru:create(lru, [named_tables, {lifetime, lifetime()}]), - % Figure out how to test this. - ok = ets_lru:destroy(LRU). + {ok, LRU} = ets_lru:start_link(lru, [{max_lifetime, lifetime()}]), + ok = test_single_entry(LRU), + ok = ets_lru:stop(LRU). + + +test_single_entry(LRU) -> + ets_lru:insert(LRU, foo, bar), + etap:is(ets_lru:lookup(LRU, foo), {ok, bar}, "Expire leaves new entries"), + timer:sleep(round(lifetime() * 1.5)), + etap:is(ets_lru:lookup(LRU, foo), not_found, "Entry was expired"), + ok. http://git-wip-us.apache.org/repos/asf/couchdb-ets-lru/blob/3f2bae1d/test/tutil.erl ---------------------------------------------------------------------- diff --git a/test/tutil.erl b/test/tutil.erl index ac258e6..3e4bd68 100644 --- a/test/tutil.erl +++ b/test/tutil.erl @@ -18,9 +18,12 @@ run(Plan, Fun) -> with_lru(Fun) -> - {ok, LRU} = ets_lru:create(?MODULE, []), + {ok, LRU} = ets_lru:start_link(test_lru, []), + Ref = erlang:monitor(process, LRU), try Fun(LRU) after - ets_lru:destroy(LRU) - end. \ No newline at end of file + ets_lru:stop(LRU), + receive {'DOWN', Ref, process, LRU, _} -> ok end + end. +
