Skip to content

Commit db7d49f

Browse files
Merge pull request #18 from esl/entries_limit
Implement limiting the number of entries
2 parents 67e3de7 + e03dbc5 commit db7d49f

File tree

5 files changed

+126
-59
lines changed

5 files changed

+126
-59
lines changed

.github/workflows/ci.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ jobs:
3838
- run: rebar3 as test do cover, covertool generate
3939
if: ${{ matrix.otp == '27' }}
4040
- name: Upload code coverage
41-
uses: codecov/codecov-action@v2
41+
uses: codecov/codecov-action@v4
4242
if: ${{ matrix.otp == '27' }}
4343
with:
44-
file: "_build/test/covertool/segmented_cache.covertool.xml"
44+
files: _build/test/covertool/segmented_cache.covertool.xml
45+
token: ${{ secrets.CODECOV_TOKEN }}
46+
fail_ci_if_error: true
47+
verbose: true

rebar.config

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{minimum_otp_vsn, "21"}.
2+
13
{erl_opts,
24
[warn_missing_doc, warn_missing_spec, warn_unused_import,
35
warn_export_vars, verbose, report, debug_info

src/segmented_cache.erl

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,20 @@ For more information, see the README, and the function documentation.
3939
-type key() :: term().
4040
?DOC("Dynamic type of _values_ from cache clients.").
4141
-type value() :: term().
42+
?DOC("Maximum number of entries per segment. When filled, rotation is ensued.").
43+
-type entries_limit() :: infinity | non_neg_integer().
4244
?DOC("Merging function to use for resolving conflicts").
4345
-type merger_fun(Value) :: fun((Value, Value) -> Value).
4446
?DOC("Configuration values for the cache.").
4547
-type opts() :: #{scope => scope(),
4648
strategy => strategy(),
49+
entries_limit => entries_limit(),
4750
segment_num => non_neg_integer(),
4851
ttl => timeout() | {erlang:time_unit(), non_neg_integer()},
4952
merger_fun => merger_fun(term())}.
5053

5154
-export_type([scope/0, name/0, key/0, value/0, hit/0, delete_error/1,
52-
strategy/0, merger_fun/1, opts/0]).
55+
entries_limit/0, strategy/0, merger_fun/1, opts/0]).
5356

5457
%%====================================================================
5558
%% API

src/segmented_cache_helpers.erl

+84-50
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
-export([purge_last_segment_and_rotate/1]).
1010

1111
-record(segmented_cache, {scope :: segmented_cache:scope(),
12+
name :: segmented_cache:name(),
1213
strategy = fifo :: segmented_cache:strategy(),
14+
entries_limit = infinity :: segmented_cache:entries_limit(),
1315
index :: atomics:atomics_ref(),
1416
segments :: tuple(),
1517
merger_fun :: merger_fun(term())}).
@@ -26,16 +28,22 @@
2628

2729
-spec init_cache_config(segmented_cache:name(), segmented_cache:opts()) ->
2830
#{scope := segmented_cache:scope(), ttl := timeout()}.
29-
init_cache_config(Name, Opts) ->
30-
{Scope, N, TTL, Strategy, MergerFun} = assert_parameters(Opts),
31-
SegmentOpts = ets_settings(),
31+
init_cache_config(Name, Opts0) ->
32+
#{scope := Scope,
33+
strategy := Strategy,
34+
entries_limit := EntriesLimit,
35+
segment_num := N,
36+
ttl := TTL,
37+
merger_fun := MergerFun} = Opts = assert_parameters(Opts0),
38+
SegmentOpts = ets_settings(Opts),
3239
SegmentsList = lists:map(fun(_) -> ets:new(undefined, SegmentOpts) end, lists:seq(1, N)),
3340
Segments = list_to_tuple(SegmentsList),
3441
Index = atomics:new(1, [{signed, false}]),
3542
atomics:put(Index, 1, 1),
36-
Config = #segmented_cache{scope = Scope, strategy = Strategy, index = Index,
37-
segments = Segments, merger_fun = MergerFun},
38-
set_cache_config(Name, Config),
43+
Config = #segmented_cache{scope = Scope, name = Name, strategy = Strategy,
44+
index = Index, entries_limit = EntriesLimit,
45+
segments = Segments, merger_fun = MergerFun},
46+
persist_cache_config(Name, Config),
3947
#{scope => Scope, ttl => TTL}.
4048

