Problem report on the usage of Structured Concurrency (5th preview)

Viktor Klang viktor.klang at oracle.com
Fri Sep 26 13:04:38 UTC 2025


Hi Adam,

First of all, thanks for reaching out with thoughts, comments, and general feedback on your use of Structured Concurrency—it is much appreciated!

Some thoughts and comments below:

>1) SCS works great when tasks are independent, and known upfront; that is, when tasks aren’t dynamically generated based on computations that are part of the scope.

I think I understand what you intend to say, but I think more specifically you're referring to when tasks are generated as a result of *other tasks*, not the scope body itself.

Case in point:

try(var scope = …) {
while(dynamicCondition) // Dynamic number of tasks
scope.fork(…);
scope.join();
}

>someone on Reddit already pointed out that a better implementation using SCS nested scopes exists.

When in doubt, consider if nested scopes could make the design clearer. This is analoguous to breaking a large, complex, method-body down into multiple smaller ones.

>Still, if we add requirements around rate limiting, per-domain connection pooling etc., a solution with a centralised coordinator becomes more viable.

Not sure how that conclusion was derived. Could you explain further?

>Other examples might include implementing an actor-like component, where the actor’s body becomes the scope's body, handles some private (mutable) state, and communicates with child processes (forks created in the scope) using queues.

Inter-task communication channels are not part of Structure Concurrency at this point in time. It is however important to note that StructuredTaskScope is not the end state of Structured Concurrency.

>If the main scope body includes any blocking logic, it might end up hanging indefinitely, while all the other forks have been cancelled.

That statement is true by definition—any code which is blocking indefinitely and is not interrupted, is by definition blocking indefinitely.

>The main scope’s body awaits for data from either of them (on a queue), and when an element is produced, sends it downstream. Now, if we’re not careful with error handling, an exception in one of the substreams will cancel the scope, but the main scope will indefinitely wait on data, not aware of the error.

This sounds, to me, like another issue with an absent feature—Inter-task communication channels.

>Moreover, I allow creating forks-in-forks, so that the main logic can create forks at will.

As this is described, this means that you could have a race condition between forking in the same scope and the call to scope.join(), or did I misunderstand?

>My work-around here is to create a `Joiner` which monitors an `isDone` flag, and submit an empty task after the work is determined to be done:

Since Joiners are one-shot, and are created before the scope is opened, it would seem more logical(?) to embed that flag into the joiner and have it be set by the scope body by referring to the joiner:

var myJoiner = new MyJoiner(…);
try(var scope = StructuredTaskScope.open(myJoiner)) {
…
myJoiner.signalDone()
scope.join();
}

>But it’s more of an ugly trick, rather than a proper solution. I think the essence of the problem is that the logic of the scope is divided between the scope body, and the `Joiner` implementation, and it’s hard to keep them in sync.

«Designing a Joiner should take into account the code at the use-site where the results from the join method are processed. It should be clear what the Joiner does vs. the application code at the use-site. In general, the Joiner implementation is not the place for "business logic". A Joiner should be designed to be as general purpose as possible.» - https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.Joiner.html

>Special-casing for this seems odd, as timeout is only one example from a family of "resiliency" methods, others including retries, repeats etc. These as well, could be implemented on top of virtual threads and SCS as methods, without special support from the SCS API itself.

While it is true that the I combinator, in SKI calculus, is not primitive because it can be implemented in terms of the S and K combinators, that doesn't necessarily mean that we should strive to distill the absolute primitives. There also exists a scientific paper on the Turing Completeness of the x86 MOV instruction—but it's Turing Completeness does not rule out the value of having specialized instructions.

Putting a deadline on a concurrent operation is established "good practice" to ensure liveness, and the duration for that timeout is most useful if it is provided by the caller, so creating a standardized configuration option for this common operation was deemed to be worth it, since we do not need to either create a scope-within-a-scope by default or filter out the Subtask handling the timeout.

> The Subtask.get() method is confusing

«public static sealed interface StructuredTaskScope.Subtask<T>
extends Supplier<T>» - https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/StructuredTaskScope.Subtask.html


All the best!

Cheers,
√


Viktor Klang
Software Architect, Java Platform Group
Oracle
________________________________
From: loom-dev <loom-dev-retn at openjdk.org> on behalf of Adam Warski <adam at warski.org>
Sent: Friday, 26 September 2025 08:37
To: loom-dev at openjdk.org <loom-dev at openjdk.org>
Subject: Problem report on the usage of Structured Concurrency (5th preview)

Good morning,

with the release of Java 25, I’ve attempted to migrate my virtual-thread-native, reactive-streaming-like library from Java 21 to Java 25 scopes. So far I’ve been using my own wrapper on StructuredConcurrencyScope, but with that migration I wanted to eliminate it, and just use SCS directly. However, I encountered some problems with SCS’s design; I've summarised them in a blog (https://softwaremill.com/critique-of-jep-505-structured-concurrency-fifth-preview), and then prompted by a discussion on Reddit that followed (https://www.reddit.com/r/java/comments/1nq25yr/critique_of_jep_505_structured_concurrency_fifth/), I’m writing here.

