otp icon indicating copy to clipboard operation
otp copied to clipboard

Add persistent_term:put_new

Open Benjamin-Philip opened this issue 7 months ago • 15 comments

This PR adds the persistent_term:put_new to conditionally add a key-value only if the key is new.

Closes #9681.

/cc @jhogberg.

Benjamin-Philip avatar Apr 07 '25 10:04 Benjamin-Philip

CT Test Results

    3 files    142 suites   50m 17s ⏱️ 1 651 tests 1 593 ✅ 57 💤 1 ❌ 2 374 runs  2 296 ✅ 77 💤 1 ❌

For more details on these failures, see this check.

Results for commit abb29a72.

:recycle: This comment has been updated with latest results.

To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.

See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.

Artifacts

// Erlang/OTP Github Action Bot

github-actions[bot] avatar Apr 07 '25 10:04 github-actions[bot]

Instead of adding put_new, would it be more useful to add a has_key function that could be used in more cases? I recognize put_new has the benefit of being atomic but I assume that the value you need to store is likely expensive to compute anyway, so you want to check if the key exists before you do any work.

josevalim avatar Apr 19 '25 21:04 josevalim

I think a has_key would be a good addition, but not as a replacement to put_new. Like you said put_new has the benefit of being atomic which enables one to "correctly" (in a formal sense) update parallely out of the box. See the linked issue for my use case.

You can define a rudimentary has_key already by setting Default in get/2 to undefined or some other more specific atom. Nevertheless, has_key would be faster and less error prone.

TLDR; I think we should add both functions.

Benjamin-Philip avatar Apr 20 '25 03:04 Benjamin-Philip

@jhogberg, I think this PR is review ready now. Sorry about the delay - I was busy the past 2 weeks...

Benjamin-Philip avatar Apr 20 '25 18:04 Benjamin-Philip

Rebased on master to fix merge conflicts.

Benjamin-Philip avatar Apr 28 '25 07:04 Benjamin-Philip

@jhogberg, Any updates on this? Do you think you can squeeze it in OTP 28? Also, do you think has_key should be added (in another PR, of course) as @josevalim suggested?

Benjamin-Philip avatar Apr 28 '25 07:04 Benjamin-Philip

@jhogberg, Any updates on this? Do you think you can squeeze it in OTP 28?

As mentioned in the issue, the window for 28 was closed before you opened it. It can be included in 28.1 at the very earliest.

The refactor I mentioned is coming along well. It's feature-complete and offers far better add/update/erase performance (so long as literal GC isn't triggered), but read performance on dynamic keys has taken a hit that is proving a bit annoying to get rid of (statically known keys are effectively free however). I'm confident it'll work out however, and am aiming for OTP 29.

Also, do you think has_key should be added (in another PR, of course) as @josevalim suggested?

I can't see why not, I'll add it to my refactored version.

jhogberg avatar Apr 28 '25 16:04 jhogberg

It can be included in 28.1 at the very earliest.

When do you think 28.1 will be released?

Benjamin-Philip avatar Apr 29 '25 07:04 Benjamin-Philip

When do you think 28.1 will be released?

Sorry for the late reply -- I missed the notification. 28.1 will be released early this autumn (September-ish).

jhogberg avatar Jun 09 '25 14:06 jhogberg

@jhogberg, I've rebased to master. I'm hoping you can accept patches for OTP 28.1 now?

PS: If your refactor is public, could you share a link? I'm curious what are the changes you plan to make.

Benjamin-Philip avatar Jun 09 '25 14:06 Benjamin-Philip

@jhogberg, I've rebased to master. I'm hoping you can accept patches for OTP 28.1 now?

28.1 would be maint, it should be relatively easy to rebase to that though since master hasn't moved in this area. :-)

PS: If your refactor is public, could you share a link? I'm curious what are the changes you plan to make.

I've paused work on it because more important things came up. The idea was to use concurrent tries to improve update performance for trivial values, sacrificing a bit of read performance but hopefully regaining it through inline caching. Inline caching of static keys resulted in great performance (marginally more expensive than reading a list head...), but I couldn't make dynamic keys fast enough.

I left it just before introducing namespacing, replacing the implicit persistent_term:put({?MODULE, Key}, Value) with an explicit persistent_term:put(?MODULE, Key, Value) where the user could define how they wanted their namespace to work -- slow writes but fast reads through the old hashtable method, or slightly slower reads but fast writes through concurrent tries.

I've pushed what I've got to https://github.com/jhogberg/otp/tree/john/erts/ctrie-experiment, adding put_new/2 would be as simple as adding a boolean argument to persistent_term_put to BIF_ERROR(c_p, BADARG) instead of calling pt_ctrie_replace.

jhogberg avatar Jun 09 '25 14:06 jhogberg

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Jun 10 '25 04:06 CLAassistant

28.1 would be maint, it should be relatively easy to rebase to that though since master hasn't moved in this area. :-)

I've rebased to maint and changed the PR base from master to maint as well.

I've paused work on it because more important things came up. The idea was to use concurrent tries to improve update performance for trivial values, sacrificing a bit of read performance but hopefully regaining it through inline caching. Inline caching of static keys resulted in great performance (marginally more expensive than reading a list head...), but I couldn't make dynamic keys fast enough.

I left it just before introducing namespacing, replacing the implicit persistent_term:put({?MODULE, Key}, Value) with an explicit persistent_term:put(?MODULE, Key, Value) where the user could define how they wanted their namespace to work -- slow writes but fast reads through the old hashtable method, or slightly slower reads but fast writes through concurrent tries.

