├── docs ├── .python-version ├── .gitignore ├── watch.sh ├── requirements.txt ├── contributing.md ├── Makefile ├── make.bat ├── flake.nix ├── flake.lock └── index.md ├── bench ├── bench-go │ ├── go.mod │ ├── parallel.go │ └── parallel_test.go ├── bench-kotlin │ ├── src │ │ └── com │ │ │ └── softwaremill │ │ │ └── jox │ │ │ ├── constants.kt │ │ │ ├── RendezvousKotlinBenchmark.kt │ │ │ ├── BufferedKotlinBenchmark.kt │ │ │ ├── ParallelKotlinBenchmark.kt │ │ │ ├── SelectKotlinBenchmark.kt │ │ │ └── ChainedKotlinBenchmark.kt │ └── pom.xml ├── bench-java │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── softwaremill │ │ └── jox │ │ ├── BufferedBenchmark.java │ │ ├── SelectBenchmark.java │ │ ├── ParallelBenchmark.java │ │ ├── RendezvousBenchmark.java │ │ └── ChainedBenchmark.java └── pom.xml ├── .github ├── release-drafter.yml └── workflows │ └── ci.yml ├── channels ├── src │ ├── main │ │ └── java │ │ │ ├── module-info.java │ │ │ └── com │ │ │ └── softwaremill │ │ │ └── jox │ │ │ ├── ChannelDoneException.java │ │ │ ├── ChannelErrorException.java │ │ │ ├── ChannelDone.java │ │ │ ├── ChannelError.java │ │ │ ├── ChannelClosed.java │ │ │ ├── ChannelClosedException.java │ │ │ ├── SelectClause.java │ │ │ ├── Source.java │ │ │ ├── Sink.java │ │ │ └── CloseableChannel.java │ └── test │ │ └── java │ │ └── com │ │ └── softwaremill │ │ └── jox │ │ ├── TestWithCapacities.java │ │ ├── SourceOpsForEachTest.java │ │ ├── ChannelUnlimitedTest.java │ │ ├── ChannelClosedTest.java │ │ ├── SelectTest.java │ │ ├── ChannelTest.java │ │ ├── SegmentTest.java │ │ └── ChannelBufferedTest.java └── pom.xml ├── structured ├── src │ ├── main │ │ └── java │ │ │ ├── module-info.java │ │ │ └── com │ │ │ └── softwaremill │ │ │ └── jox │ │ │ └── structured │ │ │ ├── ThrowingRunnable.java │ │ │ ├── ThrowingConsumer.java │ │ │ ├── ThrowingFunction.java │ │ │ ├── ThrowingBiFunction.java │ │ │ ├── Scoped.java │ │ │ ├── SneakyThrows.java │ │ │ ├── ExternalRunner.java │ │ │ ├── Util.java │ │ │ ├── Fork.java │ │ │ ├── Supervisor.java │ │ │ ├── JoxScopeExecutionException.java │ │ │ ├── Scopes.java │ │ │ ├── CancellableFork.java │ │ │ ├── Par.java │ │ │ └── ActorRef.java │ └── test │ │ └── java │ │ └── com │ │ └── softwaremill │ │ └── jox │ │ └── structured │ │ ├── Trail.java │ │ ├── ScopeTest.java │ │ ├── ForkTest.java │ │ ├── ParTest.java │ │ └── RaceTest.java └── pom.xml ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .gitignore ├── channels-fray-tests ├── src │ └── test │ │ └── java │ │ └── com │ │ └── softwaremill │ │ └── jox │ │ └── fray │ │ ├── RunnableWithException.java │ │ ├── Config.java │ │ ├── Fork.java │ │ ├── FraySendReceiveTest.java │ │ ├── FrayCompleteTest.java │ │ └── FraySelectTest.java └── pom.xml ├── renovate.json ├── .readthedocs.yaml ├── flows ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── softwaremill │ │ │ └── jox │ │ │ └── flows │ │ │ ├── FlowStage.java │ │ │ ├── SourceBackedFlowStage.java │ │ │ ├── FlowEmit.java │ │ │ ├── MultiArrayIterator.java │ │ │ ├── LinesImpl.java │ │ │ └── WeightedHeap.java │ └── test │ │ └── java │ │ └── com │ │ └── softwaremill │ │ └── jox │ │ └── flows │ │ ├── FlowSlidingTest.java │ │ ├── ByteFlowTest.java │ │ ├── FlowsProjectReactorTest.java │ │ ├── FlowCompleteCallbacksTest.java │ │ ├── FlowPublisherTckTest.java │ │ ├── FlowZipTest.java │ │ ├── FlowInterleaveTest.java │ │ ├── FlowPekkoStreamTest.java │ │ └── FlowFlattenTest.java └── pom.xml ├── .editorconfig └── README.md /docs/.python-version: -------------------------------------------------------------------------------- 1 | 3.14 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _build_html -------------------------------------------------------------------------------- /bench/bench-go/go.mod: -------------------------------------------------------------------------------- 1 | module bench 2 | 3 | go 1.24.3 4 | -------------------------------------------------------------------------------- /docs/watch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sphinx-autobuild . _build/html 3 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What's Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /channels/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.softwaremill.jox { 2 | exports com.softwaremill.jox; 3 | } 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme==3.0.2 2 | sphinx==9.0.4 3 | sphinx-autobuild==2025.8.25 4 | myst-parser==4.0.1 5 | -------------------------------------------------------------------------------- /bench/bench-go/parallel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("hello world") 9 | } 10 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelDoneException.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | public final class ChannelDoneException extends ChannelClosedException {} 4 | -------------------------------------------------------------------------------- /structured/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module com.softwaremill.jox.structured { 2 | requires com.softwaremill.jox; 3 | 4 | exports com.softwaremill.jox.structured; 5 | } 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionType=only-script 2 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | .cursor 5 | tmp 6 | .vscode 7 | 8 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 9 | hs_err_pid* 10 | replay_pid* 11 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/RunnableWithException.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | @FunctionalInterface 4 | interface RunnableWithException { 5 | void run() throws Exception; 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "automerge": true, 7 | "labels": [ 8 | "dependency" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | python: 7 | install: 8 | - requirements: docs/requirements.txt 9 | 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.12" 14 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/constants.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | const val OPERATIONS_PER_INVOCATION = 1_000_000 4 | const val OPERATIONS_PER_INVOCATION_CHAINED = 10_000_000 5 | const val OPERATIONS_PER_INVOCATION_PARALLEL = 10_000_000 6 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelErrorException.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | public final class ChannelErrorException extends ChannelClosedException { 4 | public ChannelErrorException(Throwable cause) { 5 | super(cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ThrowingRunnable.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** {@link java.lang.Runnable} which can throw an exception. */ 4 | @FunctionalInterface 5 | public interface ThrowingRunnable { 6 | void run() throws Exception; 7 | } 8 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelDone.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | public record ChannelDone(Channel channel) implements ChannelClosed { 4 | @Override 5 | public ChannelClosedException toException() { 6 | return new ChannelDoneException(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ThrowingConsumer.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** {@link java.util.function.Consumer} which can throw an exception. */ 4 | @FunctionalInterface 5 | public interface ThrowingConsumer { 6 | void accept(T t) throws Exception; 7 | } 8 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** {@link java.util.function.Function} which can throw an exception. */ 4 | @FunctionalInterface 5 | public interface ThrowingFunction { 6 | R apply(T t) throws Exception; 7 | } 8 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelError.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | public record ChannelError(Throwable cause, Channel channel) implements ChannelClosed { 4 | @Override 5 | public ChannelClosedException toException() { 6 | return new ChannelErrorException(cause); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ThrowingBiFunction.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** {@link java.util.function.BiFunction} which can throw an exception. */ 4 | @FunctionalInterface 5 | public interface ThrowingBiFunction { 6 | R apply(T t, U u) throws Exception; 7 | } 8 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Scoped.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** 4 | * A functional interface, capturing a computation which runs using the {@link Scope} capability to 5 | * fork computations. 6 | */ 7 | @FunctionalInterface 8 | public interface Scoped { 9 | T run(Scope scope) throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/FlowStage.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | /** 4 | * Contains the logic for running a single flow stage. As part of `run`s implementation, previous 5 | * flow stages might be run, either synchronously or asynchronously. 6 | */ 7 | public interface FlowStage { 8 | void run(FlowEmit emit) throws Exception; 9 | } 10 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/SourceBackedFlowStage.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import com.softwaremill.jox.Source; 4 | 5 | record SourceBackedFlowStage(Source source) implements FlowStage { 6 | 7 | @Override 8 | public void run(FlowEmit emit) throws Exception { 9 | FlowEmit.channelToEmit(source, emit); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome all kinds of feedback and contributions: in the form of ideas, issues or PRs! :) 4 | 5 | How can you reach us and take part in the project? 6 | 7 | * through our [community forum](https://softwaremill.community/c/open-source/11) 8 | * by creating an [issue](https://github.com/softwaremill/jox/issues) 9 | * or by writing some code and creating a pull request! -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.java] 10 | indent_style = space 11 | indent_size = 4 12 | max_line_length = 120 13 | continuation_indent_size = 8 14 | 15 | [*.{xml,yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelClosed.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | /** 4 | * Returned by {@link Channel#sendOrClosed(Object)} and {@link Channel#receiveOrClosed()} when the 5 | * channel is closed. 6 | */ 7 | public sealed interface ChannelClosed permits ChannelDone, ChannelError { 8 | ChannelClosedException toException(); 9 | 10 | Channel channel(); 11 | } 12 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/Config.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | class Config { 4 | static final int CHANNEL_SIZE; 5 | 6 | static { 7 | String channelSizeEnv = System.getenv("CHANNEL_SIZE"); 8 | CHANNEL_SIZE = channelSizeEnv != null ? Integer.parseInt(channelSizeEnv) : 16; 9 | System.out.println("Using CHANNEL_SIZE: " + CHANNEL_SIZE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /structured/src/test/java/com/softwaremill/jox/structured/Trail.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import java.util.List; 4 | import java.util.Queue; 5 | import java.util.concurrent.ConcurrentLinkedQueue; 6 | 7 | public class Trail { 8 | private Queue data = new ConcurrentLinkedQueue<>(); 9 | 10 | public void add(String v) { 11 | data.add(v); 12 | } 13 | 14 | public List get() { 15 | return data.stream().toList(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/SneakyThrows.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | /** Allows bypassing compiler errors about checked exceptions. */ 4 | class SneakyThrows { 5 | static AssertionError sneakyThrow(Throwable checked) /*throws Throwable*/ { 6 | throw sneakyThrow0(checked); 7 | } 8 | 9 | @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) 10 | private static E sneakyThrow0(Throwable throwable) throws E { 11 | throw (E) throwable; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/TestWithCapacities.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.ValueSource; 10 | 11 | @Target(ElementType.METHOD) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @ParameterizedTest 14 | @ValueSource(strings = {"0", "1", "2", "10", "100", "1000", "-1"}) 15 | @interface TestWithCapacities {} 16 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/ChannelClosedException.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | /** 4 | * Thrown by {@link Channel#send(Object)} and {@link Channel#receive()} when the channel is closed. 5 | * 6 | *

