Merge pull request #9 from cloudant/time-unit-parameterization

Time unit parameterization
diff --git a/.travis.yml b/.travis.yml
index 99e6cb3..6a3648b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,6 @@
 language: erlang
 
 otp_release:
-   - 17.5
-   - 17.4
-   - 17.1
-   - 17.0
-   - R16B03-1
-   - R14B04
-   - R14B02
+   - 21.2
+   - 20.3
+   - 19.3
diff --git a/src/ets_lru.app.src b/src/ets_lru.app.src
index 2573a0f..c81ce11 100644
--- a/src/ets_lru.app.src
+++ b/src/ets_lru.app.src
@@ -13,9 +13,6 @@
 {application, ets_lru, [
     {description, "ETS Base LRU Cache"},
     {vsn, git},
-    {modules, [
-        ets_lru
-    ]},
     {registered, []},
     {applications, [
         kernel,
diff --git a/src/ets_lru.erl b/src/ets_lru.erl
index 7a366ba..0f6fdb2 100644
--- a/src/ets_lru.erl
+++ b/src/ets_lru.erl
@@ -12,7 +12,7 @@
 
 -module(ets_lru).
 -behaviour(gen_server).
--vsn(1).
+-vsn(2).
 
 
 -export([
@@ -44,11 +44,16 @@
 ]).
 
 
+-define(DEFAULT_TIME_UNIT, millisecond).
+
+-type time_value() :: integer().
+-type strict_monotonic_time() :: {time_value(), integer()}.
+
 -record(entry, {
-    key,
-    val,
-    atime,
-    ctime
+    key :: term(),
+    val :: term(),
+    atime :: strict_monotonic_time(),
+    ctime :: strict_monotonic_time()
 }).
 
 -record(st, {
@@ -56,9 +61,10 @@
     atimes,
     ctimes,
 
-    max_objs,
-    max_size,
-    max_lifetime
+    max_objs :: non_neg_integer() | undefined,
+    max_size :: non_neg_integer() | undefined,
+    max_lifetime :: non_neg_integer() | undefined,
+    time_unit = ?DEFAULT_TIME_UNIT :: atom()
 }).
 
 
@@ -164,7 +170,7 @@
     {reply, Values, St, 0};
 
 handle_call({insert, Key, Val}, _From, St) ->
-    NewATime = erlang:now(),
+    NewATime = strict_monotonic_time(St#st.time_unit),
     Pattern = #entry{key=Key, atime='$1', _='_'},
     case ets:match(St#st.objects, Pattern) of
         [[ATime]] ->
@@ -233,7 +239,7 @@
     Pattern = #entry{key=Key, atime='$1', _='_'},
     case ets:match(St#st.objects, Pattern) of
         [[ATime]] ->
-            NewATime = erlang:now(),
+            NewATime = strict_monotonic_time(St#st.time_unit),
             Update = {#entry.atime, NewATime},
             true = ets:update_element(St#st.objects, Key, Update),
             true = ets:delete(St#st.atimes, ATime),
@@ -275,13 +281,12 @@
 trim_lifetime(#st{max_lifetime=undefined}) ->
     ok;
 trim_lifetime(#st{max_lifetime=Max}=St) ->
-    Now = os:timestamp(),
+    Now = erlang:monotonic_time(St#st.time_unit),
     case ets:first(St#st.ctimes) of
         '$end_of_table' ->
             ok;
-        CTime ->
-            DiffInMilli = timer:now_diff(Now, CTime) div 1000,
-            case DiffInMilli > Max of
+        CTime = {Time, _} ->
+            case Now - Time > Max of
                 true ->
                     [{CTime, Key}] = ets:lookup(St#st.ctimes, CTime),
                     Pattern = #entry{key=Key, atime='$1', _='_'},
@@ -317,10 +322,10 @@
     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)
+        {Time, _} ->
+            Now = erlang:monotonic_time(St#st.time_unit),
+            TimeDiff = Now - Time,
+            erlang:max(St#st.max_lifetime - TimeDiff, 0)
     end.
 
 
@@ -332,6 +337,8 @@
     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(St, [{time_unit, T} | Rest]) when is_atom(T) ->
+    set_options(St#st{time_unit=T}, Rest);
 set_options(_, [Opt | _]) ->
     throw({invalid_option, Opt}).
 
@@ -350,3 +357,8 @@
 
 table_name(Name, Ext) ->
     list_to_atom(atom_to_list(Name) ++ Ext).
+
+
+-spec strict_monotonic_time(atom()) -> strict_monotonic_time().
+strict_monotonic_time(TimeUnit) ->
+    {erlang:monotonic_time(TimeUnit), erlang:unique_integer([monotonic])}.
diff --git a/test/ets_lru_test.erl b/test/ets_lru_test.erl
index 21d8e28..f54299a 100644
--- a/test/ets_lru_test.erl
+++ b/test/ets_lru_test.erl
@@ -1,6 +1,5 @@
 -module(ets_lru_test).
 
--compile([export_all]).
 
 -include_lib("eunit/include/eunit.hrl").
 
@@ -326,3 +325,16 @@
     receive {'DOWN', Ref, process, LRU, Reason} -> Reason end;
 stop_lru({error, _}) ->
     ok.
+
+valid_parameterized_time_unit_test() ->
+    Opts = [{time_unit, microsecond}],
+    {ok, LRU} = ets_lru:start_link(lru_test, Opts),
+    ?assert(is_process_alive(LRU)),
+    ok = ets_lru:insert(LRU, foo, bar),
+    ?assertEqual({ok, bar}, ets_lru:lookup(LRU, foo)),
+    ?assertEqual(ok, ets_lru:stop(LRU)).
+
+invalid_parameterized_time_unit_test() ->
+    Opts = [{time_unit, invalid}],
+    {ok, LRU} = ets_lru:start_link(lru_test, Opts),
+    ?assertExit(_, ets_lru:insert(LRU, foo, bar)).