> From: "Jige Yu" <[email protected]>
> To: "loom-dev" <[email protected]>
> Sent: Sunday, October 12, 2025 7:32:33 AM
> Subject: Feedback on Structured Concurrency (JEP 525, 6th Preview)
> Hi Project Loom.
> First and foremost, I want to express my gratitude for the effort that has
> gone
> into structured concurrency. API design in this space is notoriously
> difficult,
> and this feedback is offered with the greatest respect for the team's work and
> in the spirit of collaborative refinement.
> My perspective is that of a developer looking to use Structured Concurrency
> for
> common, IO-intensive fan-out operations. My focus is to replace everyday async
> callback hell, or reactive chains with something simpler and more readable.
> It will lack depth in the highly specialized concurrent programming area. And
> I
> acknowledge this viewpoint may bias my feedback.
[...]
> Suggestions for a Simpler Model
> My preference is that the API for the most common use cases should be more
> declarative and functional .
> 1.
> Simplify the "Gather All" Pattern: The primary "fan-out and gather" use case
> could be captured in a simple, high-level construct. An average user shouldn't
> need to learn the wide API surface of StructuredTaskScope + Joiner + the
> lifecycles. For example:
> Java
> // Ideal API for the 80% use case Robot robot = Concurrently.call(
> () -> fetchArm(),
> () -> fetchLeg(),
> (arm, leg) -> new Robot(arm, leg)
> );
I'm curious how you want to type that API, does it work only for two tasks, do
you have an overload for each arity (2 tasks, 3 tasks, etc).
And how exceptions are supposed to work given that the type system of Java is
not able to merge type variable representing exceptions correctly.
> 1.
> Separate Race Semantics into Composable Operations: The "race" pattern feels
> like a distinct use case that could be implemented more naturally using
> composable, functional APIs like Stream gatherers, rather than requiring a
> specialized API at all. For example, if mapConcurrent() fully embraced
> structured concurrency, guaranteeing fail-fast and happens-before, a
> recoverable race could be written explicitly:
> Java
> // Pseudo-code for a recoverable race using a stream gatherer <T> T race
> (Collection<Callable<T>> tasks, int maxConcurrency) { var exceptions = new
> ConcurrentLinkedQueue<RpcException>(); return tasks.stream()
> .gather(mapConcurrent(maxConcurrency, task -> { try { return
> task.call();
> } catch (RpcException e) { if (isRecoverable(e)) { // Selectively
> recover
> exceptions.add(e); return null ; // Suppress and continue } throw
> new
> RuntimeException(e); // Fail fast on non-recoverable }
> }))
> .filter(Objects::nonNull)
> .findFirst() // Short-circuiting and cancellation .orElseThrow(() ->
> new
> AggregateException(exceptions));
> }
> While this is slightly more verbose than the JEP example, it's familiar Stream
> semantics that people have already learned, and it offers explicit control
> over
> which exceptions are recoverable versus fatal. The boilerplate for exception
> aggregation could easily be wrapped in a helper method.
Several points :
- I believe the current STS API has no way to deal with if the exception is
recoverable or not because it's far easier to do that at the end of the
callable.
Your example becomes :
sts.fork(() -> {
try {
taskCall();
} catch(RPCException e) {
...
}
});
- You do not want to post the result/exception of a task into a concurrent data
structure, i think the idea of the STS API in this case is to fork all the
tasks and then take a look to all the subtasks.
I believe it's more efficient because there is no CAS to be done if the main
thread take a look to the subtasks afterward than if the joiner tries to
maintain a concurrent data structure.
> 1.
> Reserve Complexity for Complex Cases: The low-level StructuredTaskScope and
> its
> policy mechanism are powerful tools. However, they should be positioned as the
> "expert-level" API for building custom frameworks. Or perhaps just keep them
> in
> the traditional ExecutorService API. The everyday developer experience should
> be centered around simpler, declarative constructs that cover the most
> frequent
> needs.
For me, that's why you have an open Joiner interface for expert and already
available Joiner (like all.../any...) that are more for everyday developers.
regards,
Rémi