Skip to content

Commit 19e9d1a

Browse files
committed
Merge pull request #1406 from TheSobkiewicz/thesobkiewicz/nifs/ets/update_counter
Add ets:update_counter Currently this solution doesn't support list of UpdateOp to update multiple counters. Based on: #1405 https://www.erlang.org/docs/23/man/ets#update_counter-3 These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents 431cc4a + 2e927e7 commit 19e9d1a

File tree

7 files changed

+233
-3
lines changed

7 files changed

+233
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Added `esp:partition_read/3`, and documentation for `esp:partition_erase_range/2/3` and `esp:partition_write/3`
2020
- Added support for list insertion in 'ets:insert/2'.
2121
- Support to OTP-28
22+
- Added support for `ets:update_counter/3` and `ets:update_counter/4`.
2223

2324
### Fixed
2425
- ESP32: improved sntp sync speed from a cold boot.

libs/estdlib/src/ets.erl

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
insert/2,
3030
lookup/2,
3131
lookup_element/3,
32-
delete/2
32+
delete/2,
33+
update_counter/3,
34+
update_counter/4
3335
]).
3436

3537
-export_type([
@@ -101,3 +103,41 @@ lookup_element(_Table, _Key, _Pos) ->
101103
-spec delete(Table :: table(), Key :: term()) -> true.
102104
delete(_Table, _Key) ->
103105
erlang:nif_error(undefined).
106+
107+
%%-----------------------------------------------------------------------------
108+
%% @param Table a reference to the ets table
109+
%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer
110+
%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue},
111+
%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. Value is clamped to SetValue if it exceeds Threshold after update.
112+
%% @returns the updated element's value after performing the increment, or the default value if applicable
113+
%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key.
114+
%% @end
115+
%%-----------------------------------------------------------------------------
116+
-spec update_counter(
117+
Table :: table(),
118+
Key :: term(),
119+
Params ::
120+
integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()}
121+
) -> integer().
122+
update_counter(_Table, _Key, _Params) ->
123+
erlang:nif_error(undefined).
124+
125+
%%-----------------------------------------------------------------------------
126+
%% @param Table a reference to the ets table
127+
%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer
128+
%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue},
129+
%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. If after incrementation value exceeds the Treshold, it is set to SetValue.
130+
%% @param Default the default value used if the entry at Key doesn't exist or doesn't contain a valid tuple with a sufficient size or integer at Pos
131+
%% @returns the updated element's value after performing the increment, or the default value if applicable
132+
%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key. If the needed element does not exist, uses Default value as a fallback.
133+
%% @end
134+
%%-----------------------------------------------------------------------------
135+
-spec update_counter(
136+
Table :: table(),
137+
Key :: term(),
138+
Params ::
139+
integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()},
140+
Default :: integer()
141+
) -> integer().
142+
update_counter(_Table, _Key, _Params, _Default) ->
143+
erlang:nif_error(undefined).

src/libAtomVM/ets.c

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "ets_hashtable.h"
2626
#include "list.h"
2727
#include "memory.h"
28+
#include "overflow_helpers.h"
2829
#include "term.h"
2930

3031
#ifndef AVM_NO_SMP
@@ -445,3 +446,129 @@ EtsErrorCode ets_delete(term name_or_ref, term key, term *ret, Context *ctx)
445446