~ Will be made abstract in future major releases: use {@link ChannelDoneException} or {@link 7 | * ChannelErrorException}. 8 | */ 9 | public sealed class ChannelClosedException extends RuntimeException 10 | permits ChannelDoneException, ChannelErrorException { 11 | public ChannelClosedException() {} 12 | 13 | public ChannelClosedException(Throwable cause) { 14 | super(cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = jox 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /bench/bench-go/parallel_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | // go test -bench=. -benchtime=1000000000x -count 5 9 | func BenchmarkParallel(b *testing.B) { 10 | const capacity = 16 11 | const parallelism = 10000 12 | 13 | elementsPerChannel := b.N / parallelism 14 | 15 | var wg sync.WaitGroup 16 | 17 | for i := 0; i < parallelism; i++ { 18 | c := make(chan int, capacity) 19 | 20 | wg.Add(1) 21 | go func() { 22 | defer wg.Done() 23 | for j := 0; j < elementsPerChannel; j++ { 24 | c <- 91 25 | } 26 | }() 27 | 28 | wg.Add(1) 29 | go func() { 30 | defer wg.Done() 31 | for j := 0; j < elementsPerChannel; j++ { 32 | <-c 33 | } 34 | }() 35 | } 36 | 37 | wg.Wait() 38 | } 39 | -------------------------------------------------------------------------------- /bench/bench-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | bench 9 | 1.1.1 10 | 11 | 12 | bench-java 13 | 1.1.1 14 | jar 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-shade-plugin 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ExternalRunner.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | public record ExternalRunner(ActorRef scheduler) { 4 | /** 5 | * Allows to runs the given function asynchronously, in the scope of the concurrency scope in 6 | * which this runner was created. 7 | * 8 | *

`f` should return promptly, not to obstruct execution of other scheduled functions. 9 | * Typically, it should start a background fork. 10 | */ 11 | public void runAsync(ThrowingConsumer f) { 12 | try { 13 | scheduler.ask( 14 | s -> { 15 | s.run(f); 16 | return null; 17 | }); 18 | } catch (Exception e) { 19 | SneakyThrows.sneakyThrow(e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=ox 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Python shell flake"; 3 | 4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | 7 | outputs = { self, nixpkgs, flake-utils, ... }: 8 | flake-utils.lib.eachDefaultSystem (system: 9 | let 10 | pkgs = nixpkgs.legacyPackages.${system}; 11 | python = pkgs.python3.withPackages (ps: with ps; [ 12 | pip 13 | ]); 14 | in 15 | { 16 | devShell = pkgs.mkShell { 17 | buildInputs = [ python ]; 18 | 19 | shellHook = '' 20 | # Create a Python virtual environment and activate it 21 | python -m venv .env 22 | source .env/bin/activate 23 | # Install the Python dependencies from requirements.txt 24 | if [ -f requirements.txt ]; then 25 | pip install -r requirements.txt 26 | fi 27 | ''; 28 | }; 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/SourceOpsForEachTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class SourceOpsForEachTest { 12 | @Test 13 | void testIterateOverSource() throws Exception { 14 | var c = Channel.newBufferedChannel(10); 15 | c.sendOrClosed(1); 16 | c.sendOrClosed(2); 17 | c.sendOrClosed(3); 18 | c.doneOrClosed(); 19 | 20 | List r = new ArrayList<>(); 21 | c.forEach(v -> r.add(v)); 22 | 23 | assertIterableEquals(List.of(1, 2, 3), r); 24 | } 25 | 26 | @Test 27 | void testConvertSourceToList() throws Exception { 28 | var c = Channel.newBufferedChannel(10); 29 | c.sendOrClosed(1); 30 | c.sendOrClosed(2); 31 | c.sendOrClosed(3); 32 | c.doneOrClosed(); 33 | 34 | List resultList = c.toList(); 35 | assertEquals(List.of(1, 2, 3), resultList); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Util.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | public class Util { 6 | /** 7 | * Prevent {@code f} from being interrupted. Any interrupted exceptions that occur while 8 | * evaluating {@code f} will be re-thrown once it completes. 9 | */ 10 | public static T uninterruptible(Callable f) throws InterruptedException { 11 | return Scopes.supervised( 12 | c -> { 13 | var fork = c.forkUnsupervised(f); 14 | InterruptedException caught = null; 15 | try { 16 | while (true) { 17 | try { 18 | return fork.join(); 19 | } catch (InterruptedException e) { 20 | caught = e; 21 | } 22 | } 23 | } finally { 24 | if (caught != null) { 25 | throw caught; 26 | } 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/RendezvousKotlinBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import org.openjdk.jmh.annotations.* 8 | import java.util.concurrent.TimeUnit 9 | 10 | // same parameters as in the java benchmark 11 | @Warmup(iterations = 3, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 10, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Timeout(time = 5100, timeUnit = TimeUnit.MILLISECONDS) 14 | @Fork(value = 3) 15 | @BenchmarkMode(Mode.AverageTime) 16 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 17 | open class RendezvousKotlinBenchmark { 18 | @Benchmark 19 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 20 | fun channel_defaultDispatcher() { 21 | runBlocking { 22 | val channel = Channel(0) 23 | launch(Dispatchers.Default) { 24 | for (x in 1..OPERATIONS_PER_INVOCATION) channel.send(63) 25 | channel.close() 26 | } 27 | 28 | launch(Dispatchers.Default) { 29 | for (x in 1..OPERATIONS_PER_INVOCATION) channel.receive() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/BufferedKotlinBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import org.openjdk.jmh.annotations.* 8 | import java.util.concurrent.TimeUnit 9 | 10 | // same parameters as in the java benchmark 11 | @Warmup(iterations = 3, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 10, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Timeout(time = 5100, timeUnit = TimeUnit.MILLISECONDS) 14 | @Fork(value = 3) 15 | @BenchmarkMode(Mode.AverageTime) 16 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 17 | @State(Scope.Benchmark) 18 | open class BufferedKotlinBenchmark { 19 | @Param("16", "100") 20 | var capacity: Int = 0 21 | 22 | @Benchmark 23 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 24 | fun channel_defaultDispatcher() { 25 | runBlocking { 26 | val channel = Channel(capacity) 27 | launch(Dispatchers.Default) { 28 | for (x in 1..OPERATIONS_PER_INVOCATION) channel.send(63) 29 | channel.close() 30 | } 31 | 32 | launch(Dispatchers.Default) { 33 | for (x in 1..OPERATIONS_PER_INVOCATION) channel.receive() 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Fork.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.ExecutionException; 5 | 6 | /** 7 | * A fork started using {@link Scope#fork}, {@link Scope#forkUser}, {@link Scope#forkCancellable} or 8 | * {@link Scope#forkUnsupervised}, backed by a (virtual) thread. 9 | */ 10 | @FunctionalInterface 11 | public interface Fork { 12 | /** 13 | * Blocks until the fork completes with a result. 14 | * 15 | * @throws ExecutionException If the fork completed with an exception, and is unsupervised 16 | * (started with {@link Scope#forkUnsupervised} or {@link Scope#forkCancellable}). 17 | */ 18 | T join() throws InterruptedException, ExecutionException; 19 | } 20 | 21 | class ForkUsingResult extends CompletableFuture implements Fork { 22 | /** 23 | * In runtime throws InterruptedException, ExecutionException One parent: {@link 24 | * CompletableFuture#join()} doesn't throw any checked exception ⇒ we can't add them. But the 25 | * caller's code sees {@link Fork#join()}, which throws checked exceptions, so everything looks 26 | * fine! 27 | * 28 | * @see CompletableFuture#get() 29 | */ 30 | @Override 31 | public T join() { 32 | try { 33 | return get(); 34 | } catch (Exception e) { // InterruptedException, ExecutionException, CancellationException 35 | throw SneakyThrows.sneakyThrow(e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/FlowEmit.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import com.softwaremill.jox.ChannelDone; 4 | import com.softwaremill.jox.ChannelError; 5 | import com.softwaremill.jox.Source; 6 | 7 | /** 8 | * Instances of this interface should be considered thread-unsafe, and only used within the scope in 9 | * which they have been obtained, e.g. as part of {@link Flows#usingEmit} or {@link 10 | * Flow#mapUsingEmit}. 11 | */ 12 | public interface FlowEmit { 13 | 14 | /** 15 | * Emit a value to be processed downstream. Blocks until the value is fully processed, or throws 16 | * an exception if an error occurred. 17 | */ 18 | void apply(T t) throws Exception; 19 | 20 | /** 21 | * Propagates all elements to the given emit. Completes once the channel completes as done. 22 | * Throws an exception if the channel transits to an error state. 23 | */ 24 | static void channelToEmit(Source source, FlowEmit emit) throws Exception { 25 | boolean shouldRun = true; 26 | while (shouldRun) { 27 | Object t = source.receiveOrClosed(); 28 | shouldRun = 29 | switch (t) { 30 | case ChannelDone done -> false; 31 | case ChannelError error -> throw error.toException(); 32 | default -> { 33 | //noinspection unchecked 34 | emit.apply((T) t); 35 | yield true; 36 | } 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/ParallelKotlinBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import org.openjdk.jmh.annotations.* 8 | import java.util.concurrent.TimeUnit 9 | 10 | // same parameters as in the java benchmark 11 | @Warmup(iterations = 3, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 10, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Fork(value = 3) 14 | @BenchmarkMode(Mode.AverageTime) 15 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 16 | @State(Scope.Benchmark) 17 | open class ParallelKotlinBenchmark { 18 | @Param("0", "16", "100") 19 | var capacity: Int = 0 20 | 21 | @Param("10000") 22 | var parallelism: Int = 0 23 | 24 | @Benchmark 25 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION_PARALLEL) 26 | fun parallelChannels_defaultDispatcher() { 27 | runBlocking { 28 | // we want to measure the amount of time a send-receive pair takes 29 | val elements = OPERATIONS_PER_INVOCATION_PARALLEL / parallelism 30 | 31 | for (t in 0 until parallelism) { 32 | val ch = Channel(capacity) 33 | 34 | // sender 35 | launch(Dispatchers.Default) { 36 | for (x in 1..elements) ch.send(91) 37 | } 38 | 39 | // receiver 40 | launch(Dispatchers.Default) { 41 | for (x in 1..elements) ch.receive() 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1715534503, 24 | "narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "2057814051972fa1453ddfb0d98badbea9b83c06", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Supervisor.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import java.util.Set; 4 | import java.util.concurrent.Callable; 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | import com.softwaremill.jox.Channel; 10 | 11 | final class Supervisor { 12 | private final AtomicInteger runningUserForks = new AtomicInteger(0); 13 | // used a single-complete cell to record the first exception (or success) 14 | private final CompletableFuture result = new CompletableFuture<>(); 15 | private final Set otherExceptions = ConcurrentHashMap.newKeySet(); 16 | private final Channel commands = Channel.newBufferedDefaultChannel(); 17 | 18 | void forkUserStarts() { 19 | runningUserForks.incrementAndGet(); 20 | } 21 | 22 | void forkUserSuccess() { 23 | int v = runningUserForks.decrementAndGet(); 24 | if (v == 0) { 25 | result.complete(null); 26 | commands.done(); 27 | } 28 | } 29 | 30 | boolean forkException(Throwable e) { 31 | if (!result.completeExceptionally(e)) { 32 | otherExceptions.add(e); 33 | } else { 34 | commands.error(e); 35 | } 36 | return true; 37 | } 38 | 39 | void addSuppressedErrors(Throwable e) { 40 | for (Throwable e2 : otherExceptions) { 41 | if (!e.equals(e2)) { 42 | e.addSuppressed(e2); 43 | } 44 | } 45 | } 46 | 47 | Channel getCommands() { 48 | return commands; 49 | } 50 | } 51 | 52 | interface SupervisorCommand {} 53 | 54 | interface RunFork extends SupervisorCommand, Callable {} 55 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/JoxScopeExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | public class JoxScopeExecutionException extends RuntimeException { 4 | 5 | public JoxScopeExecutionException(String message) { 6 | super(message); 7 | } 8 | 9 | public JoxScopeExecutionException(Throwable cause) { 10 | super(cause); 11 | } 12 | 13 | /** 14 | * If the cause of this {@link JoxScopeExecutionException} is an instance of any of the given 15 | * exceptions, it is thrown. Method signature points to the closest super class of the passed 16 | * exception classes. 17 | * 18 | *

Be careful if the cause is not any of given arguments, nothing happens, so it is 19 | * advised to rethrow this exception after calling this method. 20 | * 21 | *

e.g. 22 | * 23 | *

{@code
24 |      * try {
25 |      *     Scopes.supervised(scope -> {
26 |      *         throw new TestException("x");
27 |      *     });
28 |      * } catch (JoxScopeExecutionException e) {
29 |      *     e.unwrapAndThrow(OtherException.class, TestException.class, YetAnotherException.class);
30 |      *     throw e;
31 |      * }
32 |      * }
33 | */ 34 | @SafeVarargs 35 | public final void unwrapAndThrow( 36 | Class... exceptionsToRethrow) throws T { 37 | Throwable cause = getCause(); 38 | for (Class exceptionToRethrow : exceptionsToRethrow) { 39 | if (exceptionToRethrow.isInstance(cause)) { 40 | // rewrite suppressed exceptions 41 | for (var suppressed : getSuppressed()) { 42 | cause.addSuppressed(suppressed); 43 | } 44 | throw (T) exceptionToRethrow.cast(cause); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Scopes.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | public final class Scopes { 4 | /** 5 | * Starts a new concurrency scope, which allows starting forks in the given code block {@code 6 | * f}. Forks can be started using {@link Scope#fork}, {@link Scope#forkUser}, {@link 7 | * Scope#forkCancellable} or {@link Scope#forkUnsupervised}. All forks are guaranteed to 8 | * complete before this scope completes. 9 | * 10 | *

The scope is ran in supervised mode, that is: 11 | * 12 | *

    13 | *
  • the scope ends once all user, supervised forks (started using {@link Scope#forkUser}), 14 | * including the {@code f} body, succeed. Forks started using {@link Scope#fork} (daemon) 15 | * don't have to complete successfully for the scope to end. 16 | *
  • the scope also ends once the first supervised fork (including the {@code f} main body) 17 | * fails with an exception 18 | *
  • when the scope ends, all running forks are cancelled 19 | *
  • the scope completes (that is, this method returns) only once all forks 20 | * started by {@code f} have completed (either successfully, or with an exception) 21 | *
22 | * 23 | *

Upon successful completion, returns the result of evaluating {@code f}. Upon failure, the 24 | * exception that caused the scope to end is re-thrown, wrapped in an {@link 25 | * JoxScopeExecutionException} (regardless if the exception was thrown from the main body, or 26 | * from a fork). Any other exceptions that occur when completing the scope are added as 27 | * suppressed. 28 | * 29 | * @throws JoxScopeExecutionException When the main body, or any of the forks, throw an 30 | * exception 31 | */ 32 | public static T supervised(Scoped f) throws InterruptedException { 33 | return new Scope().run(f); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /channels/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | parent 9 | 1.1.1 10 | 11 | 12 | channels 13 | 1.1.1 14 | jar 15 | ${project.groupId}:${project.artifactId} 16 | 17 | 18 | 19 | org.junit.jupiter 20 | junit-jupiter 21 | 6.0.1 22 | test 23 | 24 | 25 | org.awaitility 26 | awaitility 27 | test 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | com.github.siom79.japicmp 36 | japicmp-maven-plugin 37 | 38 | 39 | true 40 | true 41 | true 42 | 43 | false 44 | 45 | 46 | 47 | verify 48 | 49 | cmp 50 | 51 | 52 | 53 | 54 | 55 | com.diffplug.spotless 56 | spotless-maven-plugin 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/SelectClause.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.function.Supplier; 4 | 5 | /** 6 | * A clause to use as part of {@link Select#select(SelectClause[])}. Clauses can be created having a 7 | * channel instance, using {@link Channel#receiveClause()} and {@link Channel#sendClause(Object)}}. 8 | * 9 | *

A clause instance is immutable and can be reused in multiple `select` calls. 10 | */ 11 | public abstract class SelectClause { 12 | Channel getChannel() { 13 | return null; 14 | } 15 | 16 | /** 17 | * @return Either a {@link StoredSelectClause}, {@link ChannelClosed} when the channel is 18 | * already closed, or the selected value (not {@code null}). 19 | */ 20 | abstract Object register(SelectInstance select); 21 | 22 | /** 23 | * Transforms the raw value with the transformation function provided when creating the clause. 24 | * 25 | *

Might throw any exceptions that the provided transformation function throws. 26 | */ 27 | abstract T transformedRawValue(Object rawValue); 28 | } 29 | 30 | abstract class DefaultClause extends SelectClause { 31 | /** 32 | * Used as a result of {@link DefaultClause#register(SelectInstance)}, instead of {@code null}, 33 | * to indicate that the default clause has been selected during registration. 34 | */ 35 | @Override 36 | Object register(SelectInstance select) { 37 | return this; 38 | } 39 | } 40 | 41 | final class DefaultClauseValue extends DefaultClause { 42 | private final T value; 43 | 44 | public DefaultClauseValue(T value) { 45 | this.value = value; 46 | } 47 | 48 | @Override 49 | T transformedRawValue(Object rawValue) { 50 | return value; 51 | } 52 | } 53 | 54 | final class DefaultClauseCallback extends DefaultClause { 55 | private final Supplier callback; 56 | 57 | public DefaultClauseCallback(Supplier callback) { 58 | this.callback = callback; 59 | } 60 | 61 | @Override 62 | T transformedRawValue(Object rawValue) { 63 | return callback.get(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/ChannelUnlimitedTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.Timeout; 8 | 9 | /** Tests which always use unlimited channels. */ 10 | public class ChannelUnlimitedTest { 11 | @Test 12 | @Timeout(1) 13 | void testSimpleSendReceiveUnlimited() throws InterruptedException { 14 | // given 15 | Channel channel = Channel.newUnlimitedChannel(); 16 | 17 | // when 18 | channel.send("x"); // should not block 19 | channel.send("y"); // should not block 20 | var r1 = channel.receive(); // also should not block 21 | var r2 = channel.receive(); // also should not block 22 | 23 | // then 24 | assertEquals("x", r1); 25 | assertEquals("y", r2); 26 | } 27 | 28 | @Test 29 | @Timeout(1) 30 | void shouldReceiveFromAChannelUntilDone() throws InterruptedException { 31 | // given 32 | Channel c = Channel.newUnlimitedChannel(); 33 | c.send(1); 34 | c.send(2); 35 | c.send(3); 36 | c.done(); 37 | 38 | // when 39 | var r1 = c.receiveOrClosed(); 40 | var r2 = c.receiveOrClosed(); 41 | var r3 = c.receiveOrClosed(); 42 | var r4 = c.receiveOrClosed(); 43 | 44 | // then 45 | assertEquals(1, r1); 46 | assertEquals(2, r2); 47 | assertEquals(3, r3); 48 | assertInstanceOf(ChannelClosed.class, r4); 49 | } 50 | 51 | @Test 52 | @Timeout(1) 53 | void shouldNotReceiveFromAChannelInCaseOfAnError() throws InterruptedException { 54 | // given 55 | Channel c = Channel.newUnlimitedChannel(); 56 | c.send(1); 57 | c.send(2); 58 | c.error(new RuntimeException()); 59 | 60 | // when 61 | var r1 = c.receiveOrClosed(); 62 | var r2 = c.receiveOrClosed(); 63 | 64 | // then 65 | assertInstanceOf(ChannelError.class, r1); 66 | assertInstanceOf(ChannelError.class, r2); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/SelectKotlinBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import kotlinx.coroutines.selects.select 8 | import org.openjdk.jmh.annotations.* 9 | import java.util.concurrent.TimeUnit 10 | 11 | // same parameters as in the java benchmark 12 | @Warmup(iterations = 3, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Measurement(iterations = 10, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 14 | @Timeout(time = 5100, timeUnit = TimeUnit.MILLISECONDS) 15 | @Fork(value = 3) 16 | @BenchmarkMode(Mode.AverageTime) 17 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 18 | open class SelectKotlinBenchmark { 19 | @Benchmark 20 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 21 | fun selectWithSingleClause_defaultDispatcher() { 22 | runBlocking { 23 | val channel = Channel(0) 24 | launch(Dispatchers.Default) { 25 | for (x in 1..OPERATIONS_PER_INVOCATION) channel.send(63) 26 | channel.close() 27 | } 28 | 29 | launch(Dispatchers.Default) { 30 | for (x in 1..OPERATIONS_PER_INVOCATION) select { channel.onReceive { x -> x } } 31 | } 32 | } 33 | } 34 | 35 | @Benchmark 36 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 37 | fun selectWithTwoClauses_defaultDispatcher() { 38 | runBlocking { 39 | val channel1 = Channel(0) 40 | val channel2 = Channel(0) 41 | launch(Dispatchers.Default) { 42 | for (x in 1..OPERATIONS_PER_INVOCATION / 2) channel1.send(63) 43 | } 44 | 45 | launch(Dispatchers.Default) { 46 | for (x in 1..OPERATIONS_PER_INVOCATION / 2) channel2.send(63) 47 | } 48 | 49 | launch(Dispatchers.Default) { 50 | for (x in 1..OPERATIONS_PER_INVOCATION) select { 51 | channel1.onReceive { x -> x } 52 | channel2.onReceive { x -> x } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/MultiArrayIterator.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import java.util.Iterator; 4 | import java.util.List; 5 | import java.util.NoSuchElementException; 6 | 7 | /** 8 | * An iterator that efficiently iterates across multiple byte arrays without copying. This enables 9 | * O(1) concatenation of ByteChunks by avoiding array copying. 10 | */ 11 | class MultiArrayIterator implements Iterator { 12 | private final List arrays; 13 | private int currentArrayIndex = 0; 14 | private int currentPosition = 0; 15 | 16 | public MultiArrayIterator(List arrays) { 17 | this.arrays = arrays; 18 | } 19 | 20 | @Override 21 | public boolean hasNext() { 22 | return currentArrayIndex < arrays.size() 23 | && (currentArrayIndex < arrays.size() - 1 24 | || currentPosition < arrays.get(currentArrayIndex).length); 25 | } 26 | 27 | @Override 28 | public Byte next() { 29 | if (!hasNext()) { 30 | throw new NoSuchElementException(); 31 | } 32 | 33 | // Move to next non-empty array if current is exhausted 34 | while (currentArrayIndex < arrays.size() 35 | && currentPosition >= arrays.get(currentArrayIndex).length) { 36 | currentArrayIndex++; 37 | currentPosition = 0; 38 | } 39 | 40 | if (currentArrayIndex >= arrays.size()) { 41 | throw new NoSuchElementException(); 42 | } 43 | 44 | return arrays.get(currentArrayIndex)[currentPosition++]; 45 | } 46 | 47 | /** 48 | * Returns the number of bytes available for reading without blocking. This is used by 49 | * InputStream implementations. 50 | */ 51 | public int available() { 52 | int remaining = 0; 53 | for (int i = currentArrayIndex; i < arrays.size(); i++) { 54 | byte[] array = arrays.get(i); 55 | if (i == currentArrayIndex) { 56 | remaining += array.length - currentPosition; 57 | } else { 58 | remaining += array.length; 59 | } 60 | } 61 | return remaining; 62 | } 63 | 64 | /** Creates an empty MultiArrayIterator. */ 65 | public static MultiArrayIterator empty() { 66 | return new MultiArrayIterator(List.of()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/CancellableFork.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import java.lang.invoke.MethodHandles; 4 | import java.lang.invoke.VarHandle; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Semaphore; 7 | 8 | public interface CancellableFork extends Fork { 9 | /** 10 | * Interrupts the fork, and blocks until it completes with a result. 11 | * 12 | * @throws ExecutionException When the cancelled fork threw an exception. 13 | */ 14 | T cancel() throws InterruptedException, ExecutionException; 15 | 16 | /** 17 | * Interrupts the fork, and returns immediately, without waiting for the fork to complete. Note 18 | * that the enclosing scope will only complete once all forks have completed. 19 | */ 20 | void cancelNow(); 21 | } 22 | 23 | final class CancellableForkUsingResult extends ForkUsingResult implements CancellableFork { 24 | /** interrupt signal */ 25 | final Semaphore done = new Semaphore(0); 26 | 27 | private volatile boolean started; 28 | 29 | /** VarHandle for atomic operations on the 'started' field */ 30 | private static final VarHandle STARTED; 31 | 32 | static { 33 | try { 34 | MethodHandles.Lookup l = // MethodHandles.lookup() 35 | MethodHandles.privateLookupIn( 36 | CancellableForkUsingResult.class, MethodHandles.lookup()); 37 | STARTED = l.findVarHandle(CancellableForkUsingResult.class, "started", boolean.class); 38 | } catch (ReflectiveOperationException e) { 39 | throw new ExceptionInInitializerError(e); 40 | } 41 | } 42 | 43 | @Override 44 | public T cancel() throws InterruptedException, ExecutionException { 45 | cancelNow(); 46 | return join(); // ForkUsingResult#join throws InterruptedException,ExecutionException 47 | } 48 | 49 | @Override 50 | public void cancelNow() { 51 | // will cause the scope to end, interrupting the task if it hasn't yet finished (or 52 | // potentially never starting it) 53 | done.release(); 54 | if (checkNotStartedThenStart()) { // !started.getAndSet(true) 55 | completeExceptionally(new InterruptedException("fork was cancelled before it started")); 56 | } 57 | } 58 | 59 | boolean checkNotStartedThenStart() { 60 | return (Boolean) STARTED.getAndSet(this, true) == false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /structured/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | parent 9 | 1.1.1 10 | 11 | 12 | structured 13 | 0.5.0 14 | jar 15 | ${project.groupId}:${project.artifactId} 16 | 17 | 18 | 25 19 | 20 | 21 | 22 | 23 | 24 | com.diffplug.spotless 25 | spotless-maven-plugin 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 34 | true 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-surefire-plugin 40 | 41 | --enable-preview 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-javadoc-plugin 47 | 48 | 25 49 | --enable-preview 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | com.softwaremill.jox 59 | channels 60 | 1.1.1 61 | 62 | 63 | org.junit.jupiter 64 | junit-jupiter 65 | 6.0.1 66 | test 67 | 68 | 69 | org.awaitility 70 | awaitility 71 | test 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/Source.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | 8 | /** 9 | * A channel source, which can be used to receive values from the channel. See {@link Channel} for 10 | * more details. 11 | */ 12 | public interface Source extends CloseableChannel { 13 | /** 14 | * Receive a value from the channel. 15 | * 16 | * @throws ChannelClosedException When the channel is closed. 17 | */ 18 | T receive() throws InterruptedException; 19 | 20 | /** 21 | * Receive a value from the channel. Doesn't throw exceptions when the channel is closed, but 22 | * returns a value. 23 | * 24 | * @return Either a value of type {@code T}, or {@link ChannelClosed}, when the channel is 25 | * closed. 26 | */ 27 | Object receiveOrClosed() throws InterruptedException; 28 | 29 | /** 30 | * Create a clause which can be used in {@link Select#select(SelectClause[])}. The clause will 31 | * receive a value from the current channel. 32 | */ 33 | SelectClause receiveClause(); 34 | 35 | /** 36 | * Create a clause which can be used in {@link Select#select(SelectClause[])}. The clause will 37 | * receive a value from the current channel, and transform it using the provided {@code 38 | * callback}. 39 | */ 40 | SelectClause receiveClause(Function callback); 41 | 42 | // draining operations 43 | 44 | /** 45 | * Invokes the given function for each received element. Blocks until the channel is done. 46 | * 47 | * @throws ChannelErrorException When there is an upstream error. 48 | */ 49 | default void forEach(Consumer c) throws InterruptedException { 50 | var repeat = true; 51 | while (repeat) { 52 | switch (receiveOrClosed()) { 53 | case ChannelDone cd -> repeat = false; 54 | case ChannelError ce -> throw ce.toException(); 55 | case Object t -> c.accept((T) t); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Accumulates all elements received from the channel into a list. Blocks until the channel is 62 | * done. 63 | * 64 | * @throws ChannelErrorException When there is an upstream error. 65 | */ 66 | default List toList() throws InterruptedException { 67 | var l = new ArrayList(); 68 | forEach(l::add); 69 | return l; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /structured/src/test/java/com/softwaremill/jox/structured/ScopeTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.Queue; 6 | import java.util.concurrent.ConcurrentLinkedQueue; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class ScopeTest { 13 | 14 | @Test 15 | void externalRunnerShouldBeInitializedOnce() throws ExecutionException, InterruptedException { 16 | // given 17 | Queue> results = new ConcurrentLinkedQueue<>(); 18 | int numberOfCalls = 1_000_000; 19 | 20 | // when 21 | Scopes.supervised( 22 | scope -> { 23 | for (int i = 0; i < numberOfCalls; i++) { 24 | scope.forkUser( 25 | () -> { 26 | results.add(scope.externalRunner().scheduler()); 27 | return null; 28 | }); 29 | } 30 | return null; 31 | }); 32 | 33 | // then 34 | assertEquals(numberOfCalls, results.size()); 35 | 36 | ActorRef peek = results.peek(); // all elements should be the same 37 | results.forEach(r -> assertEquals(peek, r)); 38 | } 39 | 40 | /** 41 | * @see com.softwaremill.jox.structured.CancellableForkUsingResult#cancel 42 | */ 43 | @Test 44 | void testForkCancelBehavior() throws InterruptedException, ExecutionException { 45 | var run = new AtomicBoolean(false); 46 | var fork = 47 | (CancellableForkUsingResult) 48 | new Scope() 49 | .forkCancellable( 50 | () -> { 51 | run.set(true); 52 | return 42; 53 | }); 54 | ExecutionException ee = assertThrows(ExecutionException.class, fork::cancel); 55 | assertInstanceOf(InterruptedException.class, ee.getCause()); 56 | assertFalse(run.get()); 57 | 58 | ee = assertThrows(ExecutionException.class, fork::join); 59 | assertInstanceOf(InterruptedException.class, ee.getCause()); 60 | 61 | ee = assertThrows(ExecutionException.class, fork::cancel); 62 | assertInstanceOf(InterruptedException.class, ee.getCause()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bench/bench-kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | bench 9 | 1.1.1 10 | 11 | 12 | bench-kotlin 13 | 1.1.1 14 | jar 15 | 16 | 17 | 2.3.0 18 | 19 | 20 | 21 | 22 | org.jetbrains.kotlin 23 | kotlin-stdlib 24 | ${kotlin.version} 25 | 26 | 27 | org.jetbrains.kotlinx 28 | kotlinx-coroutines-core 29 | 1.10.2 30 | 31 | 32 | 33 | 34 | ${project.basedir}/src 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-shade-plugin 40 | 41 | 42 | kotlin-maven-plugin 43 | org.jetbrains.kotlin 44 | ${kotlin.version} 45 | 46 | 47 | enable 48 | 49 | 50 | 51 | 52 | kapt 53 | 54 | kapt 55 | 56 | 57 | 58 | src 59 | 60 | 61 | 62 | 63 | compile 64 | process-sources 65 | 66 | compile 67 | 68 | 69 | 70 | test-compile 71 | test-compile 72 | 73 | test-compile 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/Fork.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | interface Fork { 9 | void start(); 10 | 11 | void interrupt(); 12 | 13 | T join() throws InterruptedException; 14 | 15 | static Fork newNoResult(RunnableWithException runnable) { 16 | var thread = 17 | new Thread( 18 | () -> { 19 | try { 20 | runnable.run(); 21 | } catch (Exception e) { 22 | throw new RuntimeException(e); 23 | } 24 | }); 25 | return new Fork<>() { 26 | @Override 27 | public void start() { 28 | thread.start(); 29 | } 30 | 31 | @Override 32 | public void interrupt() { 33 | thread.interrupt(); 34 | } 35 | 36 | @Override 37 | public Void join() throws InterruptedException { 38 | thread.join(); 39 | return null; 40 | } 41 | }; 42 | } 43 | 44 | static Fork newWithResult(Callable callable) { 45 | var result = new AtomicReference(); 46 | var thread = 47 | new Thread( 48 | () -> { 49 | try { 50 | result.set(callable.call()); 51 | } catch (Exception e) { 52 | throw new RuntimeException(e); 53 | } 54 | }); 55 | 56 | return new Fork<>() { 57 | @Override 58 | public void start() { 59 | thread.start(); 60 | } 61 | 62 | @Override 63 | public void interrupt() { 64 | thread.interrupt(); 65 | } 66 | 67 | @Override 68 | public T join() throws InterruptedException { 69 | thread.join(); 70 | return result.get(); 71 | } 72 | }; 73 | } 74 | 75 | static void startAll(Fork... fork) { 76 | for (Fork f : fork) { 77 | f.start(); 78 | } 79 | } 80 | 81 | @SafeVarargs 82 | static List joinAll(Fork... fork) throws InterruptedException { 83 | var result = new ArrayList(); 84 | for (Fork f : fork) { 85 | result.add(f.join()); 86 | } 87 | return result; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/FraySendReceiveTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | import static com.softwaremill.jox.fray.Config.CHANNEL_SIZE; 4 | 5 | import java.util.ArrayList; 6 | 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.pastalab.fray.junit.junit5.FrayTestExtension; 9 | import org.pastalab.fray.junit.junit5.annotations.ConcurrencyTest; 10 | 11 | import com.softwaremill.jox.Channel; 12 | 13 | @ExtendWith(FrayTestExtension.class) 14 | public class FraySendReceiveTest { 15 | // send | receive 16 | 17 | @ConcurrencyTest 18 | public void sendReceiveTest() throws InterruptedException { 19 | Channel ch = Channel.newBufferedChannel(CHANNEL_SIZE); 20 | 21 | Fork f1 = Fork.newNoResult(() -> ch.send(10)); 22 | Fork f2 = Fork.newWithResult(ch::receive); 23 | 24 | Fork.startAll(f1, f2); 25 | f1.join(); 26 | 27 | assert (f2.join() == 10); 28 | } 29 | 30 | // send | send | receive | receive 31 | 32 | @ConcurrencyTest 33 | public void sendSendReceiveReceiveTest() throws InterruptedException { 34 | Channel ch = Channel.newBufferedChannel(CHANNEL_SIZE); 35 | 36 | Fork f1 = Fork.newNoResult(() -> ch.send(10)); 37 | Fork f2 = Fork.newNoResult(() -> ch.send(20)); 38 | Fork f3 = Fork.newWithResult(ch::receive); 39 | Fork f4 = Fork.newWithResult(ch::receive); 40 | 41 | Fork.startAll(f1, f2, f3, f4); 42 | 43 | assert (f3.join() + f4.join() == 30); 44 | } 45 | 46 | // many sends | many receives 47 | 48 | @ConcurrencyTest 49 | public void multiSendMultipleReceiveTest() throws InterruptedException { 50 | Channel ch = Channel.newBufferedChannel(CHANNEL_SIZE); 51 | 52 | // segment size is 32 by default, this covers more than 1 segment 53 | int concurrency = 40; 54 | 55 | var sendForks = new ArrayList>(); 56 | var receiveForks = new ArrayList>(); 57 | 58 | for (int i = 0; i < concurrency; i++) { 59 | final var finalI = i; 60 | sendForks.add(Fork.newNoResult(() -> ch.send(finalI))); 61 | receiveForks.add(Fork.newWithResult(ch::receive)); 62 | } 63 | 64 | Fork.startAll(sendForks.toArray(new Fork[0])); 65 | Fork.startAll(receiveForks.toArray(new Fork[0])); 66 | 67 | for (Fork sendFork : sendForks) { 68 | sendFork.join(); 69 | } 70 | 71 | var result = 0; 72 | for (Fork receiveFork : receiveForks) { 73 | result += receiveFork.join(); 74 | } 75 | 76 | assert (result == concurrency * (concurrency - 1) / 2); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: [ main ] 5 | push: 6 | branches: [ main ] 7 | tags: [ v* ] 8 | permissions: 9 | contents: write # release-drafter requirement 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | - name: Set up JDK 17 | uses: actions/setup-java@v5 18 | with: 19 | distribution: 'zulu' 20 | java-version: '25' 21 | cache: 'maven' 22 | - name: Test 23 | run: mvn --batch-mode --update-snapshots verify 24 | - name: Prepare release notes 25 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6, specifically v6.1.0 26 | with: 27 | config-name: release-drafter.yml 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | fray-tests: 32 | runs-on: ubuntu-24.04 33 | strategy: 34 | matrix: 35 | channel_size: [ 0, 1, 10 ] 36 | segment_size: [ 4, 32 ] 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v6 40 | - name: Set up JDK 41 | uses: actions/setup-java@v5 42 | with: 43 | distribution: 'zulu' 44 | java-version: '25' 45 | cache: 'maven' 46 | - name: Install current Jox # so that latest channels code is available 47 | run: mvn install -DskipTests=true 48 | - name: Run Fray tests (CHANNEL_SIZE=${{ matrix.channel_size }}, JOX_SEGMENT_SIZE=${{ matrix.segment_size }}) 49 | run: mvn --batch-mode verify -Pintegration-tests -pl channels-fray-tests 50 | env: 51 | CHANNEL_SIZE: ${{ matrix.channel_size }} 52 | JOX_SEGMENT_SIZE: ${{ matrix.segment_size }} 53 | 54 | publishReleaseNotes: 55 | name: Publish release notes 56 | needs: [ ci, fray-tests ] 57 | runs-on: ubuntu-24.04 58 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v6 62 | - name: Set up JDK 63 | uses: actions/setup-java@v5 64 | with: 65 | distribution: 'zulu' 66 | java-version: '25' 67 | - name: Extract version from tag name 68 | run: | 69 | version=${GITHUB_REF/refs\/tags\/v/} 70 | echo "VERSION=$version" >> $GITHUB_ENV 71 | - name: Publish release notes 72 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6, specifically v6.1.0 73 | with: 74 | config-name: release-drafter.yml 75 | publish: true 76 | name: "v${{ env.VERSION }}" 77 | tag: "v${{ env.VERSION }}" 78 | version: "v${{ env.VERSION }}" 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | -------------------------------------------------------------------------------- /bench/bench-kotlin/src/com/softwaremill/jox/ChainedKotlinBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | import org.openjdk.jmh.annotations.* 8 | import java.util.concurrent.TimeUnit 9 | 10 | // same parameters as in the java benchmark 11 | @Warmup(iterations = 3, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 10, time = 5000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Fork(value = 3) 14 | @BenchmarkMode(Mode.AverageTime) 15 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 16 | @State(Scope.Benchmark) 17 | open class ChainedKotlinBenchmark { 18 | @Param("0", "16", "100") 19 | var capacity: Int = 0 20 | 21 | @Param("10000") 22 | var chainLength: Int = 0 23 | 24 | @Benchmark 25 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION_CHAINED) 26 | fun channelChain_defaultDispatcher() { 27 | runBlocking { 28 | // we want to measure the amount of time a send-receive pair takes 29 | var elements = OPERATIONS_PER_INVOCATION_CHAINED / chainLength 30 | 31 | // create an array of channelCount channels 32 | val channels = Array(chainLength) { Channel(capacity) } 33 | 34 | launch(Dispatchers.Default) { 35 | var ch = channels[0] 36 | for (x in 1..elements) ch.send(63) 37 | } 38 | 39 | for (t in 1 until chainLength) { 40 | val ch1 = channels[t - 1] 41 | val ch2 = channels[t] 42 | launch(Dispatchers.Default) { 43 | for (x in 1..elements) ch2.send(ch1.receive()) 44 | } 45 | } 46 | 47 | launch(Dispatchers.Default) { 48 | var ch = channels[chainLength - 1] 49 | for (x in 1..elements) ch.receive() 50 | } 51 | } 52 | } 53 | 54 | @Benchmark 55 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION_CHAINED) 56 | fun channelChain_eventLoop() { 57 | runBlocking { 58 | // we want to measure the amount of time a send-receive pair takes 59 | var elements = OPERATIONS_PER_INVOCATION_CHAINED / chainLength 60 | 61 | // create an array of channelCount channels 62 | val channels = Array(chainLength) { Channel(capacity) } 63 | 64 | launch { 65 | var ch = channels[0] 66 | for (x in 1..elements) ch.send(63) 67 | } 68 | 69 | for (t in 1 until chainLength) { 70 | val ch1 = channels[t - 1] 71 | val ch2 = channels[t] 72 | launch { 73 | for (x in 1..elements) ch2.send(ch1.receive()) 74 | } 75 | } 76 | 77 | launch { 78 | var ch = channels[chainLength - 1] 79 | for (x in 1..elements) ch.receive() 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /channels-fray-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | parent 9 | 1.1.1 10 | 11 | 12 | channels-fray-tests 13 | 1.1.1 14 | jar 15 | ${project.groupId}:${project.artifactId} 16 | 17 | 18 | 19 | com.softwaremill.jox 20 | channels 21 | 1.1.1 22 | test 23 | 24 | 25 | org.junit.jupiter 26 | junit-jupiter 27 | 5.14.1 28 | test 29 | 30 | 31 | org.pastalab.fray 32 | fray-junit 33 | 0.7.1 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 41 | com.diffplug.spotless 42 | spotless-maven-plugin 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-surefire-plugin 47 | 48 | true 49 | 50 | 51 | 52 | org.pastalab.fray.maven 53 | fray-plugins-maven 54 | 0.7.1 55 | 56 | 57 | prepare-fray 58 | 59 | prepare-fray 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | integration-tests 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-failsafe-plugin 75 | 76 | 77 | **/*Test.java 78 | **/*Tests.java 79 | 80 | 81 | 82 | 83 | 84 | integration-test 85 | verify 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/LinesImpl.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import java.nio.charset.Charset; 4 | import java.util.*; 5 | 6 | class LinesImpl { 7 | 8 | static Flow lines(Charset charset, Flow.ByteFlow parentFlow) { 9 | return parentFlow 10 | .mapStatefulConcat( 11 | Optional::empty, 12 | (buffer, nextChunk) -> { 13 | ByteChunk chunk = nextChunk; 14 | if (chunk.length() == 0) { 15 | // get next incoming chunk 16 | return Map.entry(Optional.empty(), Collections.emptyList()); 17 | } 18 | 19 | // check if chunk contains newline character, if not proceed to the next 20 | // chunk 21 | int newLineIndex = chunk.indexWhere(b -> b == '\n'); 22 | if (newLineIndex == -1) { 23 | if (buffer.isEmpty()) { 24 | return Map.entry(Optional.of(chunk), Collections.emptyList()); 25 | } 26 | return Map.entry( 27 | Optional.of(buffer.get().concat(chunk)), 28 | Collections.emptyList()); 29 | } 30 | 31 | // buffer for lines, if chunk contains more than one newline character 32 | List lines = new ArrayList<>(); 33 | 34 | // variable used to clear buffer after using it 35 | ByteChunk bufferFromPreviousChunk = buffer.orElse(ByteChunk.empty()); 36 | while (chunk.length() > 0 && newLineIndex != -1) { 37 | Map.Entry split = chunk.splitAt(newLineIndex); 38 | var line = split.getKey(); 39 | var newChunk = split.getValue().drop(1); 40 | 41 | if (bufferFromPreviousChunk.length() > 0) { 42 | // concat accumulated buffer and line 43 | lines.add(bufferFromPreviousChunk.concat(line)); 44 | // cleanup buffer 45 | bufferFromPreviousChunk = ByteChunk.empty(); 46 | } else { 47 | lines.add(line); 48 | } 49 | chunk = newChunk; 50 | newLineIndex = chunk.indexWhere(b -> b == '\n'); 51 | } 52 | return Map.entry(Optional.of(chunk), lines); 53 | }, 54 | buf -> buf) 55 | .map(chunk -> chunk.convertToString(charset)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/ChannelClosedTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.TestUtil.forkCancelable; 4 | import static com.softwaremill.jox.TestUtil.scoped; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class ChannelClosedTest { 12 | @Test 13 | void testClosed_noValues_whenError() throws InterruptedException { 14 | // given 15 | Channel c = Channel.newRendezvousChannel(); 16 | RuntimeException reason = new RuntimeException(); 17 | 18 | // when 19 | c.error(reason); 20 | 21 | // then 22 | assertTrue(c.isClosedForReceive()); 23 | assertTrue(c.isClosedForSend()); 24 | assertEquals(new ChannelError(reason, c), c.receiveOrClosed()); 25 | } 26 | 27 | @Test 28 | void testClosed_noValues_whenDone() throws InterruptedException { 29 | // given 30 | Channel c = Channel.newRendezvousChannel(); 31 | 32 | // when 33 | c.done(); 34 | 35 | // then 36 | assertTrue(c.isClosedForReceive()); 37 | assertTrue(c.isClosedForSend()); 38 | assertEquals(new ChannelDone(c), c.receiveOrClosed()); 39 | } 40 | 41 | @Test 42 | void testClosed_hasSuspendedValues_whenDone() throws InterruptedException, ExecutionException { 43 | // given 44 | Channel c = Channel.newRendezvousChannel(); 45 | 46 | // when 47 | scoped( 48 | scope -> { 49 | var f = 50 | forkCancelable( 51 | scope, 52 | () -> { 53 | c.send(1); 54 | }); 55 | 56 | try { 57 | Thread.sleep(100); // let the send suspend 58 | c.done(); 59 | 60 | // then 61 | assertFalse(c.isClosedForReceive()); 62 | assertTrue(c.isClosedForSend()); 63 | } finally { 64 | f.cancel(); 65 | } 66 | }); 67 | } 68 | 69 | @Test 70 | void testClosed_hasBufferedValues_whenDone() throws InterruptedException { 71 | // given 72 | Channel c = Channel.newBufferedChannel(5); 73 | 74 | // when 75 | c.send(1); 76 | c.send(2); 77 | c.done(); 78 | 79 | // then 80 | assertFalse(c.isClosedForReceive()); 81 | assertTrue(c.isClosedForSend()); 82 | } 83 | 84 | @Test 85 | void testClosed_hasValues_whenError() throws InterruptedException { 86 | // given 87 | Channel c = Channel.newBufferedChannel(5); 88 | 89 | // when 90 | c.send(1); 91 | c.send(2); 92 | c.error(new RuntimeException()); 93 | 94 | // then 95 | assertTrue(c.isClosedForReceive()); 96 | assertTrue(c.isClosedForSend()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/SelectTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.Select.defaultClause; 4 | import static com.softwaremill.jox.Select.defaultClauseNull; 5 | import static com.softwaremill.jox.Select.select; 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class SelectTest { 11 | @Test 12 | public void testSelectDefaultValue() throws InterruptedException { 13 | // given 14 | Channel ch1 = Channel.newBufferedChannel(1); 15 | Channel ch2 = Channel.newBufferedChannel(1); 16 | 17 | // when 18 | String received = select(ch1.receiveClause(), ch2.receiveClause(), defaultClause("x")); 19 | 20 | // then 21 | assertEquals("x", received); 22 | } 23 | 24 | @Test 25 | public void testSelectDefaultCallback() throws InterruptedException { 26 | // given 27 | Channel ch1 = Channel.newBufferedChannel(1); 28 | Channel ch2 = Channel.newBufferedChannel(1); 29 | 30 | // when 31 | String received = 32 | select(ch1.receiveClause(), ch2.receiveClause(), defaultClause(() -> "x")); 33 | 34 | // then 35 | assertEquals("x", received); 36 | } 37 | 38 | @Test 39 | public void testSelectDefaultNull() throws InterruptedException { 40 | // given 41 | Channel ch1 = Channel.newBufferedChannel(1); 42 | Channel ch2 = Channel.newBufferedChannel(1); 43 | 44 | // when 45 | String received = select(ch1.receiveClause(), ch2.receiveClause(), defaultClauseNull()); 46 | 47 | // then 48 | assertNull(received); 49 | } 50 | 51 | @Test 52 | public void testDoNotSelectDefault() throws InterruptedException { 53 | // given 54 | Channel ch1 = Channel.newBufferedChannel(1); 55 | Channel ch2 = Channel.newBufferedChannel(1); 56 | ch2.send("a"); 57 | 58 | // when 59 | String received = select(ch1.receiveClause(), ch2.receiveClause(), defaultClause("x")); 60 | 61 | // then 62 | assertEquals("a", received); 63 | } 64 | 65 | @Test 66 | public void testDefaultCanOnlyBeLast() throws InterruptedException { 67 | // given 68 | Channel ch1 = Channel.newBufferedChannel(1); 69 | Channel ch2 = Channel.newBufferedChannel(1); 70 | 71 | // when 72 | try { 73 | select(ch1.receiveClause(), defaultClause("x"), ch2.receiveClause()); 74 | fail("Select should have failed"); 75 | } catch (IllegalArgumentException e) { 76 | // then ok 77 | } 78 | } 79 | 80 | @Test 81 | public void testSelectObject() throws InterruptedException { 82 | // given 83 | Channel ch1 = Channel.newBufferedChannel(1); 84 | Channel ch2 = Channel.newBufferedChannel(1); 85 | ch1.send("x"); 86 | 87 | // when 88 | Object received = select(ch1.receiveClause(), ch2.receiveClause()); 89 | 90 | // then 91 | assertEquals("x", received); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowSlidingTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static com.softwaremill.jox.structured.Scopes.supervised; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import com.softwaremill.jox.ChannelError; 11 | 12 | public class FlowSlidingTest { 13 | 14 | @Test 15 | void shouldCreateSlidingWindowsForN2AndStep1() throws Exception { 16 | List> result = Flows.fromValues(1, 2, 3, 4).sliding(2, 1).runToList(); 17 | 18 | assertEquals(List.of(List.of(1, 2), List.of(2, 3), List.of(3, 4)), result); 19 | } 20 | 21 | @Test 22 | void shouldCreateSlidingWindowsForN3AndStep1() throws Exception { 23 | List> result = Flows.fromValues(1, 2, 3, 4, 5).sliding(3, 1).runToList(); 24 | 25 | assertEquals(List.of(List.of(1, 2, 3), List.of(2, 3, 4), List.of(3, 4, 5)), result); 26 | } 27 | 28 | @Test 29 | void shouldCreateSlidingWindowsForN2AndStep2() throws Exception { 30 | List> result = Flows.fromValues(1, 2, 3, 4, 5).sliding(2, 2).runToList(); 31 | 32 | assertEquals(List.of(List.of(1, 2), List.of(3, 4), List.of(5)), result); 33 | } 34 | 35 | @Test 36 | void shouldCreateSlidingWindowsForN3AndStep2() throws Exception { 37 | List> result = Flows.fromValues(1, 2, 3, 4, 5, 6).sliding(3, 2).runToList(); 38 | 39 | assertEquals(List.of(List.of(1, 2, 3), List.of(3, 4, 5), List.of(5, 6)), result); 40 | } 41 | 42 | @Test 43 | void shouldCreateSlidingWindowsForN1AndStep2() throws Exception { 44 | List> result = Flows.fromValues(1, 2, 3, 4, 5).sliding(1, 2).runToList(); 45 | 46 | assertEquals(List.of(List.of(1), List.of(3), List.of(5)), result); 47 | } 48 | 49 | @Test 50 | void shouldCreateSlidingWindowsForN2AndStep3() throws Exception { 51 | List> result = Flows.fromValues(1, 2, 3, 4, 5, 6).sliding(2, 3).runToList(); 52 | 53 | assertEquals(List.of(List.of(1, 2), List.of(4, 5)), result); 54 | } 55 | 56 | @Test 57 | void shouldCreateSlidingWindowsForN2AndStep3With1ElementRemainingInTheEnd() throws Exception { 58 | List> result = 59 | Flows.fromValues(1, 2, 3, 4, 5, 6, 7).sliding(2, 3).runToList(); 60 | assertEquals(List.of(List.of(1, 2), List.of(4, 5), List.of(7)), result); 61 | } 62 | 63 | @Test 64 | void shouldReturnFailedSourceWhenTheOriginalSourceIsFailed() throws Exception { 65 | supervised( 66 | scope -> { 67 | RuntimeException failure = new RuntimeException(); 68 | ChannelError received = 69 | (ChannelError) 70 | Flows.failed(failure) 71 | .sliding(1, 2) 72 | .runToChannel(scope) 73 | .receiveOrClosed(); 74 | assertEquals(failure, received.cause()); 75 | return null; 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/FrayCompleteTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | import static com.softwaremill.jox.fray.Config.CHANNEL_SIZE; 4 | 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.pastalab.fray.junit.junit5.FrayTestExtension; 7 | import org.pastalab.fray.junit.junit5.annotations.ConcurrencyTest; 8 | 9 | import com.softwaremill.jox.Channel; 10 | import com.softwaremill.jox.ChannelDone; 11 | import com.softwaremill.jox.ChannelError; 12 | 13 | @ExtendWith(FrayTestExtension.class) 14 | public class FrayCompleteTest { 15 | // send + done | receive 16 | 17 | @ConcurrencyTest 18 | public void sendDoneReceiveTest() throws InterruptedException { 19 | Channel ch = Channel.newBufferedChannel(CHANNEL_SIZE); 20 | 21 | var f1 = 22 | Fork.newNoResult( 23 | () -> { 24 | ch.send(10); 25 | ch.send(11); 26 | ch.send(12); 27 | ch.done(); 28 | }); 29 | var f2 = 30 | Fork.newNoResult( 31 | () -> { 32 | assert (ch.receive() == 10); 33 | assert (ch.receive() == 11); 34 | assert (ch.receive() == 12); 35 | assert (ch.receiveOrClosed() instanceof ChannelDone); 36 | }); 37 | 38 | Fork.startAll(f1, f2); 39 | Fork.joinAll(f1, f2); 40 | } 41 | 42 | // send + error | receive 43 | 44 | @ConcurrencyTest 45 | public void sendErrorReceiveTest_rendezvous() throws InterruptedException { 46 | Channel ch = Channel.newRendezvousChannel(); 47 | 48 | var f1 = 49 | Fork.newNoResult( 50 | () -> { 51 | ch.send(10); 52 | ch.send(11); 53 | ch.send(12); 54 | ch.error(new RuntimeException("boom!")); 55 | }); 56 | var f2 = 57 | Fork.newNoResult( 58 | () -> { 59 | var r1 = ch.receiveOrClosed(); 60 | assert (Integer.valueOf(10).equals(r1) || r1 instanceof ChannelError); 61 | 62 | var r2 = ch.receiveOrClosed(); 63 | assert (((!(r1 instanceof ChannelError)) 64 | && Integer.valueOf(11).equals(r2)) 65 | || r2 instanceof ChannelError); 66 | 67 | var r3 = ch.receiveOrClosed(); 68 | assert (((!(r2 instanceof ChannelError)) 69 | && Integer.valueOf(12).equals(r3)) 70 | || r3 instanceof ChannelError); 71 | 72 | var r4 = ch.receiveOrClosed(); 73 | assert (r4 instanceof ChannelError); 74 | }); 75 | 76 | Fork.startAll(f1, f2); 77 | Fork.joinAll(f1, f2); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/ByteFlowTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.List; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class ByteFlowTest { 12 | 13 | @Test 14 | void shouldThrowWhenMappingToByteFlowWhenFlowDoesNotContainByteChunksNorByteArrays() { 15 | // given 16 | var flow = Flows.fromValues(1, 2, 3); 17 | 18 | // when 19 | var exception = assertThrows(IllegalArgumentException.class, () -> flow.toByteFlow()); 20 | 21 | // then 22 | assertEquals( 23 | "requirement failed: ByteFlow can only be created from ByteChunk or byte[]", 24 | exception.getMessage()); 25 | } 26 | 27 | @Test 28 | void shouldMapToByteFlowUsingByteArrayMappingFunction() throws Exception { 29 | // given 30 | var flow = Flows.fromValues(1, 2, 3); 31 | 32 | // when 33 | Flow.ByteFlow byteFlow = 34 | flow.toByteFlow( 35 | (Flow.ByteArrayMapper) 36 | integer -> new byte[] {integer.byteValue()}); 37 | 38 | // then 39 | assertEquals( 40 | List.of( 41 | ByteChunk.fromArray(new byte[] {1}), 42 | ByteChunk.fromArray(new byte[] {2}), 43 | ByteChunk.fromArray(new byte[] {3})), 44 | byteFlow.runToList()); 45 | } 46 | 47 | @Test 48 | void shouldMapToByteFlowUsingByteChunkMappingFunction() throws Exception { 49 | // given 50 | var flow = Flows.fromValues(1, 2, 3); 51 | 52 | // when 53 | Flow.ByteFlow byteFlow = 54 | flow.toByteFlow( 55 | (Flow.ByteChunkMapper) 56 | integer -> ByteChunk.fromArray(new byte[] {integer.byteValue()})); 57 | 58 | // then 59 | assertEquals( 60 | List.of( 61 | ByteChunk.fromArray(new byte[] {1}), 62 | ByteChunk.fromArray(new byte[] {2}), 63 | ByteChunk.fromArray(new byte[] {3})), 64 | byteFlow.runToList()); 65 | } 66 | 67 | @Test 68 | void shouldCreateByteFlowFromByteArrays() throws Exception { 69 | Flow.ByteFlow byteFlow = 70 | Flows.fromByteArrays( 71 | "MjE".getBytes(StandardCharsets.UTF_8), 72 | "zNw==".getBytes(StandardCharsets.UTF_8)); 73 | 74 | assertEquals(List.of("MjEzNw=="), byteFlow.linesUtf8().runToList()); 75 | } 76 | 77 | @Test 78 | void shouldCreateByteFlowFromByteChunks() throws Exception { 79 | Flow.ByteFlow byteFlow = 80 | Flows.fromByteChunks( 81 | ByteChunk.fromArray("MjE".getBytes(StandardCharsets.UTF_8)), 82 | ByteChunk.fromArray("zNw==".getBytes(StandardCharsets.UTF_8))); 83 | 84 | assertEquals(List.of("MjEzNw=="), byteFlow.linesUtf8().runToList()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/ChannelTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.TestUtil.forkVoid; 4 | import static com.softwaremill.jox.TestUtil.scoped; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.util.HashSet; 8 | import java.util.concurrent.ConcurrentSkipListSet; 9 | import java.util.concurrent.ExecutionException; 10 | import java.util.concurrent.Future; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** Channel tests which are run for various capacities. */ 15 | public class ChannelTest { 16 | @TestWithCapacities 17 | void testSendReceiveInManyForks(int capacity) throws ExecutionException, InterruptedException { 18 | // given 19 | Channel channel = 20 | capacity == 0 21 | ? Channel.newRendezvousChannel() 22 | : Channel.newBufferedChannel(capacity); 23 | var fs = new HashSet>(); 24 | var s = new ConcurrentSkipListSet(); 25 | 26 | // when 27 | scoped( 28 | scope -> { 29 | for (int i = 1; i <= 1000; i++) { 30 | int ii = i; 31 | forkVoid(scope, () -> channel.send(ii)); 32 | } 33 | for (int i = 1; i <= 1000; i++) { 34 | fs.add(forkVoid(scope, () -> s.add(channel.receive()))); 35 | } 36 | for (Future f : fs) { 37 | f.get(); 38 | } 39 | 40 | // then 41 | assertEquals(1000, s.size()); 42 | }); 43 | } 44 | 45 | @TestWithCapacities 46 | void testSendReceiveManyElementsInTwoForks(int capacity) 47 | throws ExecutionException, InterruptedException { 48 | // given 49 | Channel channel = 50 | capacity == 0 51 | ? Channel.newRendezvousChannel() 52 | : Channel.newBufferedChannel(capacity); 53 | var s = new ConcurrentSkipListSet(); 54 | 55 | // when 56 | scoped( 57 | scope -> { 58 | forkVoid( 59 | scope, 60 | () -> { 61 | for (int i = 1; i <= 1000; i++) { 62 | channel.send(i); 63 | } 64 | }); 65 | forkVoid( 66 | scope, 67 | () -> { 68 | for (int i = 1; i <= 1000; i++) { 69 | s.add(channel.receive()); 70 | } 71 | }) 72 | .get(); 73 | 74 | // then 75 | assertEquals(1000, s.size()); 76 | }); 77 | } 78 | 79 | @Test 80 | void testNullItem() throws InterruptedException { 81 | Channel ch = Channel.newBufferedChannel(4); 82 | assertThrows(NullPointerException.class, () -> ch.send(null)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowsProjectReactorTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.time.Duration; 6 | import java.util.List; 7 | import java.util.stream.IntStream; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.reactivestreams.FlowAdapters; 11 | 12 | import com.softwaremill.jox.structured.Scopes; 13 | 14 | import reactor.core.publisher.Flux; 15 | 16 | public class FlowsProjectReactorTest { 17 | 18 | @Test 19 | void simpleFlowShouldEmitElementsToBeProcessedByFlux() throws InterruptedException { 20 | Scopes.supervised( 21 | scope -> { 22 | // given 23 | var flow = Flows.range(1, 4, 1); 24 | 25 | // when 26 | var result = 27 | Flux.from(FlowAdapters.toPublisher(flow.toPublisher(scope))) 28 | .map(i -> i * 2) 29 | .collectList() 30 | .block(); 31 | 32 | // then 33 | assertEquals(List.of(2, 4, 6, 8), result); 34 | return null; 35 | }); 36 | } 37 | 38 | @Test 39 | void concurrentFlowShouldEmitElementsToBeProcessedByFlux() throws InterruptedException { 40 | Scopes.supervised( 41 | scope -> { 42 | // given 43 | var flow = 44 | Flows.tick(Duration.ofMillis(100), "x") 45 | .merge(Flows.tick(Duration.ofMillis(200), "y"), false, false) 46 | .take(5); 47 | 48 | // when 49 | var result = 50 | Flux.from(FlowAdapters.toPublisher(flow.toPublisher(scope))) 51 | .map(s -> s + s) 52 | .collectList() 53 | .block(); 54 | 55 | // then 56 | result.sort(String::compareTo); 57 | assertEquals(List.of("xx", "xx", "xx", "yy", "yy"), result); 58 | return null; 59 | }); 60 | } 61 | 62 | @Test 63 | void shouldCreateFlowFromASimplePublisher() throws Exception { 64 | // given 65 | Flux map = Flux.fromStream(IntStream.rangeClosed(1, 4).boxed()).map(i -> i * 2); 66 | 67 | // when 68 | List result = Flows.fromPublisher(FlowAdapters.toFlowPublisher(map)).runToList(); 69 | 70 | // then 71 | assertEquals(List.of(2, 4, 6, 8), result); 72 | } 73 | 74 | @Test 75 | void shouldCreateFlowFromAConcurrentPublisher() throws Exception { 76 | // given 77 | Flux flux = 78 | Flux.interval(Duration.ofMillis(100)) 79 | .map(_ -> "x") 80 | .mergeWith(Flux.interval(Duration.ofMillis(150)).map(_ -> "y")) 81 | .take(5); 82 | 83 | // when 84 | List result = 85 | Flows.fromPublisher(FlowAdapters.toFlowPublisher(flux)).map(s -> s + s).runToList(); 86 | 87 | // then 88 | result.sort(String::compareTo); 89 | assertEquals(List.of("xx", "xx", "xx", "yy", "yy"), result); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/Par.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static com.softwaremill.jox.structured.Scopes.supervised; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.Callable; 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.Semaphore; 10 | 11 | public class Par { 12 | /** 13 | * Runs the given computations in parallel. If any fails because of an exception, or if any 14 | * returns an application error, other computations are interrupted. Then, the exception is 15 | * re-thrown, or the error value returned. 16 | */ 17 | public static List par(List> fs) throws InterruptedException { 18 | return supervised( 19 | scope -> { 20 | var forks = new ArrayList<>(fs.size()); 21 | for (Callable f : fs) { 22 | forks.add(scope.fork(f)); 23 | } 24 | return collect(forks); 25 | }); 26 | } 27 | 28 | /** 29 | * 🔞 To use less memory (1 ArrayList vs 2), we use the same ArrayList for {@link Fork} and 30 | * result. Generics are actually Object in runtime! 31 | * 32 | *

I.e: {@code collect} "consumes" forks and it shouldn't be used afterwards. It's not an 33 | * everyday pattern 34 | */ 35 | @SuppressWarnings("unchecked") 36 | private static List collect(ArrayList forksAndResults) 37 | throws InterruptedException, ExecutionException { 38 | for (int i = 0, len = forksAndResults.size(); i < len; i++) { 39 | var fork = (Fork) forksAndResults.get(i); 40 | forksAndResults.set(i, fork.join()); 41 | } 42 | return (List) forksAndResults; 43 | } 44 | 45 | /** 46 | * Runs the given computations in parallel, with at most {@code parallelism} running in parallel 47 | * at the same time. If any computation fails because of an exception, or if any returns an 48 | * application error, other computations are interrupted. Then, the exception is re-thrown, or 49 | * the error value returned. 50 | */ 51 | public static List parLimit(int parallelism, List> fs) 52 | throws InterruptedException { 53 | return supervised( 54 | scope -> { 55 | var s = new Semaphore(parallelism); 56 | var forks = new ArrayList<>(fs.size()); 57 | for (Callable f : fs) { 58 | forks.add( 59 | scope.fork( 60 | () -> { 61 | s.acquire(); 62 | T r = f.call(); 63 | // no try-finally as there's no 64 | // point in releasing in case of an 65 | // exception, as any newly started 66 | // forks will be interrupted 67 | s.release(); 68 | return r; 69 | })); 70 | } 71 | return collect(forks); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/Sink.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.function.Supplier; 4 | 5 | /** 6 | * A channel sink, which can be used to send values to the channel. See {@link Channel} for more 7 | * details. 8 | */ 9 | public interface Sink extends CloseableChannel { 10 | /** 11 | * Send a value to the channel. 12 | * 13 | * @param value The value to send. Not {@code null}. 14 | * @throws ChannelClosedException When the channel is closed. 15 | */ 16 | void send(T value) throws InterruptedException; 17 | 18 | /** 19 | * Send a value to the channel. Doesn't throw exceptions when the channel is closed but returns 20 | * a value. 21 | * 22 | * @param value The value to send. Not {@code null}. 23 | * @return Either {@code null}, or {@link ChannelClosed}, when the channel is closed. 24 | */ 25 | Object sendOrClosed(T value) throws InterruptedException; 26 | 27 | /** 28 | * Attempt to send a value to the channel if there's a waiting receiver, or space in the buffer. 29 | * 30 | * @param value The value to send. Not {@code null}. 31 | * @return {@code true} if the value was sent, {@code false} otherwise. 32 | * @throws ChannelClosedException When the channel is closed. 33 | */ 34 | default boolean trySend(T value) { 35 | Object sent; 36 | try { 37 | sent = Select.select(sendClause(value), Channel.DEFAULT_NOT_SENT_CLAUSE); 38 | } catch (InterruptedException e) { 39 | throw new IllegalStateException( 40 | "Interrupted during trySend, which should not be possible", e); 41 | } 42 | return sent != Channel.DEFAULT_NOT_SENT_VALUE; 43 | } 44 | 45 | /** 46 | * Attempt to send a value to one of the given channels if in any of them there's a waiting 47 | * receiver, or space in the buffer. 48 | * 49 | * @param value The value to send. Not {@code null}. 50 | * @return {@code true} if the value was sent, {@code false} otherwise. 51 | * @throws ChannelClosedException When the channel is closed. 52 | */ 53 | @SafeVarargs 54 | static boolean trySend(T value, Sink... toOneOfChannels) { 55 | if (toOneOfChannels == null || toOneOfChannels.length == 0) return false; 56 | 57 | var selectCauses = new SelectClause[toOneOfChannels.length + 1]; 58 | for (int i = 0; i < toOneOfChannels.length; i++) { 59 | selectCauses[i] = toOneOfChannels[i].sendClause(value); 60 | } 61 | selectCauses[toOneOfChannels.length] = Channel.DEFAULT_NOT_SENT_CLAUSE; 62 | 63 | Object sent; 64 | try { 65 | sent = Select.select(selectCauses); 66 | } catch (InterruptedException e) { 67 | throw new IllegalStateException( 68 | "Interrupted during trySend, which should not be possible", e); 69 | } 70 | return sent != Channel.DEFAULT_NOT_SENT_VALUE; 71 | } 72 | 73 | // 74 | 75 | /** 76 | * Create a clause which can be used in {@link Select#select(SelectClause[])}. The clause will 77 | * send the given value to the current channel, and return {@code null} as the clause's result. 78 | */ 79 | SelectClause sendClause(T value); 80 | 81 | /** 82 | * Create a clause which can be used in {@link Select#select(SelectClause[])}. The clause will 83 | * send the given value to the current channel and return the value of the provided callback as 84 | * the clause's result. 85 | */ 86 | SelectClause sendClause(T value, Supplier callback); 87 | } 88 | -------------------------------------------------------------------------------- /bench/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | 9 | com.softwaremill.jox 10 | parent 11 | 1.1.1 12 | 13 | 14 | bench 15 | pom 16 | 1.1.1 17 | 18 | 19 | bench-java 20 | bench-kotlin 21 | 22 | 23 | 24 | 1.37 25 | benchmarks 26 | 27 | 28 | 29 | 30 | org.openjdk.jmh 31 | jmh-core 32 | ${jmh.version} 33 | 34 | 35 | org.openjdk.jmh 36 | jmh-generator-annprocess 37 | ${jmh.version} 38 | provided 39 | 40 | 41 | com.softwaremill.jox 42 | channels 43 | 1.1.1 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-shade-plugin 53 | 3.6.1 54 | 55 | 56 | package 57 | 58 | shade 59 | 60 | 61 | ${uberjar.name} 62 | 63 | 65 | org.openjdk.jmh.Main 66 | 67 | 69 | 70 | 71 | 72 | 76 | *:* 77 | 78 | META-INF/*.SF 79 | META-INF/*.DSA 80 | META-INF/*.RSA 81 | 82 | META-INF/MANIFEST.MF 83 | 84 | META-INF/versions/9/module-info.class 85 | 86 | 87 | 88 | false 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowCompleteCallbacksTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class FlowCompleteCallbacksTest { 10 | @Test 11 | void ensureOnCompleteRunsInCaseOfSuccess() throws Exception { 12 | // given 13 | AtomicBoolean didRun = new AtomicBoolean(false); 14 | Flow f = Flows.fromValues(1, 2, 3).onComplete(() -> didRun.set(true)); 15 | assertFalse(didRun.get()); 16 | 17 | // when 18 | f.runDrain(); 19 | 20 | // then 21 | assertTrue(didRun.get()); 22 | } 23 | 24 | @Test 25 | void ensureOnCompleteRunsInCaseOfError() { 26 | // given 27 | AtomicBoolean didRun = new AtomicBoolean(false); 28 | Flow f = 29 | Flows.fromValues(1, 2, 3) 30 | .tap( 31 | _ -> { 32 | throw new RuntimeException(); 33 | }) 34 | .onComplete(() -> didRun.set(true)); 35 | assertFalse(didRun.get()); 36 | 37 | // when 38 | assertThrows(RuntimeException.class, f::runDrain); 39 | 40 | // then 41 | assertTrue(didRun.get()); 42 | } 43 | 44 | @Test 45 | void ensureOnDoneRunsInCaseOfSuccess() throws Exception { 46 | // given 47 | AtomicBoolean didRun = new AtomicBoolean(false); 48 | Flow f = Flows.fromValues(1, 2, 3).onDone(() -> didRun.set(true)); 49 | assertFalse(didRun.get()); 50 | 51 | // when 52 | f.runDrain(); 53 | 54 | // then 55 | assertTrue(didRun.get()); 56 | } 57 | 58 | @Test 59 | void ensureOnDoneDoesNotRunInCaseOfError() { 60 | // given 61 | AtomicBoolean didRun = new AtomicBoolean(false); 62 | Flow f = 63 | Flows.fromValues(1, 2, 3) 64 | .tap( 65 | _ -> { 66 | throw new RuntimeException(); 67 | }) 68 | .onDone(() -> didRun.set(true)); 69 | assertFalse(didRun.get()); 70 | 71 | // when 72 | assertThrows(RuntimeException.class, f::runDrain); 73 | 74 | // then 75 | assertFalse(didRun.get()); 76 | } 77 | 78 | @Test 79 | void ensureOnErrorDoesNotRunInCaseOfSuccess() throws Exception { 80 | // given 81 | AtomicBoolean didRun = new AtomicBoolean(false); 82 | Flow f = Flows.fromValues(1, 2, 3).onError(_ -> didRun.set(true)); 83 | assertFalse(didRun.get()); 84 | 85 | // when 86 | f.runDrain(); 87 | 88 | // then 89 | assertFalse(didRun.get()); 90 | } 91 | 92 | @Test 93 | void ensureOnErrorRunsInCaseOfError() { 94 | // given 95 | AtomicBoolean didRun = new AtomicBoolean(false); 96 | Flow f = 97 | Flows.fromValues(1, 2, 3) 98 | .tap( 99 | _ -> { 100 | throw new RuntimeException(); 101 | }) 102 | .onError(_ -> didRun.set(true)); 103 | assertFalse(didRun.get()); 104 | 105 | // when 106 | assertThrows(RuntimeException.class, f::runDrain); 107 | 108 | // then 109 | assertTrue(didRun.get()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowPublisherTckTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.lang.reflect.Method; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.Flow.Publisher; 10 | import java.util.concurrent.atomic.AtomicReference; 11 | 12 | import org.junit.jupiter.api.Test; 13 | import org.reactivestreams.tck.TestEnvironment; 14 | import org.reactivestreams.tck.flow.FlowPublisherVerification; 15 | 16 | import com.softwaremill.jox.structured.Scope; 17 | import com.softwaremill.jox.structured.Scopes; 18 | 19 | public class FlowPublisherTckTest { 20 | 21 | private final AtomicReference scope = new AtomicReference<>(); 22 | private final FlowPublisherVerification verification = 23 | new FlowPublisherVerification<>(new TestEnvironment()) { 24 | @Override 25 | public Publisher createFlowPublisher(long l) { 26 | Flow flow = Flows.range(1, (int) l, 1); 27 | try { 28 | return flow.toPublisher(scope.get()); 29 | } catch (InterruptedException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | @Override 35 | public Publisher createFailedFlowPublisher() { 36 | try { 37 | return Flows.failed(new RuntimeException("boom")) 38 | .toPublisher(scope.get()); 39 | } catch (InterruptedException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | }; 44 | 45 | @Test 46 | void verifyTckScenarios() throws InterruptedException { 47 | List errors = new ArrayList<>(); 48 | // We are invoking tests manually as we need to set separate supervised scope for each test 49 | for (Method method : verification.getClass().getMethods()) { 50 | if (method.getAnnotation(org.testng.annotations.Test.class) != null) { 51 | if (method.getName().startsWith("untested_")) { 52 | continue; 53 | } 54 | Scopes.supervised( 55 | s -> { 56 | scope.set(s); 57 | try { 58 | method.invoke(verification); 59 | } catch (InvocationTargetException e) { 60 | handleInvocationTargetException(method, e); 61 | errors.add(e.getCause()); 62 | } 63 | return null; 64 | }); 65 | scope.set(null); 66 | } 67 | } 68 | assertTrue(errors.isEmpty(), "Test suite returned errors"); 69 | } 70 | 71 | private static void handleInvocationTargetException(Method method, Throwable e) { 72 | Throwable cause = e.getCause(); 73 | String errorMessage = 74 | String.format( 75 | "Error in method %s:%n%s%n%s", 76 | method.getName(), cause.getMessage(), getStackTrace(cause)); 77 | System.err.println(errorMessage); 78 | } 79 | 80 | private static String getStackTrace(Throwable t) { 81 | StringBuilder sb = new StringBuilder(); 82 | for (StackTraceElement element : t.getStackTrace()) { 83 | sb.append(element.toString()).append("\n"); 84 | } 85 | return sb.toString(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bench/bench-java/src/main/java/com/softwaremill/jox/BufferedBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.concurrent.ArrayBlockingQueue; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | import org.openjdk.jmh.annotations.*; 7 | 8 | /** Buffered tests for {@link ArrayBlockingQueue} and {@link Channel}. */ 9 | @Warmup(iterations = 3, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 10 | @Measurement(iterations = 10, time = 3000, timeUnit = TimeUnit.MILLISECONDS) 11 | @Fork(value = 2) 12 | @BenchmarkMode(Mode.AverageTime) 13 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 14 | @State(Scope.Benchmark) 15 | public class BufferedBenchmark { 16 | @Param({"16", "100"}) 17 | public int capacity; 18 | 19 | // going against jmh's best practises, the benchmarks are "iterative" (not using groups), for 20 | // two reasons: 21 | // (1) direct comparison w/ Kotlin, as we can't write a group-based benchmark there, due to 22 | // suspended functions 23 | // (2) the more complex benchmarks (which use higher numbers of threads) need to be enclosed in 24 | // a single method anyway 25 | 26 | private static final int OPERATIONS_PER_INVOCATION = 1_000_000; 27 | 28 | @Benchmark 29 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 30 | public void arrayBlockingQueue() throws InterruptedException { 31 | var queue = new ArrayBlockingQueue<>(capacity); 32 | var t1 = 33 | Thread.startVirtualThread( 34 | () -> { 35 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 36 | try { 37 | queue.put(63); 38 | } catch (InterruptedException e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | }); 43 | 44 | var t2 = 45 | Thread.startVirtualThread( 46 | () -> { 47 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 48 | try { 49 | queue.take(); 50 | } catch (InterruptedException e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | }); 55 | 56 | t1.join(); 57 | t2.join(); 58 | } 59 | 60 | @Benchmark 61 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 62 | public void channel() throws InterruptedException { 63 | var ch = Channel.newBufferedChannel(capacity); 64 | var t1 = 65 | Thread.startVirtualThread( 66 | () -> { 67 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 68 | try { 69 | ch.send(63); 70 | } catch (InterruptedException e) { 71 | throw new RuntimeException(e); 72 | } 73 | } 74 | }); 75 | 76 | var t2 = 77 | Thread.startVirtualThread( 78 | () -> { 79 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 80 | try { 81 | ch.receive(); 82 | } catch (InterruptedException e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | }); 87 | 88 | t1.join(); 89 | t2.join(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /channels-fray-tests/src/test/java/com/softwaremill/jox/fray/FraySelectTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.fray; 2 | 3 | import static com.softwaremill.jox.Select.select; 4 | import static com.softwaremill.jox.fray.Config.CHANNEL_SIZE; 5 | 6 | import java.util.ArrayList; 7 | 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.pastalab.fray.junit.junit5.FrayTestExtension; 10 | import org.pastalab.fray.junit.junit5.annotations.ConcurrencyTest; 11 | 12 | import com.softwaremill.jox.Channel; 13 | import com.softwaremill.jox.SelectClause; 14 | 15 | @ExtendWith(FrayTestExtension.class) 16 | public class FraySelectTest { 17 | // select(send) | select(receive) 18 | 19 | @ConcurrencyTest 20 | public void sendReceiveWithSelectTest() throws InterruptedException { 21 | Channel ch = Channel.newBufferedChannel(CHANNEL_SIZE); 22 | 23 | Fork f1 = Fork.newNoResult(() -> select(ch.sendClause(10))); 24 | Fork f2 = Fork.newWithResult(() -> select(ch.receiveClause())); 25 | 26 | Fork.startAll(f1, f2); 27 | f1.join(); 28 | 29 | assert (f2.join() == 10); 30 | } 31 | 32 | // send | send | select(receive, receive) 33 | 34 | @ConcurrencyTest 35 | public void sendSendSelectTest() throws InterruptedException { 36 | Channel ch1 = Channel.newBufferedChannel(CHANNEL_SIZE); 37 | Channel ch2 = Channel.newBufferedChannel(CHANNEL_SIZE); 38 | 39 | Fork f1 = Fork.newNoResult(() -> select(ch1.sendClause(10))); 40 | Fork f2 = Fork.newNoResult(() -> select(ch2.sendClause(20))); 41 | Fork f3 = 42 | Fork.newWithResult(() -> select(ch1.receiveClause(), ch2.receiveClause())); 43 | 44 | Fork.startAll(f1, f2, f3); 45 | 46 | int joined = f3.join(); 47 | if (joined == 10) { 48 | assert (ch2.receive() == 20); 49 | } else if (joined == 20) { 50 | assert (ch1.receive() == 10); 51 | } else { 52 | assert false; 53 | } 54 | 55 | f1.join(); 56 | f2.join(); 57 | } 58 | 59 | // many sends | many select(many receives) 60 | 61 | @ConcurrencyTest 62 | public void multiSendMultipleSelectReceiveTest() throws InterruptedException { 63 | // segment size is 32, this covers more than 1 segment 64 | int concurrency = 40; 65 | 66 | var channels = new ArrayList>(); 67 | for (int i = 0; i < concurrency; i++) { 68 | channels.add(Channel.newBufferedChannel(CHANNEL_SIZE)); 69 | } 70 | 71 | var sendForks = new ArrayList>(); 72 | var receiveForks = new ArrayList>(); 73 | 74 | for (int i = 0; i < concurrency; i++) { 75 | final var finalI = i; 76 | sendForks.add(Fork.newNoResult(() -> channels.get(finalI).send(finalI))); 77 | receiveForks.add( 78 | Fork.newWithResult( 79 | () -> { 80 | var clauses = new SelectClause[concurrency]; 81 | for (int j = 0; j < concurrency; j++) { 82 | clauses[j] = channels.get(j).receiveClause(); 83 | } 84 | return (Integer) select(clauses); 85 | })); 86 | } 87 | 88 | Fork.startAll(sendForks.toArray(new Fork[0])); 89 | Fork.startAll(receiveForks.toArray(new Fork[0])); 90 | 91 | for (Fork sendFork : sendForks) { 92 | sendFork.join(); 93 | } 94 | 95 | var result = 0; 96 | for (Fork receiveFork : receiveForks) { 97 | result += receiveFork.join(); 98 | } 99 | 100 | assert (result == concurrency * (concurrency - 1) / 2); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /bench/bench-java/src/main/java/com/softwaremill/jox/SelectBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.Select.select; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.openjdk.jmh.annotations.*; 8 | 9 | /** Tests for {@link Select#select(SelectClause[])}. */ 10 | @Warmup(iterations = 3, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 11 | @Measurement(iterations = 10, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Fork(value = 2) 13 | @BenchmarkMode(Mode.AverageTime) 14 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 15 | public class SelectBenchmark { 16 | private static final int OPERATIONS_PER_INVOCATION = 1_000_000; 17 | 18 | @Benchmark 19 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 20 | public void selectWithSingleClause() throws InterruptedException { 21 | var ch = Channel.newRendezvousChannel(); 22 | var t1 = 23 | Thread.startVirtualThread( 24 | () -> { 25 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 26 | try { 27 | ch.send(63); 28 | } catch (InterruptedException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | }); 33 | 34 | var t2 = 35 | Thread.startVirtualThread( 36 | () -> { 37 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 38 | try { 39 | select(ch.receiveClause()); 40 | } catch (InterruptedException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | }); 45 | 46 | t1.join(); 47 | t2.join(); 48 | } 49 | 50 | @Benchmark 51 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 52 | public void selectWithTwoClauses() throws InterruptedException { 53 | var ch1 = Channel.newRendezvousChannel(); 54 | var ch2 = Channel.newRendezvousChannel(); 55 | var t1 = 56 | Thread.startVirtualThread( 57 | () -> { 58 | for (int i = 0; i < OPERATIONS_PER_INVOCATION / 2; i++) { 59 | try { 60 | ch1.send(63); 61 | } catch (InterruptedException e) { 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | }); 66 | 67 | var t2 = 68 | Thread.startVirtualThread( 69 | () -> { 70 | for (int i = 0; i < OPERATIONS_PER_INVOCATION / 2; i++) { 71 | try { 72 | ch2.send(63); 73 | } catch (InterruptedException e) { 74 | throw new RuntimeException(e); 75 | } 76 | } 77 | }); 78 | 79 | var t3 = 80 | Thread.startVirtualThread( 81 | () -> { 82 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 83 | try { 84 | select(ch1.receiveClause(), ch2.receiveClause()); 85 | } catch (InterruptedException e) { 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | }); 90 | 91 | t1.join(); 92 | t2.join(); 93 | t3.join(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/SegmentTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.Segment.SEGMENT_SIZE; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class SegmentTest { 9 | @Test 10 | void segmentShouldBecomeRemovedOnceAllCellsInterruptedAndProcessed() { 11 | // given 12 | var ss = createSegmentChain(3, 0, false); 13 | 14 | // when 15 | // receiver-interrupting all cells 16 | for (int i = 0; i < SEGMENT_SIZE; i++) { 17 | ss[1].cellInterruptedReceiver(); 18 | // nothing should happen 19 | assertFalse(ss[1].isRemoved()); 20 | assertEquals(ss[1].getPrev(), ss[0]); 21 | assertEquals(ss[1].getNext(), ss[2]); 22 | assertNull(ss[0].getPrev()); 23 | assertEquals(ss[0].getNext(), ss[1]); 24 | assertEquals(ss[2].getPrev(), ss[1]); 25 | assertNull(ss[2].getNext()); 26 | } 27 | 28 | // processing all cells but one 29 | for (int i = 0; i < SEGMENT_SIZE - 1; i++) { 30 | ss[1].cellProcessed_notInterruptedSender(); 31 | // nothing should happen 32 | assertFalse(ss[1].isRemoved()); 33 | assertEquals(ss[1].getPrev(), ss[0]); 34 | assertEquals(ss[1].getNext(), ss[2]); 35 | assertNull(ss[0].getPrev()); 36 | assertEquals(ss[0].getNext(), ss[1]); 37 | assertEquals(ss[2].getPrev(), ss[1]); 38 | assertNull(ss[2].getNext()); 39 | } 40 | 41 | ss[1].cellProcessed_notInterruptedSender(); // last cell 42 | assertTrue(ss[1].isRemoved()); 43 | 44 | // then 45 | assertNull(ss[0].getPrev()); 46 | assertEquals(ss[0].getNext(), ss[2]); 47 | assertEquals(ss[2].getPrev(), ss[0]); 48 | assertNull(ss[2].getNext()); 49 | } 50 | 51 | @Test 52 | void segmentShouldBecomeRemovedOnceAllCellsSenderInterrupted() { 53 | // given 54 | var ss = createSegmentChain(3, 0, false); 55 | 56 | // when 57 | for (int i = 0; i < SEGMENT_SIZE - 1; i++) { 58 | ss[1].cellInterruptedSender(); 59 | // nothing should happen 60 | assertFalse(ss[1].isRemoved()); 61 | assertEquals(ss[1].getPrev(), ss[0]); 62 | assertEquals(ss[1].getNext(), ss[2]); 63 | assertNull(ss[0].getPrev()); 64 | assertEquals(ss[0].getNext(), ss[1]); 65 | assertEquals(ss[2].getPrev(), ss[1]); 66 | assertNull(ss[2].getNext()); 67 | } 68 | 69 | ss[1].cellInterruptedSender(); // last cell 70 | assertTrue(ss[1].isRemoved()); 71 | 72 | // then 73 | assertNull(ss[0].getPrev()); 74 | assertEquals(ss[0].getNext(), ss[2]); 75 | assertEquals(ss[2].getPrev(), ss[0]); 76 | assertNull(ss[2].getNext()); 77 | } 78 | 79 | @Test 80 | void shouldReturnTheLastSegmentWhenClosing() { 81 | // given 82 | var ss = createSegmentChain(3, 0, false); 83 | 84 | // when 85 | var s = ss[0].close(); 86 | 87 | // then 88 | assertEquals(ss[2].getId(), s.getId()); 89 | } 90 | 91 | static Segment[] createSegmentChain(int count, long id, boolean isRendezvous) { 92 | var segments = new Segment[count]; 93 | var thisSegment = new Segment(id, null, 0, isRendezvous); 94 | segments[0] = thisSegment; 95 | for (int i = 1; i < count; i++) { 96 | var nextSegment = new Segment(id + i, thisSegment, 0, isRendezvous); 97 | thisSegment.setNext(nextSegment); 98 | segments[i] = nextSegment; 99 | thisSegment = nextSegment; 100 | } 101 | return segments; 102 | } 103 | 104 | static void sendInterruptAllCells(Segment s) { 105 | for (int i = 0; i < SEGMENT_SIZE; i++) { 106 | s.cellInterruptedSender(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Jox 2 | 3 | [Virtual-thread](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) based safe concurrency & streaming 4 | for Java. Open-source, Apache2 licensed. 5 | 6 | Jox contains three main modules: 7 | 8 | * Fast & scalable, completable [channels](channels.md), with Go-like `select`s (Java 21+) 9 | * Programmer-friendly [structured concurrency](structured.md) (Java 25 only) 10 | * Finite & infinite streaming using [flows](flows.md), with reactive streams compatibility, (blocking) I/O integration 11 | and a high-level, "functional" API (Java 25 only) 12 | 13 | Source code is [available on GitHub](https://github.com/softwaremill/jox). 14 | 15 | ## A tour of Jox 16 | 17 | Selectable [channels](channels.md): 18 | 19 | ``` 20 | var ch1 = Channel.newBufferedDefaultChannel(); 21 | var ch2 = Channel.newBufferedDefaultChannel(); 22 | var ch3 = Channel.newBufferedDefaultChannel(); 23 | 24 | // send a value to two channels 25 | ch2.send(29); 26 | ch3.send(32); 27 | 28 | var received = select(ch1.receiveClause(), ch2.receiveClause(), ch3.receiveClause()); 29 | ``` 30 | 31 | A push-based, backpressured [flow](flows.md) with time-based & parallel processing: 32 | 33 | ``` 34 | var nats = 35 | Flows.unfold(0, i -> Optional.of(Map.entry(i+1, i+1))); 36 | 37 | Flows.range(1, 100, 1) 38 | .throttle(1, Duration.ofSeconds(1)) 39 | .mapPar(4, i -> { 40 | Thread.sleep(5000); 41 | var j = i*3; 42 | return j+1; 43 | }) 44 | .filter(i -> i % 2 == 0) 45 | .zip(nats) 46 | .runForeach(IO::println); 47 | ``` 48 | 49 | [Sructured concurrency](structured.md) scope: 50 | 51 | ``` 52 | var result = supervised(scope -> { 53 | var f1 = scope.fork(() -> { 54 | Thread.sleep(500); 55 | return 5; 56 | }); 57 | var f2 = scope.fork(() -> { 58 | Thread.sleep(1000); 59 | return 6; 60 | }); 61 | return f1.join() + f2.join(); 62 | }); 63 | IO.println("result = " + result); 64 | ``` 65 | 66 | ## Sponsors 67 | 68 | Development and maintenance of Jox is sponsored by [SoftwareMill](https://softwaremill.com), a software development and 69 | consulting company. We help clients scale their business through software. Our areas of expertise include performant 70 | backends, distributed systems, integrating data pipelines and ML/AI "science as a service". 71 | 72 | [![](https://files.softwaremill.com/logo/logo.png "SoftwareMill")](https://softwaremill.com) 73 | 74 | ## Commercial Support 75 | 76 | We offer commercial support for Jox and related technologies, as well as development services. 77 | [Contact us](https://softwaremill.com/contact/) to learn more about our offer! 78 | 79 | ## Other materials 80 | 81 | Articles: 82 | 83 | * [Announcing jox: Fast and Scalable Channels in Java](https://softwaremill.com/announcing-jox-fast-and-scalable-channels-in-java/) 84 | * [Go-like selects using jox channels in Java](https://softwaremill.com/go-like-selects-using-jox-channels-in-java/) 85 | * [Jox 0.1: virtual-thread friendly channels for Java](https://softwaremill.com/jox-0-1-virtual-thread-friendly-channels-for-java/) 86 | * [Programmer-friendly structured concurrency for Java](https://softwaremill.com/programmer-friendly-structured-concurrency-for-java/) 87 | * [Java data processing using modern concurrent programming](https://softwaremill.com/java-data-processing-using-modern-concurrent-programming/) 88 | * [Flows - simple Java asynchronous data processing in action](https://softwaremill.com/flows-simple-java-asynchronous-data-processing-in-action/) 89 | * [Comparing Java Streams with Jox Flows](https://softwaremill.com/comparing-java-streams-with-jox-flows/) 90 | 91 | Videos: 92 | 93 | * [A 10-minute introduction to Jox Channels](https://www.youtube.com/watch?v=Ss9b1HpPDt0) 94 | * [Passing control information through channels](https://www.youtube.com/watch?v=VjiCzaiRro8) 95 | 96 | For a Scala version, see the [Ox project](https://github.com/softwaremill/ox). 97 | 98 | ## Table of contents 99 | 100 | ```{eval-rst} 101 | 102 | .. toctree:: 103 | :maxdepth: 2 104 | :caption: Jox 105 | 106 | channels 107 | flows 108 | structured 109 | contributing 110 | -------------------------------------------------------------------------------- /channels/src/test/java/com/softwaremill/jox/ChannelBufferedTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import static com.softwaremill.jox.TestUtil.forkVoid; 4 | import static com.softwaremill.jox.TestUtil.scoped; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.Timeout; 11 | 12 | /** Tests which always use buffered channels. */ 13 | public class ChannelBufferedTest { 14 | @Test 15 | @Timeout(1) 16 | void testSimpleSendReceiveBuffer1() throws InterruptedException { 17 | // given 18 | Channel channel = Channel.newBufferedChannel(1); 19 | 20 | // when 21 | channel.send("x"); // should not block 22 | var r = channel.receive(); // also should not block 23 | 24 | // then 25 | assertEquals("x", r); 26 | } 27 | 28 | @Test 29 | @Timeout(1) 30 | void testSimpleSendReceiveBuffer2() throws InterruptedException { 31 | // given 32 | Channel channel = Channel.newBufferedChannel(2); 33 | 34 | // when 35 | channel.send("x"); // should not block 36 | channel.send("y"); // should not block 37 | var r1 = channel.receive(); // also should not block 38 | var r2 = channel.receive(); // also should not block 39 | 40 | // then 41 | assertEquals("x", r1); 42 | assertEquals("y", r2); 43 | } 44 | 45 | @Test 46 | @Timeout(2) 47 | void testBufferCapacityStaysTheSameAfterSendsReceives() 48 | throws ExecutionException, InterruptedException { 49 | // given 50 | Channel channel = Channel.newBufferedChannel(2); 51 | 52 | // when 53 | scoped( 54 | scope -> { 55 | forkVoid( 56 | scope, 57 | () -> { 58 | channel.send(1); // should not block 59 | channel.send(2); // should not block 60 | channel.send(3); 61 | channel.send(4); 62 | }); 63 | 64 | // then 65 | Thread.sleep(100L); 66 | assertEquals(1, channel.receive()); 67 | Thread.sleep(100L); 68 | assertEquals(2, channel.receive()); 69 | Thread.sleep(100L); 70 | assertEquals(3, channel.receive()); 71 | Thread.sleep(100L); 72 | assertEquals(4, channel.receive()); 73 | 74 | channel.send(5); // should not block 75 | channel.send(6); // should not block 76 | }); 77 | } 78 | 79 | @Test 80 | @Timeout(1) 81 | void shouldReceiveFromAChannelUntilDone() throws InterruptedException { 82 | // given 83 | Channel c = Channel.newBufferedChannel(3); 84 | c.send(1); 85 | c.send(2); 86 | c.done(); 87 | 88 | // when 89 | var r1 = c.receiveOrClosed(); 90 | var r2 = c.receiveOrClosed(); 91 | var r3 = c.receiveOrClosed(); 92 | 93 | // then 94 | assertEquals(1, r1); 95 | assertEquals(2, r2); 96 | assertInstanceOf(ChannelClosed.class, r3); 97 | } 98 | 99 | @Test 100 | @Timeout(1) 101 | void shouldNotReceiveFromAChannelInCaseOfAnError() throws InterruptedException { 102 | // given 103 | Channel c = Channel.newBufferedChannel(3); 104 | c.send(1); 105 | c.send(2); 106 | c.error(new RuntimeException()); 107 | 108 | // when 109 | var r1 = c.receiveOrClosed(); 110 | var r2 = c.receiveOrClosed(); 111 | 112 | // then 113 | assertInstanceOf(ChannelError.class, r1); 114 | assertInstanceOf(ChannelError.class, r2); 115 | } 116 | 117 | @Test 118 | void shouldProcessCellsInitially() { 119 | assertTrue(Channel.newBufferedChannel(1).toString().contains("notProcessed=31")); 120 | assertTrue(Channel.newBufferedChannel(31).toString().contains("notProcessed=1")); 121 | assertTrue(Channel.newBufferedChannel(32).toString().contains("notProcessed=0")); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /bench/bench-java/src/main/java/com/softwaremill/jox/ParallelBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.concurrent.*; 4 | 5 | import org.openjdk.jmh.annotations.*; 6 | 7 | /** 8 | * Send-receive test for {@link Channel} and {@link BlockingQueue} - a number of (send, receive) 9 | * thread pairs, sending/receiving on a dedicated channel. 10 | */ 11 | @Warmup(iterations = 3, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Measurement(iterations = 10, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 13 | @Fork(value = 2) 14 | @BenchmarkMode(Mode.AverageTime) 15 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 16 | @State(Scope.Benchmark) 17 | public class ParallelBenchmark { 18 | @Param({"0", "16", "100"}) 19 | public int capacity; 20 | 21 | @Param({"10000"}) 22 | public int parallelism; 23 | 24 | private static final int OPERATIONS_PER_INVOCATION = 10_000_000; 25 | 26 | @Benchmark 27 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 28 | public void parallelChannels() throws InterruptedException { 29 | // we want to measure the amount of time a send-receive pair takes 30 | int elementsPerChannel = OPERATIONS_PER_INVOCATION / parallelism; 31 | 32 | var latch = new CountDownLatch(parallelism); 33 | 34 | for (int t = 0; t < parallelism; t++) { 35 | var ch = 36 | (capacity == 0) 37 | ? Channel.newRendezvousChannel() 38 | : Channel.newBufferedChannel(capacity); 39 | // sender 40 | Thread.startVirtualThread( 41 | () -> { 42 | for (int i = 0; i < elementsPerChannel; i++) { 43 | try { 44 | ch.send(91); 45 | } catch (InterruptedException e) { 46 | throw new RuntimeException(e); 47 | } 48 | } 49 | }); 50 | 51 | // receiver 52 | Thread.startVirtualThread( 53 | () -> { 54 | for (int i = 0; i < elementsPerChannel; i++) { 55 | try { 56 | ch.receive(); 57 | } catch (InterruptedException e) { 58 | throw new RuntimeException(e); 59 | } 60 | } 61 | latch.countDown(); 62 | }); 63 | } 64 | 65 | latch.await(); 66 | } 67 | 68 | @Benchmark 69 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 70 | public void parallelQueues() throws InterruptedException { 71 | // we want to measure the amount of time a send-receive pair takes 72 | int elementsPerChannel = OPERATIONS_PER_INVOCATION / parallelism; 73 | 74 | var latch = new CountDownLatch(parallelism); 75 | 76 | for (int t = 0; t < parallelism; t++) { 77 | BlockingQueue q; 78 | if (capacity == 0) { 79 | q = new SynchronousQueue<>(); 80 | } else { 81 | q = new ArrayBlockingQueue<>(capacity); 82 | } 83 | 84 | // sender 85 | Thread.startVirtualThread( 86 | () -> { 87 | for (int i = 0; i < elementsPerChannel; i++) { 88 | try { 89 | q.put(91); 90 | } catch (InterruptedException e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | }); 95 | 96 | // receiver 97 | Thread.startVirtualThread( 98 | () -> { 99 | for (int i = 0; i < elementsPerChannel; i++) { 100 | try { 101 | q.take(); 102 | } catch (InterruptedException e) { 103 | throw new RuntimeException(e); 104 | } 105 | } 106 | latch.countDown(); 107 | }); 108 | } 109 | 110 | latch.await(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /flows/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.softwaremill.jox 8 | parent 9 | 1.1.1 10 | 11 | 12 | flows 13 | 0.5.0 14 | jar 15 | ${project.groupId}:${project.artifactId} 16 | 17 | 18 | 25 19 | 20 | 21 | 22 | 23 | 24 | com.diffplug.spotless 25 | spotless-maven-plugin 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 34 | true 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-surefire-plugin 40 | 41 | --enable-preview 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-javadoc-plugin 47 | 48 | 25 49 | --enable-preview 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.apache.pekko 60 | pekko-bom_3 61 | 1.4.0 62 | pom 63 | import 64 | 65 | 66 | 67 | 68 | 69 | 70 | com.softwaremill.jox 71 | channels 72 | 1.1.1 73 | 74 | 75 | com.softwaremill.jox 76 | structured 77 | 0.5.0 78 | 79 | 80 | org.junit.jupiter 81 | junit-jupiter 82 | 6.0.1 83 | test 84 | 85 | 86 | org.awaitility 87 | awaitility 88 | test 89 | 90 | 91 | io.projectreactor 92 | reactor-core 93 | 3.8.1 94 | test 95 | 96 | 97 | org.reactivestreams 98 | reactive-streams 99 | 1.0.4 100 | test 101 | 102 | 103 | org.reactivestreams 104 | reactive-streams-tck-flow 105 | 1.0.4 106 | test 107 | 108 | 109 | org.testng 110 | testng 111 | 7.11.0 112 | test 113 | 114 | 115 | org.slf4j 116 | slf4j-api 117 | 118 | 119 | 120 | 121 | org.apache.pekko 122 | pekko-stream_3 123 | test 124 | 125 | 126 | org.apache.pekko 127 | pekko-stream-testkit_3 128 | test 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowZipTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class FlowZipTest { 13 | 14 | @Test 15 | void shouldNotZipAnythingFromEmptyFlow() throws Exception { 16 | // given 17 | Flow c = Flows.empty(); 18 | Flow> s = c.zipWithIndex(); 19 | 20 | // when & then 21 | assertEquals(Collections.emptyList(), s.runToList()); 22 | } 23 | 24 | @Test 25 | void shouldZipFlowWithIndex() throws Exception { 26 | // given 27 | Flow c = Flows.fromValues(1, 2, 3, 4, 5); 28 | List> expected = 29 | Arrays.asList( 30 | Map.entry(1, 0L), 31 | Map.entry(2, 1L), 32 | Map.entry(3, 2L), 33 | Map.entry(4, 3L), 34 | Map.entry(5, 4L)); 35 | 36 | // when & then 37 | Flow> s = c.zipWithIndex(); 38 | assertEquals(expected, s.runToList()); 39 | } 40 | 41 | @Test 42 | void zipAll_shouldNotEmitAnyElementWhenBothFlowsAreEmpty() throws Exception { 43 | // given 44 | Flow s = Flows.empty(); 45 | Flow other = Flows.empty(); 46 | 47 | // when & then 48 | List> result = s.zipAll(other, -1, "foo").runToList(); 49 | assertEquals(Collections.emptyList(), result); 50 | } 51 | 52 | @Test 53 | void zipAll_shouldEmitThisElementWhenOtherFlowIsEmpty() throws Exception { 54 | // given 55 | Flow s = Flows.fromValues(1); 56 | Flow other = Flows.empty(); 57 | 58 | // when & then 59 | List> result = s.zipAll(other, -1, "foo").runToList(); 60 | assertEquals(List.of(Map.entry(1, "foo")), result); 61 | } 62 | 63 | @Test 64 | void zipAll_shouldEmitOtherElementWhenThisFlowIsEmpty() throws Exception { 65 | // given 66 | Flow s = Flows.empty(); 67 | Flow other = Flows.fromValues("a"); 68 | 69 | // when & then 70 | List> result = s.zipAll(other, -1, "foo").runToList(); 71 | assertEquals(List.of(Map.entry(-1, "a")), result); 72 | } 73 | 74 | @Test 75 | void zipAll_shouldEmitMatchingElementsWhenBothFlowsAreOfTheSameSize() throws Exception { 76 | // given 77 | Flow s = Flows.fromValues(1, 2); 78 | Flow other = Flows.fromValues("a", "b"); 79 | 80 | // when & then 81 | List> result = s.zipAll(other, -1, "foo").runToList(); 82 | assertEquals(List.of(Map.entry(1, "a"), Map.entry(2, "b")), result); 83 | } 84 | 85 | @Test 86 | void zipAll_shouldEmitDefaultForOtherFlowIfThisFlowIsLonger() throws Exception { 87 | // given 88 | Flow s = Flows.fromValues(1, 2, 3); 89 | Flow other = Flows.fromValues("a"); 90 | 91 | // when & then 92 | List> result = s.zipAll(other, -1, "foo").runToList(); 93 | assertEquals(List.of(Map.entry(1, "a"), Map.entry(2, "foo"), Map.entry(3, "foo")), result); 94 | } 95 | 96 | @Test 97 | void zipAll_shouldEmitDefaultForThisFlowIfOtherFlowIsLonger() throws Exception { 98 | // given 99 | Flow s = Flows.fromValues(1); 100 | Flow other = Flows.fromValues("a", "b", "c"); 101 | 102 | // when & then 103 | List> result = s.zipAll(other, -1, "foo").runToList(); 104 | assertEquals(List.of(Map.entry(1, "a"), Map.entry(-1, "b"), Map.entry(-1, "c")), result); 105 | } 106 | 107 | @Test 108 | void zip_shouldZipTwoSources() throws Exception { 109 | // given 110 | Flow c1 = Flows.fromValues(1, 2, 3, 0); 111 | Flow c2 = Flows.fromValues(4, 5, 6); 112 | List> expected = 113 | List.of(Map.entry(1, 4), Map.entry(2, 5), Map.entry(3, 6)); 114 | 115 | // when & then 116 | List> s = c1.zip(c2).runToList(); 117 | assertEquals(expected, s); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /bench/bench-java/src/main/java/com/softwaremill/jox/RendezvousBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.concurrent.Exchanger; 4 | import java.util.concurrent.SynchronousQueue; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.openjdk.jmh.annotations.*; 8 | 9 | /** Rendezvous tests for {@link SynchronousQueue}, {@link Exchanger} and {@link Channel}. */ 10 | @Warmup(iterations = 3, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 11 | @Measurement(iterations = 10, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 12 | @Fork(value = 2) 13 | @BenchmarkMode(Mode.AverageTime) 14 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 15 | public class RendezvousBenchmark { 16 | private static final int OPERATIONS_PER_INVOCATION = 1_000_000; 17 | 18 | @Benchmark 19 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 20 | public void synchronousQueue() throws InterruptedException { 21 | var queue = new SynchronousQueue<>(); 22 | var t1 = 23 | Thread.startVirtualThread( 24 | () -> { 25 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 26 | try { 27 | queue.put(63); 28 | } catch (InterruptedException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | }); 33 | 34 | var t2 = 35 | Thread.startVirtualThread( 36 | () -> { 37 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 38 | try { 39 | queue.take(); 40 | } catch (InterruptedException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | }); 45 | 46 | t1.join(); 47 | t2.join(); 48 | } 49 | 50 | @Benchmark 51 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 52 | public void exchanger() throws InterruptedException { 53 | var exchanger = new Exchanger<>(); 54 | var t1 = 55 | Thread.startVirtualThread( 56 | () -> { 57 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 58 | try { 59 | exchanger.exchange(63); 60 | } catch (InterruptedException e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | }); 65 | 66 | var t2 = 67 | Thread.startVirtualThread( 68 | () -> { 69 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 70 | try { 71 | exchanger.exchange(64); 72 | } catch (InterruptedException e) { 73 | throw new RuntimeException(e); 74 | } 75 | } 76 | }); 77 | 78 | t1.join(); 79 | t2.join(); 80 | } 81 | 82 | @Benchmark 83 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 84 | public void channel() throws InterruptedException { 85 | var ch = Channel.newRendezvousChannel(); 86 | var t1 = 87 | Thread.startVirtualThread( 88 | () -> { 89 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 90 | try { 91 | ch.send(63); 92 | } catch (InterruptedException e) { 93 | throw new RuntimeException(e); 94 | } 95 | } 96 | }); 97 | 98 | var t2 = 99 | Thread.startVirtualThread( 100 | () -> { 101 | for (int i = 0; i < OPERATIONS_PER_INVOCATION; i++) { 102 | try { 103 | ch.receive(); 104 | } catch (InterruptedException e) { 105 | throw new RuntimeException(e); 106 | } 107 | } 108 | }); 109 | 110 | t1.join(); 111 | t2.join(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jox 2 | 3 | [![Ideas, suggestions, problems, questions](https://img.shields.io/badge/Discourse-ask%20question-blue)](https://softwaremill.community/c/open-source/11) 4 | [![CI](https://github.com/softwaremill/jox/workflows/CI/badge.svg)](https://github.com/softwaremill/jox/actions?query=workflow%3A%22CI%22) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.jox/channels/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.jox/channels) 6 | [![javadoc](https://javadoc.io/badge2/com.softwaremill.jox/channels/javadoc.svg)](https://javadoc.io/doc/com.softwaremill.jox/channels) 7 | 8 | [Virtual-thread](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) based safe concurrency & streaming 9 | for Java. 10 | 11 | Includes: 12 | 13 | * Fast & scalable, completable channels, with Go-like `select`s (Java 21+) 14 | * Programmer-friendly structured concurrency (Java 25 only) 15 | * Finite & infinite streaming using flows, with reactive streams compatibility, (blocking) I/O integration and a 16 | high-level, “functional” API (Java 25 only) 17 | 18 | Find out more in the documentation available at [jox.softwaremill.com](https://jox.softwaremill.com/). 19 | 20 | ## A tour of Jox 21 | 22 | Selectable [channels](https://jox.softwaremill.com/latest/channels.html): 23 | 24 | ``` 25 | var ch1 = Channel.newBufferedDefaultChannel(); 26 | var ch2 = Channel.newBufferedDefaultChannel(); 27 | var ch3 = Channel.newBufferedDefaultChannel(); 28 | 29 | // send a value to two channels 30 | ch2.send(29); 31 | ch3.send(32); 32 | 33 | var received = select(ch1.receiveClause(), ch2.receiveClause(), ch3.receiveClause()); 34 | ``` 35 | 36 | A push-based, backpressured [flow](https://jox.softwaremill.com/latest/flows.html) with time-based & parallel 37 | processing: 38 | 39 | ``` 40 | var nats = 41 | Flows.unfold(0, i -> Optional.of(Map.entry(i+1, i+1))); 42 | 43 | Flows.range(1, 100, 1) 44 | .throttle(1, Duration.ofSeconds(1)) 45 | .mapPar(4, i -> { 46 | Thread.sleep(5000); 47 | var j = i*3; 48 | return j+1; 49 | }) 50 | .filter(i -> i % 2 == 0) 51 | .zip(nats) 52 | .runForeach(IO::println); 53 | ``` 54 | 55 | [Structured concurrency](https://jox.softwaremill.com/latest/structured.html) scope: 56 | 57 | ``` 58 | var result = supervised(scope -> { 59 | var f1 = scope.fork(() -> { 60 | Thread.sleep(500); 61 | return 5; 62 | }); 63 | var f2 = scope.fork(() -> { 64 | Thread.sleep(1000); 65 | return 6; 66 | }); 67 | return f1.join() + f2.join(); 68 | }); 69 | IO.println("result = " + result); 70 | ``` 71 | 72 | ## Feedback 73 | 74 | Is what we are looking for! 75 | 76 | Let us know in the issues, or our [community forum](https://softwaremill.community/c/open-source/11). 77 | 78 | ## Project sponsor 79 | 80 | We offer commercial development services. [Contact us](https://softwaremill.com) to learn more! 81 | 82 | ## Building & working on Jox 83 | 84 | Jox uses the spotless maven plugin to format code. The formatting is automatically applied during compilation. 85 | Formatting can be checked using `mvn spotless:check` and applied with `mvn spotless:apply`. 86 | 87 | When using VS Code, format-on-save should be disabled (see the pom.xml). 88 | For IntelliJ, it might be necessary to install the spotless plugin to properly reformat the sources. 89 | 90 | ## Channel concurrency tests using Fray 91 | 92 | Apart from unit & stress tests, which live in the `channels/src/test/java` directory, Jox includes additional 93 | concurrency tests using the [Fray](https://github.com/cmu-pasta/fray) library. The library runs a number of randomized, 94 | but deterministic tests using orchestrated code & JVM. That way, even though the tests are not exhaustive, so we are 95 | not able to prove that the code is 100% race & deadlock-free, we gain a new approach to verifying thread interleavings 96 | which would otherwise be hard to obtain. 97 | 98 | The concurrency tests are only run when the `integration-tests` profile is enabled. That is, when in the 99 | `channels-fray-tests` module: 100 | 101 | - `mvn test` - Tests are skipped 102 | - `mvn verify` - Tests are skipped 103 | - `mvn integration-test -Pintegration-tests` - Tests run during the integration-test phase 104 | - `mvn verify -Pintegration-tests` - Tests run during the integration-test phase 105 | 106 | The test runs can be parametrized using the `CHANNEL_SIZE` and `JOX_SEGMENT_SIZE` environment variables. By default, 107 | these have the values 16 and 32. Note that the segment size affects not only the tests, but all channels, so be careful 108 | to change it only for scoped test runs. 109 | 110 | ## Copyright 111 | 112 | Copyright (C) 2023-2025 SoftwareMill [https://softwaremill.com](https://softwaremill.com). 113 | -------------------------------------------------------------------------------- /structured/src/test/java/com/softwaremill/jox/structured/ForkTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static com.softwaremill.jox.structured.Scopes.supervised; 4 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 5 | 6 | import java.util.Arrays; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class ForkTest { 11 | @Test 12 | void testRunTwoForksConcurrently() throws Exception { 13 | var trail = new Trail(); 14 | supervised( 15 | scope -> { 16 | var f1 = 17 | scope.forkUnsupervised( 18 | () -> { 19 | Thread.sleep(500); 20 | trail.add("f1 complete"); 21 | return 5; 22 | }); 23 | var f2 = 24 | scope.forkUnsupervised( 25 | () -> { 26 | Thread.sleep(1000); 27 | trail.add("f2 complete"); 28 | return 6; 29 | }); 30 | trail.add("main mid"); 31 | var result = f1.join() + f2.join(); 32 | trail.add("result = " + result); 33 | return null; 34 | }); 35 | 36 | assertIterableEquals( 37 | Arrays.asList("main mid", "f1 complete", "f2 complete", "result = 11"), 38 | trail.get()); 39 | } 40 | 41 | @Test 42 | void testNestedForks() throws Exception { 43 | Trail trail = new Trail(); 44 | Scopes.supervised( 45 | scope -> { 46 | var f1 = 47 | scope.forkUnsupervised( 48 | () -> { 49 | var f2 = 50 | scope.forkUnsupervised( 51 | () -> { 52 | try { 53 | return 6; 54 | } finally { 55 | trail.add("f2 complete"); 56 | } 57 | }); 58 | 59 | try { 60 | return 5 + f2.join(); 61 | } finally { 62 | trail.add("f1 complete"); 63 | } 64 | }); 65 | 66 | trail.add("result = " + f1.join()); 67 | return null; 68 | }); 69 | 70 | assertIterableEquals( 71 | Arrays.asList("f2 complete", "f1 complete", "result = 11"), trail.get()); 72 | } 73 | 74 | @Test 75 | void testInterruptChildForksWhenParentsComplete() throws Exception { 76 | Trail trail = new Trail(); 77 | Scopes.supervised( 78 | scope -> { 79 | var f1 = 80 | scope.forkUnsupervised( 81 | () -> { 82 | scope.forkUnsupervised( 83 | () -> { 84 | try { 85 | Thread.sleep(1000); 86 | trail.add("f2 complete"); 87 | return 6; 88 | } catch (InterruptedException e) { 89 | trail.add("f2 interrupted"); 90 | throw e; 91 | } 92 | }); 93 | 94 | Thread.sleep(500); 95 | trail.add("f1 complete"); 96 | return 5; 97 | }); 98 | 99 | trail.add("main mid"); 100 | trail.add("result = " + f1.join()); 101 | return null; 102 | }); 103 | 104 | assertIterableEquals( 105 | Arrays.asList("main mid", "f1 complete", "result = 5", "f2 interrupted"), 106 | trail.get()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /structured/src/main/java/com/softwaremill/jox/structured/ActorRef.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static com.softwaremill.jox.structured.Util.uninterruptible; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.function.Consumer; 8 | 9 | import com.softwaremill.jox.Channel; 10 | import com.softwaremill.jox.Sink; 11 | 12 | public class ActorRef { 13 | 14 | private final Sink> c; 15 | 16 | public ActorRef(Sink> c) { 17 | this.c = c; 18 | } 19 | 20 | /** 21 | * Send an invocation to the actor and await for the result. 22 | * 23 | *

The `f` function should be an invocation of a method on `T` and should not directly or 24 | * indirectly return the `T` value, as this might expose the actor's internal mutable state to 25 | * other threads. 26 | * 27 | *

Any non-fatal exceptions thrown by `f` will be propagated to the caller and the actor will 28 | * continue processing other invocations. Fatal exceptions will be propagated to the actor's 29 | * enclosing scope, and the actor will close. 30 | */ 31 | public U ask(ThrowingFunction f) throws Exception { 32 | CompletableFuture cf = new CompletableFuture<>(); 33 | c.send( 34 | t -> { 35 | try { 36 | cf.complete(f.apply(t)); 37 | } catch (Throwable e) { 38 | if (e instanceof RuntimeException) { 39 | cf.completeExceptionally(e); 40 | } else { 41 | cf.completeExceptionally(e); 42 | throw e; 43 | } 44 | } 45 | }); 46 | try { 47 | return cf.get(); 48 | } catch (ExecutionException e) { 49 | throw (Exception) e.getCause(); 50 | } 51 | } 52 | 53 | /** 54 | * Send an invocation to the actor that should be processed in the background (fire-and-forget). 55 | * Might block until there's enough space in the actor's mailbox (incoming channel). 56 | * 57 | *

Any exceptions thrown by `f` will be propagated to the actor's enclosing scope, and the 58 | * actor will close. 59 | */ 60 | public void tell(ThrowingConsumer f) throws InterruptedException { 61 | c.send(f); 62 | } 63 | 64 | /** The same as {@link ActorRef#create(Scope, Object, Consumer)} but with empty close action. */ 65 | public static ActorRef create(Scope scope, T logic) throws InterruptedException { 66 | return create(scope, logic, null); 67 | } 68 | 69 | /** 70 | * Creates a new actor ref, that is a fork in the current concurrency scope, which protects a 71 | * mutable resource (`logic`) and executes invocations on it serially, one after another. It is 72 | * guaranteed that `logic` will be accessed by at most one thread at a time. The methods of 73 | * `logic: T` define the actor's interface (the messages that can be "sent to the actor"). 74 | * 75 | *

Invocations can be scheduled using the returned `ActorRef`. When an invocation is an 76 | * `ActorRef.ask`, any non-fatal exceptions are propagated to the caller, and the actor 77 | * continues. Fatal exceptions, or exceptions that occur during `ActorRef.tell` invocations, 78 | * cause the actor's channel to be closed with an error, and are propagated to the enclosing 79 | * scope. 80 | * 81 | *

The actor's mailbox (incoming channel) will have a capacity of {@link 82 | * Channel#DEFAULT_BUFFER_SIZE}. 83 | */ 84 | public static ActorRef create(Scope scope, T logic, Consumer close) 85 | throws InterruptedException { 86 | Channel> c = Channel.newBufferedDefaultChannel(); 87 | ActorRef ref = new ActorRef<>(c); 88 | scope.fork( 89 | () -> { 90 | try { 91 | while (true) { 92 | ThrowingConsumer m = c.receive(); 93 | try { 94 | m.accept(logic); 95 | } catch (Throwable t) { 96 | c.error(t); 97 | throw t; 98 | } 99 | } 100 | } finally { 101 | if (close != null) { 102 | uninterruptible( 103 | () -> { 104 | close.accept(logic); 105 | return null; 106 | }); 107 | } 108 | } 109 | }); 110 | return ref; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowInterleaveTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.List; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class FlowInterleaveTest { 11 | 12 | @Test 13 | void shouldInterleaveWithEmptySource() throws Exception { 14 | // given 15 | var c1 = Flows.fromValues(1, 2, 3); 16 | var c2 = Flows.fromValues(); 17 | 18 | // when 19 | var result = c1.interleave(c2, 1, false, 10).runToList(); 20 | 21 | // then 22 | assertEquals(List.of(1, 2, 3), result); 23 | } 24 | 25 | @Test 26 | void shouldInterleaveTwoSourcesWithSegmentSizeEqualTo1() throws Exception { 27 | // given 28 | var c1 = Flows.fromValues(1, 3, 5); 29 | var c2 = Flows.fromValues(2, 4, 6); 30 | 31 | // when 32 | var result = c1.interleave(c2, 1, false, 10).runToList(); 33 | 34 | // then 35 | assertEquals(List.of(1, 2, 3, 4, 5, 6), result); 36 | } 37 | 38 | @Test 39 | void shouldInterleaveTwoSourcesWithSegmentSizeEqualTo1AndDifferentLengths() throws Exception { 40 | // given 41 | var c1 = Flows.fromValues(1, 3, 5); 42 | var c2 = Flows.fromValues(2, 4, 6, 8, 10, 12); 43 | 44 | // when 45 | var result = c1.interleave(c2, 1, false, 10).runToList(); 46 | 47 | // then 48 | assertEquals(List.of(1, 2, 3, 4, 5, 6, 8, 10, 12), result); 49 | } 50 | 51 | @Test 52 | void shouldInterleaveWithDefaultBufferCapacity() throws Exception { 53 | // given 54 | var c1 = Flows.fromValues(1, 3, 5); 55 | var c2 = Flows.fromValues(2, 4, 6, 8, 10, 12); 56 | 57 | // when 58 | var result = c1.interleave(c2, 1, false).runToList(); 59 | 60 | // then 61 | assertEquals(List.of(1, 2, 3, 4, 5, 6, 8, 10, 12), result); 62 | } 63 | 64 | @Test 65 | void shouldInterleaveWithBufferCapacityTakenFromScope() throws Exception { 66 | // given 67 | var c1 = Flows.fromValues(1, 3, 5); 68 | var c2 = Flows.fromValues(2, 4, 6, 8, 10, 12); 69 | 70 | // when 71 | var result = 72 | ScopedValue.where(Flow.CHANNEL_BUFFER_SIZE, 10) 73 | .call(() -> c1.interleave(c2, 1, false).runToList()); 74 | 75 | // then 76 | assertEquals(List.of(1, 2, 3, 4, 5, 6, 8, 10, 12), result); 77 | } 78 | 79 | @Test 80 | void shouldInterleaveTwoSourcesSegmentSizeBiggerThan1() throws Exception { 81 | // given 82 | var c1 = Flows.fromValues(1, 2, 3, 4); 83 | var c2 = Flows.fromValues(10, 20, 30, 40); 84 | 85 | // when 86 | var result = c1.interleave(c2, 2, false, 10).runToList(); 87 | 88 | // then 89 | assertEquals(List.of(1, 2, 10, 20, 3, 4, 30, 40), result); 90 | } 91 | 92 | @Test 93 | void shouldInterleaveTwoSourcesWitSegmentSizeBiggerThan1AndDifferentLengths() throws Exception { 94 | // given 95 | var c1 = Flows.fromValues(1, 2, 3, 4, 5, 6, 7); 96 | var c2 = Flows.fromValues(10, 20, 30, 40); 97 | 98 | // when 99 | var result = c1.interleave(c2, 2, false, 10).runToList(); 100 | 101 | // then 102 | assertEquals(List.of(1, 2, 10, 20, 3, 4, 30, 40, 5, 6, 7), result); 103 | } 104 | 105 | @Test 106 | void shouldInterleaveTwoSourcesWithDifferentLengthsAndCompleteEagerly() throws Exception { 107 | // given 108 | var c1 = Flows.fromValues(1, 3, 5); 109 | var c2 = Flows.fromValues(2, 4, 6, 8, 10, 12); 110 | 111 | // when 112 | var result = c1.interleave(c2, 1, true, 10).runToList(); 113 | 114 | // then 115 | assertEquals(List.of(1, 2, 3, 4, 5, 6), result); 116 | } 117 | 118 | @Test 119 | void shouldWhenEmptyInterleaveWithNonEmptySourceAndCompleteEagerly() throws Exception { 120 | // given 121 | var c1 = Flows.fromValues(); 122 | var c2 = Flows.fromValues(1, 2, 3); 123 | 124 | // when 125 | var s1 = c1.interleave(c2, 1, true, 10).runToList(); 126 | 127 | // then 128 | assertTrue(s1.isEmpty()); 129 | } 130 | 131 | @Test 132 | void shouldInterleaveWithEmptySourceAndCompleteEagerly() throws Exception { 133 | // given 134 | var c1 = Flows.fromValues(1, 2, 3); 135 | var c2 = Flows.fromValues(); 136 | 137 | // when 138 | var s1 = c1.interleave(c2, 1, true, 10); 139 | 140 | // then 141 | assertEquals(List.of(1), s1.runToList()); 142 | } 143 | 144 | @Test 145 | void shouldInterleaveWithBufferCapacityEqualTo0() throws Exception { 146 | // given 147 | var c1 = Flows.fromValues(1, 2, 3); 148 | var c2 = Flows.fromValues(10, 20, 30); 149 | 150 | // when 151 | var s1 = c1.interleave(c2, 1, true, 0); 152 | 153 | // then 154 | assertEquals(List.of(1, 10, 2, 20, 3, 30), s1.runToList()); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowPekkoStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.time.Duration; 6 | import java.util.List; 7 | import java.util.concurrent.ExecutionException; 8 | 9 | import org.apache.pekko.actor.ActorSystem; 10 | import org.apache.pekko.stream.javadsl.AsPublisher; 11 | import org.apache.pekko.stream.javadsl.Sink; 12 | import org.apache.pekko.stream.javadsl.Source; 13 | import org.junit.jupiter.api.AfterEach; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | import org.reactivestreams.FlowAdapters; 17 | import org.reactivestreams.Publisher; 18 | 19 | import com.softwaremill.jox.structured.Scopes; 20 | 21 | public class FlowPekkoStreamTest { 22 | 23 | private ActorSystem system; 24 | 25 | @BeforeEach 26 | void setUp() { 27 | system = ActorSystem.create("test"); 28 | } 29 | 30 | @AfterEach 31 | void cleanUp() { 32 | system.terminate(); 33 | } 34 | 35 | @Test 36 | void test() throws ExecutionException, InterruptedException { 37 | Scopes.supervised( 38 | scope -> { 39 | var flow = 40 | Flows.fromIterable(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) 41 | .map(i -> i * 2) 42 | .filter(i -> i % 3 == 0); 43 | var result = 44 | Source.fromPublisher(FlowAdapters.toPublisher(flow.toPublisher(scope))) 45 | .map(i -> i * 2) 46 | .runWith(Sink.seq(), system) 47 | .toCompletableFuture() 48 | .get(); 49 | 50 | assertEquals(List.of(12, 24, 36), result); 51 | return null; 52 | }); 53 | } 54 | 55 | @Test 56 | public void testSimpleFlow() throws InterruptedException { 57 | Scopes.supervised( 58 | scope -> { 59 | var flow = 60 | Flows.fromIterable(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) 61 | .map(i -> i * 2) 62 | .filter(i -> i % 3 == 0); 63 | var result = 64 | Source.fromPublisher(FlowAdapters.toPublisher(flow.toPublisher(scope))) 65 | .map(i -> i * 2) 66 | .runWith(Sink.seq(), system) 67 | .toCompletableFuture() 68 | .get(); 69 | 70 | assertEquals(List.of(12, 24, 36), result); 71 | return null; 72 | }); 73 | } 74 | 75 | @Test 76 | public void testConcurrentFlow() throws InterruptedException { 77 | Scopes.supervised( 78 | scope -> { 79 | var flow = 80 | Flows.tick(Duration.ofMillis(100), "x") 81 | .merge(Flows.tick(Duration.ofMillis(200), "y"), false, false) 82 | .take(5); 83 | var result = 84 | Source.fromPublisher(FlowAdapters.toPublisher(flow.toPublisher(scope))) 85 | .map(s -> s + s) 86 | .runWith(Sink.seq(), system) 87 | .toCompletableFuture() 88 | .get(); 89 | 90 | result = result.stream().sorted().toList(); 91 | assertEquals(List.of("xx", "xx", "xx", "yy", "yy"), result); 92 | return null; 93 | }); 94 | } 95 | 96 | @Test 97 | public void testFlowFromSimplePublisher() throws Exception { 98 | Publisher publisher = 99 | Source.fromIterator(() -> List.of(1, 2, 3).iterator()) 100 | .map(i -> i * 2) 101 | .runWith(Sink.asPublisher(AsPublisher.WITHOUT_FANOUT), system); 102 | 103 | var result = 104 | Flows.fromPublisher(FlowAdapters.toFlowPublisher(publisher)) 105 | .map(i -> i * 10) 106 | .runToList(); 107 | 108 | assertEquals(List.of(20, 40, 60), result); 109 | } 110 | 111 | @Test 112 | public void testFlowFromConcurrentPublisher() throws Exception { 113 | Publisher publisher = 114 | Source.tick(Duration.ZERO, Duration.ofMillis(100), "x") 115 | .merge(Source.tick(Duration.ZERO, Duration.ofMillis(200), "y")) 116 | .take(5) 117 | .runWith(Sink.asPublisher(AsPublisher.WITHOUT_FANOUT), system); 118 | 119 | var result = 120 | Flows.fromPublisher(FlowAdapters.toFlowPublisher(publisher)) 121 | .map(s -> s + s) 122 | .runToList(); 123 | 124 | result.sort(String::compareTo); 125 | assertEquals(List.of("xx", "xx", "xx", "yy", "yy"), result); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /flows/src/test/java/com/softwaremill/jox/flows/FlowFlattenTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.contains; 5 | import static org.hamcrest.Matchers.containsInAnyOrder; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | import java.time.Duration; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import org.hamcrest.Matchers; 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class FlowFlattenTest { 17 | 18 | @Test 19 | void flattenTest() throws Exception { 20 | // given 21 | Flow> flow = Flows.fromValues(Flows.fromValues(1, 2), Flows.fromValues(5, 9)); 22 | 23 | // when 24 | List integers = flow.flatten().runToList(); 25 | 26 | // then 27 | assertEquals(List.of(1, 2, 5, 9), integers); 28 | } 29 | 30 | @Test 31 | void flatten_shouldThrowWhenCalledOnFlowNotContainingFlows() { 32 | // when & then 33 | IllegalArgumentException exception = 34 | assertThrows( 35 | IllegalArgumentException.class, () -> Flows.fromValues(3, 3).flatten()); 36 | assertEquals( 37 | "requirement failed: flatten can be called on Flow containing Flows", 38 | exception.getMessage()); 39 | } 40 | 41 | @Test 42 | void flattenPar_shouldThrowWhenCalledOnFlowNotContainingFlows() { 43 | // when & then 44 | IllegalArgumentException exception = 45 | assertThrows( 46 | IllegalArgumentException.class, () -> Flows.fromValues(3, 3).flattenPar(1)); 47 | assertEquals( 48 | "requirement failed: flattenPar can be called on Flow containing Flows", 49 | exception.getMessage()); 50 | } 51 | 52 | @Test 53 | void shouldPipeAllElementsOfTheChildFlowsIntoTheOutputFlow() throws Exception { 54 | // given 55 | var flow = 56 | Flows.fromValues( 57 | Flows.fromValues(10), 58 | Flows.fromValues(20, 30), 59 | Flows.fromValues(40, 50, 60)); 60 | 61 | // when & then 62 | List actual = flow.flattenPar(10).runToList(); 63 | assertThat(actual, containsInAnyOrder(10, 20, 30, 40, 50, 60)); 64 | } 65 | 66 | @Test 67 | void shouldHandleEmptyFlow() throws Exception { 68 | // given 69 | var flow = Flows.>empty(); 70 | 71 | // when & then 72 | assertThat(flow.flattenPar(10).runToList(), Matchers.empty()); 73 | } 74 | 75 | @Test 76 | void shouldHandleSingletonFlow() throws Exception { 77 | // given 78 | var flow = Flows.fromValues(Flows.fromValues(10)); 79 | 80 | // when & then 81 | List objects = flow.flattenPar(10).runToList(); 82 | assertThat(objects, contains(10)); 83 | } 84 | 85 | @Test 86 | void shouldNotFlattenNestedFlows() throws Exception { 87 | // given 88 | var flow = Flows.fromValues(Flows.fromValues(Flows.fromValues(10))); 89 | 90 | // when 91 | List> flows = flow.flattenPar(10).runToList(); 92 | 93 | // then 94 | List result = new ArrayList<>(); 95 | for (Flow f : flows) { 96 | List integers = f.runToList(); 97 | result.addAll(integers); 98 | } 99 | assertThat(result, contains(10)); 100 | } 101 | 102 | @Test 103 | void shouldHandleSubsequentFlattenCalls() throws Exception { 104 | // given 105 | var flow = Flows.fromValues(Flows.fromValues(Flows.fromValues(10), Flows.fromValues(20))); 106 | 107 | // when & then 108 | var result = 109 | flow.flattenPar(10).runToList().stream() 110 | .flatMap( 111 | f -> { 112 | try { 113 | return f.runToList().stream(); 114 | } catch (Exception e) { 115 | throw new RuntimeException(e); 116 | } 117 | }) 118 | .toList(); 119 | 120 | assertThat(result, containsInAnyOrder(10, 20)); 121 | } 122 | 123 | @Test 124 | void shouldRunAtMostParallelismChildFlows() throws Exception { 125 | // given 126 | var flow = 127 | Flows.fromValues( 128 | Flows.timeout(Duration.ofMillis(200)).concat(Flows.fromValues(10)), 129 | Flows.timeout(Duration.ofMillis(100)).concat(Flows.fromValues(20, 30)), 130 | Flows.fromValues(40, 50, 60)); 131 | 132 | // when & then 133 | // only one flow can run at a time 134 | assertThat(flow.flattenPar(1).runToList(), contains(10, 20, 30, 40, 50, 60)); 135 | // when parallelism is increased, all flows are run concurrently 136 | assertThat(flow.flattenPar(3).runToList(), contains(40, 50, 60, 20, 30, 10)); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /structured/src/test/java/com/softwaremill/jox/structured/ParTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static com.softwaremill.jox.structured.Par.par; 4 | import static com.softwaremill.jox.structured.Par.parLimit; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertIterableEquals; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.concurrent.Callable; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.stream.IntStream; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class ParTest { 17 | @Test 18 | void testParRunsComputationsInParallel() throws Exception { 19 | Trail trail = new Trail(); 20 | var result = 21 | par( 22 | List.of( 23 | () -> { 24 | Thread.sleep(200); 25 | trail.add("a"); 26 | return 1; 27 | }, 28 | () -> { 29 | Thread.sleep(100); 30 | trail.add("b"); 31 | return 2; 32 | })); 33 | 34 | trail.add("done"); 35 | 36 | assertIterableEquals(List.of(1, 2), result); 37 | assertIterableEquals(Arrays.asList("b", "a", "done"), trail.get()); 38 | } 39 | 40 | @Test 41 | void testParInterruptsOtherComputationsIfOneFails() throws InterruptedException { 42 | Trail trail = new Trail(); 43 | try { 44 | par( 45 | List.of( 46 | () -> { 47 | Thread.sleep(200); 48 | trail.add("par 1 done"); 49 | return null; 50 | }, 51 | () -> { 52 | Thread.sleep(100); 53 | trail.add("exception"); 54 | throw new Exception("boom"); 55 | })); 56 | } catch (JoxScopeExecutionException e) { 57 | if (e.getCause().getMessage().equals("boom")) { 58 | trail.add("catch"); 59 | } 60 | } 61 | 62 | // Checking if the forks aren't left running 63 | Thread.sleep(300); 64 | trail.add("all done"); 65 | 66 | assertIterableEquals(Arrays.asList("exception", "catch", "all done"), trail.get()); 67 | } 68 | 69 | @Test 70 | void testParLimitRunsUpToGivenNumberOfComputationsInParallel() throws Exception { 71 | AtomicInteger running = new AtomicInteger(0); 72 | AtomicInteger max = new AtomicInteger(0); 73 | var result = 74 | parLimit( 75 | 2, 76 | IntStream.rangeClosed(1, 9) 77 | .>mapToObj( 78 | i -> 79 | () -> { 80 | int current = running.incrementAndGet(); 81 | max.updateAndGet(m -> Math.max(current, m)); 82 | Thread.sleep(100); 83 | running.decrementAndGet(); 84 | return i * 2; 85 | }) 86 | .toList()); 87 | 88 | assertIterableEquals(List.of(2, 4, 6, 8, 10, 12, 14, 16, 18), result); 89 | assertEquals(2, max.get()); 90 | } 91 | 92 | @Test 93 | void testParLimitInterruptsOtherComputationsIfOneFails() throws InterruptedException { 94 | AtomicInteger counter = new AtomicInteger(0); 95 | Trail trail = new Trail(); 96 | try { 97 | parLimit( 98 | 2, 99 | IntStream.rangeClosed(1, 5) 100 | .>mapToObj( 101 | i -> 102 | () -> { 103 | if (counter.incrementAndGet() == 4) { 104 | Thread.sleep(10); 105 | trail.add("exception"); 106 | throw new Exception("boom"); 107 | } else { 108 | Thread.sleep(200); 109 | trail.add("x"); 110 | return null; 111 | } 112 | }) 113 | .toList()); 114 | } catch (JoxScopeExecutionException e) { 115 | if (e.getCause().getMessage().equals("boom")) { 116 | trail.add("catch"); 117 | } 118 | } 119 | 120 | Thread.sleep(300); 121 | trail.add("all done"); 122 | 123 | assertIterableEquals( 124 | Arrays.asList("x", "x", "exception", "catch", "all done"), trail.get()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /structured/src/test/java/com/softwaremill/jox/structured/RaceTest.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.structured; 2 | 3 | import static com.softwaremill.jox.structured.Race.race; 4 | import static com.softwaremill.jox.structured.Race.timeout; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.concurrent.TimeoutException; 11 | import java.util.stream.Collectors; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class RaceTest { 16 | @Test 17 | void testTimeoutShortCircuitsLongComputation() throws Exception { 18 | Trail trail = new Trail(); 19 | try { 20 | timeout( 21 | 1000, 22 | () -> { 23 | Thread.sleep(2000); 24 | trail.add("no timeout"); 25 | return null; 26 | }); 27 | } catch (TimeoutException e) { 28 | trail.add("timeout"); 29 | } 30 | 31 | trail.add("done"); 32 | Thread.sleep(2000); 33 | 34 | assertIterableEquals(Arrays.asList("timeout", "done"), trail.get()); 35 | } 36 | 37 | @Test 38 | void testTimeoutDoesNotInterruptShortComputation() throws Exception { 39 | Trail trail = new Trail(); 40 | try { 41 | var r = 42 | timeout( 43 | 1000, 44 | () -> { 45 | Thread.sleep(500); 46 | trail.add("no timeout"); 47 | return 5; 48 | }); 49 | assertEquals(5, r); 50 | trail.add("asserted"); 51 | } catch (TimeoutException e) { 52 | trail.add("timeout"); 53 | } 54 | 55 | trail.add("done"); 56 | Thread.sleep(2000); 57 | 58 | assertIterableEquals(Arrays.asList("no timeout", "asserted", "done"), trail.get()); 59 | } 60 | 61 | @Test 62 | void testRaceRacesSlowerAndFasterComputation() throws Exception { 63 | Trail trail = new Trail(); 64 | long start = System.currentTimeMillis(); 65 | race( 66 | () -> { 67 | Thread.sleep(1000); 68 | trail.add("slow"); 69 | return null; 70 | }, 71 | () -> { 72 | Thread.sleep(500); 73 | trail.add("fast"); 74 | return null; 75 | }); 76 | long end = System.currentTimeMillis(); 77 | 78 | Thread.sleep(1000); 79 | assertIterableEquals(List.of("fast"), trail.get()); 80 | assertTrue(end - start < 1000); 81 | } 82 | 83 | @Test 84 | void testRaceRacesFasterAndSlowerComputation() throws Exception { 85 | Trail trail = new Trail(); 86 | long start = System.currentTimeMillis(); 87 | race( 88 | () -> { 89 | Thread.sleep(500); 90 | trail.add("fast"); 91 | return null; 92 | }, 93 | () -> { 94 | Thread.sleep(1000); 95 | trail.add("slow"); 96 | return null; 97 | }); 98 | long end = System.currentTimeMillis(); 99 | 100 | Thread.sleep(1000); 101 | assertIterableEquals(List.of("fast"), trail.get()); 102 | assertTrue(end - start < 1000); 103 | } 104 | 105 | @Test 106 | void testRaceReturnsFirstSuccessfulComputationToComplete() throws Exception { 107 | Trail trail = new Trail(); 108 | long start = System.currentTimeMillis(); 109 | race( 110 | () -> { 111 | Thread.sleep(200); 112 | trail.add("error"); 113 | throw new RuntimeException("boom!"); 114 | }, 115 | () -> { 116 | Thread.sleep(500); 117 | trail.add("slow"); 118 | return null; 119 | }, 120 | () -> { 121 | Thread.sleep(1000); 122 | trail.add("very slow"); 123 | return null; 124 | }); 125 | long end = System.currentTimeMillis(); 126 | 127 | Thread.sleep(1000); 128 | assertIterableEquals(Arrays.asList("error", "slow"), trail.get()); 129 | assertTrue(end - start < 1000); 130 | } 131 | 132 | @Test 133 | void testRaceShouldAddOtherExceptionsAsSuppressed() { 134 | var exception = 135 | assertThrows( 136 | JoxScopeExecutionException.class, 137 | () -> { 138 | race( 139 | () -> { 140 | throw new RuntimeException("boom1!"); 141 | }, 142 | () -> { 143 | Thread.sleep(200); 144 | throw new RuntimeException("boom2!"); 145 | }, 146 | () -> { 147 | Thread.sleep(200); 148 | throw new RuntimeException("boom3!"); 149 | }); 150 | }); 151 | 152 | assertEquals("boom1!", exception.getCause().getMessage()); 153 | assertEquals( 154 | Set.of("boom2!", "boom3!"), 155 | Arrays.stream(exception.getSuppressed()) 156 | .map(Throwable::getMessage) 157 | .collect(Collectors.toSet())); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /channels/src/main/java/com/softwaremill/jox/CloseableChannel.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | /** 4 | * A channel which can be closed. 5 | * 6 | *

A channel can be closed in two ways: 7 | * 8 | *

    9 | *
  • using {@link #done()} or {@link #doneOrClosed()}, indicating that no more elements will be 10 | * sent 11 | *
  • using {@link #error(Throwable)} or {@link #errorOrClosed(Throwable)}, indicating an error 12 | *
13 | * 14 | *

A channel can be closed only once. Subsequent calls to {@link #done()} or {@link 15 | * #error(Throwable)} will throw {@link ChannelClosedException}, or return the original closing 16 | * reason (when using {@link #doneOrClosed()} or {@link #errorOrClosed(Throwable)}). 17 | * 18 | *

Closing the channel is thread-safe. 19 | */ 20 | public interface CloseableChannel { 21 | /** 22 | * Close the channel, indicating that no more elements will be sent. 23 | * 24 | *

Any elements that are already buffered will be delivered. Any send operations that are in 25 | * progress will complete normally, when a receiver arrives. Any pending receive operations will 26 | * complete with a channel closed result. 27 | * 28 | *

Subsequent {@link Sink#send(Object)} operations will throw {@link ChannelClosedException}. 29 | * 30 | * @throws ChannelClosedException When the channel is already closed. 31 | */ 32 | void done(); 33 | 34 | /** 35 | * Close the channel, indicating that no more elements will be sent. Doesn't throw exceptions 36 | * when the channel is closed, but returns a value. 37 | * 38 | *

Any elements that are already buffered will be delivered. Any send operations that are in 39 | * progress will complete normally, when a receiver arrives. Any pending receive operations will 40 | * complete with a channel closed result. 41 | * 42 | *

Subsequent {@link Sink#send(Object)} operations will throw {@link ChannelClosedException}. 43 | * 44 | * @return Either {@code null}, or {@link ChannelClosed}, when the channel is already closed. 45 | */ 46 | Object doneOrClosed(); 47 | 48 | // 49 | 50 | /** 51 | * Close the channel, indicating an error. 52 | * 53 | *

Any elements that are already buffered won't be delivered. Any send or receive operations 54 | * that are in progress will complete with a channel closed result. 55 | * 56 | *

Subsequent {@link Sink#send(Object)} and {@link Source#receive()} operations will throw 57 | * {@link ChannelClosedException}. 58 | * 59 | * @param reason The reason of the error. Not {@code null}. 60 | * @throws ChannelClosedException When the channel is already closed. 61 | */ 62 | void error(Throwable reason); 63 | 64 | /** 65 | * Close the channel, indicating an error. Doesn't throw exceptions when the channel is closed, 66 | * but returns a value. 67 | * 68 | *

Any elements that are already buffered won't be delivered. Any send or receive operations 69 | * that are in progress will complete with a channel closed result. 70 | * 71 | *

Subsequent {@link Sink#send(Object)} and {@link Source#receive()} operations will throw 72 | * {@link ChannelClosedException}. 73 | * 74 | * @return Either {@code null}, or {@link ChannelClosed}, when the channel is already closed. 75 | */ 76 | Object errorOrClosed(Throwable reason); 77 | 78 | // 79 | 80 | /** 81 | * @return {@code true} if no more values can be sent to this channel; {@link Sink#send(Object)} 82 | * will throw {@link ChannelClosedException} or return {@link ChannelClosed} (in the 83 | * or-closed variant). 84 | *

When closed for send, receiving using {@link Source#receive()} might still be 85 | * possible, if the channel is done, and not in an error. This can be verified using {@link 86 | * #isClosedForReceive()}. 87 | */ 88 | default boolean isClosedForSend() { 89 | return closedForSend() != null; 90 | } 91 | 92 | /** 93 | * @return {@code true} if no more values can be received from this channel; {@link 94 | * Source#receive()} will throw {@link ChannelClosedException} or return {@link 95 | * ChannelClosed} (in the or-closed variant). 96 | *

When closed for receive, sending values is also not possible, {@link 97 | * #isClosedForSend()} will return {@code true}. 98 | *

When {@code false}, values might be received from the channel, when 99 | * calling {@link Source#receive()}, but it's not guaranteed that some values will be 100 | * available. They might be received concurrently. 101 | */ 102 | default boolean isClosedForReceive() { 103 | return closedForReceive() != null; 104 | } 105 | 106 | /** 107 | * @return Non-{@code null} if no more values can be sent to this channel; {@link 108 | * Sink#send(Object)} will throw {@link ChannelClosedException} or return {@link 109 | * ChannelClosed} (in the or-closed variant). 110 | *

{@code null} if the channel is not closed, and values can be sent. 111 | *

When closed for send, receiving using {@link Source#receive()} might still be 112 | * possible, if the channel is done, and not in an error. This can be verified using {@link 113 | * #isClosedForReceive()}. 114 | */ 115 | ChannelClosed closedForSend(); 116 | 117 | /** 118 | * @return Non-{@code null} if no more values can be received from this channel; {@link 119 | * Source#receive()} will throw {@link ChannelClosedException} or return {@link 120 | * ChannelClosed} (in the or-closed variant). 121 | *

{@code null} if the channel is not closed, and values can be received. 122 | *

When closed for receive, sending values is also not possible, {@link 123 | * #isClosedForSend()} will return {@code true}. 124 | */ 125 | ChannelClosed closedForReceive(); 126 | } 127 | -------------------------------------------------------------------------------- /flows/src/main/java/com/softwaremill/jox/flows/WeightedHeap.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox.flows; 2 | 3 | import java.util.*; 4 | 5 | class WeightedHeap { 6 | public record HeapNode(T item, long weight) { 7 | private HeapNode copy() { 8 | return new HeapNode<>(item, weight); 9 | } 10 | } 11 | 12 | private final List> heap; 13 | private final Map valueToIndex; 14 | 15 | private WeightedHeap(List> heap, Map valueToIndex) { 16 | this.heap = heap; 17 | this.valueToIndex = valueToIndex; 18 | } 19 | 20 | public WeightedHeap() { 21 | this.heap = new ArrayList<>(); 22 | this.valueToIndex = new HashMap<>(); 23 | } 24 | 25 | public WeightedHeap insert(T item, long weight) { 26 | if (valueToIndex.containsKey(item)) { 27 | return updateWeight(item, weight); 28 | } 29 | var newHeap = new ArrayList<>(heap); 30 | newHeap.add(new HeapNode<>(item, weight)); 31 | 32 | var newValuesToIndex = new HashMap<>(valueToIndex); 33 | newValuesToIndex.put(item, newHeap.size() - 1); 34 | 35 | var result = bubbleUp(newHeap, newValuesToIndex, newHeap.size() - 1); 36 | return new WeightedHeap<>(result.getKey(), result.getValue()); 37 | } 38 | 39 | public WeightedHeap updateWeight(T item, long newWeight) { 40 | if (!valueToIndex.containsKey(item)) { 41 | throw new NoSuchElementException("Item %s not found in the heap".formatted(item)); 42 | } 43 | int index = valueToIndex.get(item); 44 | HeapNode node = heap.get(index); 45 | long oldWeight = node.weight; 46 | 47 | if (newWeight == oldWeight) { 48 | return this; 49 | } 50 | 51 | List> newHeap = new ArrayList<>(heap); 52 | newHeap.set(index, new HeapNode<>(item, newWeight)); 53 | 54 | Map.Entry>, Map> result; 55 | if (newWeight < oldWeight) { 56 | result = bubbleUp(newHeap, valueToIndex, index); 57 | } else { 58 | result = bubbleDown(newHeap, valueToIndex, index); 59 | } 60 | return new WeightedHeap<>(result.getKey(), result.getValue()); 61 | } 62 | 63 | public Map.Entry>, WeightedHeap> extractMin() { 64 | if (heap.isEmpty()) { 65 | return Map.entry(Optional.empty(), this); 66 | } else if (heap.size() == 1) { 67 | return Map.entry(Optional.of(heap.getFirst().copy()), new WeightedHeap<>()); 68 | } 69 | var min = heap.getFirst(); 70 | var last = heap.getLast(); 71 | 72 | var newHeap = new ArrayList<>(heap); 73 | newHeap.set(0, last); 74 | newHeap.removeLast(); 75 | 76 | var newValueToIndex = new HashMap<>(valueToIndex); 77 | newValueToIndex.put(last.item, 0); 78 | newValueToIndex.remove(min.item); 79 | 80 | var result = bubbleDown(newHeap, newValueToIndex, 0); 81 | return Map.entry( 82 | Optional.of(min.copy()), new WeightedHeap<>(result.getKey(), result.getValue())); 83 | } 84 | 85 | public Optional> peekMin() { 86 | if (heap.isEmpty()) { 87 | return Optional.empty(); 88 | } 89 | return Optional.of(heap.getFirst().copy()); 90 | } 91 | 92 | public boolean isEmpty() { 93 | return heap.isEmpty(); 94 | } 95 | 96 | public int size() { 97 | return heap.size(); 98 | } 99 | 100 | public boolean contains(T item) { 101 | return valueToIndex.containsKey(item); 102 | } 103 | 104 | private int parentIndex(int i) { 105 | return (i - 1) / 2; 106 | } 107 | 108 | private int leftChildIndex(int i) { 109 | return 2 * i + 1; 110 | } 111 | 112 | private int rightChildIndex(int i) { 113 | return 2 * i + 2; 114 | } 115 | 116 | private Map.Entry>, Map> swap( 117 | List> heap, Map valueToIndex, int i, int j) { 118 | List> newHeap = new ArrayList<>(heap); 119 | Collections.swap(newHeap, i, j); 120 | 121 | Map newValueToIndex = new HashMap<>(valueToIndex); 122 | newValueToIndex.put(heap.get(i).item, j); 123 | newValueToIndex.put(heap.get(j).item, i); 124 | 125 | return Map.entry(newHeap, newValueToIndex); 126 | } 127 | 128 | private Map.Entry>, Map> bubbleUp( 129 | List> heap, Map valueToIndex, int i) { 130 | int currentIndex = i; 131 | List> currentHeap = heap; 132 | Map currentMap = valueToIndex; 133 | 134 | while (currentIndex > 0 135 | && currentHeap.get(currentIndex).weight 136 | < currentHeap.get(parentIndex(currentIndex)).weight) { 137 | var result = swap(currentHeap, currentMap, currentIndex, parentIndex(currentIndex)); 138 | 139 | currentHeap = result.getKey(); 140 | currentMap = result.getValue(); 141 | currentIndex = parentIndex(currentIndex); 142 | } 143 | return Map.entry(currentHeap, currentMap); 144 | } 145 | 146 | private Map.Entry>, Map> bubbleDown( 147 | List> heap, Map valueToIndex, int i) { 148 | int currentIndex = i; 149 | var currentHeap = heap; 150 | Map currentMap = valueToIndex; 151 | 152 | while (true) { 153 | int left = leftChildIndex(currentIndex); 154 | int right = rightChildIndex(currentIndex); 155 | int smallest = currentIndex; 156 | 157 | if (left < currentHeap.size() 158 | && currentHeap.get(left).weight < currentHeap.get(smallest).weight) { 159 | smallest = left; 160 | } 161 | if (right < currentHeap.size() 162 | && currentHeap.get(right).weight < currentHeap.get(smallest).weight) { 163 | smallest = right; 164 | } 165 | 166 | if (smallest == currentIndex) { 167 | return Map.entry(currentHeap, currentMap); 168 | } 169 | 170 | var result = swap(currentHeap, currentMap, currentIndex, smallest); 171 | currentHeap = result.getKey(); 172 | currentMap = result.getValue(); 173 | currentIndex = smallest; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /bench/bench-java/src/main/java/com/softwaremill/jox/ChainedBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.softwaremill.jox; 2 | 3 | import java.util.concurrent.ArrayBlockingQueue; 4 | import java.util.concurrent.BlockingQueue; 5 | import java.util.concurrent.SynchronousQueue; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | import org.openjdk.jmh.annotations.*; 9 | 10 | /** 11 | * Chained send-receive test for {@link Channel} and {@link BlockingQueue} - a series of threads 12 | * proxying values to subsequent channels/queues. 13 | */ 14 | @Warmup(iterations = 3, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 15 | @Measurement(iterations = 10, time = 4000, timeUnit = TimeUnit.MILLISECONDS) 16 | @Fork(value = 2) 17 | @BenchmarkMode(Mode.AverageTime) 18 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 19 | @State(Scope.Benchmark) 20 | public class ChainedBenchmark { 21 | @Param({"0", "16", "100"}) 22 | public int capacity; 23 | 24 | @Param({"10000"}) 25 | public int chainLength; 26 | 27 | private static final int OPERATIONS_PER_INVOCATION = 10_000_000; 28 | 29 | @Benchmark 30 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 31 | public void channelChain() throws InterruptedException { 32 | // we want to measure the amount of time a send-receive pair takes 33 | int elements = OPERATIONS_PER_INVOCATION / chainLength; 34 | Channel[] channels = new Channel[chainLength]; 35 | for (int i = 0; i < chainLength; i++) { 36 | channels[i] = 37 | (capacity == 0) 38 | ? Channel.newRendezvousChannel() 39 | : Channel.newBufferedChannel(capacity); 40 | } 41 | 42 | Thread[] threads = new Thread[chainLength + 1]; 43 | threads[0] = 44 | Thread.startVirtualThread( 45 | () -> { 46 | var ch = channels[0]; 47 | for (int i = 0; i < elements; i++) { 48 | try { 49 | ch.send(63); 50 | } catch (InterruptedException e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | }); 55 | 56 | for (int t = 1; t < chainLength; t++) { 57 | int finalT = t; 58 | threads[t] = 59 | Thread.startVirtualThread( 60 | () -> { 61 | var ch1 = channels[finalT - 1]; 62 | var ch2 = channels[finalT]; 63 | for (int i = 0; i < elements; i++) { 64 | try { 65 | ch2.send(ch1.receive()); 66 | } catch (InterruptedException e) { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | threads[chainLength] = 74 | Thread.startVirtualThread( 75 | () -> { 76 | var ch = channels[chainLength - 1]; 77 | for (int i = 0; i < elements; i++) { 78 | try { 79 | ch.receive(); 80 | } catch (InterruptedException e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | }); 85 | 86 | for (Thread thread : threads) { 87 | thread.join(); 88 | } 89 | } 90 | 91 | @Benchmark 92 | @OperationsPerInvocation(OPERATIONS_PER_INVOCATION) 93 | public void queueChain() throws InterruptedException { 94 | // we want to measure the amount of time a send-receive pair takes 95 | int elements = OPERATIONS_PER_INVOCATION / chainLength; 96 | BlockingQueue[] queues = new BlockingQueue[chainLength]; 97 | if (capacity == 0) { 98 | for (int i = 0; i < chainLength; i++) { 99 | queues[i] = new SynchronousQueue<>(); 100 | } 101 | } else { 102 | for (int i = 0; i < chainLength; i++) { 103 | queues[i] = new ArrayBlockingQueue<>(capacity); 104 | } 105 | } 106 | 107 | Thread[] threads = new Thread[chainLength + 1]; 108 | threads[0] = 109 | Thread.startVirtualThread( 110 | () -> { 111 | var q = queues[0]; 112 | for (int i = 0; i < elements; i++) { 113 | try { 114 | q.put(63); 115 | } catch (InterruptedException e) { 116 | throw new RuntimeException(e); 117 | } 118 | } 119 | }); 120 | 121 | for (int t = 1; t < chainLength; t++) { 122 | int finalT = t; 123 | threads[t] = 124 | Thread.startVirtualThread( 125 | () -> { 126 | var q1 = queues[finalT - 1]; 127 | var q2 = queues[finalT]; 128 | for (int i = 0; i < elements; i++) { 129 | try { 130 | q2.put(q1.take()); 131 | } catch (InterruptedException e) { 132 | throw new RuntimeException(e); 133 | } 134 | } 135 | }); 136 | } 137 | 138 | threads[chainLength] = 139 | Thread.startVirtualThread( 140 | () -> { 141 | var q = queues[chainLength - 1]; 142 | for (int i = 0; i < elements; i++) { 143 | try { 144 | q.take(); 145 | } catch (InterruptedException e) { 146 | throw new RuntimeException(e); 147 | } 148 | } 149 | }); 150 | 151 | for (Thread thread : threads) { 152 | thread.join(); 153 | } 154 | } 155 | } 156 | --------------------------------------------------------------------------------