Skip to content
Permalink
Browse files
Parameterize time unit
Previously, time values took the form {MegaSec, Sec, MicroSec}, and
erlang:now/0 was used to guarantee strict monotonic uniqueness for
access time. With the deprecation of erlang:now/0, tuples of the form
{time_value, unique_monotonic_integer} now guarantee uniqueness of
access time. By default, the time unit for internal comparison remains
'millisecond', although it can now be optionally parameterized by any
time unit supported by erlang:monotonic_time/1.
  • Loading branch information
jaydoane committed Dec 23, 2018
1 parent 5891eee commit 40d705dc585f07ecb1cbe6c2c75026a13fc8e339
Showing 2 changed files with 44 additions and 17 deletions.
@@ -44,21 +44,27 @@
]).


-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 :: time_value()
}).

-record(st, {
objects,
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 @@ handle_call({match, KeySpec, ValueSpec}, _From, St) ->
{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]] ->
@@ -173,10 +179,11 @@ handle_call({insert, Key, Val}, _From, St) ->
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},
NewCTime = element(1, NewATime),
Entry = #entry{key=Key, val=Val, atime=NewATime, ctime=NewCTime},
true = ets:insert(St#st.objects, Entry),
true = ets:insert(St#st.atimes, {NewATime, Key}),
true = ets:insert(St#st.ctimes, {NewATime, Key})
true = ets:insert(St#st.ctimes, {NewCTime, Key})
end,
{reply, ok, St, 0};

@@ -233,7 +240,7 @@ accessed(Key, St) ->
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 +282,13 @@ trim_size(#st{max_size=Max}=St) ->
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
TimeDiff = Now - CTime,
case TimeDiff > Max of
true ->
[{CTime, Key}] = ets:lookup(St#st.ctimes, CTime),
Pattern = #entry{key=Key, atime='$1', _='_'},
@@ -318,9 +325,9 @@ next_timeout(St) ->
'$end_of_table' ->
infinity;
CTime ->
Now = os:timestamp(),
DiffInMilli = timer:now_diff(Now, CTime) div 1000,
erlang:max(St#st.max_lifetime - DiffInMilli, 0)
Now = erlang:monotonic_time(St#st.time_unit),
TimeDiff = Now - CTime,
erlang:max(St#st.max_lifetime - TimeDiff, 0)
end.


@@ -332,6 +339,8 @@ 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(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 +359,8 @@ ct_table(Name) ->

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])}.
@@ -325,3 +325,16 @@ stop_lru({ok, LRU}) ->
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)).

0 comments on commit 40d705d

Please sign in to comment.