Skip to content

Commit f3a8d9b

Browse files
committed
Initial testing of ets_ring_buffer_ro
1 parent b65930d commit f3a8d9b

4 files changed

Lines changed: 240 additions & 78 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ERLC_OPTS := +debug_info +"{cover_enabled, true}"
88
TEST_ERLC_OPTS := -I include -I test/epocxy $(ERLC_OPTS)
99

1010
CT_OPTS := -cover test/epocxy.coverspec
11-
CT_SUITES = epocxy_sup epocxy_ets_fsm ets_buffer cxy_ctl cxy_cache
11+
CT_SUITES = epocxy_sup epocxy_ets_fsm ets_ring_buffer_ro ets_buffer cxy_ctl cxy_cache
1212

1313
DIALYZER_OPTS := -I include -Werror_handling -Wrace_conditions -Wunmatched_returns
1414

src/ets_ring_buffer_ro.erl

Lines changed: 136 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
delete/1, % remove data and delete metadata
3737
list/0, % provide a list of metadata for all ring buffers
3838
list/1, % provide metadata for a single ring buffer
39-
read/1 % read the next value and increment read position
39+
read/1, % read the next value and increment read position
40+
read_all/1, % read all values starting with current read pointer
41+
ring_size/1 % determine the number of ring buffer entries
4042
]).
4143

4244
-type ring_name() :: atom().
@@ -61,36 +63,44 @@
6163

6264
%% Record stored in ets table (also used for matchspecs).
6365
-record(ring_ro_metadata, {
64-
name :: {meta, ring_name()} | {meta, '_'},
65-
generation = 1 :: ring_generation() | '_',
66-
buffer :: ets:tid() | '_',
67-
created :: erlang:timestamp() | '_',
68-
size = 0 :: ring_size() | '_',
69-
read_loc = 0 :: ring_loc() | 0 | '_' % 0 signifies a buffer that has never been read
66+
name :: {meta, ring_name()} | {meta, '_'},
67+
generation = 1 :: ring_generation() | '_',
68+
buffer :: ets:tid() | '_',
69+
created :: erlang:timestamp() | '_',
70+
anti_swap_lock = 0 :: non_neg_integer() | '_',
71+
swapping_lock = 0 :: non_neg_integer() | '_',
72+
ring_size = 0 :: ring_size() | '_',
73+
read_loc = 0 :: ring_loc() | 0 | '_' % 0 signifies a buffer that has never been read
7074
}).
7175