446447
return EtsOk;
447448
}
449+
450+
static bool operation_to_tuple4(term operation, size_t default_pos, term *position, term *increment, term *threshold, term *set_value)
451+
{
452+
if (term_is_integer(operation)) {
453+
*increment = operation;
454+
*position = term_from_int(default_pos);
455+
*threshold = term_invalid_term();
456+
*set_value = term_invalid_term();
457+
return true;
458+
}
459+
460+
if (UNLIKELY(!term_is_tuple(operation))) {
461+
return false;
462+
}
463+
int n = term_get_tuple_arity(operation);
464+
if (UNLIKELY(n != 2 && n != 4)) {
465+
return false;
466+
}
467+
468+
term pos = term_get_tuple_element(operation, 0);
469+
term incr = term_get_tuple_element(operation, 1);
470+
if (UNLIKELY(!term_is_integer(pos) || !term_is_integer(incr))) {
471+
return false;
472+
}
473+
474+
if (n == 2) {
475+
*position = pos;
476+
*increment = incr;
477+
*threshold = term_invalid_term();
478+
*set_value = term_invalid_term();
479+
return true;
480+
}
481+
482+
term tresh = term_get_tuple_element(operation, 2);
483+
term set_val = term_get_tuple_element(operation, 3);
484+
if (UNLIKELY(!term_is_integer(tresh) || !term_is_integer(set_val))) {
485+
return false;
486+
}
487+
488+
*position = pos;
489+
*increment = incr;
490+
*threshold = tresh;
491+
*set_value = set_val;
492+
return true;
493+
}
494+
495+
EtsErrorCode ets_update_counter(term ref, term key, term operation, term default_value, term *ret, Context *ctx)
496+
{
497+
struct EtsTable *ets_table = term_is_atom(ref)
498+
? ets_get_table_by_name(&ctx->global->ets, ref, TableAccessWrite)
499+
: ets_get_table_by_ref(&ctx->global->ets, term_to_ref_ticks(ref), TableAccessWrite);
500+
if (IS_NULL_PTR(ets_table)) {
501+
return EtsTableNotFound;
502+
}
503+
504+
term list;
505+
EtsErrorCode result = ets_table_lookup(ets_table, key, &list, ctx);
506+
if (UNLIKELY(result != EtsOk)) {
507+
SMP_UNLOCK(ets_table);
508+
return result;
509+
}
510+
511+
term to_insert;
512+
if (term_is_nil(list)) {
513+
if (term_is_invalid_term(default_value)) {
514+
SMP_UNLOCK(ets_table);
515+
return EtsBadEntry;
516+
}
517+
to_insert = default_value;
518+
} else {
519+
to_insert = term_get_list_head(list);
520+
}
521+
522+
if (UNLIKELY(!term_is_tuple(to_insert))) {
523+
SMP_UNLOCK(ets_table);
524+
return EtsBadEntry;
525+
}
526+
term position_term;
527+
term increment_term;
528+
term threshold_term;
529+
term set_value_term;
530+
// +1 to position, +1 to elem after key
531+
size_t default_pos = (ets_table->keypos + 1) + 1;
532+
533+
if (UNLIKELY(!operation_to_tuple4(operation, default_pos, &position_term, &increment_term, &threshold_term, &set_value_term))) {
534+
SMP_UNLOCK(ets_table);
535+
return EtsBadEntry;
536+
}
537+
int arity = term_get_tuple_arity(to_insert);
538+
avm_int_t position = term_to_int(position_term) - 1;
539+
if (UNLIKELY(arity <= position || position < 1)) {
540+
SMP_UNLOCK(ets_table);
541+
return EtsBadEntry;
542+
}
543+
544+
term elem = term_get_tuple_element(to_insert, position);
545+
if (UNLIKELY(!term_is_integer(elem))) {
546+
SMP_UNLOCK(ets_table);
547+
return EtsBadEntry;
548+
}
549+
avm_int_t increment = term_to_int(increment_term);
550+
avm_int_t elem_value;
551+
if (BUILTIN_ADD_OVERFLOW_INT(increment, term_to_int(elem), &elem_value)) {
552+
SMP_UNLOCK(ets_table);
553+
return EtsOverlfow;
554+
}
555+
if (!term_is_invalid_term(threshold_term) && !term_is_invalid_term(set_value_term)) {
556+
avm_int_t threshold = term_to_int(threshold_term);
557+
avm_int_t set_value = term_to_int(set_value_term);
558+
559+
if (increment >= 0 && elem_value > threshold) {
560+
elem_value = set_value;
561+
} else if (increment < 0 && elem_value < threshold) {
562+
elem_value = set_value;
563+
}
564+
}
565+
566+
term final_value = term_from_int(elem_value);
567+
term_put_tuple_element(to_insert, position, final_value);
568+
EtsErrorCode insert_result = ets_table_insert(ets_table, to_insert, ctx);
569+
if (insert_result == EtsOk) {
570+
*ret = final_value;
571+
}
572+
SMP_UNLOCK(ets_table);
573+
return insert_result;
574+
}

src/libAtomVM/ets.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ typedef enum EtsErrorCode
5757
EtsBadEntry,
5858
EtsAllocationFailure,
5959
EtsEntryNotFound,
60-
EtsBadPosition
60+
EtsBadPosition,
61+
EtsOverlfow
6162
} EtsErrorCode;
6263
struct Ets
6364
{
@@ -77,7 +78,7 @@ EtsErrorCode ets_insert(term ref, term entry, Context *ctx);
7778
EtsErrorCode ets_lookup(term ref, term key, term *ret, Context *ctx);
7879
EtsErrorCode ets_lookup_element(term ref, term key, size_t pos, term *ret, Context *ctx);
7980
EtsErrorCode ets_delete(term ref, term key, term *ret, Context *ctx);
80-
81+
EtsErrorCode ets_update_counter(term ref, term key, term value, term pos, term *ret, Context *ctx);
8182
#ifdef __cplusplus
8283
}
8384
#endif