I think namespacing is a great idea - you reduce module, key pairs into a trivial atom. If you're creating a new hashtable/ctrie for each namespace you also get some additional isolation between namespaces in terms of failures and maybe even GC.

Regarding static and dynamic keys, do you mean keys known at compile time? Maybe we could cache all trivial keys (i.e. a key that is an atom)? This may not scale well with persistent terms as they are now, but if caching like this for each namespace was configured intentionally on a case by case basis it may work quite well.

Benjamin-Philip avatar Jun 10 '25 05:06 Benjamin-Philip

I've rebased to maint and changed the PR base from master to maint as well.

It looks like you dragged some unrelated commits into it.

I think namespacing is a great idea - you reduce module, key pairs into a trivial atom. If you're creating a new hashtable/ctrie for each namespace you also get some additional isolation between namespaces in terms of failures and maybe even GC.

It doesn't help GC at all, but it does help lookups and update speed as unrelated things won't need to be considered.

Regarding static and dynamic keys, do you mean keys known at compile time? Maybe we could cache all trivial keys (i.e. a key that is an atom)? This may not scale well with persistent terms as they are now, but if caching like this for each namespace was configured intentionally on a case by case basis it may work quite well.

Any key known at load-time (regardless of complexity) was cached at load-time in my prototype, and scaled perfectly without namespaces, so it's something we'll want to do regardless. The barrier required to check if the value was stale (and update if so) was effectively free in my tests as long as it wasn't triggered.

Dynamic keys are the tricky bit, where namespacing mainly help by splitting up different use cases; the concurrent tries were miles ahead of the old hash table on updates, and only slightly behind on lookups. If your use-case requires quick updates, you might accept the small loss in lookup speed from using concurrent tries, and otherwise you could still use the old method.

jhogberg avatar Jun 10 '25 06:06 jhogberg

It looks like you dragged some unrelated commits into it.

I rebased and forced pushed before updating the PR's base from master to maint. So GitHub dragged commits from maint that weren't on master.

I'm going to make a new branch and cherry-pick my commits on to it and update the PR source branch.

Edit: All fixed. Forced pushed from this new branch and renamed it to the old branch's name.

Benjamin-Philip avatar Jun 17 '25 10:06 Benjamin-Philip

@jhogberg Do you plan not to pursue this right now in favour of your planned changes?

Benjamin-Philip avatar Aug 29 '25 05:08 Benjamin-Philip

@jhogberg Do you plan not to pursue this right now in favour of your planned changes?

Sorry, I’ve had to put everything on ice and am not working much until October for personal reasons. 🙁

We can merge this PR once the issues are resolved, I can fix that come October.

jhogberg avatar Aug 29 '25 07:08 jhogberg

@jhogberg , I've managed to push a working trap that passes all the tests within persistent_term_SUITE. However, I am facing 2 seemingly unrelated issues:

  1. CHECK_TERM(*c_p->arg_reg[i])) in beam/beam_common.c fails on init and stop (see previous CI)
  2. I have random segfaults on make emulator_test (see current CI).

Running cerl to check 1 in gdb and inspecting c_p, I can confirm that it's during the trap. From what I can tell, one of the trap arguments have not been set correctly. However, I don't understand why I couldn't reproduce this in the unit tests.

Any idea what gives?

Benjamin-Philip avatar Nov 14 '25 08:11 Benjamin-Philip

@jhogberg, I pushed a working solution few days ago. Not sure why the 32-bit and cross-compile builds failed though. Everything else seems to be in order.

Benjamin-Philip avatar Nov 22 '25 14:11 Benjamin-Philip

I never did find the cause, except that it was while trapping. The latest implementation does not crash.

Benjamin-Philip avatar Nov 26 '25 10:11 Benjamin-Philip

About the formatting, I reformatted the functions I was working on with Emacs' c-fill-paragraph. I'll revert the changes. Do you have a configuration for better formatting?

Benjamin-Philip avatar Nov 26 '25 10:11 Benjamin-Philip

About the formatting, I reformatted the functions I was working on with Emacs' c-fill-paragraph. I'll revert the changes. Do you have a configuration for better formatting?

I believe the problem is that, at least in persistent_term_erase_1, there's a missing ; just before the formatting becomes weird.

I never did find the cause, except that it was while trapping. The latest implementation does not crash.

Alright, we'll see if it crashes in the nightly runs. :-)

jhogberg avatar Nov 26 '25 11:11 jhogberg

Fixed the formatting. You were right about the semicolon: https://www.gnu.org/software/emacs/manual/html_node/ccmode/Macros-with-_003b.html

I suggested that OTP moves towards something like clang-format for more consistent and reproducible formatting in an issue a few months ago. Maybe we should revisit that again. If I remember correctly, the main reason for rejecting it was that it would introduce a lot of merge conflicts.

Benjamin-Philip avatar Nov 27 '25 13:11 Benjamin-Philip

@jhogberg Any further changes? Is maint still the right branch to merge into?

Benjamin-Philip avatar Dec 03 '25 02:12 Benjamin-Philip

Not at the moment, maint is still the right branch. It’s been tested briefly without anything fishy showing up, and it’ll get some more testing after 28.3 is released this Friday. 🙂

jhogberg avatar Dec 03 '25 05:12 jhogberg