Skip to content

Conversation

@mpkorstanje
Copy link
Contributor

@mpkorstanje mpkorstanje commented Dec 15, 2025

Users of the extension context may occasionally want to build a non-trivial object graph, of which the individual components are also stored in the extension context store. This naturally leads to a pattern where the store is updated recursively. For example:

store.getOrComputeIfAbsent(("a", __ -> {
  B b = store.getOrComputeIfAbsent("b", _ -> new B(), B.class);
  C c = store.getOrComputeIfAbsent(("c", _ -> new C(), C.class);
  return new A(b, c);
}, A.class);

While the backing ConcurrentHashMap does not support recursive updates this does work as getOrComputeIfAbsent defers the recursive update until the after the map has been updated. This is not the case for computeIfAbsent which immediately calls the default creator. This poses a problem as computeIfAbsent is supposed to be a drop-in replacement for getOrComputeIfAbsent with better null and exception semantics.

The solution is to defer the execution of the default value creator until after the map has been updated. This is handled by the DeferredSupplier which uses a FutureTask to support a nice separation of compute and get operations.

Because there are some differences between both compute methods in how deferred values are to be handled, this PR introduces a StoredValue.Value, .DeferredValue and .DeferredOptionalValue containers for put, getOrComputeIfAbsent and computeIfAbsent respectively. This allows:

  • DeferredOptionalValue containing an exception to be treated as absent in getStoredValue.
  • DeferredOptionalValue containing a a value to return its contents on evaluation.
  • DeferredValue to throw an exception or returns its contents on evaluation
  • Value to simply return its contents on evaluation

This PR does introduce a small change in behavior. Previously any of the concurrent callers of getOrComputeIfAbsent could be the one executing the default creator. This can now only be done by the caller that created the stored value that was actually inserted. I don't expect this will break anything.

The PR is also slightly bigger than it needs to be. StoredValue.Value could be replaced with .DeferredValue. But as DeferredValue only exists to support a deprecated method, this makes for easier clean up in the future.

Thanks to @martinfrancois who contributed the CollidingKey, DeferredSupplier and several other useful concepts in #5209.

Fixes: #5171


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@mpkorstanje mpkorstanje force-pushed the rien/enable-store-recursive-updates branch from 164169b to 708bb35 Compare December 15, 2025 17:24
@testlens-app
Copy link

testlens-app bot commented Dec 15, 2025

🔎 No tests executed 🔎

🏷️ Commit: d40ae31
▶️ Tests: 0 executed
⚪️ Checks: 15/15 completed


Learn more about TestLens at testlens.app.

@mpkorstanje mpkorstanje force-pushed the rien/enable-store-recursive-updates branch from 46f2099 to be7ab95 Compare December 15, 2025 17:49
@mpkorstanje mpkorstanje force-pushed the rien/enable-store-recursive-updates branch from be7ab95 to 1808119 Compare December 15, 2025 18:42
@mpkorstanje mpkorstanje force-pushed the rien/enable-store-recursive-updates branch from b0e3f2d to 3c5a571 Compare December 15, 2025 18:48
@mpkorstanje mpkorstanje changed the title Enable store recursive updates Enable recursive updates to NamespacedHierarchicalStore Dec 15, 2025
@mpkorstanje
Copy link
Contributor Author

@martinfrancois would you have time to review this?

@mpkorstanje mpkorstanje added this to the 6.1.0-M2 milestone Dec 16, 2025
@mpkorstanje mpkorstanje requested review from marcphilipp and sbrannen and removed request for marcphilipp December 16, 2025 01:56
@mpkorstanje mpkorstanje marked this pull request as ready for review December 16, 2025 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrency problem in NamespacedHierarchicalStore#computeIfAbsent

2 participants