Here’s a summary of the issues:

1) SCS works great when tasks are independent, and known upfront; that is, when tasks aren’t dynamically generated based on computations that are part of the scope. In that case, some communication between the forks & the main scope body is needed - typically using queues. The example I give in the article is of a web crawler, however it might not be the best one, as someone on Reddit already pointed out that a better implementation using SCS nested scopes exists. Still, if we add requirements around rate limiting, per-domain connection pooling etc., a solution with a centralised coordinator becomes more viable. Other examples might include implementing an actor-like component, where the actor’s body becomes the scope's body, handles some private (mutable) state, and communicates with child processes (forks created in the scope) using queues.

Such a centralised coordinator (implemented as the scope’s body) does not, however, participate in the error handling contract of the forks. If there’s an exception in the forks, (using the default Joiner) the scope will be cancelled, and all other forks will be interrupted - and that’s of course correct. However, the main scope body won’t be (and it can’t be, as the interruption could escape the scope). If the main scope body includes any blocking logic, it might end up hanging indefinitely, while all the other forks have been cancelled.

To make the problem a little bit more concrete, I encountered the above (and problem number 2 as well) when implementing Jox (https://github.com/softwaremill/jox) `Flow` stages. For example, when merging two streams, the merged substreams need to be run in the background, concurrently, within a concurrency scope. The main scope’s body awaits for data from either of them (on a queue), and when an element is produced, sends it downstream. Now, if we’re not careful with error handling, an exception in one of the substreams will cancel the scope, but the main scope will indefinitely wait on data, not aware of the error.

I’m currently solving this by still having my custom wrapper on top of scopes. The wrapper essentially provides custom concurrency scopes, which run the main scope logic in a fork, making it participate in the error handling/cancellation properties of all other forks. Moreover, I allow creating forks-in-forks, so that the main logic can create forks at will.

2) If the scope’s body does include some non-trivial coordination logic, then it should also be able to decide that a scope is "done". My example here is that there might be two kinds of forks: some implementing the actual logic, and some serving "helper" functions. Following the crawling example, the main-logic forks would be the crawlers; helper forks could e.g. implement monitoring. Now, when the coordinator (main scope body) decides that the computation is done, the scope should be completed, cancelling any helper forks. Currently this can be implemented through a `Joiner`. However, if the data needed to decide, if a scope should complete is present in the coordinator (scope body), it’s problematic to pass that information to the `Joiner`. My work-around here is to create a `Joiner` which monitors an `isDone` flag, and submit an empty task after the work is determined to be done:

void main() throws ExecutionException, InterruptedException {
    var isDone = new AtomicBoolean(false);
    try (var scope = StructuredTaskScope.open(
        new CancellableJoiner<>(isDone))) {
        // some logic

        isDone.set(true);
        scope.fork(() -> {});

        scope.join();
    }
}

class CancellableJoiner<T>
    implements StructuredTaskScope.Joiner<T, Void> {

    private final AtomicBoolean isDone;
    CancellableJoiner(AtomicBoolean isDone) { this.isDone = isDone; }

    public boolean onFork(
        StructuredTaskScope.Subtask<? extends T> subtask) {
        return isDone.get();
    }

    // ...
}

But it’s more of an ugly trick, rather than a proper solution. I think the essence of the problem is that the logic of the scope is divided between the scope body, and the `Joiner` implementation, and it’s hard to keep them in sync.

3) The final two problems are more of nitpicks. First, a `timeout` method can easily be implemented using the machinery of the SCS, without additional configuration parameters. Special-casing for this seems odd, as timeout is only one example from a family of "resiliency" methods, others including retries, repeats etc. These as well, could be implemented on top of virtual threads and SCS as methods, without special support from the SCS API itself.

4) The Subtask.get() method is confusing, as it has the semantics of Future.resultNow(), but the nomenclature of Future.get(). Since Future.get() is quite well established, I think it’s reasonable to assume, without prior knowledge of the SCS API, that Subtask.get() is blocking as well. However, it works rather differently. I understand that .get() is a fitting name, however given the existing functionalities in place, I would consider changing the name to something without naming or semantical clashes.

----

One might say, that my requirements are above regular use-cases for SCS. However, I view SCS as the new base building block for IO-bound concurrency in Java, and that any future concurrency libraries or projects should use it. This is supported by the fact that `ScopedValue`s are only inherited within SCSs - and I think any concurrency library or feature should provide such propagation. Hence, it should build on top of SCS. That’s why I’m seeking how to make SCS more flexible, to accommodate also for the more unusual or advanced use-cases.

Thank you for your work on SCS and Virtual Threads! I think what’s currently there is really great and powerful, even if not (yet) perfect :).

Regards,
Adam Warski

--
Adam Warski

https://warski.org
https://twitter.com/adamwarski

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/loom-dev/attachments/20250926/6ce48fe3/attachment-0001.htm>


More information about the loom-dev mailing list