You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2019/02/21 03:52:34 UTC

[couchdb-ets-lru] 04/30: Refactor ets_lru into a gen_server

This is an automated email from the ASF dual-hosted git repository.

jaydoane pushed a commit to branch time-unit-parameterization
in repository https://gitbox.apache.org/repos/asf/couchdb-ets-lru.git

commit 3f2bae1d7aa30c47316e55232050a416d144ac92
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Fri Dec 28 02:14:00 2012 -0600

    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.
---
 .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(-)

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
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).
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),
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").
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) ->
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) ->
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.
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.
+