72-
-define(RING_SIZE, {#ring_ro_metadata.size, 0}).
73-
-define(RING_BUFFER, {#ring_ro_metadata.buffer, 0}).
74-
-define(LAST_READ_LOC, {#ring_ro_metadata.read_loc, 0}).
75-
-define(BUMP_GENERATION, {#ring_ro_metadata.generation, 1}).
76+
-define(RING_SIZE, {#ring_ro_metadata.ring_size, 0}).
77+
-define(RING_BUFFER, {#ring_ro_metadata.buffer, 0}).
78+
-define(LAST_READ_LOC, {#ring_ro_metadata.read_loc, 0}).
79+
-define(BUMP_GENERATION, {#ring_ro_metadata.generation, 1}).
80+
81+
-define(READ_ANTI_SWAP, {#ring_ro_metadata.anti_swap_lock, 0}).
82+
-define(LOCK_ANTI_SWAP, {#ring_ro_metadata.anti_swap_lock, 1}).
83+
-define(UNLOCK_ANTI_SWAP, {#ring_ro_metadata.anti_swap_lock, -1}).
7684

7785
-define(RESERVE_READ_LOC(__Size), [?RING_BUFFER, {#ring_ro_metadata.read_loc, 1, __Size, 1}]).
86+
-define(RESERVE_READ_ALL_LOCS, [?RING_BUFFER, ?RING_SIZE, ?LAST_READ_LOC]).
7887

7988
meta_key(Ring_Name) -> {meta, Ring_Name}.
8089
make_meta(Ring_Name, Ring_Table_Id, Size) ->
81-
#ring_ro_metadata{name=meta_key(Ring_Name), buffer=Ring_Table_Id, created=os:timestamp(), size=Size}.
90+
#ring_ro_metadata{name=meta_key(Ring_Name), buffer=Ring_Table_Id, created=os:timestamp(), ring_size=Size}.
8291

8392
%% Convert record to proplist.
8493
make_ring_proplist(#ring_ro_metadata{name={meta, Name}, generation=Gen, buffer=Buffer,
85-
created=Created, size=Size, read_loc=Read_Loc}) ->
94+
created=Created, ring_size=Size, read_loc=Read_Loc}) ->
8695
[{name, Name}, {generation, Gen}, {buffer, Buffer},
87-
{created, Created}, {size, Size}, {read_loc, Read_Loc}].
96+
{created, Created}, {ring_size, Size}, {read_loc, Read_Loc}].
8897

8998
%% Match specs for buffers.
9099
all_rings() ->
91100
try ets:match_object(?RING_RO_TABLE, #ring_ro_metadata{name=meta_key('_'), _='_'})
92101
catch error:badarg -> []
93102
end.
103+
94104
one_ring(Ring_Name) ->
95105
try ets:match_object(?RING_RO_TABLE, #ring_ro_metadata{name=meta_key(Ring_Name), _='_'})
96106
catch error:badarg -> []
@@ -99,7 +109,11 @@ one_ring(Ring_Name) ->
99109
%% Use only writers to get the values so that a read lock isn't used on metadata.
100110
get_ring_size (Ring_Name) -> get_ring_metadata_field(Ring_Name, ?RING_SIZE).
101111
get_ring_last_read (Ring_Name) -> get_ring_metadata_field(Ring_Name, ?LAST_READ_LOC).
112+
get_ring_read_all (Ring_Name) -> get_ring_metadata_field(Ring_Name, ?RESERVE_READ_ALL_LOCS).
102113

114+
get_ring_size_lock (Ring_Name) -> get_ring_metadata_field(Ring_Name, [?RING_SIZE, ?LOCK_ANTI_SWAP]).
115+
unlock_anti_swap (Ring_Name) -> get_ring_metadata_field(Ring_Name, ?UNLOCK_ANTI_SWAP).
116+
103117
%% Reserve the next read location for the calling process to read.
104118
%% Since the pointer starts at 0, the reserved location is after increment
105119
%% and wrapping is applied. The LAST_READ_LOC will return this same value
@@ -113,10 +127,12 @@ get_ring_last_read (Ring_Name) -> get_ring_metadata_field(Ring_Name, ?LAST_READ_
113127
%% will be highly contended itself.
114128
get_ring_next_read(Ring_Name) ->
115129
%% Unfortunately, must fetch size before increment so we can properly wrap around the read pointer.
116-
case get_ring_size(Ring_Name) of
117-
0 -> {undefined, 0, 0};
118-
Size -> [Ring_Buffer, Read_Loc] = get_ring_metadata_field(Ring_Name, ?RESERVE_READ_LOC(Size)),
119-
{Ring_Buffer, Size, Read_Loc}
130+
%% This creates a race that could fail when a ring buffer is replaced, so we lock against swaps.
131+
try get_ring_size_lock(Ring_Name) of
132+
[0, _] -> {undefined, 0, 0};
133+
[Size, _] -> [Ring_Buffer, Read_Loc] = get_ring_metadata_field(Ring_Name, ?RESERVE_READ_LOC(Size)),
134+
{Ring_Buffer, Size, Read_Loc}
135+
after _ = unlock_anti_swap(Ring_Name)
120136
end.
121137

122138
%% Uses update_counter to maintain the write_concurrency lock.
@@ -152,68 +168,70 @@ make_ring_data(Name, Loc, Data) -> #ring_ro_data{key=ring_key(Name, Loc), data=D
152168
%%% External API
153169
%%%------------------------------------------------------------------------------
154170

171+
-define(ENSURE_METADATA,
172+
ets:info(?RING_RO_TABLE, named_table) =/= undefined
173+
orelse epocxy_ets_fsm:create_ets_table(?RING_RO_TABLE, write_only)).
174+
155175
-spec list() -> [proplists:proplist()].
156176
-spec list(ring_name()) -> proplists:proplist().
157177

158-
-spec create (ring_name(), [ring_data()]) -> boolean(). % with values
159-
-spec create (ring_name()) -> boolean(). % empty ring
160-
-spec clear (ring_name()) -> boolean(). % eliminate ring data only
161-
-spec delete (ring_name()) -> boolean(). % eliminate ring and metadata
162-
-spec read (ring_name()) -> {ok, ring_data()} | {error, ring_error()}.
178+
-spec create (ring_name(), [ring_data()]) -> boolean(). % with values
179+
-spec create (ring_name()) -> boolean(). % empty ring
180+
-spec clear (ring_name()) -> boolean(). % eliminate ring data only
181+
-spec delete (ring_name()) -> boolean(). % eliminate ring and metadata
182+
-spec read (ring_name()) -> {ok, ring_data() } | {error, ring_error()}.
183+
-spec read_all (ring_name()) -> {ok, [ring_data()] } | {error, ring_error()}.
184+
-spec ring_size (ring_name()) -> {ok, ring_size() } | {error, ring_error()}.
163185

164186
%% @doc Get a set of proplists for all ring buffers in the metadata ets table.
165-
list() -> [make_ring_proplist(Ring_Metadata) || Ring_Metadata <- all_rings()].
187+
list() ->
188+
?ENSURE_METADATA,
189+
[make_ring_proplist(Ring_Metadata) || Ring_Metadata <- all_rings()].
166190

167191
%% @doc Get a single proplist for a given ring buffer in the metadata ets table.
168192
list(Ring_Name) when is_atom(Ring_Name) ->
193+
?ENSURE_METADATA,
169194
case one_ring(Ring_Name) of
170195
[] -> [];
171196
[Ring_Metadata] -> make_ring_proplist(Ring_Metadata)
172197
end.
173198

174199
%% @doc Initialize an empty ring buffer.
175200
create(Ring_Name) when is_atom(Ring_Name) ->
176-
177-
%% Create global read_only ring ets meta-table when first ring is created...
178-
ets:info(?RING_RO_TABLE, named_table) =/= undefined
179-
orelse epocxy_ets_fsm:create_ets_table(?RING_RO_TABLE, write_only),
180-
181-
%% Then insert the metadata record.
201+
?ENSURE_METADATA,
182202
ets:insert_new(?RING_RO_TABLE, make_meta(Ring_Name, undefined, 0)).
183203

184-
185204
%% @doc
186205
%% Initialize a ring buffer with a set of values. The metadata is initialized
187206
%% with the buffer size set to the number of values and the current read pointer
188207
%% set to an initial value prior to the first element of the ring.
189208
%% @end
190209
create(Ring_Name, Ring_Values)
191210
when is_atom(Ring_Name), is_list(Ring_Values) ->
192-
193-
%% Create global read_only ring ets meta-table when first ring is created...
194-
ets:info(?RING_RO_TABLE, named_table) =/= undefined
195-
orelse epocxy_ets_fsm:create_ets_table(?RING_RO_TABLE, write_only),
196-
197211
%% Allocate a new unnamed ets table to hold the ring values...
212+
?ENSURE_METADATA,
198213
Ring_Buffer = epocxy_ets_fsm:create_ets_table(read_only),
199214
case create_ring(Ring_Name, Ring_Buffer, Ring_Values) of
200215
true -> true;
201216

202217
%% Eliminating created tables if there are errors inserting any of the values:
203218
%% 1) Ring name already exists
204-
%% 2) Duplicate keys already exist in the ring_buffer
219+
%% 2) Duplicate keys in the list of values (or someone else beat us inserting)
205220
false -> ets:delete(?RING_RO_TABLE, meta_key(Ring_Name)),
206221
epocxy_ets_fsm:delete_ets_table(Ring_Buffer)
207222
end.
208223

209224
%% @doc
210225
%% Create a new ring buffer table, then replace the metadata definition of the
211226
%% ring buffer, and finally delete the original ring buffer data table.
227+
%% A synchronous block of all readers should be applied during the call to
228+
%% replace the ring buffer.
212229
%% @end
213230
replace(Ring_Name, Ring_Values)
214231
when is_atom(Ring_Name), is_list(Ring_Values) ->
215232

216233
%% Allocate a new unnamed ets table to hold the ring values...
234+
?ENSURE_METADATA,
217235
Ring_Buffer = epocxy_ets_fsm:create_ets_table(read_only),
218236
replace_ring(Ring_Name, Ring_Buffer, Ring_Values).
219237

@@ -223,74 +241,110 @@ replace(Ring_Name, Ring_Values)
223241
%% lock penalty, but it is used infrequently.
224242
%% @end
225243
clear(Ring_Name) when is_atom(Ring_Name) ->
226-
case get_ring_metadata(Ring_Name) of
227-
missing -> false;
228-
229-
#ring_ro_metadata{buffer=Ring_Buffer} ->
230-
%% Clear the pointers in the metadata first, for immediate effect...
231-
try reset_metadata(Ring_Name, undefined, [])
232-
after epocxy_ets_fsm:delete_ets_table(Ring_Buffer)
233-
end
234-
end.
244+
clear(Ring_Name, get_ring_metadata(Ring_Name)).
245+
246+
clear(_Ring_Name, false) -> false;
247+
clear(_Ring_Name, missing) -> false;
248+
clear( Ring_Name, #ring_ro_metadata{anti_swap_lock=0, buffer=Ring_Buffer}) ->
249+
%% Clear the pointers in the metadata first, for mmediate effect...
250+
try reset_metadata(Ring_Name, undefined, [])
251+
after epocxy_ets_fsm:delete_ets_table(Ring_Buffer)
252+
end;
253+
%% Anti swap is locked, try again after swap action finishes.
254+
clear( Ring_Name, #ring_ro_metadata{}) ->
255+
erlang:yield(),
256+
clear(Ring_Name).
235257

236258
%% @doc
237259
%% Delete the ring metadata, then delete the ring data buffer. Thie function
238260
%% does a read on the ring metadata table so it will incur a slow lock
239261
%% penalty, but it is used infrequently.
240262
%% @end
241263
delete(Ring_Name) when is_atom(Ring_Name) ->
242-
case get_ring_metadata(Ring_Name) of
243-
missing -> false;
244-
#ring_ro_metadata{buffer=Ring_Buffer} ->
245-
try true = ets:delete(?RING_RO_TABLE, meta_key(Ring_Name))
246-
after Ring_Buffer =/= undefined
247-
andalso epocxy_ets_fsm:delete_ets_table(Ring_Buffer)
248-
end
249-
end.
264+
delete(Ring_Name, get_ring_metadata(Ring_Name)).
265+
266+
delete(_Ring_Name, false) -> false;
267+
delete(_Ring_Name, missing) -> false;
268+
delete( Ring_Name, #ring_ro_metadata{anti_swap_lock=0, buffer=Ring_Buffer}) ->
269+
try true = ets:delete(?RING_RO_TABLE, meta_key(Ring_Name))
270+
after Ring_Buffer =/= undefined
271+
andalso epocxy_ets_fsm:delete_ets_table(Ring_Buffer)
272+
end;
273+
%% Anti swap is locked, try again after swap action finishes.
274+
delete( Ring_Name, #ring_ro_metadata{}) ->
275+
erlang:yield(),
276+
delete(Ring_Name).
250277

251278
%% @doc
252279
%% Reserve the next read location, then read the data from the ring
253280
%% buffer ets table. The reservation is highly contended on a metadata
254281
%% record lock, but the actual read is much more concurrent.
255282
%% @end
256283
read(Ring_Name) when is_atom(Ring_Name) ->
257-
case get_ring_next_read(Ring_Name) of
258-
{undefined, 0, 0} ->
259-
{error, {buffer_is_empty, Ring_Name}};
260-
{Ring_Buffer, _Size, Location} when is_integer(Location), Location > 0 ->
261-
read_value(Ring_Name, Ring_Buffer, Location)
262-
end.
284+
read(Ring_Name, get_ring_next_read(Ring_Name)).
285+
286+
read(Ring_Name, false ) -> {error, {buffer_is_empty, Ring_Name}};
287+
read(Ring_Name, {undefined, 0, 0} ) -> {error, {buffer_is_empty, Ring_Name}};
288+
read(Ring_Name, {Ring_Buffer, _Size, Location})
289+
when is_integer(Location), Location > 0 ->
290+
read_value(Ring_Name, Ring_Buffer, Location).
291+
292+
%% @doc
293+
%% Discover the ring size and current read location, then read all
294+
%% data from the buffer ets table. The data is read starting with
295+
%% the current read location forward, wrapping and reading all data.
296+
%% @end
297+
read_all(Ring_Name) when is_atom(Ring_Name) ->
298+
read_all(Ring_Name, get_ring_read_all(Ring_Name)).
299+
300+
read_all(Ring_Name, false ) -> {error, {buffer_missing, Ring_Name}};
301+
read_all(Ring_Name, {undefined, 0, 0}) -> {error, {buffer_is_empty, Ring_Name}};
302+
read_all(Ring_Name, {Ring_Buffer, Size, Location})
303+
when is_integer(Size), Size > 0,
304+
is_integer(Location), Location > 0 ->
305+
read_all_values(Ring_Name, Ring_Buffer, Location, Size).
306+
307+
%% @doc Return the ring size of a buffer.
308+
ring_size(Ring_Name) when is_atom(Ring_Name) ->
309+
?ENSURE_METADATA,
310+
get_ring_size(Ring_Name).
263311

264312

265313
%%%------------------------------------------------------------------------------
266314
%%% Internal functions
267315
%%%------------------------------------------------------------------------------
268316

269317
create_ring(Ring_Name, Ring_Buffer, Ring_Values) ->
318+
insert_values(Ring_Name, Ring_Buffer, Ring_Values, 1),
270319
case ets:insert_new(?RING_RO_TABLE, make_meta(Ring_Name, Ring_Buffer, length(Ring_Values))) of
271-
false -> false;
272-
true -> insert_values(Ring_Name, Ring_Buffer, Ring_Values, 1)
320+
false -> epocxy_ets_fsm:delete_ets_table(Ring_Buffer),
321+
false;
322+
true -> true
273323
end.
274324

275325
replace_ring(Ring_Name, New_Ring_Buffer, Ring_Values) ->
276-
case get_ring_metadata(Ring_Name) of
277-
missing ->
278-
epocxy_ets_fsm:delete_ets_table(New_Ring_Buffer),
279-
false;
280-
#ring_ro_metadata{buffer=Old_Ring_Buffer} ->
281-
true = insert_values (Ring_Name, New_Ring_Buffer, Ring_Values, 1),
282-
try reset_metadata (Ring_Name, New_Ring_Buffer, Ring_Values)
283-
after Old_Ring_Buffer =/= undefined
284-
andalso epocxy_ets_fsm:delete_ets_table(Old_Ring_Buffer)
285-
end
286-
end.
326+
true = insert_values(Ring_Name, New_Ring_Buffer, Ring_Values, 1),
327+
replace_ring(Ring_Name, New_Ring_Buffer, Ring_Values, get_ring_metadata(Ring_Name)).
328+
329+
replace_ring(_Ring_Name, New_Ring_Buffer, _Ring_Values, missing) ->
330+
epocxy_ets_fsm:delete_ets_table(New_Ring_Buffer),
331+
false;
332+
replace_ring( Ring_Name, New_Ring_Buffer, Ring_Values,
333+
#ring_ro_metadata{anti_swap_lock=0, buffer=Old_Ring_Buffer}) ->
334+
try reset_metadata (Ring_Name, New_Ring_Buffer, Ring_Values)
335+
after Old_Ring_Buffer =:= undefined
336+
orelse epocxy_ets_fsm:delete_ets_table(Old_Ring_Buffer)
337+
end;
338+
%% Anti-swap lock is set, wait for it to clear.
339+
replace_ring( Ring_Name, New_Ring_Buffer, Ring_Values, #ring_ro_metadata{}) ->
340+
replace_ring(Ring_Name, New_Ring_Buffer, Ring_Values, get_ring_metadata(Ring_Name)).
287341

288342
reset_metadata(Ring_Name, New_Ring_Buffer, Ring_Values) ->
289343
Meta_Key = meta_key(Ring_Name),
290-
Reset_Values = [{#ring_ro_metadata.buffer, New_Ring_Buffer},
291-
{#ring_ro_metadata.created, os:timestamp()},
292-
{#ring_ro_metadata.size, length(Ring_Values)},
293-
{#ring_ro_metadata.read_loc, 0}],
344+
Reset_Values = [{#ring_ro_metadata.buffer, New_Ring_Buffer},
345+
{#ring_ro_metadata.created, os:timestamp()},
346+
{#ring_ro_metadata.ring_size, length(Ring_Values)},
347+
{#ring_ro_metadata.read_loc, 0}],
294348
ets:update_element(?RING_RO_TABLE, Meta_Key, Reset_Values),
295349

296350
%% Then increment the generation (in case someone else updated), and remove ring data.
@@ -314,3 +368,8 @@ read_value(Ring_Name, Ring_Buffer, Location) ->
314368
try ets:lookup_element(Ring_Buffer, Key, #ring_ro_data.data)
315369
catch error:badarg -> {missing_ring_data, Key}
316370
end.
371+
372+
read_all_values(Ring_Name, Ring_Buffer, Location, Size) ->
373+
[read_value(Ring_Name, Ring_Buffer, Pos)
374+
|| Pos <- lists:seq(Location, Size-Location)
375+
++ lists:seq(1, Location)].

test/epocxy.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{suites, epocxy, [
66
epocxy_sup_SUITE,
77
epocxy_ets_fsm_SUITE,
8+
ets_ring_buffer_ro_SUITE,
89
ets_buffer_SUITE,
910
cxy_ctl_SUITE,
1011
cxy_cache_SUITE

0 commit comments

Comments
 (0)