4149
-spec get_cache_scope(segmented_cache:name()) -> segmented_cache:scope().
@@ -51,8 +59,8 @@ erase_cache_config(Name) ->
5159
get_cache_config(Name) ->
5260
persistent_term:get({?APP_KEY, Name}).
5361

54-
-spec set_cache_config(segmented_cache:name(), config()) -> ok.
55-
set_cache_config(Name, Config) ->
62+
-spec persist_cache_config(segmented_cache:name(), config()) -> ok.
63+
persist_cache_config(Name, Config) ->
5664
persistent_term:put({?APP_KEY, Name}, Config).
5765

5866
%%====================================================================
@@ -77,7 +85,7 @@ get_entry_span(Name, Key) when is_atom(Name) ->
7785
-spec put_entry_front(segmented_cache:name(), segmented_cache:key(), segmented_cache:value()) -> boolean().
7886
put_entry_front(Name, Key, Value) ->
7987
SegmentRecord = get_cache_config(Name),
80-
do_put_entry_front(SegmentRecord, Key, Value).
88+
do_put_entry_front(SegmentRecord, Key, Value, 3).
8189

8290
-spec merge_entry(segmented_cache:name(), segmented_cache:key(), segmented_cache:value()) -> boolean().
8391
merge_entry(Name, Key, Value) when is_atom(Name) ->
@@ -91,7 +99,7 @@ merge_entry(Name, Key, Value) when is_atom(Name) ->
9199
end,
92100
case iterate_fun_in_tables(Name, Key, F) of
93101
true -> true;
94-
false -> do_put_entry_front(SegmentRecord, Key, Value)
102+
false -> do_put_entry_front(SegmentRecord, Key, Value, 3)
95103
end.
96104

97105
-spec delete_entry(segmented_cache:name(), segmented_cache:key()) -> true.
@@ -187,27 +195,49 @@ apply_strategy(lru, _CurrentIndex, FoundIndex, Key, SegmentRecord) ->
187195
Segments = SegmentRecord#segmented_cache.segments,
188196
FoundInSegment = element(FoundIndex, Segments),
189197
try [{_, Value}] = ets:lookup(FoundInSegment, Key),
190-
do_put_entry_front(SegmentRecord, Key, Value)
198+
do_put_entry_front(SegmentRecord, Key, Value, 3)
191199
catch _:_ -> false
192200
end.
193201