src/libAtomVM/nifs.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ static term nif_ets_insert(Context *ctx, int argc, term argv[]);
153153
static term nif_ets_lookup(Context *ctx, int argc, term argv[]);
154154
static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]);
155155
static term nif_ets_delete(Context *ctx, int argc, term argv[]);
156+
static term nif_ets_update_counter(Context *ctx, int argc, term argv[]);
156157
static term nif_erlang_pid_to_list(Context *ctx, int argc, term argv[]);
157158
static term nif_erlang_ref_to_list(Context *ctx, int argc, term argv[]);
158159
static term nif_erlang_fun_to_list(Context *ctx, int argc, term argv[]);
@@ -680,6 +681,12 @@ static const struct Nif ets_delete_nif =
680681
.nif_ptr = nif_ets_delete
681682
};
682683

684+
static const struct Nif ets_update_counter_nif =
685+
{
686+
.base.type = NIFFunctionType,
687+
.nif_ptr = nif_ets_update_counter
688+
};
689+
683690
static const struct Nif atomvm_add_avm_pack_binary_nif =
684691
{
685692
.base.type = NIFFunctionType,
@@ -3294,6 +3301,39 @@ static term nif_ets_delete(Context *ctx, int argc, term argv[])
32943301
}
32953302
}
32963303

3304+
static term nif_ets_update_counter(Context *ctx, int argc, term argv[])
3305+
{
3306+
term ref = argv[0];
3307+
VALIDATE_VALUE(ref, is_ets_table_id);
3308+
3309+
term key = argv[1];
3310+
term operation = argv[2];
3311+
term default_value;
3312+
if (argc == 4) {
3313+
default_value = argv[3];
3314+
VALIDATE_VALUE(default_value, term_is_tuple);
3315+
term_put_tuple_element(default_value, 0, key);
3316+
} else {
3317+
default_value = term_invalid_term();
3318+
}
3319+
term ret;
3320+
EtsErrorCode result = ets_update_counter(ref, key, operation, default_value, &ret, ctx);
3321+
switch (result) {
3322+
case EtsOk:
3323+
return ret;
3324+
case EtsTableNotFound:
3325+
case EtsPermissionDenied:
3326+
case EtsBadEntry:
3327+
RAISE_ERROR(BADARG_ATOM);
3328+
case EtsAllocationFailure:
3329+
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
3330+
case EtsOverlfow:
3331+
RAISE_ERROR(OVERFLOW_ATOM);
3332+
default:
3333+
UNREACHABLE();
3334+
}
3335+
}
3336+
32973337
static term nif_erts_debug_flat_size(Context *ctx, int argc, term argv[])
32983338
{
32993339
UNUSED(ctx);

src/libAtomVM/nifs.gperf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ ets:insert/2, &ets_insert_nif
130130
ets:lookup/2, &ets_lookup_nif
131131
ets:lookup_element/3, &ets_lookup_element_nif
132132
ets:delete/2, &ets_delete_nif
133+
ets:update_counter/3, &ets_update_counter_nif
134+
ets:update_counter/4, &ets_update_counter_nif
133135
atomvm:add_avm_pack_binary/2, &atomvm_add_avm_pack_binary_nif
134136
atomvm:add_avm_pack_file/2, &atomvm_add_avm_pack_file_nif
135137
atomvm:close_avm_pack/2, &atomvm_close_avm_pack_nif

tests/erlang_tests/test_ets.erl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ start() ->
3232
ok = test_public_access(),
3333
ok = test_lookup_element(),
3434
ok = test_insert_list(),
35+
ok = test_update_counter(),
3536
0.
3637

3738
test_basic() ->
@@ -367,3 +368,21 @@ test_insert_list() ->
367368
end),
368369
expect_failure(fun() -> ets:insert(Tid, [{}]) end),
369370
ok.
371+
372+
test_update_counter() ->
373+
Tid = ets:new(test_lookup_element, []),
374+
true = ets:insert(Tid, {foo, 1, 2, 3}),
375+
3 = ets:update_counter(Tid, foo, 2),
376+
expect_failure(fun() -> ets:update_counter(Tid, tapas, 2) end),
377+
5 = ets:update_counter(Tid, tapas, 2, {batat, 3}),
378+
[] = ets:lookup(Tid, batat),
379+
[{tapas, 5}] = ets:lookup(Tid, tapas),
380+
0 = ets:update_counter(Tid, foo, {3, -2}),
381+
expect_failure(fun() -> ets:update_counter(Tid, foo, {-3, -2}) end),
382+
expect_failure(fun() -> ets:update_counter(Tid, foo, {30, -2}) end),
383+
expect_failure(fun() -> ets:update_counter(Tid, patatas, {3, -2}, {cow, 1}) end),
384+
0 = ets:update_counter(Tid, patatas, {3, -2}, {cow, 1, 2, 3}),
385+
0 = ets:update_counter(Tid, patatas, {3, -2, 0, 0}),
386+
10 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}),
387+
0 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}),
388+
ok.

0 commit comments

Comments
 (0)