Enable recursive updates to NamespacedHierarchicalStore#5231
Merged
mpkorstanje merged 15 commits intomainfrom Dec 22, 2025
Merged
Enable recursive updates to NamespacedHierarchicalStore#5231mpkorstanje merged 15 commits intomainfrom
mpkorstanje merged 15 commits intomainfrom
Conversation
Co-authored-by: martinfrancois <f.martin@fastmail.com>
Co-authored-by: martinfrancois <f.martin@fastmail.com>
164169b to
708bb35
Compare
🔎 No tests executed 🔎🏷️ Commit: 5493055 Learn more about TestLens at testlens.app. |
46f2099 to
be7ab95
Compare
be7ab95 to
1808119
Compare
b0e3f2d to
3c5a571
Compare
6 tasks
Contributor
Author
|
@martinfrancois would you have time to review this? |
Sure @mpkorstanje, I'll have a look! |
marcphilipp
approved these changes
Dec 21, 2025
Member
marcphilipp
left a comment
There was a problem hiding this comment.
Thanks for codifying the concepts so clearly! 👍
...ngine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java
Outdated
Show resolved
Hide resolved
6 tasks
…support/store/NamespacedHierarchicalStore.java Co-authored-by: Marc Philipp <mail@marcphilipp.de>
marcphilipp
pushed a commit
that referenced
this pull request
Dec 28, 2025
Enable recursive updates to NamespacedHierarchicalStore
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:
```java
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 Martin Francois who contributed the `CollidingKey`,
`DeferredSupplier` and several other useful concepts in #5209.
Fixes: #5171
(cherry picked from commit 5a3d882)
6 tasks
mpkorstanje
added a commit
that referenced
this pull request
Feb 11, 2026
In #5231 evaluation of the default value creator was deferred until after an entry was created in the concurrent hash map backing the store. This enables recursive updates. However, by calling `evaluateIfNotNull` inside `compute` threads that lost the race would wait inside `Map::compute` for the value to be evaluated. Waiting here is a problem when the backing concurrent hashmap is reaching capacity. The thread waiting inside compute holds a lock while at the same time the thread that won the race will try to acquire a lock to resize the map. By making the threads that lost the race wait outside of `Map::compute` we can prevent this dead lock. This does require that each thread retries when another thread failed to insert a value. But eventually either one other thread either successfully inserts a value or the thread uses its own default value creator. Fixes: #5346 Co-authored-by: Marc Philipp <mail@marcphilipp.de>
marcphilipp
pushed a commit
that referenced
this pull request
Feb 15, 2026
In #5231 evaluation of the default value creator was deferred until after an entry was created in the concurrent hash map backing the store. This enables recursive updates. However, by calling `evaluateIfNotNull` inside `compute` threads that lost the race would wait inside `Map::compute` for the value to be evaluated. Waiting here is a problem when the backing concurrent hashmap is reaching capacity. The thread waiting inside compute holds a lock while at the same time the thread that won the race will try to acquire a lock to resize the map. By making the threads that lost the race wait outside of `Map::compute` we can prevent this dead lock. This does require that each thread retries when another thread failed to insert a value. But eventually either one other thread either successfully inserts a value or the thread uses its own default value creator. Fixes: #5346 Co-authored-by: Marc Philipp <mail@marcphilipp.de> (cherry picked from commit f29de38)
marcphilipp
pushed a commit
that referenced
this pull request
Feb 15, 2026
In #5231 evaluation of the default value creator was deferred until after an entry was created in the concurrent hash map backing the store. This enables recursive updates. However, by calling `evaluateIfNotNull` inside `compute` threads that lost the race would wait inside `Map::compute` for the value to be evaluated. Waiting here is a problem when the backing concurrent hashmap is reaching capacity. The thread waiting inside compute holds a lock while at the same time the thread that won the race will try to acquire a lock to resize the map. By making the threads that lost the race wait outside of `Map::compute` we can prevent this dead lock. This does require that each thread retries when another thread failed to insert a value. But eventually either one other thread either successfully inserts a value or the thread uses its own default value creator. Fixes: #5346 Co-authored-by: Marc Philipp <mail@marcphilipp.de> (cherry picked from commit f29de38)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
While the backing
ConcurrentHashMapdoes not support recursive updates this does work asgetOrComputeIfAbsentdefers the recursive update until the after the map has been updated. This is not the case forcomputeIfAbsentwhich immediately calls the default creator. This poses a problem ascomputeIfAbsentis supposed to be a drop-in replacement forgetOrComputeIfAbsentwith 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
DeferredSupplierwhich uses aFutureTaskto 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,.DeferredValueand.DeferredOptionalValuecontainers forput,getOrComputeIfAbsentandcomputeIfAbsentrespectively. This allows:DeferredOptionalValuecontaining an exception to be treated as absent ingetStoredValue.DeferredOptionalValuecontaining a a value to return its contents on evaluation.DeferredValueto throw an exception or returns its contents on evaluationValueto simply return its contents on evaluationThis PR does introduce a small change in behavior. Previously any of the concurrent callers of
getOrComputeIfAbsentcould 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.Valuecould be replaced with.DeferredValue. But asDeferredValueonly exists to support a deprecated method, this makes for easier clean up in the future.Thanks to @martinfrancois who contributed the
CollidingKey,DeferredSupplierand several other useful concepts in #5209.Fixes: #5171
I hereby agree to the terms of the JUnit Contributor License Agreement.
Definition of Done
@APIannotations