194-
-spec do_put_entry_front(#segmented_cache{}, segmented_cache:key(), segmented_cache:value()) ->
202+
-spec do_put_entry_front(#segmented_cache{}, segmented_cache:key(), segmented_cache:value(), 0..3) ->
195203
boolean().
196-
do_put_entry_front(SegmentRecord, Key, Value) ->
197-
Atomic = SegmentRecord#segmented_cache.index,
204+
do_put_entry_front(_, _, _, 0) -> false;
205+
do_put_entry_front(#segmented_cache{
206+
name = Name,
207+
entries_limit = EntriesLimit,
208+
index = Atomic,
209+
segments = Segments,
210+
merger_fun = MergerFun
211+
} = SegmentRecord, Key, Value, Retry) ->
198212
Index = atomics:get(Atomic, 1),
199-
Segments = SegmentRecord#segmented_cache.segments,
200213
FrontSegment = element(Index, Segments),
201-
Inserted = case ets:insert_new(FrontSegment, {Key, Value}) of
202-
true -> true;
203-
false ->
204-
MergerFun = SegmentRecord#segmented_cache.merger_fun,
205-
compare_and_swap(3, FrontSegment, Key, Value, MergerFun)
206-
end,
207-
MaybeMovedIndex = atomics:get(Atomic, 1),
208-
case post_insert_check_should_retry(Inserted, Index, MaybeMovedIndex) of
209-
false -> Inserted;
210-
true -> do_put_entry_front(SegmentRecord, Key, Value)
214+
case insert_new(FrontSegment, Key, Value, EntriesLimit, Name) of
215+
retry ->
216+
do_put_entry_front(SegmentRecord, Key, Value, Retry - 1);
217+
true ->
218+
MaybeMovedIndex = atomics:get(Atomic, 1),
219+
case post_insert_check_should_retry(true, Index, MaybeMovedIndex) of
220+
false -> true;
221+
true -> do_put_entry_front(SegmentRecord, Key, Value, Retry - 1)
222+
end;
223+
false ->
224+
Inserted = compare_and_swap(3, FrontSegment, Key, Value, MergerFun),
225+
MaybeMovedIndex = atomics:get(Atomic, 1),
226+
case post_insert_check_should_retry(Inserted, Index, MaybeMovedIndex) of
227+
false -> Inserted;
228+
true -> do_put_entry_front(SegmentRecord, Key, Value, Retry - 1)
229+
end
230+
end.
231+
232+
insert_new(Table, Key, Value, infinity, _) ->
233+
ets:insert_new(Table, {Key, Value});
234+
insert_new(Table, Key, Value, EntriesLimit, Name) ->
235+
case EntriesLimit =< ets:info(Table, size) of
236+
false ->
237+
ets:insert_new(Table, {Key, Value});
238+
true ->
239+
purge_last_segment_and_rotate(Name),
240+
retry
211241
end.
212242

213243
-spec post_insert_check_should_retry(boolean(), integer(), integer()) -> boolean().
@@ -254,12 +284,14 @@ purge_last_segment_and_rotate(Name) ->
254284
atomics:put(SegmentRecord#segmented_cache.index, 1, NewIndex),
255285
NewIndex.
256286

257-
-spec assert_parameters(segmented_cache:opts()) ->
258-
{segmented_cache:name(), pos_integer(), timeout(), segmented_cache:strategy(), merger_fun(term())}.
259-
assert_parameters(Opts) when is_map(Opts) ->
260-
N = maps:get(segment_num, Opts, 3),
261-
true = is_integer(N) andalso N > 0,
262-
TTL0 = maps:get(ttl, Opts, {hours, 8}),
287+
-spec assert_parameters(segmented_cache:opts()) -> segmented_cache:opts().
288+
assert_parameters(Opts0) when is_map(Opts0) ->
289+
#{scope := Scope,
290+
strategy := Strategy,
291+
entries_limit := EntriesLimit,
292+
segment_num := N,
293+
ttl := TTL0,
294+
merger_fun := MergerFun} = Opts = maps:merge(defaults(), Opts0),
263295
TTL = case TTL0 of
264296
infinity -> infinity;
265297
{milliseconds, S} -> S;
@@ -268,31 +300,33 @@ assert_parameters(Opts) when is_map(Opts) ->
268300
{hours, H} -> timer:hours(H);
269301
T when is_integer(T) -> timer:minutes(T)
270302
end,
303+
true = is_integer(N) andalso N > 0,
304+
true = (EntriesLimit =:= infinity) orelse (is_integer(EntriesLimit) andalso EntriesLimit > 0),
271305
true = (TTL =:= infinity) orelse (is_integer(TTL) andalso N > 0),
272-
Strategy = maps:get(strategy, Opts, fifo),
273306
true = (Strategy =:= fifo) orelse (Strategy =:= lru),
274-
MergerFun = maps:get(merger_fun, Opts, fun segmented_cache_callbacks:default_merger_fun/2),
275307
true = is_function(MergerFun, 2),
276-
Scope = maps:get(scope, Opts, pg),
277308
true = (undefined =/= whereis(Scope)),
278-
{Scope, N, TTL, Strategy, MergerFun}.
309+
Opts#{ttl := TTL}.
310+
311+
defaults() ->
312+
#{scope => pg,
313+
strategy => fifo,
314+
entries_limit => infinity,
315+
segment_num => 3,
316+
ttl => {hours, 8},
317+
merger_fun => fun segmented_cache_callbacks:default_merger_fun/2}.
279318

