Parallel
Factories
Parallel.of(String name, List<Action> children)
Parallel.of(String name, int parallelism, List<Action> children)
Parallel.of(String name, Action... children)
Parallel.of(String name, int parallelism, Action... children)
Parallel.of(String name, ExecutorService executorService, List<Action> children)
Parallel.of(String name, ExecutorService executorService, Action... children)
Semantics
- children are submitted concurrently
- the action waits for all children to finish
- parent status is computed from child results
- when
parallelismis provided, a semaphore limits concurrent children inside this node
Parallel uses an ExecutorService.
If no executor is supplied directly to the action, it uses context.getExecutorService().
Custom executor ownership
If you pass an ExecutorService to Parallel.of(...), Paramixel uses it but does not manage its lifecycle for you.
Runner executor ownership
When no ExecutorService is supplied to Runner.builder(), each run() call creates two thread pools:
- Runner pool — used for top-level
Paralleltasks (threads namedparamixel-runner-N) - Parallel pool — used for nested
Paralleltasks using the default executor (threads namedparamixel-parallel-N)
Both pools are shut down when run() completes. A Runner can be called multiple times; each call gets fresh pools.
When Runner.builder().executorService(myPool) is used:
myPoolis used for top-level action dispatch- The parallel pool is still created and managed per
run()call myPoolis never shut down by the runner — you control its lifecycle
Deadlock Prevention
Paramixel detects nested Parallel configurations that would cause thread starvation before execution begins.
How detection works
Parallel actions that use the default executor share a thread pool sized by paramixel.parallelism. When Parallel actions are nested deeper than the pool can service, a thread-starvation deadlock occurs:
// This throws IllegalStateException when paramixel.parallelism=1:
Action danger = Parallel.of("A",
Parallel.of("B",
Parallel.of("C",
Direct.of("leaf1", ctx -> {}),
Direct.of("leaf2", ctx -> {}))));
Runner.builder()
.configuration(Map.of("paramixel.parallelism", "1"))
.build()
.run(danger); // throws IllegalStateException
Runner walks the action tree before execution and rejects configurations where the nesting depth exceeds what the shared pool can handle.
The fix
There are two ways to resolve this:
- Supply dedicated executors to inner
Parallelactions (preferred for fine-grained control):
ExecutorService innerEs = Executors.newFixedThreadPool(4);
Action safe = Parallel.of("A",
Parallel.of("B", innerEs,
Parallel.of("C",
Direct.of("leaf1", ctx -> {}),
Direct.of("leaf2", ctx -> {}))));
- Increase
paramixel.parallelismto match the nesting depth:
Runner.builder()
.configuration(Map.of("paramixel.parallelism", "2"))
.build()
.run(danger); // safe with 2 threads
When detection resets
Detection resets the depth counter at any Parallel node that has a custom ExecutorService, because custom executors provide their own thread pool and do not contend for shared pool threads.
Interrupt handling
If the executing thread is interrupted during concurrency semaphore acquisition, the Parallel action transitions to a FAIL result with the InterruptedException as the cause, fires afterAction, and then re-interrupts the thread before re-throwing a RuntimeException wrapping the cause. This ensures the lifecycle contract (result transitions to a terminal state and afterAction is always invoked) is honored even under interrupt conditions.
Examples
Default executor
Action action = Parallel.of(
"tests",
Direct.of("a", context -> {}),
Direct.of("b", context -> {}));
Node-local concurrency limit
Action action = Parallel.of(
"tests",
2,
Direct.of("a", context -> {}),
Direct.of("b", context -> {}),
Direct.of("c", context -> {}));
Custom executor
ExecutorService executorService = Executors.newFixedThreadPool(4);
Action action = Parallel.of("tests", executorService, children);
See also the examples under:
examples/test/argument/ArgumentParallelismTest.javaexamples/test/argument/ArgumentNestedParallelismTest.javaexamples/test/argument/ArgumentCustomExecutorServiceTest.java