280-
-ifdef(OTP_RELEASE).
281-
-if(?OTP_RELEASE >= 25).
282-
ets_settings() ->
283-
[set, public,
284-
{read_concurrency, true},
285-
{write_concurrency, auto},
286-
{decentralized_counters, true}].
287-
-elif(?OTP_RELEASE >= 21).
288-
ets_settings() ->
289-
[set, public,
290-
{read_concurrency, true},
291-
{write_concurrency, true},
292-
{decentralized_counters, true}].
293-
-endif.
319+
-if(?OTP_RELEASE >= 25).
320+
ets_settings(#{entries_limit := infinity}) ->
321+
[set, public,
322+
{read_concurrency, true},
323+
{write_concurrency, auto}];
324+
ets_settings(#{entries_limit := _}) ->
325+
[set, public,
326+
{read_concurrency, true},
327+
{write_concurrency, true}].
294328
-else.
295-
ets_settings() ->
329+
ets_settings(_Opts) ->
296330
[set, public,
297331
{read_concurrency, true},
298332
{write_concurrency, true},

test/segmented_cache_SUITE.erl

+31-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
init_per_testcase/2,
1414
end_per_testcase/2]).
1515

16+
-include_lib("stdlib/include/assert.hrl").
1617
-include_lib("common_test/include/ct.hrl").
1718
-include_lib("proper/include/proper.hrl").
1819

@@ -22,6 +23,7 @@
2223
all() ->
2324
[
2425
{group, basic_api},
26+
{group, cache_limits},
2527
{group, short_fifo},
2628
{group, lru}
2729
].
@@ -36,6 +38,10 @@ groups() ->
3638
put_entry_then_delete_it_then_not_member,
3739
stateful_property
3840
]},
41+
{cache_limits, [sequence],
42+
[
43+
ensure_configured_size_is_respected
44+
]},
3945
{short_fifo, [sequence],
4046
[
4147
put_entry_wait_and_check_false
@@ -72,15 +78,25 @@ end_per_suite(_Config) ->
7278
%%%===================================================================
7379
init_per_group(lru, Config) ->
7480
print_and_restart_counters(),
75-
{ok, Cleaner} = segmented_cache:start(?CACHE_NAME, #{strategy => lru,
76-
segment_num => 2,
77-
ttl => {milliseconds, 100}}),
81+
Opts = #{strategy => lru,
82+
segment_num => 2,
83+
ttl => {milliseconds, 100}},
84+
{ok, Cleaner} = segmented_cache:start(?CACHE_NAME, Opts),
7885
[{cleaner, Cleaner} | Config];
7986
init_per_group(short_fifo, Config) ->
8087
print_and_restart_counters(),
81-
{ok, Cleaner} = segmented_cache:start(?CACHE_NAME, #{strategy => fifo,
82-
segment_num => 2,
83-
ttl => {milliseconds, 5}}),
88+
Opts = #{strategy => fifo,
89+
segment_num => 2,
90+
ttl => {milliseconds, 5}},
91+
{ok, Cleaner} = segmented_cache:start(?CACHE_NAME, Opts),
92+
[{cleaner, Cleaner} | Config];
93+
init_per_group(cache_limits, Config) ->
94+
print_and_restart_counters(),
95+
Opts = #{entries_limit => 1,
96+
strategy => fifo,
97+
segment_num => 2,
98+
ttl => {seconds, 60}},
99+
{ok, Cleaner} = segmented_cache:start(?CACHE_NAME, Opts),
84100
[{cleaner, Cleaner} | Config];
85101
init_per_group(_Groupname, Config) ->
86102
print_and_restart_counters(),
@@ -109,6 +125,15 @@ end_per_testcase(_TestCase, _Config) ->
109125
%%% Stateful property Test Case
110126
%%%===================================================================
111127

128+
ensure_configured_size_is_respected(_Config) ->
129+
%% We have 2 tables with 1 element each
130+
?assert(segmented_cache:put_entry(?CACHE_NAME, one, make_ref())),
131+
?assert(segmented_cache:put_entry(?CACHE_NAME, two, make_ref())),
132+
?assert(segmented_cache:put_entry(?CACHE_NAME, three, make_ref())),
133+
?assert(segmented_cache:is_member(?CACHE_NAME, three)),
134+
?assert(segmented_cache:is_member(?CACHE_NAME, two)),
135+
?assertNot(segmented_cache:is_member(?CACHE_NAME, one)).
136+
112137
stateful_property(_Config) ->
113138
Prop =
114139
?FORALL(Cmds, commands(?CMD_MODULE),

0 commit comments

Comments
 (0)