From 8d7e0d2e13ad35b43b2fd644494e19c558d94ba7 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Sun, 24 May 2026 16:02:30 +0000 Subject: [PATCH 01/17] Add Scala Native support with ported Jox channels implementation - Add sbt-scala-native and sbt-crossproject plugins - Convert core module to crossProject(JVM, Native) with Full cross type - Move shared source to core/shared/, JVM-specific to core/jvm/, Native to core/native/ - Port the Jox channels library (Channel, Segment, Select, Continuation) from Java to Scala in ox.channels.jox package, using AtomicReference/AtomicLong/AtomicInteger instead of VarHandle - Native channel wrapper delegates to the Scala Jox port (same pattern as JVM delegates to Java Jox) - Enable multithreading in native config (requires Scala Native 0.5.12 with virtual thread support) - All 923 JVM tests pass; native compilation succeeds Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 37 +- .../src/main/scala/ox/channels/Channel.scala | 0 .../scala/ox/channels/ChannelClosed.scala | 0 .../src/main/scala/ox/channels/select.scala | 0 .../reactive/FlowPublisherPekkoTest.scala | 0 .../flow/reactive/FlowPublisherTckTest.scala | 0 .../src/main/scala/ox/channels/Channel.scala | 138 ++++ .../scala/ox/channels/ChannelClosed.scala | 28 + .../src/main/scala/ox/channels/select.scala | 444 ++++++++++++ .../src/main/scala/ox/Chunk.scala | 0 .../src/main/scala/ox/ErrorMode.scala | 0 core/{ => shared}/src/main/scala/ox/Ox.scala | 0 .../src/main/scala/ox/OxApp.scala | 0 .../scala/ox/channels/BufferCapacity.scala | 0 .../ox/channels/ChannelClosedUnion.scala | 0 .../ox/channels/SourceCompanionOps.scala | 0 .../scala/ox/channels/SourceDrainOps.scala | 0 .../main/scala/ox/channels/SourceOps.scala | 0 .../src/main/scala/ox/channels/actor.scala | 0 .../scala/ox/channels/forkPropagate.scala | 0 .../scala/ox/channels/jox/CellState.scala | 38 + .../main/scala/ox/channels/jox/Channel.scala | 661 ++++++++++++++++++ .../scala/ox/channels/jox/ChannelClosed.scala | 17 + .../ox/channels/jox/CloseableChannel.scala | 13 + .../scala/ox/channels/jox/Continuation.scala | 44 ++ .../main/scala/ox/channels/jox/Segment.scala | 181 +++++ .../main/scala/ox/channels/jox/Select.scala | 208 ++++++ .../scala/ox/channels/jox/SelectClause.scala | 18 + .../src/main/scala/ox/channels/jox/Sink.scala | 14 + .../main/scala/ox/channels/jox/Source.scala | 14 + .../ox/channels/jox/StoredSelectClause.scala | 13 + .../src/main/scala/ox/collections.scala | 0 .../src/main/scala/ox/control.scala | 0 .../src/main/scala/ox/either.scala | 0 .../src/main/scala/ox/flow/Flow.scala | 0 .../scala/ox/flow/FlowCompanionIOOps.scala | 0 .../main/scala/ox/flow/FlowCompanionOps.scala | 0 .../ox/flow/FlowCompanionReactiveOps.scala | 0 .../src/main/scala/ox/flow/FlowIOOps.scala | 0 .../src/main/scala/ox/flow/FlowOps.scala | 0 .../main/scala/ox/flow/FlowReactiveOps.scala | 0 .../src/main/scala/ox/flow/FlowRunOps.scala | 0 .../src/main/scala/ox/flow/FlowTextOps.scala | 0 .../scala/ox/flow/internal/WeightedHeap.scala | 0 .../scala/ox/flow/internal/groupByImpl.scala | 0 .../{ => shared}/src/main/scala/ox/fork.scala | 0 .../src/main/scala/ox/inScopeRunner.scala | 0 .../main/scala/ox/internal/ScopeContext.scala | 0 .../main/scala/ox/internal/ThreadHerd.scala | 0 .../src/main/scala/ox/local.scala | 0 .../src/main/scala/ox/oxThreadFactory.scala | 0 core/{ => shared}/src/main/scala/ox/par.scala | 0 .../{ => shared}/src/main/scala/ox/race.scala | 0 .../scala/ox/resilience/AdaptiveRetry.scala | 0 .../scala/ox/resilience/CircuitBreaker.scala | 0 .../ox/resilience/CircuitBreakerConfig.scala | 0 .../CircuitBreakerStateMachine.scala | 0 .../DurationRateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/RateLimiter.scala | 0 .../ox/resilience/RateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/ResultPolicy.scala | 0 .../scala/ox/resilience/RetryConfig.scala | 0 .../StartTimeRateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/TokenBucket.scala | 0 .../src/main/scala/ox/resilience/retry.scala | 0 .../src/main/scala/ox/resource.scala | 0 .../src/main/scala/ox/scheduling/Jitter.scala | 0 .../scala/ox/scheduling/RepeatConfig.scala | 0 .../main/scala/ox/scheduling/Schedule.scala | 0 .../src/main/scala/ox/scheduling/repeat.scala | 0 .../main/scala/ox/scheduling/scheduled.scala | 0 .../src/main/scala/ox/supervised.scala | 0 .../src/main/scala/ox/unsupervised.scala | 0 .../{ => shared}/src/main/scala/ox/util.scala | 0 .../src/test/scala/ox/AppErrorTest.scala | 0 .../src/test/scala/ox/CancelTest.scala | 0 .../src/test/scala/ox/ChunkTest.scala | 0 .../src/test/scala/ox/CollectParTest.scala | 0 .../src/test/scala/ox/ControlTest.scala | 0 .../src/test/scala/ox/EitherTest.scala | 0 .../src/test/scala/ox/ExceptionTest.scala | 0 .../src/test/scala/ox/FilterParTest.scala | 0 .../src/test/scala/ox/ForeachParTest.scala | 0 .../src/test/scala/ox/ForkTest.scala | 0 .../src/test/scala/ox/LocalTest.scala | 0 .../src/test/scala/ox/MapParTest.scala | 0 .../src/test/scala/ox/OxAppTest.scala | 0 .../src/test/scala/ox/ParTest.scala | 0 .../src/test/scala/ox/RaceTest.scala | 0 .../src/test/scala/ox/ResourceTest.scala | 0 .../src/test/scala/ox/SupervisedTest.scala | 0 .../src/test/scala/ox/UtilTest.scala | 0 .../test/scala/ox/channels/ActorTest.scala | 0 .../test/scala/ox/channels/ChannelTest.scala | 0 .../scala/ox/channels/ChannelTryTest.scala | 0 .../channels/SelectOrClosedWithinTest.scala | 0 .../scala/ox/channels/SelectWithinTest.scala | 0 .../ox/channels/SourceOpsEmptyTest.scala | 0 .../SourceOpsFactoryMethodsTest.scala | 0 .../ox/channels/SourceOpsFailedTest.scala | 0 .../ox/channels/SourceOpsForeachTest.scala | 0 .../channels/SourceOpsFutureSourceTest.scala | 0 .../ox/channels/SourceOpsFutureTest.scala | 0 .../scala/ox/channels/SourceOpsTest.scala | 0 .../ox/channels/SourceOpsTransformTest.scala | 0 .../ox/channels/jox/ChannelBufferedTest.scala | 50 ++ .../channels/jox/ChannelRendezvousTest.scala | 57 ++ .../jox/ChannelTrySendReceiveTest.scala | 48 ++ .../channels/jox/ChannelUnlimitedTest.scala | 21 + .../scala/ox/channels/jox/SelectTest.scala | 42 ++ .../ox/flow/FlowCompanionIOOpsTest.scala | 0 .../scala/ox/flow/FlowCompanionOpsTest.scala | 0 .../test/scala/ox/flow/FlowIOOpsTest.scala | 0 .../scala/ox/flow/FlowOpsAlsoToTapTest.scala | 0 .../scala/ox/flow/FlowOpsAlsoToTest.scala | 0 .../test/scala/ox/flow/FlowOpsBatchTest.scala | 0 .../ox/flow/FlowOpsBatchWeightedTest.scala | 0 .../scala/ox/flow/FlowOpsBufferTest.scala | 0 .../scala/ox/flow/FlowOpsCollectTest.scala | 0 .../ox/flow/FlowOpsConcatPrependTest.scala | 0 .../scala/ox/flow/FlowOpsConcatTest.scala | 0 .../scala/ox/flow/FlowOpsConflateTest.scala | 0 .../scala/ox/flow/FlowOpsDebounceByTest.scala | 0 .../scala/ox/flow/FlowOpsDebounceTest.scala | 0 .../test/scala/ox/flow/FlowOpsDrainTest.scala | 0 .../test/scala/ox/flow/FlowOpsDropTest.scala | 0 .../test/scala/ox/flow/FlowOpsEmptyTest.scala | 0 .../scala/ox/flow/FlowOpsExpandTest.scala | 0 .../ox/flow/FlowOpsExtrapolateTest.scala | 0 .../ox/flow/FlowOpsFactoryMethodsTest.scala | 0 .../scala/ox/flow/FlowOpsFailedTest.scala | 0 .../scala/ox/flow/FlowOpsFilterTest.scala | 0 .../scala/ox/flow/FlowOpsFlatMapTest.scala | 0 .../scala/ox/flow/FlowOpsFlattenParTest.scala | 0 .../scala/ox/flow/FlowOpsFlattenTest.scala | 0 .../test/scala/ox/flow/FlowOpsFoldTest.scala | 0 .../scala/ox/flow/FlowOpsForeachTest.scala | 0 .../ox/flow/FlowOpsFutureSourceTest.scala | 0 .../scala/ox/flow/FlowOpsFutureTest.scala | 0 .../scala/ox/flow/FlowOpsGroupByTest.scala | 0 .../scala/ox/flow/FlowOpsGroupedTest.scala | 0 .../ox/flow/FlowOpsInterleaveAllTest.scala | 0 .../scala/ox/flow/FlowOpsInterleaveTest.scala | 0 .../ox/flow/FlowOpsIntersperseTest.scala | 0 .../scala/ox/flow/FlowOpsLastOptionTest.scala | 0 .../test/scala/ox/flow/FlowOpsLastTest.scala | 0 .../scala/ox/flow/FlowOpsMapConcatTest.scala | 0 .../scala/ox/flow/FlowOpsMapParTest.scala | 0 .../ox/flow/FlowOpsMapParUnorderedTest.scala | 0 .../flow/FlowOpsMapStatefulConcatTest.scala | 0 .../ox/flow/FlowOpsMapStatefulTest.scala | 0 .../test/scala/ox/flow/FlowOpsMapTest.scala | 0 .../ox/flow/FlowOpsMapUsingSinkTest.scala | 0 .../ox/flow/FlowOpsMapWithResourceTest.scala | 0 .../test/scala/ox/flow/FlowOpsMergeTest.scala | 0 .../scala/ox/flow/FlowOpsOnCompleteTest.scala | 0 .../ox/flow/FlowOpsOnErrorCompleteTest.scala | 0 .../ox/flow/FlowOpsOnErrorRecoverTest.scala | 0 .../scala/ox/flow/FlowOpsOrElseTest.scala | 0 .../scala/ox/flow/FlowOpsPipeToTest.scala | 0 .../scala/ox/flow/FlowOpsRecoverTest.scala | 0 .../ox/flow/FlowOpsRecoverWithRetryTest.scala | 0 .../ox/flow/FlowOpsRecoverWithTest.scala | 0 .../scala/ox/flow/FlowOpsReduceTest.scala | 0 .../scala/ox/flow/FlowOpsRepeatEvalTest.scala | 0 .../test/scala/ox/flow/FlowOpsRetryTest.scala | 0 .../ox/flow/FlowOpsRunToChannelTest.scala | 0 .../scala/ox/flow/FlowOpsRunToMapTest.scala | 0 .../scala/ox/flow/FlowOpsRunToSetTest.scala | 0 .../scala/ox/flow/FlowOpsSampleTest.scala | 0 .../test/scala/ox/flow/FlowOpsScanTest.scala | 0 .../scala/ox/flow/FlowOpsSlidingTest.scala | 0 .../scala/ox/flow/FlowOpsSplitOnTest.scala | 0 .../test/scala/ox/flow/FlowOpsSplitTest.scala | 0 .../scala/ox/flow/FlowOpsTakeLastTest.scala | 0 .../test/scala/ox/flow/FlowOpsTakeTest.scala | 0 .../scala/ox/flow/FlowOpsTakeWhileTest.scala | 0 .../test/scala/ox/flow/FlowOpsTapTest.scala | 0 .../scala/ox/flow/FlowOpsThrottleTest.scala | 0 .../test/scala/ox/flow/FlowOpsTickTest.scala | 0 .../scala/ox/flow/FlowOpsTimeoutTest.scala | 0 .../ox/flow/FlowOpsUsingChannelTest.scala | 0 .../test/scala/ox/flow/FlowOpsUsingSink.scala | 0 .../scala/ox/flow/FlowOpsZipAllTest.scala | 0 .../test/scala/ox/flow/FlowOpsZipTest.scala | 0 .../ox/flow/FlowOpsZipWithIndexTest.scala | 0 .../test/scala/ox/flow/FlowTextOpsTest.scala | 0 .../ox/flow/internal/WeightedHeapTest.scala | 0 .../ox/resilience/AfterAttemptTest.scala | 0 .../ox/resilience/BackoffRetryTest.scala | 0 .../CircuitBreakerStateMachineTest.scala | 0 .../ox/resilience/CircuitBreakerTest.scala | 0 .../resilience/FixedIntervalRetryTest.scala | 0 .../ox/resilience/ImmediateRetryTest.scala | 0 .../resilience/RateLimiterInterfaceTest.scala | 0 .../scala/ox/resilience/RateLimiterTest.scala | 0 .../ScheduleFallingBackRetryTest.scala | 0 .../ox/scheduling/FixedRateRepeatTest.scala | 0 .../ox/scheduling/ImmediateRepeatTest.scala | 0 .../test/scala/ox/scheduling/JitterTest.scala | 0 .../src/test/scala/ox/util/ElapsedTime.scala | 0 .../src/test/scala/ox/util/MaxCounter.scala | 0 .../src/test/scala/ox/util/Trail.scala | 0 project/plugins.sbt | 2 + 204 files changed, 2076 insertions(+), 12 deletions(-) rename core/{ => jvm}/src/main/scala/ox/channels/Channel.scala (100%) rename core/{ => jvm}/src/main/scala/ox/channels/ChannelClosed.scala (100%) rename core/{ => jvm}/src/main/scala/ox/channels/select.scala (100%) rename core/{ => jvm}/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala (100%) rename core/{ => jvm}/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala (100%) create mode 100644 core/native/src/main/scala/ox/channels/Channel.scala create mode 100644 core/native/src/main/scala/ox/channels/ChannelClosed.scala create mode 100644 core/native/src/main/scala/ox/channels/select.scala rename core/{ => shared}/src/main/scala/ox/Chunk.scala (100%) rename core/{ => shared}/src/main/scala/ox/ErrorMode.scala (100%) rename core/{ => shared}/src/main/scala/ox/Ox.scala (100%) rename core/{ => shared}/src/main/scala/ox/OxApp.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/BufferCapacity.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/ChannelClosedUnion.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/SourceCompanionOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/SourceDrainOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/SourceOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/actor.scala (100%) rename core/{ => shared}/src/main/scala/ox/channels/forkPropagate.scala (100%) create mode 100644 core/shared/src/main/scala/ox/channels/jox/CellState.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Channel.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Continuation.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Segment.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Select.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/SelectClause.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Sink.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/Source.scala create mode 100644 core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala rename core/{ => shared}/src/main/scala/ox/collections.scala (100%) rename core/{ => shared}/src/main/scala/ox/control.scala (100%) rename core/{ => shared}/src/main/scala/ox/either.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/Flow.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowCompanionIOOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowCompanionOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowIOOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowReactiveOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowRunOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/FlowTextOps.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/internal/WeightedHeap.scala (100%) rename core/{ => shared}/src/main/scala/ox/flow/internal/groupByImpl.scala (100%) rename core/{ => shared}/src/main/scala/ox/fork.scala (100%) rename core/{ => shared}/src/main/scala/ox/inScopeRunner.scala (100%) rename core/{ => shared}/src/main/scala/ox/internal/ScopeContext.scala (100%) rename core/{ => shared}/src/main/scala/ox/internal/ThreadHerd.scala (100%) rename core/{ => shared}/src/main/scala/ox/local.scala (100%) rename core/{ => shared}/src/main/scala/ox/oxThreadFactory.scala (100%) rename core/{ => shared}/src/main/scala/ox/par.scala (100%) rename core/{ => shared}/src/main/scala/ox/race.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/AdaptiveRetry.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/CircuitBreaker.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/CircuitBreakerConfig.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/RateLimiter.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/ResultPolicy.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/RetryConfig.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/TokenBucket.scala (100%) rename core/{ => shared}/src/main/scala/ox/resilience/retry.scala (100%) rename core/{ => shared}/src/main/scala/ox/resource.scala (100%) rename core/{ => shared}/src/main/scala/ox/scheduling/Jitter.scala (100%) rename core/{ => shared}/src/main/scala/ox/scheduling/RepeatConfig.scala (100%) rename core/{ => shared}/src/main/scala/ox/scheduling/Schedule.scala (100%) rename core/{ => shared}/src/main/scala/ox/scheduling/repeat.scala (100%) rename core/{ => shared}/src/main/scala/ox/scheduling/scheduled.scala (100%) rename core/{ => shared}/src/main/scala/ox/supervised.scala (100%) rename core/{ => shared}/src/main/scala/ox/unsupervised.scala (100%) rename core/{ => shared}/src/main/scala/ox/util.scala (100%) rename core/{ => shared}/src/test/scala/ox/AppErrorTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/CancelTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ChunkTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/CollectParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ControlTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/EitherTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ExceptionTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/FilterParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ForeachParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ForkTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/LocalTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/MapParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/OxAppTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/RaceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/ResourceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/SupervisedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/UtilTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/ActorTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/ChannelTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/ChannelTryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SelectWithinTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsEmptyTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsFailedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsForeachTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsFutureTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/channels/SourceOpsTransformTest.scala (100%) create mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala create mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala create mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala create mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala create mode 100644 core/shared/src/test/scala/ox/channels/jox/SelectTest.scala rename core/{ => shared}/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowCompanionOpsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowIOOpsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsBatchTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsBufferTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsCollectTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsConcatTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsConflateTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsDebounceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsDrainTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsDropTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsEmptyTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsExpandTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFailedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFilterTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFlattenTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFoldTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsForeachTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsFutureTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsGroupByTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsGroupedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsLastTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapParTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsMergeTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsOrElseTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsPipeToTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRecoverTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsReduceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsSampleTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsScanTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsSlidingTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsSplitTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTakeTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsThrottleTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTickTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsUsingSink.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsZipAllTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsZipTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/FlowTextOpsTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/flow/internal/WeightedHeapTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/AfterAttemptTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/BackoffRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/CircuitBreakerTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/ImmediateRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/RateLimiterTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/scheduling/JitterTest.scala (100%) rename core/{ => shared}/src/test/scala/ox/util/ElapsedTime.scala (100%) rename core/{ => shared}/src/test/scala/ox/util/MaxCounter.scala (100%) rename core/{ => shared}/src/test/scala/ox/util/Trail.scala (100%) diff --git a/build.sbt b/build.sbt index 9008d764..fb8265b5 100644 --- a/build.sbt +++ b/build.sbt @@ -2,6 +2,8 @@ import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings import com.softwaremill.Publish.{ossPublishSettings, updateDocs} import com.softwaremill.UpdateVersionInDocs import com.typesafe.tools.mima.core.{MissingClassProblem, ProblemFilters} +import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} +import scalanative.build._ lazy val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( organization := "com.softwaremill.ox", @@ -50,21 +52,32 @@ compileDocumentation := { lazy val rootProject = (project in file(".")) .settings(commonSettings) .settings(publishArtifact := false, name := "ox") - .aggregate(core, kafka, mdcLogback, flowReactiveStreams, cron, otelContext) + .aggregate(core.jvm, core.native, kafka, mdcLogback, flowReactiveStreams, cron, otelContext) -lazy val core: Project = (project in file("core")) +lazy val core = crossProject(JVMPlatform, NativePlatform) + .crossType(CrossType.Full) + .in(file("core")) .settings(commonSettings) .settings( name := "core", + libraryDependencies ++= Seq( + scalaTest + ), + Test / fork := true + ) + .jvmSettings( libraryDependencies ++= Seq( "com.softwaremill.jox" % "channels" % "1.1.2", - scalaTest, "org.apache.pekko" %% "pekko-stream" % "1.6.0" % Test, "org.reactivestreams" % "reactive-streams-tck-flow" % "1.0.4" % Test - ), - Test / fork := true + ) + ) + .jvmSettings(enableMimaSettings) + .nativeSettings( + nativeConfig ~= { + _.withMultithreading(true) + } ) - .settings(enableMimaSettings) lazy val kafka: Project = (project in file("kafka")) .settings(commonSettings) @@ -80,7 +93,7 @@ lazy val kafka: Project = (project in file("kafka")) scalaTest ) ) - .dependsOn(core) + .dependsOn(core.jvm) lazy val mdcLogback: Project = (project in file("mdc-logback")) .settings(commonSettings) @@ -91,7 +104,7 @@ lazy val mdcLogback: Project = (project in file("mdc-logback")) scalaTest ) ) - .dependsOn(core) + .dependsOn(core.jvm) lazy val flowReactiveStreams: Project = (project in file("flow-reactive-streams")) .settings(commonSettings) @@ -102,7 +115,7 @@ lazy val flowReactiveStreams: Project = (project in file("flow-reactive-streams" scalaTest ) ) - .dependsOn(core) + .dependsOn(core.jvm) lazy val cron: Project = (project in file("cron")) .settings(commonSettings) @@ -113,7 +126,7 @@ lazy val cron: Project = (project in file("cron")) scalaTest ) ) - .dependsOn(core % "test->test;compile->compile") + .dependsOn(core.jvm % "test->test;compile->compile") lazy val otelContext: Project = (project in file("otel-context")) .settings(commonSettings) @@ -124,7 +137,7 @@ lazy val otelContext: Project = (project in file("otel-context")) scalaTest ) ) - .dependsOn(core % "test->test;compile->compile") + .dependsOn(core.jvm % "test->test;compile->compile") lazy val documentation: Project = (project in file("generated-doc")) // important: it must not be doc/ .enablePlugins(MdocPlugin) @@ -142,7 +155,7 @@ lazy val documentation: Project = (project in file("generated-doc")) // importan libraryDependencies ++= Seq(logback % Test) ) .dependsOn( - core, + core.jvm, kafka, mdcLogback, flowReactiveStreams, diff --git a/core/src/main/scala/ox/channels/Channel.scala b/core/jvm/src/main/scala/ox/channels/Channel.scala similarity index 100% rename from core/src/main/scala/ox/channels/Channel.scala rename to core/jvm/src/main/scala/ox/channels/Channel.scala diff --git a/core/src/main/scala/ox/channels/ChannelClosed.scala b/core/jvm/src/main/scala/ox/channels/ChannelClosed.scala similarity index 100% rename from core/src/main/scala/ox/channels/ChannelClosed.scala rename to core/jvm/src/main/scala/ox/channels/ChannelClosed.scala diff --git a/core/src/main/scala/ox/channels/select.scala b/core/jvm/src/main/scala/ox/channels/select.scala similarity index 100% rename from core/src/main/scala/ox/channels/select.scala rename to core/jvm/src/main/scala/ox/channels/select.scala diff --git a/core/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala b/core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala rename to core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala diff --git a/core/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala b/core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala rename to core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala diff --git a/core/native/src/main/scala/ox/channels/Channel.scala b/core/native/src/main/scala/ox/channels/Channel.scala new file mode 100644 index 00000000..1d4609a2 --- /dev/null +++ b/core/native/src/main/scala/ox/channels/Channel.scala @@ -0,0 +1,138 @@ +package ox.channels + +import ox.channels.{jox => j} +import ox.channels.jox.{Channel as JChannel, Select as JSelect, SelectClause as JSelectClause, Sink as JSink, Source as JSource} + +import ChannelClosedUnion.orThrow + +import scala.annotation.unchecked.uncheckedVariance + +// select result: needs to be defined here, as implementations are defined here as well + +/** Results of a [[select]] call, when clauses are passed (instead of a number of [[Source]]s). Each result corresponds to a clause, and can + * be pattern-matched (using a path-dependent type) to inspect which clause was selected. + */ +sealed trait SelectResult[+T]: + def value: T + +/** The result returned in case a [[Default]] clause was selected in [[select]]. */ +case class DefaultResult[T](value: T) extends SelectResult[T] + +// select clauses: needs to be defined here, as implementations are defined here as well + +/** A clause to use as part of [[select]]. Clauses can be created having a channel instance, using [[Source.receiveClause]] and + * [[Sink.sendClause]]. + * + * A clause instance is immutable and can be reused in multiple [[select]] calls. + */ +sealed trait SelectClause[+T]: + private[ox] def delegate: JSelectClause[Any] + type Result <: SelectResult[T] + +/** A default clause, which will be chosen if no other clause can be selected immediately, during a [[select]] call. + * + * There should be at most one default clause, and it should always come last in the list of clauses. + */ +case class Default[T](value: T) extends SelectClause[T]: + override private[ox] def delegate: JSelectClause[Any] = JSelect.defaultClause(() => DefaultResult(value)) + type Result = DefaultResult[T] + +// + +/** A channel source, which can be used to receive values from the channel. See [[Channel]] for more details. */ +trait Source[+T] extends SourceOps[T] with SourceDrainOps[T]: + protected def delegate: JSource[Any] + + case class Received private[channels] (value: T @uncheckedVariance) extends SelectResult[T] + + case class Receive private[channels] (delegate: JSelectClause[Any]) extends SelectClause[T]: + type Result = Received + + def receiveClause: Receive = Receive(delegate.receiveClause(t => Received(t.asInstanceOf[T]))) + + def tryReceive(): Option[T] = tryReceiveOrClosed().orThrow + + def tryReceiveOrClosed(): Option[T] | ChannelClosed = + val r = delegate.tryReceiveOrClosed() + if r == null then None + else + ChannelClosed.fromJox(r.asInstanceOf[AnyRef]) match + case c: ChannelClosed => c + case v: T @unchecked => Some(v) + + def receiveOrClosed(): T | ChannelClosed = ChannelClosed.fromJoxOrT(delegate.receiveOrClosed()) + + def receiveOrDone(): T | ChannelClosed.Done.type = receiveOrClosed() match + case e: ChannelClosed.Error => throw e.toThrowable + case ChannelClosed.Done => ChannelClosed.Done + case t: T @unchecked => t + + def receive(): T = receiveOrClosed().orThrow + + def isClosedForReceive: Boolean = delegate.isClosedForReceive + + def isClosedForReceiveDetail: Option[ChannelClosed] = Option(ChannelClosed.fromJoxOrT(delegate.closedForReceive())) +end Source + +object Source extends SourceCompanionOps + +// + +/** A channel sink, which can be used to send values to the channel. See [[Channel]] for more details. */ +trait Sink[-T]: + protected def delegate: JSink[Any] + + case class Sent private[channels] () extends SelectResult[Unit]: + override def value: Unit = () + + case class Send private[channels] (delegate: JSelectClause[Any]) extends SelectClause[Unit]: + type Result = Sent + + def sendClause(t: T): Send = Send(delegate.asInstanceOf[JSink[T]].sendClause(t, () => Sent())) + + def trySend(t: T): Boolean = trySendOrClosed(t).orThrow + + def trySendOrClosed(t: T): Boolean | ChannelClosed = + val r = delegate.asInstanceOf[JSink[T]].trySendOrClosed(t) + if r == null then true + else + ChannelClosed.fromJox(r.asInstanceOf[AnyRef]) match + case c: ChannelClosed => c + case _ => false + + def sendOrClosed(t: T): Unit | ChannelClosed = + val r = ChannelClosed.fromJoxOrUnit(delegate.asInstanceOf[JSink[T]].sendOrClosed(t)) + if r == null then () else r + + def send(t: T): Unit = sendOrClosed(t).orThrow + + def errorOrClosed(reason: Throwable): Unit | ChannelClosed = ChannelClosed.fromJoxOrUnit(delegate.errorOrClosed(reason)) + + def error(reason: Throwable): Unit = errorOrClosed(reason).orThrow + + def doneOrClosed(): Unit | ChannelClosed = ChannelClosed.fromJoxOrUnit(delegate.doneOrClosed()) + + def done(): Unit = doneOrClosed().orThrow + + def isClosedForSend: Boolean = delegate.isClosedForSend + + def isClosedForSendDetail: Option[ChannelClosed] = Option(ChannelClosed.fromJoxOrT(delegate.closedForSend())) +end Sink + +// + +class Channel[T] private (capacity: Int) extends Source[T] with Sink[T]: + protected override val delegate: JChannel[Any] = capacity match + case 0 => JChannel.newRendezvousChannel() + case -1 => JChannel.newUnlimitedChannel() + case _ => JChannel.newBufferedChannel(capacity) + + override def toString: String = delegate.toString + +object Channel: + def bufferedDefault[T]: Channel[T] = BufferCapacity.newChannel[T] + def buffered[T](capacity: Int): Channel[T] = new Channel(capacity) + def rendezvous[T]: Channel[T] = new Channel(0) + def unlimited[T]: Channel[T] = new Channel(-1) + def withCapacity[T](capacity: Int): Channel[T] = new Channel(capacity) +end Channel diff --git a/core/native/src/main/scala/ox/channels/ChannelClosed.scala b/core/native/src/main/scala/ox/channels/ChannelClosed.scala new file mode 100644 index 00000000..3090baf4 --- /dev/null +++ b/core/native/src/main/scala/ox/channels/ChannelClosed.scala @@ -0,0 +1,28 @@ +package ox.channels + +import ox.channels.{jox => j} + +/** Returned by channel methods (e.g. [[Source.receiveOrClosed]], [[Sink.sendOrClosed]], [[selectOrClosed]]) when the channel is closed. */ +sealed trait ChannelClosed: + def toThrowable: Throwable = this match + case ChannelClosed.Error(reason) => ChannelClosedException.Error(reason) + case ChannelClosed.Done => ChannelClosedException.Done() + +object ChannelClosed: + case class Error(reason: Throwable) extends ChannelClosed + case object Done extends ChannelClosed + + private[ox] def fromJoxOrT[T](joxResult: AnyRef): T | ChannelClosed = fromJox(joxResult).asInstanceOf[T | ChannelClosed] + private[ox] def fromJoxOrUnit(joxResult: AnyRef): Unit | ChannelClosed = + if joxResult == null then () else fromJox(joxResult).asInstanceOf[ChannelClosed] + + private[ox] def fromJox(joxResult: AnyRef): AnyRef | ChannelClosed = + joxResult match + case _: j.ChannelDone => Done + case e: j.ChannelError => Error(e.cause) + case _ => joxResult +end ChannelClosed + +enum ChannelClosedException(cause: Option[Throwable]) extends Exception(cause.orNull): + case Error(cause: Throwable) extends ChannelClosedException(Some(cause)) + case Done() extends ChannelClosedException(None) diff --git a/core/native/src/main/scala/ox/channels/select.scala b/core/native/src/main/scala/ox/channels/select.scala new file mode 100644 index 00000000..e38c0c6e --- /dev/null +++ b/core/native/src/main/scala/ox/channels/select.scala @@ -0,0 +1,444 @@ +package ox.channels + +import ox.channels.jox.{Select as JSelect} + +import ox.channels.ChannelClosedUnion.{map, orThrow} +import ox.{discard, forkUnsupervised, sleep, unsupervised} +import scala.concurrent.duration.FiniteDuration +import scala.concurrent.TimeoutException + +/** @see [[selectOrClosed(Seq[SelectClause])]]. */ +def selectOrClosed(clause1: SelectClause[?], clause2: SelectClause[?]): clause1.Result | clause2.Result | ChannelClosed = + selectOrClosed(List(clause1, clause2)).asInstanceOf[clause1.Result | clause2.Result | ChannelClosed] + +/** @see [[selectOrClosed(Seq[SelectClause])]]. */ +def selectOrClosed( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | ChannelClosed = + selectOrClosed(List(clause1, clause2, clause3)).asInstanceOf[clause1.Result | clause2.Result | clause3.Result | ChannelClosed] + +/** @see [[selectOrClosed(Seq[SelectClause])]]. */ +def selectOrClosed( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result | ChannelClosed = + selectOrClosed(List(clause1, clause2, clause3, clause4)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result | ChannelClosed] + +/** @see [[selectOrClosed(Seq[SelectClause])]]. */ +def selectOrClosed( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?], + clause5: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result | ChannelClosed = + selectOrClosed(List(clause1, clause2, clause3, clause4, clause5)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result | ChannelClosed] + +def selectOrClosed[T](clauses: Seq[SelectClause[T]]): SelectResult[T] | ChannelClosed = + ChannelClosed.fromJoxOrT(JSelect.selectOrClosed(clauses.map(_.delegate)*)) + +// + +/** @see [[select(Seq[SelectClause])]]. */ +def select(clause1: SelectClause[?], clause2: SelectClause[?]): clause1.Result | clause2.Result = + select(List(clause1, clause2)).asInstanceOf[clause1.Result | clause2.Result] + +/** @see [[select(Seq[SelectClause])]]. */ +def select( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result = + select(List(clause1, clause2, clause3)).asInstanceOf[clause1.Result | clause2.Result | clause3.Result] + +/** @see [[select(Seq[SelectClause])]]. */ +def select( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result = + select(List(clause1, clause2, clause3, clause4)).asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result] + +/** @see [[select(Seq[SelectClause])]]. */ +def select( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?], + clause5: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result = + select(List(clause1, clause2, clause3, clause4, clause5)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result] + +def select[T](clauses: Seq[SelectClause[T]]): SelectResult[T] = selectOrClosed(clauses).orThrow + +// + +def selectOrClosed[T1, T2](source1: Source[T1], source2: Source[T2]): T1 | T2 | ChannelClosed = + selectOrClosed(source1.receiveClause, source2.receiveClause).map { + case source1.Received(v) => v + case source2.Received(v) => v + } + +def selectOrClosed[T1, T2, T3](source1: Source[T1], source2: Source[T2], source3: Source[T3]): T1 | T2 | T3 | ChannelClosed = + selectOrClosed(source1.receiveClause, source2.receiveClause, source3.receiveClause).map { + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + } + +def selectOrClosed[T1, T2, T3, T4](source1: Source[T1], source2: Source[T2], source3: Source[T3], source4: Source[T4]): T1 | T2 | T3 | T4 | + ChannelClosed = + selectOrClosed(source1.receiveClause, source2.receiveClause, source3.receiveClause, source4.receiveClause).map { + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + } + +def selectOrClosed[T1, T2, T3, T4, T5]( + source1: Source[T1], + source2: Source[T2], + source3: Source[T3], + source4: Source[T4], + source5: Source[T5] +): T1 | T2 | T3 | T4 | T5 | ChannelClosed = + selectOrClosed(source1.receiveClause, source2.receiveClause, source3.receiveClause, source4.receiveClause, source5.receiveClause).map { + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + case source5.Received(v) => v + } + +def selectOrClosed[T](sources: Seq[Source[T]])(using DummyImplicit): T | ChannelClosed = + selectOrClosed(sources.map(_.receiveClause: SelectClause[T])) match + case r: Source[T]#Received => r.value + case c: ChannelClosed => c + case _: Sink[?]#Sent => throw new IllegalStateException() + case _: DefaultResult[?] => throw new IllegalStateException() + +// + +def select[T1, T2](source1: Source[T1], source2: Source[T2]): T1 | T2 = + select(source1.receiveClause, source2.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + +def select[T1, T2, T3](source1: Source[T1], source2: Source[T2], source3: Source[T3]): T1 | T2 | T3 = + select(source1.receiveClause, source2.receiveClause, source3.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + +def select[T1, T2, T3, T4](source1: Source[T1], source2: Source[T2], source3: Source[T3], source4: Source[T4]): T1 | T2 | T3 | T4 = + select(source1.receiveClause, source2.receiveClause, source3.receiveClause, source4.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + +def select[T1, T2, T3, T4, T5]( + source1: Source[T1], + source2: Source[T2], + source3: Source[T3], + source4: Source[T4], + source5: Source[T5] +): T1 | T2 | T3 | T4 | T5 = + select(source1.receiveClause, source2.receiveClause, source3.receiveClause, source4.receiveClause, source5.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + case source5.Received(v) => v + +def select[T](sources: Seq[Source[T]])(using DummyImplicit): T | ChannelClosed = + selectOrClosed(sources).orThrow + +// + +def selectOrClosedWithin[TV]( + timeout: FiniteDuration, + timeoutValue: TV +)(clause1: SelectClause[?]): TV | clause1.Result | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(List(clause1)) + .asInstanceOf[TV | clause1.Result | ChannelClosed] + +def selectOrClosedWithin[TV]( + timeout: FiniteDuration, + timeoutValue: TV +)(clause1: SelectClause[?], clause2: SelectClause[?]): TV | clause1.Result | clause2.Result | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(List(clause1, clause2)) + .asInstanceOf[TV | clause1.Result | clause2.Result | ChannelClosed] + +def selectOrClosedWithin[TV]( + timeout: FiniteDuration, + timeoutValue: TV +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?] +): TV | clause1.Result | clause2.Result | clause3.Result | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(List(clause1, clause2, clause3)) + .asInstanceOf[TV | clause1.Result | clause2.Result | clause3.Result | ChannelClosed] + +def selectOrClosedWithin[TV]( + timeout: FiniteDuration, + timeoutValue: TV +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?] +): TV | clause1.Result | clause2.Result | clause3.Result | clause4.Result | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(List(clause1, clause2, clause3, clause4)) + .asInstanceOf[TV | clause1.Result | clause2.Result | clause3.Result | clause4.Result | ChannelClosed] + +def selectOrClosedWithin[TV]( + timeout: FiniteDuration, + timeoutValue: TV +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?], + clause5: SelectClause[?] +): TV | clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(List(clause1, clause2, clause3, clause4, clause5)).asInstanceOf[ + TV | clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result | ChannelClosed + ] + +def selectOrClosedWithin[TV, T]( + timeout: FiniteDuration, + timeoutValue: TV +)(clauses: Seq[SelectClause[T]]): TV | SelectResult[T] | ChannelClosed = + if clauses.isEmpty then timeoutValue + else + unsupervised { + val timeoutChannel = Channel.withCapacity[Unit](1) + + forkUnsupervised { + sleep(timeout) + timeoutChannel.sendOrClosed(()).discard + }.discard + + val clausesWithTimeout = clauses :+ timeoutChannel.receiveClause + + selectOrClosed(clausesWithTimeout) match + case timeoutChannel.Received(_) => timeoutValue + case c: ChannelClosed => c + case r: SelectResult[?] @unchecked => r.asInstanceOf[SelectResult[T]] + end match + } + +// + +def selectOrClosedWithin[TV, T1]( + timeout: FiniteDuration, + timeoutValue: TV +)(source1: Source[T1]): TV | T1 | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(source1.receiveClause) match + case source1.Received(v) => v + case c: ChannelClosed => c + case tv => timeoutValue + +def selectOrClosedWithin[TV, T1, T2]( + timeout: FiniteDuration, + timeoutValue: TV +)(source1: Source[T1], source2: Source[T2]): TV | T1 | T2 | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(source1.receiveClause, source2.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case c: ChannelClosed => c + case tv => timeoutValue + +def selectOrClosedWithin[TimeoutValue, T1, T2, T3]( + timeout: FiniteDuration, + timeoutValue: TimeoutValue +)(source1: Source[T1], source2: Source[T2], source3: Source[T3]): TimeoutValue | T1 | T2 | T3 | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(source1.receiveClause, source2.receiveClause, source3.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case c: ChannelClosed => c + case tv => timeoutValue + +def selectOrClosedWithin[TV, T1, T2, T3, T4]( + timeout: FiniteDuration, + timeoutValue: TV +)(source1: Source[T1], source2: Source[T2], source3: Source[T3], source4: Source[T4]): TV | T1 | T2 | T3 | T4 | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)( + source1.receiveClause, + source2.receiveClause, + source3.receiveClause, + source4.receiveClause + ) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + case c: ChannelClosed => c + case tv => timeoutValue + +def selectOrClosedWithin[TV, T1, T2, T3, T4, T5]( + timeout: FiniteDuration, + timeoutValue: TV +)( + source1: Source[T1], + source2: Source[T2], + source3: Source[T3], + source4: Source[T4], + source5: Source[T5] +): TV | T1 | T2 | T3 | T4 | T5 | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)( + source1.receiveClause, + source2.receiveClause, + source3.receiveClause, + source4.receiveClause, + source5.receiveClause + ) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + case source5.Received(v) => v + case c: ChannelClosed => c + case tv => timeoutValue + +def selectOrClosedWithin[TV, T]( + timeout: FiniteDuration, + timeoutValue: TV +)(sources: Seq[Source[T]])(using DummyImplicit): TV | T | ChannelClosed = + selectOrClosedWithin(timeout, timeoutValue)(sources.map(_.receiveClause: SelectClause[T])) match + case r: Source[T]#Received => r.value + case c: ChannelClosed => c + case _: Sink[?]#Sent => throw new IllegalStateException() + case _: DefaultResult[?] => throw new IllegalStateException() + case _: TV @unchecked => timeoutValue + +// + +private object TimeoutMarker + +def selectWithin( + timeout: FiniteDuration +)(clause1: SelectClause[?]): clause1.Result = + selectWithin(timeout)(List(clause1)).asInstanceOf[clause1.Result] + +def selectWithin( + timeout: FiniteDuration +)(clause1: SelectClause[?], clause2: SelectClause[?]): clause1.Result | clause2.Result = + selectWithin(timeout)(List(clause1, clause2)).asInstanceOf[clause1.Result | clause2.Result] + +def selectWithin( + timeout: FiniteDuration +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result = + selectWithin(timeout)(List(clause1, clause2, clause3)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result] + +def selectWithin( + timeout: FiniteDuration +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result = + selectWithin(timeout)(List(clause1, clause2, clause3, clause4)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result] + +def selectWithin( + timeout: FiniteDuration +)( + clause1: SelectClause[?], + clause2: SelectClause[?], + clause3: SelectClause[?], + clause4: SelectClause[?], + clause5: SelectClause[?] +): clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result = + selectWithin(timeout)(List(clause1, clause2, clause3, clause4, clause5)) + .asInstanceOf[clause1.Result | clause2.Result | clause3.Result | clause4.Result | clause5.Result] + +def selectWithin[T]( + timeout: FiniteDuration +)(clauses: Seq[SelectClause[T]]): SelectResult[T] = + val result = selectOrClosedWithin(timeout, TimeoutMarker)(clauses) + if result == TimeoutMarker then throw new TimeoutException(s"select timed out after $timeout") + else result.asInstanceOf[SelectResult[T] | ChannelClosed].orThrow + +// + +def selectWithin[T1]( + timeout: FiniteDuration +)(source1: Source[T1]): T1 = + selectWithin(timeout)(source1.receiveClause) match + case source1.Received(v) => v + +def selectWithin[T1, T2]( + timeout: FiniteDuration +)(source1: Source[T1], source2: Source[T2]): T1 | T2 = + selectWithin(timeout)(source1.receiveClause, source2.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + +def selectWithin[T1, T2, T3]( + timeout: FiniteDuration +)(source1: Source[T1], source2: Source[T2], source3: Source[T3]): T1 | T2 | T3 = + selectWithin(timeout)(source1.receiveClause, source2.receiveClause, source3.receiveClause) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + +def selectWithin[T1, T2, T3, T4]( + timeout: FiniteDuration +)(source1: Source[T1], source2: Source[T2], source3: Source[T3], source4: Source[T4]): T1 | T2 | T3 | T4 = + selectWithin(timeout)( + source1.receiveClause, + source2.receiveClause, + source3.receiveClause, + source4.receiveClause + ) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + +def selectWithin[T1, T2, T3, T4, T5]( + timeout: FiniteDuration +)( + source1: Source[T1], + source2: Source[T2], + source3: Source[T3], + source4: Source[T4], + source5: Source[T5] +): T1 | T2 | T3 | T4 | T5 = + selectWithin(timeout)( + source1.receiveClause, + source2.receiveClause, + source3.receiveClause, + source4.receiveClause, + source5.receiveClause + ) match + case source1.Received(v) => v + case source2.Received(v) => v + case source3.Received(v) => v + case source4.Received(v) => v + case source5.Received(v) => v + +def selectWithin[T]( + timeout: FiniteDuration +)(sources: Seq[Source[T]])(using DummyImplicit): T = + val result = selectOrClosedWithin(timeout, TimeoutMarker)(sources) + if result == TimeoutMarker then throw new TimeoutException(s"select timed out after $timeout") + else result.asInstanceOf[T | ChannelClosed].orThrow diff --git a/core/src/main/scala/ox/Chunk.scala b/core/shared/src/main/scala/ox/Chunk.scala similarity index 100% rename from core/src/main/scala/ox/Chunk.scala rename to core/shared/src/main/scala/ox/Chunk.scala diff --git a/core/src/main/scala/ox/ErrorMode.scala b/core/shared/src/main/scala/ox/ErrorMode.scala similarity index 100% rename from core/src/main/scala/ox/ErrorMode.scala rename to core/shared/src/main/scala/ox/ErrorMode.scala diff --git a/core/src/main/scala/ox/Ox.scala b/core/shared/src/main/scala/ox/Ox.scala similarity index 100% rename from core/src/main/scala/ox/Ox.scala rename to core/shared/src/main/scala/ox/Ox.scala diff --git a/core/src/main/scala/ox/OxApp.scala b/core/shared/src/main/scala/ox/OxApp.scala similarity index 100% rename from core/src/main/scala/ox/OxApp.scala rename to core/shared/src/main/scala/ox/OxApp.scala diff --git a/core/src/main/scala/ox/channels/BufferCapacity.scala b/core/shared/src/main/scala/ox/channels/BufferCapacity.scala similarity index 100% rename from core/src/main/scala/ox/channels/BufferCapacity.scala rename to core/shared/src/main/scala/ox/channels/BufferCapacity.scala diff --git a/core/src/main/scala/ox/channels/ChannelClosedUnion.scala b/core/shared/src/main/scala/ox/channels/ChannelClosedUnion.scala similarity index 100% rename from core/src/main/scala/ox/channels/ChannelClosedUnion.scala rename to core/shared/src/main/scala/ox/channels/ChannelClosedUnion.scala diff --git a/core/src/main/scala/ox/channels/SourceCompanionOps.scala b/core/shared/src/main/scala/ox/channels/SourceCompanionOps.scala similarity index 100% rename from core/src/main/scala/ox/channels/SourceCompanionOps.scala rename to core/shared/src/main/scala/ox/channels/SourceCompanionOps.scala diff --git a/core/src/main/scala/ox/channels/SourceDrainOps.scala b/core/shared/src/main/scala/ox/channels/SourceDrainOps.scala similarity index 100% rename from core/src/main/scala/ox/channels/SourceDrainOps.scala rename to core/shared/src/main/scala/ox/channels/SourceDrainOps.scala diff --git a/core/src/main/scala/ox/channels/SourceOps.scala b/core/shared/src/main/scala/ox/channels/SourceOps.scala similarity index 100% rename from core/src/main/scala/ox/channels/SourceOps.scala rename to core/shared/src/main/scala/ox/channels/SourceOps.scala diff --git a/core/src/main/scala/ox/channels/actor.scala b/core/shared/src/main/scala/ox/channels/actor.scala similarity index 100% rename from core/src/main/scala/ox/channels/actor.scala rename to core/shared/src/main/scala/ox/channels/actor.scala diff --git a/core/src/main/scala/ox/channels/forkPropagate.scala b/core/shared/src/main/scala/ox/channels/forkPropagate.scala similarity index 100% rename from core/src/main/scala/ox/channels/forkPropagate.scala rename to core/shared/src/main/scala/ox/channels/forkPropagate.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/CellState.scala b/core/shared/src/main/scala/ox/channels/jox/CellState.scala new file mode 100644 index 00000000..578c9ae4 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/CellState.scala @@ -0,0 +1,38 @@ +package ox.channels.jox + +// Possible states of a cell: one of these enum constants, Continuation, StoredSelectClause, or a buffered value +enum CellState: + case DONE + case INTERRUPTED_SEND // the send/receive differentiation is important for expandBuffer + case INTERRUPTED_RECEIVE + case BROKEN + case IN_BUFFER // used to inform a potentially concurrent sender that the cell is now in the buffer + case RESUMING // expandBuffer is resuming a sender + case CLOSED + +enum SendResult: + case AWAITED, BUFFERED, RESUMED, FAILED, CLOSED + +enum ReceiveResult: + case FAILED, CLOSED + +enum ExpandBufferResult: + case DONE, FAILED, CLOSED + +enum ContinuationMarker: + case INTERRUPTED + +enum ChannelClosedMarker: + case CLOSED + +enum SentClauseMarker: + case SENT + +enum RestartSelectMarker: + case RESTART + +enum SelectState: + case REGISTERING, INTERRUPTED + +enum TimeoutMarker: + case INSTANCE diff --git a/core/shared/src/main/scala/ox/channels/jox/Channel.scala b/core/shared/src/main/scala/ox/channels/jox/Channel.scala new file mode 100644 index 00000000..063b5bd6 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Channel.scala @@ -0,0 +1,661 @@ +package ox.channels.jox + +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} +import java.util.concurrent.locks.LockSupport + +final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T]: + import CellState.* + import Channel.{getSendersCounter, isClosed, setClosedFlag, TRY_SEND_NOT_SENT} + import Segment.{SEGMENT_SIZE, NULL_SEGMENT, findAndMoveForward} + + val isRendezvous: Boolean = capacity == 0 + private inline def isUnlimited: Boolean = capacity < 0 + + private val sendersAndClosedFlag = new AtomicLong(0L) + private val receivers = new AtomicLong(0L) + private val bufferEnd = new AtomicLong(capacity.toLong) + + private val sendSegment: AtomicReference[Segment] = new AtomicReference(null) + private val receiveSegment: AtomicReference[Segment] = new AtomicReference(null) + private val bufferEndSegment: AtomicReference[Segment] = new AtomicReference(null) + private val closedReason: AtomicReference[ChannelClosed | Null] = new AtomicReference(null) + + locally: + val isRendezvousOrUnlimited = isRendezvous || isUnlimited + val firstSegment = new Segment(0, null, if isRendezvousOrUnlimited then 2 else 3, isRendezvousOrUnlimited) + sendSegment.set(firstSegment) + receiveSegment.set(firstSegment) + bufferEndSegment.set(if isRendezvousOrUnlimited then NULL_SEGMENT else firstSegment) + processInitialBuffer() + + private def processInitialBuffer(): Unit = + var currentSegment = bufferEndSegment.get() + val segmentsToProcess = + if capacity <= 0 then 0 + else ((capacity + SEGMENT_SIZE - 1L) / SEGMENT_SIZE).toInt + + var segmentId = 0 + while segmentId < segmentsToProcess do + currentSegment = findAndMoveForward(bufferEndSegment, currentSegment, segmentId.toLong).nn + val cellsToProcess = + val rem = if segmentId == segmentsToProcess - 1 then (capacity % SEGMENT_SIZE) else SEGMENT_SIZE + if rem == 0 then SEGMENT_SIZE else rem + currentSegment.setup_markCellsProcessed(cellsToProcess) + segmentId += 1 + + // ******* + // Sending + // ******* + + @throws[InterruptedException] + override def send(value: T): Unit = + val r = sendOrClosed(value) + r match + case c: ChannelClosed => throw c.toException() + case _ => + + @throws[InterruptedException] + override def sendOrClosed(value: T): AnyRef = + doSend(value, null, null) + + /** Returns null when sent, ChannelClosed when closed, or StoredSelectClause if select is provided. */ + private def doSend(value: T, select: SelectInstance | Null, selectClause: SelectClause[?] | Null): AnyRef = + if value == null then throw new NullPointerException() + while true do + val segment = sendSegment.get() + val scf = sendersAndClosedFlag.getAndAdd(1L) + val s = getSendersCounter(scf) + + val id = s / SEGMENT_SIZE + val i = (s % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(sendSegment, segment, id) + if seg == null then return closedReason.get().nn + + if seg.getId != id then + sendersAndClosedFlag.compareAndSet(s, seg.getId * SEGMENT_SIZE) + // continue + else + if isClosed(scf) then return closedReason.get().nn + else + val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) + return handleSendResult(sendResult, seg) match + case null => null // sent + case r: AnyRef if r eq CONTINUE_MARKER => null // will continue in the outer while + case r => r + else + if isClosed(scf) then return closedReason.get().nn + else + val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) + handleSendResult(sendResult, seg) match + case null => return null + case r: AnyRef if r eq CONTINUE_MARKER => () // continue loop + case r => return r + end while + throw new AssertionError("unreachable") + end doSend + + private val CONTINUE_MARKER = new AnyRef + + private def handleSendResult(sendResult: AnyRef, segment: Segment): AnyRef | Null = + sendResult match + case SendResult.BUFFERED => + null + case SendResult.AWAITED => + null + case SendResult.RESUMED => + segment.cleanPrev() + null + case ss: StoredSelectClause => + ss + case SendResult.FAILED => + segment.cleanPrev() + CONTINUE_MARKER + case SendResult.CLOSED => + closedReason.get() + case _ => + throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") + + // Non-blocking send + override def trySendOrClosed(value: T): AnyRef = + if value == null then throw new NullPointerException() + while true do + val segment = sendSegment.get() + val scf = sendersAndClosedFlag.get() + val s = getSendersCounter(scf) + + if isClosed(scf) then return closedReason.get().nn + + // capacity pre-check + if capacity >= 0 then + val bufEnd = bufferEnd.get() + val r = receivers.get() + if capacity == 0 then + if s >= r then return Channel.TRY_SEND_NOT_SENT + else if s >= bufEnd && s >= r then return Channel.TRY_SEND_NOT_SENT + + if !sendersAndClosedFlag.compareAndSet(scf, scf + 1) then + () // continue + else + val id = s / SEGMENT_SIZE + val i = (s % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(sendSegment, segment, id) + if seg == null then return closedReason.get().nn + if seg.getId != id then + sendersAndClosedFlag.compareAndSet(s + 1, seg.getId * SEGMENT_SIZE) + () // continue + else return trySendCell(seg, i, s, value) + else return trySendCell(seg, i, s, value) + end while + throw new AssertionError("unreachable") + + private def trySendCell(segment: Segment, i: Int, s: Long, value: T): AnyRef = + val sendResult = + try updateCellSend(segment, i, s, value, null, null, false) + catch case e: InterruptedException => throw new AssertionError("unreachable: non-blocking send", e) + sendResult match + case SendResult.BUFFERED => + null + case SendResult.RESUMED => + segment.cleanPrev() + null + case SendResult.FAILED => + segment.cleanPrev() + trySendOrClosed(value) // retry from top + case SendResult.CLOSED => + closedReason.get() + case r if r eq Channel.TRY_SEND_NOT_SENT => + Channel.TRY_SEND_NOT_SENT + case _ => + throw new IllegalStateException(s"Unexpected result: $sendResult") + + // Non-blocking receive + override def tryReceiveOrClosed(): AnyRef = + while true do + val scf = sendersAndClosedFlag.get() + val s = getSendersCounter(scf) + val r = receivers.get() + + if s <= r then + if isClosed(scf) then return closedForReceive() + else return null + + val segment = receiveSegment.get() + if !receivers.compareAndSet(r, r + 1) then () // continue + else + val id = r / SEGMENT_SIZE + val i = (r % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(receiveSegment, segment, id) + if seg == null then return closedReason.get().nn + if seg.getId != id then + receivers.compareAndSet(r + 1, seg.getId * SEGMENT_SIZE) + () // continue + else return tryReceiveCell(seg, i, r) + else return tryReceiveCell(seg, i, r) + end while + throw new AssertionError("unreachable") + + private def tryReceiveCell(segment: Segment, i: Int, r: Long): AnyRef | Null = + val result = + try updateCellReceive(segment, i, r, null, null, false) + catch case e: InterruptedException => throw new AssertionError("unreachable: non-blocking receive", e) + if result eq ReceiveResult.CLOSED then closedReason.get() + else if result eq ReceiveResult.FAILED then + segment.cleanPrev() + tryReceiveOrClosed() // retry + else if result == null then null + else + segment.cleanPrev() + result + + /** Core send logic. Returns SendResult, TRY_SEND_NOT_SENT, or StoredSelectClause. */ + @throws[InterruptedException] + private def updateCellSend( + segment: Segment, + i: Int, + s: Long, + value: T, + select: SelectInstance | Null, + selectClause: SelectClause[?] | Null, + suspend: Boolean + ): AnyRef = + while true do + val state = segment.getCell(i) + if state == null then + if capacity >= 0 && s >= (if isRendezvous then 0 else bufferEnd.get()) && s >= receivers.get() then + // no receiver, not in buffer + if !suspend then + if segment.casCell(i, null, INTERRUPTED_SEND) then + segment.cellInterruptedSender() + return Channel.TRY_SEND_NOT_SENT + else if select != null then + val storedSelect = new StoredSelectClause(select, segment, i, true, selectClause.nn, value.asInstanceOf[AnyRef]) + if segment.casCell(i, null, storedSelect) then return storedSelect + else + val c = new Continuation(value.asInstanceOf[AnyRef]) + if segment.casCell(i, null, c) then + if c.await(segment, i, isRendezvous) eq ChannelClosedMarker.CLOSED then return SendResult.CLOSED + else return SendResult.AWAITED + else + // receiver in progress or in buffer -> elimination + if segment.casCell(i, null, value.asInstanceOf[AnyRef]) then return SendResult.BUFFERED + else if state eq IN_BUFFER then + if segment.casCell(i, IN_BUFFER, value.asInstanceOf[AnyRef]) then return SendResult.BUFFERED + else + state match + case c: Continuation => + if c.tryResume(value.asInstanceOf[AnyRef]) then + segment.setCell(i, DONE) + return SendResult.RESUMED + else return SendResult.FAILED + case ss: StoredSelectClause => + ss.payload = value.asInstanceOf[AnyRef] + if ss.select.trySelect(ss) then + segment.setCell(i, DONE) + return SendResult.RESUMED + else return SendResult.FAILED + case INTERRUPTED_RECEIVE | BROKEN => + return SendResult.FAILED + case CLOSED => + return SendResult.CLOSED + case _ => + throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + end while + throw new AssertionError("unreachable") + + // ********* + // Receiving + // ********* + + @throws[InterruptedException] + override def receive(): T = + val r = receiveOrClosed() + r match + case c: ChannelClosed => throw c.toException() + case _ => r.asInstanceOf[T] + + @throws[InterruptedException] + override def receiveOrClosed(): AnyRef = + doReceive(null, null) + + private def doReceive(select: SelectInstance | Null, selectClause: SelectClause[?] | Null): AnyRef = + while true do + val segment = receiveSegment.get() + val r = receivers.getAndAdd(1L) + + val id = r / SEGMENT_SIZE + val i = (r % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(receiveSegment, segment, id) + if seg == null then return closedReason.get().nn + if seg.getId != id then + receivers.compareAndSet(r, seg.getId * SEGMENT_SIZE) + () // continue + else + val result = updateCellReceive(seg, i, r, select, selectClause, true) + if result eq ReceiveResult.CLOSED then return closedReason.get().nn + else + if !result.isInstanceOf[StoredSelectClause] then seg.cleanPrev() + if result ne ReceiveResult.FAILED then return result + else + val result = updateCellReceive(seg, i, r, select, selectClause, true) + if result eq ReceiveResult.CLOSED then return closedReason.get().nn + else + if !result.isInstanceOf[StoredSelectClause] then seg.cleanPrev() + if result ne ReceiveResult.FAILED then return result + end while + throw new AssertionError("unreachable") + + @throws[InterruptedException] + private def updateCellReceive( + segment: Segment, + i: Int, + r: Long, + select: SelectInstance | Null, + selectClause: SelectClause[?] | Null, + suspend: Boolean + ): AnyRef = + while true do + val state = segment.getCell(i) + if state == null || (state eq IN_BUFFER) then + if r >= getSendersCounter(sendersAndClosedFlag.get()) then + if !suspend then + if segment.casCell(i, state, INTERRUPTED_RECEIVE) then + segment.cellInterruptedReceiver() + expandBuffer() + return null + else if select != null then + val storedSelect = new StoredSelectClause(select, segment, i, false, selectClause.nn, null) + if segment.casCell(i, state, storedSelect) then + expandBuffer() + return storedSelect + else + val c = new Continuation(null) + if segment.casCell(i, state, c) then + expandBuffer() + val result = c.await(segment, i, isRendezvous) + if result eq ChannelClosedMarker.CLOSED then return ReceiveResult.CLOSED + else return result + else + if segment.casCell(i, state, BROKEN) then + expandBuffer() + return ReceiveResult.FAILED + else + state match + case c: Continuation => + if segment.casCell(i, state, RESUMING) then + if c.tryResume(0.asInstanceOf[AnyRef]) then + segment.setCell(i, DONE) + expandBuffer() + return c.payload + else return ReceiveResult.FAILED + case ss: StoredSelectClause => + if segment.casCell(i, state, RESUMING) then + if ss.select.trySelect(ss) then + segment.setCell(i, DONE) + expandBuffer() + return ss.payload.asInstanceOf[AnyRef] + else return ReceiveResult.FAILED + case cs: CellState => cs match + case CellState.INTERRUPTED_SEND => return ReceiveResult.FAILED + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return ReceiveResult.CLOSED + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case _ => + // buffered value + segment.setCell(i, DONE) + expandBuffer() + return state.asInstanceOf[AnyRef] + end while + throw new AssertionError("unreachable") + + // **************** + // Buffer expansion + // **************** + + private def expandBuffer(): Unit = + if capacity <= 0 then return + while true do + val segment = bufferEndSegment.get() + val b = bufferEnd.getAndAdd(1L) + + val id = b / SEGMENT_SIZE + val i = (b % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(bufferEndSegment, segment, id) + if seg == null then return + if seg.getId != id then + bufferEnd.compareAndSet(b, seg.getId * SEGMENT_SIZE) + () // continue - this cell was an interrupted sender + else + val result = updateCellExpandBuffer(seg, i) + if result == ExpandBufferResult.DONE then + seg.cellProcessed_notInterruptedSender() + return + else if result == ExpandBufferResult.CLOSED then + seg.cellProcessed_notInterruptedSender() + () // continue to mark other closed cells as processed + else + val result = updateCellExpandBuffer(seg, i) + if result == ExpandBufferResult.DONE then + seg.cellProcessed_notInterruptedSender() + return + else if result == ExpandBufferResult.CLOSED then + seg.cellProcessed_notInterruptedSender() + () // continue + + private def updateCellExpandBuffer(segment: Segment, i: Int): ExpandBufferResult = + while true do + val state = segment.getCell(i) + if state == null then + if segment.casCell(i, null, IN_BUFFER) then return ExpandBufferResult.DONE + else + state match + case DONE => return ExpandBufferResult.DONE + case c: Continuation if c.isSender => + if segment.casCell(i, state, RESUMING) then + if c.tryResume(0.asInstanceOf[AnyRef]) then + segment.setCell(i, c.payload) + return ExpandBufferResult.DONE + else return ExpandBufferResult.FAILED + case _: Continuation => return ExpandBufferResult.DONE + case ss: StoredSelectClause if ss.isSender => + if segment.casCell(i, state, RESUMING) then + if ss.select.trySelect(ss) then + segment.setCell(i, ss.payload) + return ExpandBufferResult.DONE + else return ExpandBufferResult.FAILED + case _: StoredSelectClause => return ExpandBufferResult.DONE + case cs: CellState => cs match + case CellState.INTERRUPTED_SEND => return ExpandBufferResult.FAILED + case CellState.INTERRUPTED_RECEIVE => return ExpandBufferResult.DONE + case CellState.BROKEN => return ExpandBufferResult.DONE + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return ExpandBufferResult.CLOSED + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case _ => + // buffered value + return ExpandBufferResult.DONE + end while + throw new AssertionError("unreachable") + + // ******* + // Closing + // ******* + + override def done(): Unit = + val r = doneOrClosed() + r match + case c: ChannelClosed => throw c.toException() + case _ => + + override def doneOrClosed(): AnyRef = + closeOrClosed(ChannelDone(this)) + + override def error(reason: Throwable): Unit = + if reason == null then throw new NullPointerException("Error reason cannot be null") + val r = errorOrClosed(reason) + r match + case c: ChannelClosed => throw c.toException() + case _ => + + override def errorOrClosed(reason: Throwable): AnyRef = + closeOrClosed(ChannelError(reason, this)) + + private def closeOrClosed(cc: ChannelClosed): AnyRef = + if !closedReason.compareAndSet(null, cc) then return closedReason.get().nn + + // set closed flag + var scfUpdated = false + var scf = 0L + while !scfUpdated do + val initialScf = sendersAndClosedFlag.get() + scf = setClosedFlag(initialScf) + scfUpdated = sendersAndClosedFlag.compareAndSet(initialScf, scf) + + val lastSender = getSendersCounter(scf) + val lastSegment = sendSegment.get().close() + + cc match + case _: ChannelError => closeCellsUntil(0, lastSegment) + case _ => closeCellsUntil(lastSender, lastSegment) + + if capacity > 0 then + val lastGlobalIndex = (lastSegment.getId + 1) * SEGMENT_SIZE - 1 + while bufferEnd.get() <= lastGlobalIndex do expandBuffer() + + null + end closeOrClosed + + private def closeCellsUntil(lastCellToClose: Long, segment: Segment | Null): Unit = + if segment == null then return + + val lastCellToCloseSegmentId = lastCellToClose / SEGMENT_SIZE + val lastIndexToCloseInSegment = + if lastCellToCloseSegmentId == segment.getId then (lastCellToClose % SEGMENT_SIZE).toInt + else if lastCellToCloseSegmentId < segment.getId then 0 + else return + + var i = SEGMENT_SIZE - 1 + while i >= lastIndexToCloseInSegment do + updateCellClose(segment, i) + i -= 1 + + closeCellsUntil(lastCellToClose, segment.getPrev) + + private def updateCellClose(segment: Segment, i: Int): Unit = + while true do + val state = segment.getCell(i) + if state == null || (state eq IN_BUFFER) then + if segment.casCell(i, state, CLOSED) then + segment.cellInterruptedReceiver() + return + else + state match + case c: Continuation => + if c.tryResume(ChannelClosedMarker.CLOSED) then + segment.setCell(i, CLOSED) + segment.cellInterruptedReceiver() + return + else Thread.onSpinWait() + case ss: StoredSelectClause => + if ss.select.channelClosed(closedReason.get().nn) then return + else Thread.onSpinWait() + case cs: CellState => cs match + case CellState.DONE | CellState.BROKEN => return + case CellState.INTERRUPTED_RECEIVE | CellState.INTERRUPTED_SEND => return + case CellState.RESUMING => Thread.onSpinWait() + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case _ => + // buffered value: discarding + if segment.casCell(i, state, CLOSED) then + segment.cellInterruptedReceiver() + return + + override def closedForSend(): ChannelClosed | Null = + if isClosed(sendersAndClosedFlag.get()) then closedReason.get() else null + + override def closedForReceive(): ChannelClosed | Null = + if isClosed(sendersAndClosedFlag.get()) then + val cr = closedReason.get().nn + cr match + case _: ChannelError => cr + case _ => if hasValuesToReceive() then null else cr + else null + + private def hasValuesToReceive(): Boolean = + while true do + val segment = receiveSegment.get() + val r = receivers.get() + val s = getSendersCounter(sendersAndClosedFlag.get()) + if s <= r then return false + + val id = r / SEGMENT_SIZE + val i = (r % SEGMENT_SIZE).toInt + + var seg = segment + if segment.getId != id then + seg = findAndMoveForward(receiveSegment, segment, id) + if seg == null then return false + if seg.getId != id then + receivers.compareAndSet(r, seg.getId * SEGMENT_SIZE) + () // continue + else + seg.cleanPrev() + if hasValueToReceive(seg, i) then return true + else receivers.compareAndSet(r, r + 1) + else + seg.cleanPrev() + if hasValueToReceive(seg, i) then return true + else receivers.compareAndSet(r, r + 1) + end while + false + + private def hasValueToReceive(segment: Segment, i: Int): Boolean = + while true do + val state = segment.getCell(i) + if state == null || (state eq IN_BUFFER) then Thread.onSpinWait() + else + state match + case c: Continuation => return c.isSender + case ss: StoredSelectClause => return ss.isSender + case cs: CellState => cs match + case CellState.INTERRUPTED_SEND | CellState.INTERRUPTED_RECEIVE => return false + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return false + case CellState.DONE | CellState.BROKEN => return false + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case _ => return true // buffered value + false + + // ************** + // Select clauses + // ************** + + override def receiveClause(): SelectClause[T] = receiveClause(identity) + + override def receiveClause[U](callback: T => U): SelectClause[U] = + val ch = this + new SelectClause[U]: + override private[jox] def getChannel: Channel[?] | Null = ch + override private[jox] def register(select: SelectInstance): AnyRef = + try ch.doReceive(select, this) + catch case e: InterruptedException => throw new IllegalStateException(e) + override private[jox] def transformedRawValue(rawValue: AnyRef): U = + callback(rawValue.asInstanceOf[T]) + + override def sendClause(value: T): SelectClause[Null] = sendClause(value, () => null) + + override def sendClause[U](value: T, callback: () => U): SelectClause[U] = + val ch = this + new SelectClause[U]: + override private[jox] def getChannel: Channel[?] | Null = ch + override private[jox] def register(select: SelectInstance): AnyRef = + try + val result = ch.doSend(value, select, this) + if result == null then SentClauseMarker.SENT else result + catch case e: InterruptedException => throw new IllegalStateException(e) + override private[jox] def transformedRawValue(rawValue: AnyRef): U = + callback() + + private[jox] def cleanupStoredSelectClause(segment: Segment, i: Int, isSender: Boolean): Unit = + segment.setCell(i, if isSender then INTERRUPTED_SEND else INTERRUPTED_RECEIVE) + if isSender then segment.cellInterruptedSender() + else segment.cellInterruptedReceiver() + + // **** + // Misc + // **** + + override def toString: String = s"Channel(capacity=$capacity)" + +end Channel + +object Channel: + val DEFAULT_BUFFER_SIZE: Int = 16 + val TRY_SEND_NOT_SENT: AnyRef = new AnyRef + + def newRendezvousChannel[T](): Channel[T] = new Channel(0) + def newBufferedChannel[T](capacity: Int): Channel[T] = new Channel(capacity) + def newBufferedDefaultChannel[T](): Channel[T] = new Channel(DEFAULT_BUFFER_SIZE) + def newUnlimitedChannel[T](): Channel[T] = new Channel(-1) + + private val SENDERS_AND_CLOSED_FLAG_SHIFT = 60 + private val SENDERS_COUNTER_MASK = (1L << SENDERS_AND_CLOSED_FLAG_SHIFT) - 1 + + private[jox] def getSendersCounter(scf: Long): Long = scf & SENDERS_COUNTER_MASK + private[jox] def isClosed(scf: Long): Boolean = (scf >> SENDERS_AND_CLOSED_FLAG_SHIFT) == 1 + private[jox] def setClosedFlag(scf: Long): Long = scf | (1L << SENDERS_AND_CLOSED_FLAG_SHIFT) +end Channel diff --git a/core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala b/core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala new file mode 100644 index 00000000..ddbabaf1 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala @@ -0,0 +1,17 @@ +package ox.channels.jox + +sealed trait ChannelClosed: + def toException(): ChannelClosedException + def channel: Channel[?] + +case class ChannelDone(override val channel: Channel[?]) extends ChannelClosed: + override def toException(): ChannelClosedException = new ChannelDoneException() + +case class ChannelError(cause: Throwable, override val channel: Channel[?]) extends ChannelClosed: + override def toException(): ChannelClosedException = new ChannelErrorException(cause) + +sealed class ChannelClosedException(cause: Throwable) extends RuntimeException(cause): + def this() = this(null) + +final class ChannelDoneException extends ChannelClosedException() +final class ChannelErrorException(cause: Throwable) extends ChannelClosedException(cause) diff --git a/core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala b/core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala new file mode 100644 index 00000000..ddaaee6c --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala @@ -0,0 +1,13 @@ +package ox.channels.jox + +trait CloseableChannel: + def done(): Unit + def doneOrClosed(): AnyRef + def error(reason: Throwable): Unit + def errorOrClosed(reason: Throwable): AnyRef + + def isClosedForSend: Boolean = closedForSend() != null + def isClosedForReceive: Boolean = closedForReceive() != null + + def closedForSend(): ChannelClosed | Null + def closedForReceive(): ChannelClosed | Null diff --git a/core/shared/src/main/scala/ox/channels/jox/Continuation.scala b/core/shared/src/main/scala/ox/channels/jox/Continuation.scala new file mode 100644 index 00000000..5377e9ca --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Continuation.scala @@ -0,0 +1,44 @@ +package ox.channels.jox + +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.LockSupport + +final class Continuation(val payload: AnyRef): + private val creatingThread: Thread = Thread.currentThread() + private val data: AtomicReference[AnyRef] = new AtomicReference(null) + + /** `true` if this continuation is for a sender; `false` for a receiver. */ + def isSender: Boolean = payload != null + + /** Resume the continuation with the given value. Returns `true` if successful. */ + def tryResume(value: AnyRef): Boolean = + val result = data.compareAndSet(null, value) + LockSupport.unpark(creatingThread) + result + + /** Await for the continuation to be resumed. May throw InterruptedException. */ + @throws[InterruptedException] + def await(segment: Segment, cellIndex: Int, isRendezvous: Boolean): AnyRef = + var spinIterations = if isRendezvous then Continuation.RENDEZVOUS_SPINS else 0 + while data.get() == null do + if spinIterations > 0 then + Thread.onSpinWait() + spinIterations -= 1 + else + LockSupport.park() + if Thread.interrupted() then + if data.compareAndSet(null, ContinuationMarker.INTERRUPTED) then + val _isSender = isSender + segment.setCell(cellIndex, if _isSender then CellState.INTERRUPTED_SEND else CellState.INTERRUPTED_RECEIVE) + if _isSender then segment.cellInterruptedSender() + else segment.cellInterruptedReceiver() + throw new InterruptedException() + else Thread.currentThread().interrupt() + end while + data.get() + end await + +object Continuation: + val RENDEZVOUS_SPINS: Int = + val nproc = Runtime.getRuntime.availableProcessors() + if nproc == 1 then 0 else if nproc <= 4 then 1 << 7 else 1 << 10 diff --git a/core/shared/src/main/scala/ox/channels/jox/Segment.scala b/core/shared/src/main/scala/ox/channels/jox/Segment.scala new file mode 100644 index 00000000..efb511cf --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Segment.scala @@ -0,0 +1,181 @@ +package ox.channels.jox + +import java.util.concurrent.atomic.{AtomicInteger, AtomicReference, AtomicReferenceArray} + +final class Segment( + private val id: Long, + initialPrev: Segment | Null, + pointers: Int, + val isRendezvousOrUnlimited: Boolean +): + import Segment.* + + private val data = new AtomicReferenceArray[AnyRef](SEGMENT_SIZE) + private val nextRef = new AtomicReference[Segment | Null](null) + private val prevRef = new AtomicReference[Segment | Null](initialPrev) + + // bits: [pointers(2)][notProcessed(6)][notInterrupted(6)] + private val pointersNotProcessedNotInterrupted = new AtomicInteger( + SEGMENT_SIZE + + (if isRendezvousOrUnlimited then 0 else SEGMENT_SIZE << PROCESSED_SHIFT) + + (pointers << POINTERS_SHIFT) + ) + + def getId: Long = id + + def cleanPrev(): Unit = prevRef.set(null) + + def getNext: Segment | Null = + val s = nextRef.get() + if s eq CLOSED_SENTINEL then null else s + + def getPrev: Segment | Null = prevRef.get() + + private def setNextIfNull(setTo: Segment): Boolean = + nextRef.compareAndSet(null, setTo) + + def getCell(index: Int): AnyRef | Null = data.get(index) + + def setCell(index: Int, value: AnyRef): Unit = data.set(index, value) + + def casCell(index: Int, expected: AnyRef | Null, newValue: AnyRef): Boolean = + data.compareAndSet(index, expected.asInstanceOf[AnyRef], newValue) + + private def isTail: Boolean = getNext == null + + def isRemoved: Boolean = pointersNotProcessedNotInterrupted.get() == 0 + + def tryIncPointers(): Boolean = + var p = pointersNotProcessedNotInterrupted.get() + while p != 0 do + if pointersNotProcessedNotInterrupted.compareAndSet(p, p + (1 << POINTERS_SHIFT)) then return true + p = pointersNotProcessedNotInterrupted.get() + false + + def decPointers(): Boolean = + val toAdd = -(1 << POINTERS_SHIFT) + var currentP = pointersNotProcessedNotInterrupted.get() + while true do + if pointersNotProcessedNotInterrupted.compareAndSet(currentP, currentP + toAdd) then + return (currentP + toAdd) == 0 + currentP = pointersNotProcessedNotInterrupted.get() + false // unreachable + + def cellInterruptedReceiver(): Unit = + if pointersNotProcessedNotInterrupted.getAndDecrement() == 1 then remove() + + def cellInterruptedSender(): Unit = + if isRendezvousOrUnlimited then + if pointersNotProcessedNotInterrupted.getAndDecrement() == 1 then remove() + else if pointersNotProcessedNotInterrupted.getAndAdd(-ONE_PROCESSED_AND_INTERRUPTED) == ONE_PROCESSED_AND_INTERRUPTED then remove() + + def cellProcessed_notInterruptedSender(): Unit = + if pointersNotProcessedNotInterrupted.getAndAdd(-ONE_PROCESSED) == ONE_PROCESSED then remove() + + /** Marks cells as processed during channel setup. Not thread-safe. */ + def setup_markCellsProcessed(numberOfCells: Int): Unit = + pointersNotProcessedNotInterrupted.addAndGet(-ONE_PROCESSED * numberOfCells) + () + + def remove(): Unit = + var continue = true + while continue do + if isTail then return + val _prev = aliveSegmentLeft() + val _next = aliveSegmentRight() + + // link next.prev to _prev + var prevOfNextUpdated = false + while !prevOfNextUpdated do + val currentPrevOfNext = _next.prevRef.get() + if currentPrevOfNext == null then prevOfNextUpdated = true + else prevOfNextUpdated = _next.prevRef.compareAndSet(currentPrevOfNext, _prev) + + if _prev != null then _prev.nextRef.set(_next) + + if _next.isRemoved && !_next.isTail then () // continue loop + else if _prev != null && _prev.isRemoved then () // continue loop + else continue = false + end while + + def close(): Segment = + var s: Segment = this + while true do + val n = s.nextRef.get() + if n == null then + if s.nextRef.compareAndSet(null, CLOSED_SENTINEL) then return s + else if n eq CLOSED_SENTINEL then return s + else s = n + s // unreachable + + private def aliveSegmentLeft(): Segment | Null = + var s = prevRef.get() + while s != null && s.isRemoved do s = s.prevRef.get() + s + + private def aliveSegmentRight(): Segment = + var n = nextRef.get() + while n.nn.isRemoved && !n.nn.isTail do n = n.nn.nextRef.get() + n.nn + + // for tests + def setNext(newNext: Segment | Null): Unit = nextRef.set(newNext) + + override def toString: String = + val n = nextRef.get() + val p = prevRef.get() + val c = pointersNotProcessedNotInterrupted.get() + val notInterrupted = c & ((1 << PROCESSED_SHIFT) - 1) + val notProcessed = (c & ((1 << POINTERS_SHIFT) - 1)) >> PROCESSED_SHIFT + val ptrs = c >> POINTERS_SHIFT + val nextStr = if n == null then "null" else if n eq CLOSED_SENTINEL then "closed" else n.id.toString + val prevStr = if p == null then "null" else p.id.toString + s"Segment{id=$id, next=$nextStr, prev=$prevStr, pointers=$ptrs, notProcessed=$notProcessed, notInterrupted=$notInterrupted}" + +end Segment + +object Segment: + val SEGMENT_SIZE: Int = + val env = System.getenv("JOX_SEGMENT_SIZE") + if env != null then Integer.parseInt(env) else 32 + + private val PROCESSED_SHIFT = 6 + private val POINTERS_SHIFT = 12 + private val ONE_PROCESSED = 1 << PROCESSED_SHIFT + private val ONE_PROCESSED_AND_INTERRUPTED = ONE_PROCESSED + 1 + + val NULL_SEGMENT: Segment = new Segment(-1, null, 0, false) + private val CLOSED_SENTINEL: Segment = new Segment(-1, null, 0, false) + + /** Finds or creates a non-removed segment with id >= `id`, and updates `ref` to it. */ + def findAndMoveForward(ref: AtomicReference[Segment], start: Segment, id: Long): Segment | Null = + var continue = true + while continue do + val segment = findSegment(start, id) + if segment == null then return null + if moveForward(ref, segment) then return segment + null // unreachable + + private def findSegment(start: Segment, id: Long): Segment | Null = + var current = start + while current.getId < id || current.isRemoved do + val n = current.nextRef.get() + if n eq CLOSED_SENTINEL then return null + else if n == null then + val newSegment = new Segment(current.getId + 1, current, 0, start.isRendezvousOrUnlimited) + if current.setNextIfNull(newSegment) then + if current.isRemoved then current.remove() + else current = n.nn + current + + private def moveForward(ref: AtomicReference[Segment], to: Segment): Boolean = + while true do + val current = ref.get() + if current.getId >= to.getId then return true + if !to.tryIncPointers() then return false + if ref.compareAndSet(current, to) then + if current.decPointers() then current.remove() + return true + else if to.decPointers() then to.remove() + false // unreachable +end Segment diff --git a/core/shared/src/main/scala/ox/channels/jox/Select.scala b/core/shared/src/main/scala/ox/channels/jox/Select.scala new file mode 100644 index 00000000..03e6a647 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Select.scala @@ -0,0 +1,208 @@ +package ox.channels.jox + +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.locks.LockSupport +import scala.collection.mutable + +object Select: + @throws[InterruptedException] + def select[U](clauses: SelectClause[? <: U]*): U = + val r = selectOrClosed(clauses*) + r match + case c: ChannelClosed => throw c.toException() + case _ => r.asInstanceOf[U] + + @throws[InterruptedException] + def selectOrClosed[U](clauses: SelectClause[? <: U]*): AnyRef = + if clauses == null || clauses.isEmpty then throw new IllegalArgumentException("No clauses given") + if clauses.exists(_ == null) then throw new IllegalArgumentException("Null clauses are not supported") + while true do + val r = doSelectOrClosed(clauses*) + if r ne RestartSelectMarker.RESTART then return r + throw new AssertionError("unreachable") + + @throws[InterruptedException] + def defaultClause[T](value: T): SelectClause[T] = new DefaultClauseValue(value) + + def defaultClause[T](callback: () => T): SelectClause[T] = new DefaultClauseCallback(callback) + + @throws[InterruptedException] + private def doSelectOrClosed[U](clauses: SelectClause[? <: U]*): AnyRef = + // short-circuit if any channel is in error + val anyError = getAnyChannelInError(clauses) + if anyError != null then return anyError + + val allRendezvous = verifyChannelsUnique_getAreAllRendezvous(clauses) + val si = new SelectInstance(clauses.size) + var i = 0 + var done = false + while i < clauses.size && !done do + val clause = clauses(i) + clause match + case _: DefaultClause[?] if i != clauses.size - 1 => + throw new IllegalArgumentException("The default clause can only be the last one.") + case _ => + if !si.register(clause) then done = true + i += 1 + si.checkStateAndWait(allRendezvous) + end doSelectOrClosed + + private def verifyChannelsUnique_getAreAllRendezvous(clauses: Seq[SelectClause[?]]): Boolean = + var allRendezvous = true + var i = 0 + while i < clauses.size do + val chi = clauses(i).getChannel + var j = i + 1 + while j < clauses.size do + if (chi ne null) && (chi eq clauses(j).getChannel) then + throw new IllegalArgumentException(s"Channel $chi is used in multiple clauses") + j += 1 + allRendezvous = allRendezvous && (chi == null || chi.isRendezvous) + i += 1 + allRendezvous + + private def getAnyChannelInError(clauses: Seq[SelectClause[?]]): ChannelError | Null = + for clause <- clauses do + val ch = clause.getChannel + if ch != null then + ch.closedForSend() match + case ce: ChannelError => return ce + case _ => + null +end Select + +private[jox] final class SelectInstance(clausesCount: Int): + private val state: AtomicReference[AnyRef] = new AtomicReference(SelectState.REGISTERING) + private val storedClauses = mutable.ArrayBuffer.empty[StoredSelectClause] + private var resultSelectedDuringRegistration: AnyRef = _ + + def register[U](clause: SelectClause[U]): Boolean = + val result = clause.register(this) + result match + case ss: StoredSelectClause => + storedClauses += ss + true + case cc: ChannelClosed => + state.set(cc) + false + case _ => + // clause was selected immediately + resultSelectedDuringRegistration = result + state.set(clause) + false + + @throws[InterruptedException] + def checkStateAndWait(allRendezvous: Boolean): AnyRef = + while true do + val currentState = state.get() + currentState match + case SelectState.REGISTERING => + val currentThread = Thread.currentThread() + if state.compareAndSet(SelectState.REGISTERING, currentThread) then + var spinIterations = if allRendezvous then Continuation.RENDEZVOUS_SPINS else 0 + while state.get() eq currentThread do + if spinIterations > 0 then + Thread.onSpinWait() + spinIterations -= 1 + else + LockSupport.park() + if Thread.interrupted() then + if state.compareAndSet(currentThread, SelectState.INTERRUPTED) then + cleanup(null) + throw new InterruptedException() + else Thread.currentThread().interrupt() + + case clausesToReRegister: java.util.List[?] => + if state.compareAndSet(currentState, SelectState.REGISTERING) then + val iter = clausesToReRegister.iterator() + var done = false + while iter.hasNext && !done do + val clause = iter.next().asInstanceOf[SelectClause[?]] + // cleanup the stored select for the clause we'll re-register + val storedIter = storedClauses.iterator + var found = false + val newStored = mutable.ArrayBuffer.empty[StoredSelectClause] + for stored <- storedClauses do + if !found && (stored.clause eq clause) then + stored.cleanup() + found = true + else newStored += stored + storedClauses.clear() + storedClauses ++= newStored + + if !register(clause) then done = true + + case selectedClause: SelectClause[?] @unchecked => + cleanup(selectedClause) + return selectedClause.transformedRawValue(resultSelectedDuringRegistration).asInstanceOf[AnyRef] + + case ss: StoredSelectClause => + val selectedClause = ss.clause + cleanup(selectedClause) + return selectedClause.transformedRawValue(ss.payload.asInstanceOf[AnyRef]).asInstanceOf[AnyRef] + + case cc: ChannelClosed => + cleanup(null) + return cc + + case _ => + throw new IllegalStateException(s"Unknown state: $currentState") + end while + throw new AssertionError("unreachable") + + private def cleanup(selected: SelectClause[?] | Null): Unit = + for stored <- storedClauses do + if !(stored.clause eq selected) then stored.cleanup() + storedClauses.clear() + + /** Called by another thread to try selecting this clause. */ + def trySelect(storedSelectClause: StoredSelectClause): Boolean = + while true do + val currentState = state.get() + currentState match + case SelectState.REGISTERING => + val list = new java.util.ArrayList[SelectClause[?]](1) + list.add(storedSelectClause.clause) + if state.compareAndSet(currentState, list) then return false + case clausesToReRegister: java.util.List[?] => + val newList = new java.util.ArrayList[SelectClause[?]](clausesToReRegister.size() + 1) + newList.addAll(clausesToReRegister.asInstanceOf[java.util.List[SelectClause[?]]]) + newList.add(storedSelectClause.clause) + if state.compareAndSet(currentState, newList) then return false + case _: SelectClause[?] => + return false // already selected + case _: StoredSelectClause => + return false // already selected + case t: Thread => + if state.compareAndSet(currentState, storedSelectClause) then + LockSupport.unpark(t) + return true + case SelectState.INTERRUPTED => + return false + case _: ChannelClosed => + return false + case _ => + throw new IllegalStateException(s"Unknown state: $currentState") + false // unreachable + + /** Called when a channel is closed. */ + def channelClosed(channelClosed: ChannelClosed): Boolean = + while true do + val currentState = state.get() + currentState match + case SelectState.REGISTERING | (_: java.util.List[?]) => + if state.compareAndSet(currentState, channelClosed) then return true + case _: SelectClause[?] | _: StoredSelectClause => + return false // already selected + case t: Thread => + if state.compareAndSet(currentState, channelClosed) then + LockSupport.unpark(t) + return true + case SelectState.INTERRUPTED => + return false + case _: ChannelClosed => + return false + case _ => + throw new IllegalStateException(s"Unknown state: $currentState") + false // unreachable +end SelectInstance diff --git a/core/shared/src/main/scala/ox/channels/jox/SelectClause.scala b/core/shared/src/main/scala/ox/channels/jox/SelectClause.scala new file mode 100644 index 00000000..12c3df79 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/SelectClause.scala @@ -0,0 +1,18 @@ +package ox.channels.jox + +/** A clause to use as part of `Select.select`. */ +abstract class SelectClause[T]: + private[jox] def getChannel: Channel[?] | Null = null + /** Returns a StoredSelectClause, ChannelClosed, or the selected value (not null). */ + private[jox] def register(select: SelectInstance): AnyRef + /** Transforms the raw value using the transformation function provided when creating the clause. */ + private[jox] def transformedRawValue(rawValue: AnyRef): T + +private[jox] abstract class DefaultClause[T] extends SelectClause[T]: + override private[jox] def register(select: SelectInstance): AnyRef = this + +private[jox] final class DefaultClauseValue[T](value: T) extends DefaultClause[T]: + override private[jox] def transformedRawValue(rawValue: AnyRef): T = value + +private[jox] final class DefaultClauseCallback[T](callback: () => T) extends DefaultClause[T]: + override private[jox] def transformedRawValue(rawValue: AnyRef): T = callback() diff --git a/core/shared/src/main/scala/ox/channels/jox/Sink.scala b/core/shared/src/main/scala/ox/channels/jox/Sink.scala new file mode 100644 index 00000000..68b7fe5c --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Sink.scala @@ -0,0 +1,14 @@ +package ox.channels.jox + +trait Sink[T] extends CloseableChannel: + @throws[InterruptedException] + def send(value: T): Unit + + @throws[InterruptedException] + def sendOrClosed(value: T): AnyRef + + def trySendOrClosed(value: T): AnyRef + + def sendClause(value: T): SelectClause[Null] + + def sendClause[U](value: T, callback: () => U): SelectClause[U] diff --git a/core/shared/src/main/scala/ox/channels/jox/Source.scala b/core/shared/src/main/scala/ox/channels/jox/Source.scala new file mode 100644 index 00000000..46b1cb43 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/Source.scala @@ -0,0 +1,14 @@ +package ox.channels.jox + +trait Source[T] extends CloseableChannel: + @throws[InterruptedException] + def receive(): T + + @throws[InterruptedException] + def receiveOrClosed(): AnyRef + + def tryReceiveOrClosed(): AnyRef + + def receiveClause(): SelectClause[T] + + def receiveClause[U](callback: T => U): SelectClause[U] diff --git a/core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala b/core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala new file mode 100644 index 00000000..a1f11a0a --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala @@ -0,0 +1,13 @@ +package ox.channels.jox + +/** Keeps information about a select instance stored in a channel cell, awaiting completion. */ +private[jox] final class StoredSelectClause( + val select: SelectInstance, + val segment: Segment, + val cellIndex: Int, + val isSender: Boolean, + val clause: SelectClause[?], + @volatile var payload: AnyRef | Null +): + def cleanup(): Unit = + clause.getChannel.nn.cleanupStoredSelectClause(segment, cellIndex, isSender) diff --git a/core/src/main/scala/ox/collections.scala b/core/shared/src/main/scala/ox/collections.scala similarity index 100% rename from core/src/main/scala/ox/collections.scala rename to core/shared/src/main/scala/ox/collections.scala diff --git a/core/src/main/scala/ox/control.scala b/core/shared/src/main/scala/ox/control.scala similarity index 100% rename from core/src/main/scala/ox/control.scala rename to core/shared/src/main/scala/ox/control.scala diff --git a/core/src/main/scala/ox/either.scala b/core/shared/src/main/scala/ox/either.scala similarity index 100% rename from core/src/main/scala/ox/either.scala rename to core/shared/src/main/scala/ox/either.scala diff --git a/core/src/main/scala/ox/flow/Flow.scala b/core/shared/src/main/scala/ox/flow/Flow.scala similarity index 100% rename from core/src/main/scala/ox/flow/Flow.scala rename to core/shared/src/main/scala/ox/flow/Flow.scala diff --git a/core/src/main/scala/ox/flow/FlowCompanionIOOps.scala b/core/shared/src/main/scala/ox/flow/FlowCompanionIOOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowCompanionIOOps.scala rename to core/shared/src/main/scala/ox/flow/FlowCompanionIOOps.scala diff --git a/core/src/main/scala/ox/flow/FlowCompanionOps.scala b/core/shared/src/main/scala/ox/flow/FlowCompanionOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowCompanionOps.scala rename to core/shared/src/main/scala/ox/flow/FlowCompanionOps.scala diff --git a/core/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala b/core/shared/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala rename to core/shared/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala diff --git a/core/src/main/scala/ox/flow/FlowIOOps.scala b/core/shared/src/main/scala/ox/flow/FlowIOOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowIOOps.scala rename to core/shared/src/main/scala/ox/flow/FlowIOOps.scala diff --git a/core/src/main/scala/ox/flow/FlowOps.scala b/core/shared/src/main/scala/ox/flow/FlowOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowOps.scala rename to core/shared/src/main/scala/ox/flow/FlowOps.scala diff --git a/core/src/main/scala/ox/flow/FlowReactiveOps.scala b/core/shared/src/main/scala/ox/flow/FlowReactiveOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowReactiveOps.scala rename to core/shared/src/main/scala/ox/flow/FlowReactiveOps.scala diff --git a/core/src/main/scala/ox/flow/FlowRunOps.scala b/core/shared/src/main/scala/ox/flow/FlowRunOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowRunOps.scala rename to core/shared/src/main/scala/ox/flow/FlowRunOps.scala diff --git a/core/src/main/scala/ox/flow/FlowTextOps.scala b/core/shared/src/main/scala/ox/flow/FlowTextOps.scala similarity index 100% rename from core/src/main/scala/ox/flow/FlowTextOps.scala rename to core/shared/src/main/scala/ox/flow/FlowTextOps.scala diff --git a/core/src/main/scala/ox/flow/internal/WeightedHeap.scala b/core/shared/src/main/scala/ox/flow/internal/WeightedHeap.scala similarity index 100% rename from core/src/main/scala/ox/flow/internal/WeightedHeap.scala rename to core/shared/src/main/scala/ox/flow/internal/WeightedHeap.scala diff --git a/core/src/main/scala/ox/flow/internal/groupByImpl.scala b/core/shared/src/main/scala/ox/flow/internal/groupByImpl.scala similarity index 100% rename from core/src/main/scala/ox/flow/internal/groupByImpl.scala rename to core/shared/src/main/scala/ox/flow/internal/groupByImpl.scala diff --git a/core/src/main/scala/ox/fork.scala b/core/shared/src/main/scala/ox/fork.scala similarity index 100% rename from core/src/main/scala/ox/fork.scala rename to core/shared/src/main/scala/ox/fork.scala diff --git a/core/src/main/scala/ox/inScopeRunner.scala b/core/shared/src/main/scala/ox/inScopeRunner.scala similarity index 100% rename from core/src/main/scala/ox/inScopeRunner.scala rename to core/shared/src/main/scala/ox/inScopeRunner.scala diff --git a/core/src/main/scala/ox/internal/ScopeContext.scala b/core/shared/src/main/scala/ox/internal/ScopeContext.scala similarity index 100% rename from core/src/main/scala/ox/internal/ScopeContext.scala rename to core/shared/src/main/scala/ox/internal/ScopeContext.scala diff --git a/core/src/main/scala/ox/internal/ThreadHerd.scala b/core/shared/src/main/scala/ox/internal/ThreadHerd.scala similarity index 100% rename from core/src/main/scala/ox/internal/ThreadHerd.scala rename to core/shared/src/main/scala/ox/internal/ThreadHerd.scala diff --git a/core/src/main/scala/ox/local.scala b/core/shared/src/main/scala/ox/local.scala similarity index 100% rename from core/src/main/scala/ox/local.scala rename to core/shared/src/main/scala/ox/local.scala diff --git a/core/src/main/scala/ox/oxThreadFactory.scala b/core/shared/src/main/scala/ox/oxThreadFactory.scala similarity index 100% rename from core/src/main/scala/ox/oxThreadFactory.scala rename to core/shared/src/main/scala/ox/oxThreadFactory.scala diff --git a/core/src/main/scala/ox/par.scala b/core/shared/src/main/scala/ox/par.scala similarity index 100% rename from core/src/main/scala/ox/par.scala rename to core/shared/src/main/scala/ox/par.scala diff --git a/core/src/main/scala/ox/race.scala b/core/shared/src/main/scala/ox/race.scala similarity index 100% rename from core/src/main/scala/ox/race.scala rename to core/shared/src/main/scala/ox/race.scala diff --git a/core/src/main/scala/ox/resilience/AdaptiveRetry.scala b/core/shared/src/main/scala/ox/resilience/AdaptiveRetry.scala similarity index 100% rename from core/src/main/scala/ox/resilience/AdaptiveRetry.scala rename to core/shared/src/main/scala/ox/resilience/AdaptiveRetry.scala diff --git a/core/src/main/scala/ox/resilience/CircuitBreaker.scala b/core/shared/src/main/scala/ox/resilience/CircuitBreaker.scala similarity index 100% rename from core/src/main/scala/ox/resilience/CircuitBreaker.scala rename to core/shared/src/main/scala/ox/resilience/CircuitBreaker.scala diff --git a/core/src/main/scala/ox/resilience/CircuitBreakerConfig.scala b/core/shared/src/main/scala/ox/resilience/CircuitBreakerConfig.scala similarity index 100% rename from core/src/main/scala/ox/resilience/CircuitBreakerConfig.scala rename to core/shared/src/main/scala/ox/resilience/CircuitBreakerConfig.scala diff --git a/core/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala b/core/shared/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala similarity index 100% rename from core/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala rename to core/shared/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala diff --git a/core/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala b/core/shared/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala similarity index 100% rename from core/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala rename to core/shared/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala diff --git a/core/src/main/scala/ox/resilience/RateLimiter.scala b/core/shared/src/main/scala/ox/resilience/RateLimiter.scala similarity index 100% rename from core/src/main/scala/ox/resilience/RateLimiter.scala rename to core/shared/src/main/scala/ox/resilience/RateLimiter.scala diff --git a/core/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala b/core/shared/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala similarity index 100% rename from core/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala rename to core/shared/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala diff --git a/core/src/main/scala/ox/resilience/ResultPolicy.scala b/core/shared/src/main/scala/ox/resilience/ResultPolicy.scala similarity index 100% rename from core/src/main/scala/ox/resilience/ResultPolicy.scala rename to core/shared/src/main/scala/ox/resilience/ResultPolicy.scala diff --git a/core/src/main/scala/ox/resilience/RetryConfig.scala b/core/shared/src/main/scala/ox/resilience/RetryConfig.scala similarity index 100% rename from core/src/main/scala/ox/resilience/RetryConfig.scala rename to core/shared/src/main/scala/ox/resilience/RetryConfig.scala diff --git a/core/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala b/core/shared/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala similarity index 100% rename from core/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala rename to core/shared/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala diff --git a/core/src/main/scala/ox/resilience/TokenBucket.scala b/core/shared/src/main/scala/ox/resilience/TokenBucket.scala similarity index 100% rename from core/src/main/scala/ox/resilience/TokenBucket.scala rename to core/shared/src/main/scala/ox/resilience/TokenBucket.scala diff --git a/core/src/main/scala/ox/resilience/retry.scala b/core/shared/src/main/scala/ox/resilience/retry.scala similarity index 100% rename from core/src/main/scala/ox/resilience/retry.scala rename to core/shared/src/main/scala/ox/resilience/retry.scala diff --git a/core/src/main/scala/ox/resource.scala b/core/shared/src/main/scala/ox/resource.scala similarity index 100% rename from core/src/main/scala/ox/resource.scala rename to core/shared/src/main/scala/ox/resource.scala diff --git a/core/src/main/scala/ox/scheduling/Jitter.scala b/core/shared/src/main/scala/ox/scheduling/Jitter.scala similarity index 100% rename from core/src/main/scala/ox/scheduling/Jitter.scala rename to core/shared/src/main/scala/ox/scheduling/Jitter.scala diff --git a/core/src/main/scala/ox/scheduling/RepeatConfig.scala b/core/shared/src/main/scala/ox/scheduling/RepeatConfig.scala similarity index 100% rename from core/src/main/scala/ox/scheduling/RepeatConfig.scala rename to core/shared/src/main/scala/ox/scheduling/RepeatConfig.scala diff --git a/core/src/main/scala/ox/scheduling/Schedule.scala b/core/shared/src/main/scala/ox/scheduling/Schedule.scala similarity index 100% rename from core/src/main/scala/ox/scheduling/Schedule.scala rename to core/shared/src/main/scala/ox/scheduling/Schedule.scala diff --git a/core/src/main/scala/ox/scheduling/repeat.scala b/core/shared/src/main/scala/ox/scheduling/repeat.scala similarity index 100% rename from core/src/main/scala/ox/scheduling/repeat.scala rename to core/shared/src/main/scala/ox/scheduling/repeat.scala diff --git a/core/src/main/scala/ox/scheduling/scheduled.scala b/core/shared/src/main/scala/ox/scheduling/scheduled.scala similarity index 100% rename from core/src/main/scala/ox/scheduling/scheduled.scala rename to core/shared/src/main/scala/ox/scheduling/scheduled.scala diff --git a/core/src/main/scala/ox/supervised.scala b/core/shared/src/main/scala/ox/supervised.scala similarity index 100% rename from core/src/main/scala/ox/supervised.scala rename to core/shared/src/main/scala/ox/supervised.scala diff --git a/core/src/main/scala/ox/unsupervised.scala b/core/shared/src/main/scala/ox/unsupervised.scala similarity index 100% rename from core/src/main/scala/ox/unsupervised.scala rename to core/shared/src/main/scala/ox/unsupervised.scala diff --git a/core/src/main/scala/ox/util.scala b/core/shared/src/main/scala/ox/util.scala similarity index 100% rename from core/src/main/scala/ox/util.scala rename to core/shared/src/main/scala/ox/util.scala diff --git a/core/src/test/scala/ox/AppErrorTest.scala b/core/shared/src/test/scala/ox/AppErrorTest.scala similarity index 100% rename from core/src/test/scala/ox/AppErrorTest.scala rename to core/shared/src/test/scala/ox/AppErrorTest.scala diff --git a/core/src/test/scala/ox/CancelTest.scala b/core/shared/src/test/scala/ox/CancelTest.scala similarity index 100% rename from core/src/test/scala/ox/CancelTest.scala rename to core/shared/src/test/scala/ox/CancelTest.scala diff --git a/core/src/test/scala/ox/ChunkTest.scala b/core/shared/src/test/scala/ox/ChunkTest.scala similarity index 100% rename from core/src/test/scala/ox/ChunkTest.scala rename to core/shared/src/test/scala/ox/ChunkTest.scala diff --git a/core/src/test/scala/ox/CollectParTest.scala b/core/shared/src/test/scala/ox/CollectParTest.scala similarity index 100% rename from core/src/test/scala/ox/CollectParTest.scala rename to core/shared/src/test/scala/ox/CollectParTest.scala diff --git a/core/src/test/scala/ox/ControlTest.scala b/core/shared/src/test/scala/ox/ControlTest.scala similarity index 100% rename from core/src/test/scala/ox/ControlTest.scala rename to core/shared/src/test/scala/ox/ControlTest.scala diff --git a/core/src/test/scala/ox/EitherTest.scala b/core/shared/src/test/scala/ox/EitherTest.scala similarity index 100% rename from core/src/test/scala/ox/EitherTest.scala rename to core/shared/src/test/scala/ox/EitherTest.scala diff --git a/core/src/test/scala/ox/ExceptionTest.scala b/core/shared/src/test/scala/ox/ExceptionTest.scala similarity index 100% rename from core/src/test/scala/ox/ExceptionTest.scala rename to core/shared/src/test/scala/ox/ExceptionTest.scala diff --git a/core/src/test/scala/ox/FilterParTest.scala b/core/shared/src/test/scala/ox/FilterParTest.scala similarity index 100% rename from core/src/test/scala/ox/FilterParTest.scala rename to core/shared/src/test/scala/ox/FilterParTest.scala diff --git a/core/src/test/scala/ox/ForeachParTest.scala b/core/shared/src/test/scala/ox/ForeachParTest.scala similarity index 100% rename from core/src/test/scala/ox/ForeachParTest.scala rename to core/shared/src/test/scala/ox/ForeachParTest.scala diff --git a/core/src/test/scala/ox/ForkTest.scala b/core/shared/src/test/scala/ox/ForkTest.scala similarity index 100% rename from core/src/test/scala/ox/ForkTest.scala rename to core/shared/src/test/scala/ox/ForkTest.scala diff --git a/core/src/test/scala/ox/LocalTest.scala b/core/shared/src/test/scala/ox/LocalTest.scala similarity index 100% rename from core/src/test/scala/ox/LocalTest.scala rename to core/shared/src/test/scala/ox/LocalTest.scala diff --git a/core/src/test/scala/ox/MapParTest.scala b/core/shared/src/test/scala/ox/MapParTest.scala similarity index 100% rename from core/src/test/scala/ox/MapParTest.scala rename to core/shared/src/test/scala/ox/MapParTest.scala diff --git a/core/src/test/scala/ox/OxAppTest.scala b/core/shared/src/test/scala/ox/OxAppTest.scala similarity index 100% rename from core/src/test/scala/ox/OxAppTest.scala rename to core/shared/src/test/scala/ox/OxAppTest.scala diff --git a/core/src/test/scala/ox/ParTest.scala b/core/shared/src/test/scala/ox/ParTest.scala similarity index 100% rename from core/src/test/scala/ox/ParTest.scala rename to core/shared/src/test/scala/ox/ParTest.scala diff --git a/core/src/test/scala/ox/RaceTest.scala b/core/shared/src/test/scala/ox/RaceTest.scala similarity index 100% rename from core/src/test/scala/ox/RaceTest.scala rename to core/shared/src/test/scala/ox/RaceTest.scala diff --git a/core/src/test/scala/ox/ResourceTest.scala b/core/shared/src/test/scala/ox/ResourceTest.scala similarity index 100% rename from core/src/test/scala/ox/ResourceTest.scala rename to core/shared/src/test/scala/ox/ResourceTest.scala diff --git a/core/src/test/scala/ox/SupervisedTest.scala b/core/shared/src/test/scala/ox/SupervisedTest.scala similarity index 100% rename from core/src/test/scala/ox/SupervisedTest.scala rename to core/shared/src/test/scala/ox/SupervisedTest.scala diff --git a/core/src/test/scala/ox/UtilTest.scala b/core/shared/src/test/scala/ox/UtilTest.scala similarity index 100% rename from core/src/test/scala/ox/UtilTest.scala rename to core/shared/src/test/scala/ox/UtilTest.scala diff --git a/core/src/test/scala/ox/channels/ActorTest.scala b/core/shared/src/test/scala/ox/channels/ActorTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/ActorTest.scala rename to core/shared/src/test/scala/ox/channels/ActorTest.scala diff --git a/core/src/test/scala/ox/channels/ChannelTest.scala b/core/shared/src/test/scala/ox/channels/ChannelTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/ChannelTest.scala rename to core/shared/src/test/scala/ox/channels/ChannelTest.scala diff --git a/core/src/test/scala/ox/channels/ChannelTryTest.scala b/core/shared/src/test/scala/ox/channels/ChannelTryTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/ChannelTryTest.scala rename to core/shared/src/test/scala/ox/channels/ChannelTryTest.scala diff --git a/core/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala b/core/shared/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala rename to core/shared/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala diff --git a/core/src/test/scala/ox/channels/SelectWithinTest.scala b/core/shared/src/test/scala/ox/channels/SelectWithinTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SelectWithinTest.scala rename to core/shared/src/test/scala/ox/channels/SelectWithinTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsEmptyTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsEmptyTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsEmptyTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsEmptyTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsFailedTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsFailedTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsFailedTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsFailedTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsForeachTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsForeachTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsForeachTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsForeachTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsFutureTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsFutureTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsFutureTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsFutureTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsTest.scala diff --git a/core/src/test/scala/ox/channels/SourceOpsTransformTest.scala b/core/shared/src/test/scala/ox/channels/SourceOpsTransformTest.scala similarity index 100% rename from core/src/test/scala/ox/channels/SourceOpsTransformTest.scala rename to core/shared/src/test/scala/ox/channels/SourceOpsTransformTest.scala diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala new file mode 100644 index 00000000..6854f6cb --- /dev/null +++ b/core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala @@ -0,0 +1,50 @@ +package ox.channels.jox + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelBufferedTest extends AnyFlatSpec with Matchers: + "buffered channel" should "send and receive without blocking when buffer has space" in: + val ch = Channel.newBufferedChannel[Int](3) + ch.send(1) + ch.send(2) + ch.send(3) + ch.receive() shouldBe 1 + ch.receive() shouldBe 2 + ch.receive() shouldBe 3 + + it should "block when buffer is full" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.send(1) + ch.send(2) + @volatile var sent = false + val t = Thread.ofVirtual().start { () => + ch.send(3) + sent = true + } + Thread.sleep(50) + sent shouldBe false + ch.receive() shouldBe 1 + t.join() + sent shouldBe true + ch.receive() shouldBe 2 + ch.receive() shouldBe 3 + + it should "handle done with buffered values" in: + val ch = Channel.newBufferedChannel[Int](5) + ch.send(1) + ch.send(2) + ch.done() + ch.receive() shouldBe 1 + ch.receive() shouldBe 2 + ch.receiveOrClosed() shouldBe a[ChannelDone] + + it should "discard buffered values on error" in: + val ch = Channel.newBufferedChannel[Int](5) + ch.send(1) + ch.send(2) + val ex = new RuntimeException("test error") + ch.error(ex) + ch.receiveOrClosed() match + case e: ChannelError => e.cause shouldBe ex + case other => fail(s"Expected ChannelError, got $other") diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala new file mode 100644 index 00000000..4e81a1d2 --- /dev/null +++ b/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala @@ -0,0 +1,57 @@ +package ox.channels.jox + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelRendezvousTest extends AnyFlatSpec with Matchers: + "rendezvous channel" should "send and receive" in: + val ch = Channel.newRendezvousChannel[String]() + scoped { + fork { + ch.send("hello") + } + ch.receive() shouldBe "hello" + } + + it should "send and receive multiple values" in: + val ch = Channel.newRendezvousChannel[Int]() + scoped { + fork { + for i <- 1 to 5 do ch.send(i) + } + for i <- 1 to 5 do ch.receive() shouldBe i + } + + it should "block sender until receiver arrives" in: + val ch = Channel.newRendezvousChannel[Int]() + @volatile var sent = false + scoped { + fork { + ch.send(42) + sent = true + } + Thread.sleep(50) + sent shouldBe false + ch.receive() shouldBe 42 + Thread.sleep(50) + sent shouldBe true + } + + it should "handle done" in: + val ch = Channel.newRendezvousChannel[Int]() + ch.done() + ch.receiveOrClosed() shouldBe a[ChannelDone] + + it should "handle error" in: + val ch = Channel.newRendezvousChannel[Int]() + val ex = new RuntimeException("test") + ch.error(ex) + ch.receiveOrClosed() match + case e: ChannelError => e.cause shouldBe ex + case other => fail(s"Expected ChannelError, got $other") + + private def scoped(f: => Unit): Unit = f + + private def fork(f: => Unit): Thread = + val t = Thread.ofVirtual().start(() => f) + t diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala new file mode 100644 index 00000000..56a32e81 --- /dev/null +++ b/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala @@ -0,0 +1,48 @@ +package ox.channels.jox + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelTrySendReceiveTest extends AnyFlatSpec with Matchers: + "trySendOrClosed" should "return null when value is sent to buffered channel" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) shouldBe null + + it should "return sentinel when buffered channel is full" in: + val ch = Channel.newBufferedChannel[Int](1) + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) should be(Channel.TRY_SEND_NOT_SENT) + + it should "return ChannelClosed when channel is closed" in: + val ch = Channel.newBufferedChannel[Int](1) + ch.done() + ch.trySendOrClosed(1) shouldBe a[ChannelClosed] + + it should "send to unlimited channel" in: + val ch = Channel.newUnlimitedChannel[Int]() + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) shouldBe null + ch.receive() shouldBe 1 + ch.receive() shouldBe 2 + + "tryReceiveOrClosed" should "return null when nothing is available" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.tryReceiveOrClosed() shouldBe null + + it should "return a value when one is available" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.send(42) + ch.tryReceiveOrClosed() shouldBe 42.asInstanceOf[AnyRef] + + it should "return ChannelClosed when channel is done and empty" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.done() + ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] + + it should "return buffered value even after done" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.send(1) + ch.done() + ch.tryReceiveOrClosed() shouldBe 1.asInstanceOf[AnyRef] + ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala new file mode 100644 index 00000000..e2b5d0d1 --- /dev/null +++ b/core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala @@ -0,0 +1,21 @@ +package ox.channels.jox + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelUnlimitedTest extends AnyFlatSpec with Matchers: + "unlimited channel" should "never block on send" in: + val ch = Channel.newUnlimitedChannel[Int]() + for i <- 1 to 1000 do ch.send(i) + for i <- 1 to 1000 do ch.receive() shouldBe i + + it should "handle done with buffered values" in: + val ch = Channel.newUnlimitedChannel[Int]() + ch.send(1) + ch.send(2) + ch.send(3) + ch.done() + ch.receive() shouldBe 1 + ch.receive() shouldBe 2 + ch.receive() shouldBe 3 + ch.receiveOrClosed() shouldBe a[ChannelDone] diff --git a/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala b/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala new file mode 100644 index 00000000..7ceec5f4 --- /dev/null +++ b/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala @@ -0,0 +1,42 @@ +package ox.channels.jox + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class SelectTest extends AnyFlatSpec with Matchers: + "select" should "receive from the first available channel" in: + val ch1 = Channel.newBufferedChannel[Int](1) + val ch2 = Channel.newBufferedChannel[String](1) + ch1.send(42) + val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) + result shouldBe Integer.valueOf(42) + + it should "receive from the second channel if first is empty" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ch2 = Channel.newBufferedChannel[String](1) + ch2.send("hello") + val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) + result shouldBe "hello" + + it should "select default when no channel is ready" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ch2 = Channel.newRendezvousChannel[String]() + val result = Select.selectOrClosed( + ch1.receiveClause(), + ch2.receiveClause(), + Select.defaultClause("default_value") + ) + result shouldBe "default_value" + + it should "return closed when channel is in error" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ex = new RuntimeException("error") + ch1.error(ex) + val result = Select.selectOrClosed(ch1.receiveClause()) + result shouldBe a[ChannelError] + + it should "support send clauses" in: + val ch = Channel.newBufferedChannel[Int](1) + val result = Select.selectOrClosed(ch.sendClause(42)) + result shouldBe null // sent successfully + ch.receive() shouldBe 42 diff --git a/core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala b/core/shared/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala rename to core/shared/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala diff --git a/core/src/test/scala/ox/flow/FlowCompanionOpsTest.scala b/core/shared/src/test/scala/ox/flow/FlowCompanionOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowCompanionOpsTest.scala rename to core/shared/src/test/scala/ox/flow/FlowCompanionOpsTest.scala diff --git a/core/src/test/scala/ox/flow/FlowIOOpsTest.scala b/core/shared/src/test/scala/ox/flow/FlowIOOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowIOOpsTest.scala rename to core/shared/src/test/scala/ox/flow/FlowIOOpsTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsBatchTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsBatchTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsBatchTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsBatchTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsBufferTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsBufferTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsBufferTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsBufferTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsCollectTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsCollectTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsCollectTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsCollectTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsConcatTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsConcatTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsConcatTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsConcatTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsConflateTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsConflateTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsConflateTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsConflateTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsDebounceTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsDebounceTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsDebounceTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsDebounceTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsDrainTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsDrainTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsDrainTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsDrainTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsDropTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsDropTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsDropTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsDropTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsEmptyTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsEmptyTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsEmptyTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsEmptyTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsExpandTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsExpandTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsExpandTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsExpandTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFailedTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFailedTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFailedTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFailedTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFilterTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFilterTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFilterTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFilterTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFlattenTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFlattenTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFlattenTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFlattenTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFoldTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFoldTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFoldTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFoldTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsForeachTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsForeachTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsForeachTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsForeachTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsFutureTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsFutureTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsFutureTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsFutureTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsGroupByTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsGroupByTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsGroupByTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsGroupByTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsGroupedTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsGroupedTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsGroupedTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsGroupedTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsLastTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsLastTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsLastTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsLastTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapParTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapParTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapParTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapParTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsMergeTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsMergeTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsMergeTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsMergeTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsOrElseTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsOrElseTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsOrElseTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsOrElseTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsPipeToTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsPipeToTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsPipeToTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsPipeToTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRecoverTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRecoverTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRecoverTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRecoverTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsReduceTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsReduceTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsReduceTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsReduceTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRetryTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRetryTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRetryTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsSampleTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsSampleTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsSampleTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsSampleTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsScanTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsScanTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsScanTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsScanTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsSlidingTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsSlidingTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsSlidingTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsSlidingTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsSplitTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsSplitTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsSplitTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsSplitTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTakeTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTakeTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTakeTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTakeTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTapTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTapTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTapTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsThrottleTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsThrottleTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsThrottleTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsThrottleTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTickTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTickTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTickTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTickTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsUsingSink.scala b/core/shared/src/test/scala/ox/flow/FlowOpsUsingSink.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsUsingSink.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsUsingSink.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsZipAllTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsZipAllTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsZipAllTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsZipAllTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsZipTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsZipTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsZipTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsZipTest.scala diff --git a/core/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala b/core/shared/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala rename to core/shared/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala diff --git a/core/src/test/scala/ox/flow/FlowTextOpsTest.scala b/core/shared/src/test/scala/ox/flow/FlowTextOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowTextOpsTest.scala rename to core/shared/src/test/scala/ox/flow/FlowTextOpsTest.scala diff --git a/core/src/test/scala/ox/flow/internal/WeightedHeapTest.scala b/core/shared/src/test/scala/ox/flow/internal/WeightedHeapTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/internal/WeightedHeapTest.scala rename to core/shared/src/test/scala/ox/flow/internal/WeightedHeapTest.scala diff --git a/core/src/test/scala/ox/resilience/AfterAttemptTest.scala b/core/shared/src/test/scala/ox/resilience/AfterAttemptTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/AfterAttemptTest.scala rename to core/shared/src/test/scala/ox/resilience/AfterAttemptTest.scala diff --git a/core/src/test/scala/ox/resilience/BackoffRetryTest.scala b/core/shared/src/test/scala/ox/resilience/BackoffRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/BackoffRetryTest.scala rename to core/shared/src/test/scala/ox/resilience/BackoffRetryTest.scala diff --git a/core/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala b/core/shared/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala rename to core/shared/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala diff --git a/core/src/test/scala/ox/resilience/CircuitBreakerTest.scala b/core/shared/src/test/scala/ox/resilience/CircuitBreakerTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/CircuitBreakerTest.scala rename to core/shared/src/test/scala/ox/resilience/CircuitBreakerTest.scala diff --git a/core/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala b/core/shared/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala rename to core/shared/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala diff --git a/core/src/test/scala/ox/resilience/ImmediateRetryTest.scala b/core/shared/src/test/scala/ox/resilience/ImmediateRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/ImmediateRetryTest.scala rename to core/shared/src/test/scala/ox/resilience/ImmediateRetryTest.scala diff --git a/core/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala b/core/shared/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala rename to core/shared/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala diff --git a/core/src/test/scala/ox/resilience/RateLimiterTest.scala b/core/shared/src/test/scala/ox/resilience/RateLimiterTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/RateLimiterTest.scala rename to core/shared/src/test/scala/ox/resilience/RateLimiterTest.scala diff --git a/core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala b/core/shared/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala similarity index 100% rename from core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala rename to core/shared/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala diff --git a/core/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala b/core/shared/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala similarity index 100% rename from core/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala rename to core/shared/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala diff --git a/core/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala b/core/shared/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala similarity index 100% rename from core/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala rename to core/shared/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala diff --git a/core/src/test/scala/ox/scheduling/JitterTest.scala b/core/shared/src/test/scala/ox/scheduling/JitterTest.scala similarity index 100% rename from core/src/test/scala/ox/scheduling/JitterTest.scala rename to core/shared/src/test/scala/ox/scheduling/JitterTest.scala diff --git a/core/src/test/scala/ox/util/ElapsedTime.scala b/core/shared/src/test/scala/ox/util/ElapsedTime.scala similarity index 100% rename from core/src/test/scala/ox/util/ElapsedTime.scala rename to core/shared/src/test/scala/ox/util/ElapsedTime.scala diff --git a/core/src/test/scala/ox/util/MaxCounter.scala b/core/shared/src/test/scala/ox/util/MaxCounter.scala similarity index 100% rename from core/src/test/scala/ox/util/MaxCounter.scala rename to core/shared/src/test/scala/ox/util/MaxCounter.scala diff --git a/core/src/test/scala/ox/util/Trail.scala b/core/shared/src/test/scala/ox/util/Trail.scala similarity index 100% rename from core/src/test/scala/ox/util/Trail.scala rename to core/shared/src/test/scala/ox/util/Trail.scala diff --git a/project/plugins.sbt b/project/plugins.sbt index 4a1ee523..682e5f18 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,3 +3,5 @@ addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % s addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion) addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.9.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.5") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") From f0009f6b08409ec8890cf4eaedadad53de356e54 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Sun, 24 May 2026 18:10:08 +0000 Subject: [PATCH 02/17] Document Jox port divergences, versions, and removal conditions Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/scala/ox/channels/jox/README.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 core/shared/src/main/scala/ox/channels/jox/README.md diff --git a/core/shared/src/main/scala/ox/channels/jox/README.md b/core/shared/src/main/scala/ox/channels/jox/README.md new file mode 100644 index 00000000..afe31652 --- /dev/null +++ b/core/shared/src/main/scala/ox/channels/jox/README.md @@ -0,0 +1,72 @@ +# Scala Port of Jox Channels + +Pure Scala port of [softwaremill/jox](https://github.com/softwaremill/jox) `channels` module, +enabling cross-compilation to Scala Native. + +## Source Version + +Ported from: **jox 1.1.2** (`v1.1.2-channels` tag) + +## Target Platform + +Requires: **Scala Native 0.5.12+** (for virtual threads, `java.util.concurrent` atomics, `LockSupport`) + +## Divergences from Java Jox + +### 1. AtomicXxx instead of VarHandle + +Java Jox uses `java.lang.invoke.VarHandle` for all atomic field and array operations. +This port uses: + +| Java Jox | Scala Port | Reason | +|----------|-----------|--------| +| `VarHandle` on `long` fields | `AtomicLong` | VarHandle is a stub in Scala Native (not implemented) | +| `VarHandle` on reference fields | `AtomicReference[T]` | Same | +| `VarHandle` on `int` fields | `AtomicInteger` | Same | +| `MethodHandles.arrayElementVarHandle` | `AtomicReferenceArray` | Same | + +**Impact**: Slightly more indirection (fields are objects rather than plain volatiles with CAS via handles). +Performance difference is negligible for virtual-thread-based workloads. + +**Removal condition**: If Scala Native implements `java.lang.invoke.VarHandle` with `findVarHandle` +and `arrayElementVarHandle`, this port could switch back to VarHandle for parity. + +### 2. Segment.findAndMoveForward signature + +Java Jox passes a `VarHandle` + owning object to `findAndMoveForward` / `moveForward` for generic +field updates. The Scala port passes `AtomicReference[Segment]` directly. + +**Impact**: None on behavior. Slightly less generic but type-safe. + +### 3. No forEach / toList on Source + +The Java `Source` interface has default `forEach` and `toList` methods. These are omitted because +the Ox wrapper (`ox.channels.SourceOps` / `SourceDrainOps`) provides equivalent functionality +with better Scala ergonomics. + +### 4. No Sink.trySend(value, channels...) static method + +The Java `Sink` has a static `trySend` that selects across multiple sinks. Omitted because +Ox exposes this via its own select API. + +### 5. Channel.toString is simplified + +The Java version prints full segment-by-segment cell state. The Scala port returns a short +`Channel(capacity=N)` string. The verbose version can be added if needed for debugging. + +## File Mapping + +| Java Jox file | Scala port file | +|--------------|-----------------| +| `Channel.java` (1640 lines) | `Channel.scala` | +| `Segment.java` | `Segment.scala` | +| `Select.java` | `Select.scala` (includes `SelectInstance`) | +| `SelectClause.java` | `SelectClause.scala` | +| `Source.java` | `Source.scala` | +| `Sink.java` | `Sink.scala` | +| `CloseableChannel.java` | `CloseableChannel.scala` | +| `ChannelClosed.java`, `ChannelDone.java`, `ChannelError.java` | `ChannelClosed.scala` | +| `ChannelClosedException.java`, `*Exception.java` | `ChannelClosed.scala` | +| (inner classes in Channel.java) | `CellState.scala` | +| (inner class in Select.java) | `StoredSelectClause.scala` | +| (inner class in Channel.java) | `Continuation.scala` | From 085b1cb24109aae04af88faf875ea4037cbd5d03 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Sun, 24 May 2026 19:46:16 +0000 Subject: [PATCH 03/17] Fix critical doSend bug, eliminate recursive retries, move jox port to native-only Fixes: - doSend: FAILED result in segment-forward-resolution branch now correctly retries instead of returning null (success). Bug was caused by `return` before a match expression. - trySendOrClosed/tryReceiveOrClosed: replaced recursive retry with loop via RETRY_SENTINEL to prevent stack overflow under extreme contention. Structural: - Move ox.channels.jox package from shared/ to native/ (no dead code on JVM) - Expand test coverage: port ChannelClosedTest, ChannelInterruptionTest, more SelectTest cases, more TrySendReceive tests from Java jox 1.1.2 - Add TestUtil.scala (scoped/fork/forkCancelable) matching Java TestUtil - Document test mapping in README (what's ported, what's not and why) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../scala/ox/channels/jox/CellState.scala | 0 .../main/scala/ox/channels/jox/Channel.scala | 106 +++++++++--------- .../scala/ox/channels/jox/ChannelClosed.scala | 0 .../ox/channels/jox/CloseableChannel.scala | 0 .../scala/ox/channels/jox/Continuation.scala | 0 .../src/main/scala/ox/channels/jox/README.md | 24 +++- .../main/scala/ox/channels/jox/Segment.scala | 0 .../main/scala/ox/channels/jox/Select.scala | 0 .../scala/ox/channels/jox/SelectClause.scala | 0 .../src/main/scala/ox/channels/jox/Sink.scala | 0 .../main/scala/ox/channels/jox/Source.scala | 0 .../ox/channels/jox/StoredSelectClause.scala | 0 .../ox/channels/jox/ChannelBufferedTest.scala | 29 +++-- .../ox/channels/jox/ChannelClosedTest.scala | 51 +++++++++ .../jox/ChannelInterruptionTest.scala | 71 ++++++++++++ .../channels/jox/ChannelRendezvousTest.scala | 76 +++++++++++++ .../jox/ChannelTrySendReceiveTest.scala | 100 +++++++++++++++++ .../channels/jox/ChannelUnlimitedTest.scala | 2 + .../scala/ox/channels/jox/SelectTest.scala | 88 +++++++++++++++ .../test/scala/ox/channels/jox/TestUtil.scala | 63 +++++++++++ .../channels/jox/ChannelRendezvousTest.scala | 57 ---------- .../jox/ChannelTrySendReceiveTest.scala | 48 -------- .../scala/ox/channels/jox/SelectTest.scala | 42 ------- 23 files changed, 548 insertions(+), 209 deletions(-) rename core/{shared => native}/src/main/scala/ox/channels/jox/CellState.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Channel.scala (90%) rename core/{shared => native}/src/main/scala/ox/channels/jox/ChannelClosed.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/CloseableChannel.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Continuation.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/README.md (65%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Segment.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Select.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/SelectClause.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Sink.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/Source.scala (100%) rename core/{shared => native}/src/main/scala/ox/channels/jox/StoredSelectClause.scala (100%) rename core/{shared => native}/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala (61%) create mode 100644 core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala create mode 100644 core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala create mode 100644 core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala create mode 100644 core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala rename core/{shared => native}/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala (86%) create mode 100644 core/native/src/test/scala/ox/channels/jox/SelectTest.scala create mode 100644 core/native/src/test/scala/ox/channels/jox/TestUtil.scala delete mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala delete mode 100644 core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala delete mode 100644 core/shared/src/test/scala/ox/channels/jox/SelectTest.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/CellState.scala b/core/native/src/main/scala/ox/channels/jox/CellState.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/CellState.scala rename to core/native/src/main/scala/ox/channels/jox/CellState.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/Channel.scala b/core/native/src/main/scala/ox/channels/jox/Channel.scala similarity index 90% rename from core/shared/src/main/scala/ox/channels/jox/Channel.scala rename to core/native/src/main/scala/ox/channels/jox/Channel.scala index 063b5bd6..27cc7fdf 100644 --- a/core/shared/src/main/scala/ox/channels/jox/Channel.scala +++ b/core/native/src/main/scala/ox/channels/jox/Channel.scala @@ -76,48 +76,41 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if seg.getId != id then sendersAndClosedFlag.compareAndSet(s, seg.getId * SEGMENT_SIZE) - // continue - else - if isClosed(scf) then return closedReason.get().nn - else - val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) - return handleSendResult(sendResult, seg) match - case null => null // sent - case r: AnyRef if r eq CONTINUE_MARKER => null // will continue in the outer while - case r => r - else - if isClosed(scf) then return closedReason.get().nn + // continue - skipping interrupted cells + else if isClosed(scf) then + return closedReason.get().nn else val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) - handleSendResult(sendResult, seg) match - case null => return null - case r: AnyRef if r eq CONTINUE_MARKER => () // continue loop - case r => return r + sendResult match + case SendResult.BUFFERED | SendResult.AWAITED => return null + case SendResult.RESUMED => + seg.cleanPrev() + return null + case ss: StoredSelectClause => return ss + case SendResult.FAILED => + seg.cleanPrev() + // continue - trying with a new cell + case SendResult.CLOSED => return closedReason.get().nn + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") + else if isClosed(scf) then + return closedReason.get().nn + else + val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) + sendResult match + case SendResult.BUFFERED | SendResult.AWAITED => return null + case SendResult.RESUMED => + seg.cleanPrev() + return null + case ss: StoredSelectClause => return ss + case SendResult.FAILED => + seg.cleanPrev() + // continue - trying with a new cell + case SendResult.CLOSED => return closedReason.get().nn + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") end while throw new AssertionError("unreachable") end doSend - private val CONTINUE_MARKER = new AnyRef - - private def handleSendResult(sendResult: AnyRef, segment: Segment): AnyRef | Null = - sendResult match - case SendResult.BUFFERED => - null - case SendResult.AWAITED => - null - case SendResult.RESUMED => - segment.cleanPrev() - null - case ss: StoredSelectClause => - ss - case SendResult.FAILED => - segment.cleanPrev() - CONTINUE_MARKER - case SendResult.CLOSED => - closedReason.get() - case _ => - throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") - // Non-blocking send override def trySendOrClosed(value: T): AnyRef = if value == null then throw new NullPointerException() @@ -149,30 +142,33 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if seg.getId != id then sendersAndClosedFlag.compareAndSet(s + 1, seg.getId * SEGMENT_SIZE) () // continue - else return trySendCell(seg, i, s, value) - else return trySendCell(seg, i, s, value) + else + val r = finishTrySend(seg, i, s, value) + if r ne RETRY_SENTINEL then return r + else + val r = finishTrySend(seg, i, s, value) + if r ne RETRY_SENTINEL then return r end while throw new AssertionError("unreachable") - private def trySendCell(segment: Segment, i: Int, s: Long, value: T): AnyRef = + private val RETRY_SENTINEL: AnyRef = new AnyRef + + /** Returns result or RETRY_SENTINEL to indicate the caller should loop. */ + private def finishTrySend(segment: Segment, i: Int, s: Long, value: T): AnyRef = val sendResult = try updateCellSend(segment, i, s, value, null, null, false) catch case e: InterruptedException => throw new AssertionError("unreachable: non-blocking send", e) sendResult match - case SendResult.BUFFERED => - null + case SendResult.BUFFERED => null case SendResult.RESUMED => segment.cleanPrev() null case SendResult.FAILED => segment.cleanPrev() - trySendOrClosed(value) // retry from top - case SendResult.CLOSED => - closedReason.get() - case r if r eq Channel.TRY_SEND_NOT_SENT => - Channel.TRY_SEND_NOT_SENT - case _ => - throw new IllegalStateException(s"Unexpected result: $sendResult") + RETRY_SENTINEL + case SendResult.CLOSED => closedReason.get() + case r if r eq Channel.TRY_SEND_NOT_SENT => Channel.TRY_SEND_NOT_SENT + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult") // Non-blocking receive override def tryReceiveOrClosed(): AnyRef = @@ -198,19 +194,25 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if seg.getId != id then receivers.compareAndSet(r + 1, seg.getId * SEGMENT_SIZE) () // continue - else return tryReceiveCell(seg, i, r) - else return tryReceiveCell(seg, i, r) + else + val res = finishTryReceive(seg, i) + if res ne RETRY_SENTINEL then return res + else + val res = finishTryReceive(seg, i) + if res ne RETRY_SENTINEL then return res end while throw new AssertionError("unreachable") - private def tryReceiveCell(segment: Segment, i: Int, r: Long): AnyRef | Null = + /** Returns result, null (nothing available), or RETRY_SENTINEL to indicate the caller should loop. */ + private def finishTryReceive(segment: Segment, i: Int): AnyRef | Null = + val r = receivers.get() - 1 // the cell index we just reserved val result = try updateCellReceive(segment, i, r, null, null, false) catch case e: InterruptedException => throw new AssertionError("unreachable: non-blocking receive", e) if result eq ReceiveResult.CLOSED then closedReason.get() else if result eq ReceiveResult.FAILED then segment.cleanPrev() - tryReceiveOrClosed() // retry + RETRY_SENTINEL else if result == null then null else segment.cleanPrev() diff --git a/core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala b/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/ChannelClosed.scala rename to core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala b/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/CloseableChannel.scala rename to core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/Continuation.scala b/core/native/src/main/scala/ox/channels/jox/Continuation.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/Continuation.scala rename to core/native/src/main/scala/ox/channels/jox/Continuation.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/README.md b/core/native/src/main/scala/ox/channels/jox/README.md similarity index 65% rename from core/shared/src/main/scala/ox/channels/jox/README.md rename to core/native/src/main/scala/ox/channels/jox/README.md index afe31652..8a4969b6 100644 --- a/core/shared/src/main/scala/ox/channels/jox/README.md +++ b/core/native/src/main/scala/ox/channels/jox/README.md @@ -1,7 +1,8 @@ # Scala Port of Jox Channels Pure Scala port of [softwaremill/jox](https://github.com/softwaremill/jox) `channels` module, -enabling cross-compilation to Scala Native. +enabling Scala Native support. This code lives in `core/native/` and is only compiled for the +Native platform; the JVM continues to use the Java Jox library directly. ## Source Version @@ -70,3 +71,24 @@ The Java version prints full segment-by-segment cell state. The Scala port retur | (inner classes in Channel.java) | `CellState.scala` | | (inner class in Select.java) | `StoredSelectClause.scala` | | (inner class in Channel.java) | `Continuation.scala` | + +## Test Mapping + +Tests are in `core/native/src/test/scala/ox/channels/jox/`. They require clang/LLVM to link and run. + +| Java test file | Scala test file | Notes | +|---------------|----------------|-------| +| `TestUtil.java` | `TestUtil.scala` | Scope/fork helpers | +| `ChannelRendezvousTest.java` | `ChannelRendezvousTest.scala` | All tests except perf benchmark | +| `ChannelBufferedTest.java` | `ChannelBufferedTest.scala` | All tests | +| `ChannelUnlimitedTest.java` | `ChannelUnlimitedTest.scala` | All tests | +| `ChannelClosedTest.java` | `ChannelClosedTest.scala` | All tests | +| `ChannelTrySendReceiveTest.java` | `ChannelTrySendReceiveTest.scala` | All tests | +| `ChannelInterruptionTest.java` | `ChannelInterruptionTest.scala` | Core interruption tests (no memory leak tests) | +| `SelectReceiveTest.java` | `SelectTest.scala` | Merged into one file | +| `SelectSendTest.java` | `SelectTest.scala` | Merged into one file | +| `SelectTest.java` | `SelectTest.scala` | Default clause tests | +| `StressTest.java` | — | Not ported (Fray-specific, requires JVM tooling) | +| `SegmentTest.java` | — | Not ported (tests internals via reflection) | +| `SegmentRendezvousTest.java` | — | Not ported (tests internals via reflection) | +| `SelectWithinTest.java` | — | Not ported (uses Thread.ofVirtual for timeout, covered by Ox wrapper tests) | diff --git a/core/shared/src/main/scala/ox/channels/jox/Segment.scala b/core/native/src/main/scala/ox/channels/jox/Segment.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/Segment.scala rename to core/native/src/main/scala/ox/channels/jox/Segment.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/Select.scala b/core/native/src/main/scala/ox/channels/jox/Select.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/Select.scala rename to core/native/src/main/scala/ox/channels/jox/Select.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/SelectClause.scala b/core/native/src/main/scala/ox/channels/jox/SelectClause.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/SelectClause.scala rename to core/native/src/main/scala/ox/channels/jox/SelectClause.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/Sink.scala b/core/native/src/main/scala/ox/channels/jox/Sink.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/Sink.scala rename to core/native/src/main/scala/ox/channels/jox/Sink.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/Source.scala b/core/native/src/main/scala/ox/channels/jox/Source.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/Source.scala rename to core/native/src/main/scala/ox/channels/jox/Source.scala diff --git a/core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala b/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/jox/StoredSelectClause.scala rename to core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala similarity index 61% rename from core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala rename to core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala index 6854f6cb..bdebff56 100644 --- a/core/shared/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala @@ -1,9 +1,14 @@ package ox.channels.jox +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelBufferedTest.java (jox 1.1.2) + import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import java.util.concurrent.ConcurrentSkipListSet class ChannelBufferedTest extends AnyFlatSpec with Matchers: + import TestUtil.* + "buffered channel" should "send and receive without blocking when buffer has space" in: val ch = Channel.newBufferedChannel[Int](3) ch.send(1) @@ -13,22 +18,30 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: ch.receive() shouldBe 2 ch.receive() shouldBe 3 - it should "block when buffer is full" in: + it should "block when buffer is full" in scoped { scope => val ch = Channel.newBufferedChannel[Int](2) ch.send(1) ch.send(2) @volatile var sent = false - val t = Thread.ofVirtual().start { () => - ch.send(3) - sent = true - } + forkVoid(scope, () => { ch.send(3); sent = true }) Thread.sleep(50) sent shouldBe false ch.receive() shouldBe 1 - t.join() + Thread.sleep(50) sent shouldBe true ch.receive() shouldBe 2 ch.receive() shouldBe 3 + } + + it should "send and receive in many forks" in scoped { scope => + val ch = Channel.newBufferedChannel[Int](16) + val s = new ConcurrentSkipListSet[Int]() + + for i <- 1 to 1000 do forkVoid(scope, () => ch.send(i)) + val fs = (1 to 1000).map(_ => forkVoid(scope, () => { s.add(ch.receive()); () })) + fs.foreach(_.get()) + s.size() shouldBe 1000 + } it should "handle done with buffered values" in: val ch = Channel.newBufferedChannel[Int](5) @@ -45,6 +58,4 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: ch.send(2) val ex = new RuntimeException("test error") ch.error(ex) - ch.receiveOrClosed() match - case e: ChannelError => e.cause shouldBe ex - case other => fail(s"Expected ChannelError, got $other") + ch.receiveOrClosed() shouldBe a[ChannelError] diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala new file mode 100644 index 00000000..8c097e0e --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala @@ -0,0 +1,51 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelClosedTest.java (jox 1.1.2) + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelClosedTest extends AnyFlatSpec with Matchers: + import TestUtil.* + + "closed channel" should "report closed with no values when error" in: + val c = Channel.newRendezvousChannel[Int]() + val reason = new RuntimeException() + c.error(reason) + c.isClosedForReceive shouldBe true + c.isClosedForSend shouldBe true + c.receiveOrClosed() shouldBe a[ChannelError] + + it should "report closed with no values when done" in: + val c = Channel.newRendezvousChannel[Int]() + c.done() + c.isClosedForReceive shouldBe true + c.isClosedForSend shouldBe true + c.receiveOrClosed() shouldBe a[ChannelDone] + + it should "not be closed for receive when done with suspended sender" in scoped { scope => + val c = Channel.newRendezvousChannel[Int]() + val f = forkCancelable(scope, () => c.send(1)) + try + Thread.sleep(100) + c.done() + c.isClosedForReceive shouldBe false + c.isClosedForSend shouldBe true + finally f.cancel() + } + + it should "not be closed for receive when done with buffered values" in: + val c = Channel.newBufferedChannel[Int](5) + c.send(1) + c.send(2) + c.done() + c.isClosedForReceive shouldBe false + c.isClosedForSend shouldBe true + + it should "be closed for receive when error with buffered values" in: + val c = Channel.newBufferedChannel[Int](5) + c.send(1) + c.send(2) + c.error(new RuntimeException()) + c.isClosedForReceive shouldBe true + c.isClosedForSend shouldBe true diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala new file mode 100644 index 00000000..5f30bba4 --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala @@ -0,0 +1,71 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelInterruptionTest.java (jox 1.1.2) +// Note: Memory leak tests and Fray tests not ported (require JVM-specific tooling) + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelInterruptionTest extends AnyFlatSpec with Matchers: + import TestUtil.* + + "interruption" should "interrupt a blocked send on rendezvous channel" in scoped { scope => + val ch = Channel.newRendezvousChannel[Int]() + val f = forkCancelable(scope, () => ch.send(1)) + Thread.sleep(50) + val result = f.cancel() + result shouldBe a[InterruptedException] + } + + it should "interrupt a blocked receive on rendezvous channel" in scoped { scope => + val ch = Channel.newRendezvousChannel[Int]() + val f = forkCancelable(scope, () => ch.receive()) + Thread.sleep(50) + val result = f.cancel() + result shouldBe a[InterruptedException] + } + + it should "interrupt a blocked send on full buffered channel" in scoped { scope => + val ch = Channel.newBufferedChannel[Int](1) + ch.send(1) // fill buffer + val f = forkCancelable(scope, () => ch.send(2)) + Thread.sleep(50) + val result = f.cancel() + result shouldBe a[InterruptedException] + } + + it should "interrupt a blocked receive on empty buffered channel" in scoped { scope => + val ch = Channel.newBufferedChannel[Int](1) + val f = forkCancelable(scope, () => ch.receive()) + Thread.sleep(50) + val result = f.cancel() + result shouldBe a[InterruptedException] + } + + it should "allow channel to continue working after interrupted send" in scoped { scope => + val ch = Channel.newRendezvousChannel[Int]() + + // start a sender, then interrupt it + val f = forkCancelable(scope, () => ch.send(1)) + Thread.sleep(50) + f.cancel() + + // channel should still work + forkVoid(scope, () => ch.send(2)) + Thread.sleep(50) + ch.receive() shouldBe 2 + } + + it should "allow channel to continue working after interrupted receive" in scoped { scope => + val ch = Channel.newRendezvousChannel[Int]() + + // start a receiver, then interrupt it + val f = forkCancelable(scope, () => ch.receive()) + Thread.sleep(50) + f.cancel() + + // channel should still work + forkVoid(scope, () => ch.send(3)) + Thread.sleep(50) + ch.receive() shouldBe 3 + } diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala new file mode 100644 index 00000000..6da577eb --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala @@ -0,0 +1,76 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelRendezvousTest.java (jox 1.1.2) + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import java.util.concurrent.{ConcurrentLinkedQueue, ConcurrentSkipListSet} + +class ChannelRendezvousTest extends AnyFlatSpec with Matchers: + import TestUtil.* + + "rendezvous channel" should "send and receive" in scoped { scope => + val channel = Channel.newRendezvousChannel[String]() + forkVoid(scope, () => channel.send("x")) + val t2 = fork(scope, () => channel.receive()) + t2.get() shouldBe "x" + } + + it should "send and receive in many forks" in scoped { scope => + val channel = Channel.newRendezvousChannel[Int]() + val s = new ConcurrentSkipListSet[Int]() + + for i <- 1 to 1000 do forkVoid(scope, () => channel.send(i)) + val fs = (1 to 1000).map(_ => forkVoid(scope, () => { s.add(channel.receive()); () })) + fs.foreach(_.get()) + s.size() shouldBe 1000 + } + + it should "send and receive many elements in two forks" in scoped { scope => + val channel = Channel.newRendezvousChannel[Int]() + val s = new ConcurrentSkipListSet[Int]() + + forkVoid(scope, () => for i <- 1 to 1000 do channel.send(i)) + forkVoid(scope, () => for _ <- 1 to 1000 do s.add(channel.receive())).get() + s.size() shouldBe 1000 + } + + it should "block sender until receiver arrives" in scoped { scope => + val channel = Channel.newRendezvousChannel[Int]() + val trail = new ConcurrentLinkedQueue[String]() + + forkVoid(scope, () => { channel.send(1); trail.add("S"); () }) + forkVoid(scope, () => { channel.send(2); trail.add("S"); () }) + forkVoid(scope, () => { + Thread.sleep(100L) + trail.add("R1") + channel.receive() + Thread.sleep(100L) + trail.add("R2") + channel.receive() + }).get() + + Thread.sleep(100L) + import scala.jdk.CollectionConverters.* + trail.asScala.toList shouldBe List("R1", "S", "R2", "S") + } + + it should "notify pending receives when channel is done" in scoped { scope => + val c = Channel.newRendezvousChannel[Int]() + val f = fork(scope, () => c.receiveOrClosed()) + + Thread.sleep(100L) + c.done() + f.get() shouldBe a[ChannelDone] + c.receiveOrClosed() shouldBe a[ChannelDone] + } + + it should "notify pending sends when channel is errored" in scoped { scope => + val c = Channel.newRendezvousChannel[Int]() + val f = fork(scope, () => c.sendOrClosed(1)) + + Thread.sleep(100L) + c.error(new RuntimeException()) + f.get() shouldBe a[ChannelError] + c.sendOrClosed(2) shouldBe a[ChannelError] + } diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala new file mode 100644 index 00000000..e6b483b8 --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala @@ -0,0 +1,100 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelTrySendReceiveTest.java (jox 1.1.2) + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ChannelTrySendReceiveTest extends AnyFlatSpec with Matchers: + import TestUtil.* + "trySendOrClosed" should "return null when value is sent to buffered channel" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) shouldBe null + + it should "return sentinel when buffered channel is full" in: + val ch = Channel.newBufferedChannel[Int](1) + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) should be(Channel.TRY_SEND_NOT_SENT) + + it should "return ChannelClosed when channel is closed" in: + val ch = Channel.newBufferedChannel[Int](1) + ch.done() + ch.trySendOrClosed(1) shouldBe a[ChannelClosed] + + it should "send to unlimited channel" in: + val ch = Channel.newUnlimitedChannel[Int]() + ch.trySendOrClosed(1) shouldBe null + ch.trySendOrClosed(2) shouldBe null + ch.receive() shouldBe 1 + ch.receive() shouldBe 2 + + "tryReceiveOrClosed" should "return null when nothing is available" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.tryReceiveOrClosed() shouldBe null + + it should "return a value when one is available" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.send(42) + ch.tryReceiveOrClosed() shouldBe 42.asInstanceOf[AnyRef] + + it should "return ChannelClosed when channel is done and empty" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.done() + ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] + + it should "return buffered value even after done" in: + val ch = Channel.newBufferedChannel[Int](2) + ch.send(1) + ch.done() + ch.tryReceiveOrClosed() shouldBe 1.asInstanceOf[AnyRef] + ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] + + "trySend on rendezvous" should "return false when no receiver" in: + val ch = Channel.newRendezvousChannel[String]() + val r = ch.trySendOrClosed("a") + r should not be null + r should not be a[ChannelClosed] + + it should "send when receiver is waiting" in scoped { scope => + val ch = Channel.newRendezvousChannel[String]() + fork(scope, () => ch.receive()) + var sent = false + for _ <- 0 until 10 if !sent do + Thread.sleep(10) + if ch.trySendOrClosed("x") == null then sent = true + sent shouldBe true + } + + "tryReceive on rendezvous" should "return null when no sender" in: + val ch = Channel.newRendezvousChannel[String]() + ch.tryReceiveOrClosed() shouldBe null + + it should "receive when sender is waiting" in scoped { scope => + val ch = Channel.newRendezvousChannel[String]() + forkVoid(scope, () => ch.send("x")) + var received: AnyRef | Null = null + for _ <- 0 until 10 if received == null do + Thread.sleep(10) + val r = ch.tryReceiveOrClosed() + if r != null && !r.isInstanceOf[ChannelClosed] then received = r + received shouldBe "x" + } + + "trySend on unlimited" should "always send" in: + val ch = Channel.newUnlimitedChannel[String]() + for i <- 0 until 1000 do ch.trySendOrClosed(s"v$i") shouldBe null + + "trySend on closed" should "throw on done" in: + val ch = Channel.newBufferedChannel[String](1) + ch.done() + ch.trySendOrClosed("x") shouldBe a[ChannelClosed] + + it should "throw on error" in: + val ch = Channel.newBufferedChannel[String](1) + ch.error(new RuntimeException("boom")) + ch.trySendOrClosed("x") shouldBe a[ChannelClosed] + + "trySend with null" should "throw NPE" in: + val ch = Channel.newBufferedChannel[String](1) + a[NullPointerException] should be thrownBy ch.trySendOrClosed(null.asInstanceOf[String]) diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala similarity index 86% rename from core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala rename to core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala index e2b5d0d1..33f7af15 100644 --- a/core/shared/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: channels/src/test/java/com/softwaremill/jox/ChannelUnlimitedTest.java (jox 1.1.2) + import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers diff --git a/core/native/src/test/scala/ox/channels/jox/SelectTest.scala b/core/native/src/test/scala/ox/channels/jox/SelectTest.scala new file mode 100644 index 00000000..5a899428 --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/SelectTest.scala @@ -0,0 +1,88 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/SelectTest.java, +// channels/src/test/java/com/softwaremill/jox/SelectReceiveTest.java, +// channels/src/test/java/com/softwaremill/jox/SelectSendTest.java (jox 1.1.2) +// Note: Fray/stress tests not ported (require JVM-specific infrastructure) + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class SelectTest extends AnyFlatSpec with Matchers: + import TestUtil.* + + "selectOrClosed" should "receive from the first available channel" in: + val ch1 = Channel.newBufferedChannel[Int](1) + val ch2 = Channel.newBufferedChannel[String](1) + ch1.send(42) + val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) + result shouldBe Integer.valueOf(42) + + it should "receive from the second channel if first is empty" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ch2 = Channel.newBufferedChannel[String](1) + ch2.send("hello") + val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) + result shouldBe "hello" + + it should "select default when no channel is ready" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ch2 = Channel.newRendezvousChannel[String]() + val result = Select.selectOrClosed( + ch1.receiveClause(), + ch2.receiveClause(), + Select.defaultClause("default_value") + ) + result shouldBe "default_value" + + it should "return closed when channel is in error" in: + val ch1 = Channel.newRendezvousChannel[Int]() + val ex = new RuntimeException("error") + ch1.error(ex) + val result = Select.selectOrClosed(ch1.receiveClause()) + result shouldBe a[ChannelError] + + it should "support send clauses" in: + val ch = Channel.newBufferedChannel[Int](1) + val result = Select.selectOrClosed(ch.sendClause(42)) + result shouldBe null + ch.receive() shouldBe 42 + + it should "select from a channel with a waiting sender (rendezvous)" in scoped { scope => + val ch1 = Channel.newRendezvousChannel[Int]() + val ch2 = Channel.newRendezvousChannel[Int]() + forkVoid(scope, () => ch1.send(1)) + Thread.sleep(50) + val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) + result shouldBe Integer.valueOf(1) + } + + it should "select a send clause when receiver is waiting" in scoped { scope => + val ch = Channel.newRendezvousChannel[Int]() + val f = fork(scope, () => ch.receive()) + Thread.sleep(50) + Select.selectOrClosed(ch.sendClause(99)) + f.get() shouldBe 99 + } + + it should "apply transformation callback on receive" in: + val ch = Channel.newBufferedChannel[Int](1) + ch.send(10) + val result = Select.selectOrClosed(ch.receiveClause(v => s"got:$v")) + result shouldBe "got:10" + + it should "apply callback on send" in: + val ch = Channel.newBufferedChannel[Int](1) + val result = Select.selectOrClosed(ch.sendClause(5, () => "sent!")) + result shouldBe "sent!" + ch.receive() shouldBe 5 + + it should "throw when default clause is not last" in: + val ch = Channel.newRendezvousChannel[Int]() + an[IllegalArgumentException] should be thrownBy + Select.selectOrClosed(Select.defaultClause(0), ch.receiveClause()) + + it should "throw when same channel appears in multiple clauses" in: + val ch = Channel.newBufferedChannel[Int](1) + an[IllegalArgumentException] should be thrownBy + Select.selectOrClosed(ch.receiveClause(), ch.receiveClause()) diff --git a/core/native/src/test/scala/ox/channels/jox/TestUtil.scala b/core/native/src/test/scala/ox/channels/jox/TestUtil.scala new file mode 100644 index 00000000..c86252a7 --- /dev/null +++ b/core/native/src/test/scala/ox/channels/jox/TestUtil.scala @@ -0,0 +1,63 @@ +package ox.channels.jox + +// Ported from: channels/src/test/java/com/softwaremill/jox/TestUtil.java (jox 1.1.2) + +import java.util.concurrent.{CompletableFuture, ExecutionException, Future} +import scala.collection.mutable + +object TestUtil: + def scoped(f: Scope => Unit): Unit = + val scope = new Scope + val mainTask = Thread.ofVirtual().start(() => + try f(scope) + catch case e: Exception => scope.completeExceptionally(e) + ) + mainTask.join() + scope.waitForCompletion() + + def fork[T](scope: Scope, f: () => T): Future[T] = + val cf = new CompletableFuture[T]() + Thread.ofVirtual().start(() => + try cf.complete(f()) + catch case ex: Exception => cf.completeExceptionally(ex) + ) + scope.addFuture(cf) + cf + + def forkVoid(scope: Scope, f: () => Unit): Future[Void] = + fork(scope, () => { f(); null }) + + def forkCancelable[T](scope: Scope, f: () => T): CancelableFork[T] = + val cf = new CompletableFuture[T]() + val t = Thread.ofVirtual().start(() => + try cf.complete(f()) + catch case ex: Exception => cf.completeExceptionally(ex) + ) + new CancelableFork(t, cf) + + class Scope: + private val futures = mutable.ArrayBuffer.empty[CompletableFuture[?]] + @volatile private var exception: Exception | Null = null + + def addFuture(f: CompletableFuture[?]): Unit = synchronized { futures += f } + + def completeExceptionally(e: Exception): Unit = exception = e + + def waitForCompletion(): Unit = + if exception != null then throw new ExecutionException(exception) + synchronized { + for f <- futures do + try f.get() + catch case e: ExecutionException => + if exception == null then exception = e.getCause.asInstanceOf[Exception] + } + if exception != null then throw new ExecutionException(exception) + + class CancelableFork[T](thread: Thread, future: CompletableFuture[T]): + def get(): T = future.get() + def cancel(): AnyRef = + thread.interrupt() + thread.join() + if future.isCompletedExceptionally then future.exceptionNow() + else future.get().asInstanceOf[AnyRef] +end TestUtil diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala deleted file mode 100644 index 4e81a1d2..00000000 --- a/core/shared/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala +++ /dev/null @@ -1,57 +0,0 @@ -package ox.channels.jox - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ChannelRendezvousTest extends AnyFlatSpec with Matchers: - "rendezvous channel" should "send and receive" in: - val ch = Channel.newRendezvousChannel[String]() - scoped { - fork { - ch.send("hello") - } - ch.receive() shouldBe "hello" - } - - it should "send and receive multiple values" in: - val ch = Channel.newRendezvousChannel[Int]() - scoped { - fork { - for i <- 1 to 5 do ch.send(i) - } - for i <- 1 to 5 do ch.receive() shouldBe i - } - - it should "block sender until receiver arrives" in: - val ch = Channel.newRendezvousChannel[Int]() - @volatile var sent = false - scoped { - fork { - ch.send(42) - sent = true - } - Thread.sleep(50) - sent shouldBe false - ch.receive() shouldBe 42 - Thread.sleep(50) - sent shouldBe true - } - - it should "handle done" in: - val ch = Channel.newRendezvousChannel[Int]() - ch.done() - ch.receiveOrClosed() shouldBe a[ChannelDone] - - it should "handle error" in: - val ch = Channel.newRendezvousChannel[Int]() - val ex = new RuntimeException("test") - ch.error(ex) - ch.receiveOrClosed() match - case e: ChannelError => e.cause shouldBe ex - case other => fail(s"Expected ChannelError, got $other") - - private def scoped(f: => Unit): Unit = f - - private def fork(f: => Unit): Thread = - val t = Thread.ofVirtual().start(() => f) - t diff --git a/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala b/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala deleted file mode 100644 index 56a32e81..00000000 --- a/core/shared/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala +++ /dev/null @@ -1,48 +0,0 @@ -package ox.channels.jox - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class ChannelTrySendReceiveTest extends AnyFlatSpec with Matchers: - "trySendOrClosed" should "return null when value is sent to buffered channel" in: - val ch = Channel.newBufferedChannel[Int](2) - ch.trySendOrClosed(1) shouldBe null - ch.trySendOrClosed(2) shouldBe null - - it should "return sentinel when buffered channel is full" in: - val ch = Channel.newBufferedChannel[Int](1) - ch.trySendOrClosed(1) shouldBe null - ch.trySendOrClosed(2) should be(Channel.TRY_SEND_NOT_SENT) - - it should "return ChannelClosed when channel is closed" in: - val ch = Channel.newBufferedChannel[Int](1) - ch.done() - ch.trySendOrClosed(1) shouldBe a[ChannelClosed] - - it should "send to unlimited channel" in: - val ch = Channel.newUnlimitedChannel[Int]() - ch.trySendOrClosed(1) shouldBe null - ch.trySendOrClosed(2) shouldBe null - ch.receive() shouldBe 1 - ch.receive() shouldBe 2 - - "tryReceiveOrClosed" should "return null when nothing is available" in: - val ch = Channel.newBufferedChannel[Int](2) - ch.tryReceiveOrClosed() shouldBe null - - it should "return a value when one is available" in: - val ch = Channel.newBufferedChannel[Int](2) - ch.send(42) - ch.tryReceiveOrClosed() shouldBe 42.asInstanceOf[AnyRef] - - it should "return ChannelClosed when channel is done and empty" in: - val ch = Channel.newBufferedChannel[Int](2) - ch.done() - ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] - - it should "return buffered value even after done" in: - val ch = Channel.newBufferedChannel[Int](2) - ch.send(1) - ch.done() - ch.tryReceiveOrClosed() shouldBe 1.asInstanceOf[AnyRef] - ch.tryReceiveOrClosed() shouldBe a[ChannelClosed] diff --git a/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala b/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala deleted file mode 100644 index 7ceec5f4..00000000 --- a/core/shared/src/test/scala/ox/channels/jox/SelectTest.scala +++ /dev/null @@ -1,42 +0,0 @@ -package ox.channels.jox - -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class SelectTest extends AnyFlatSpec with Matchers: - "select" should "receive from the first available channel" in: - val ch1 = Channel.newBufferedChannel[Int](1) - val ch2 = Channel.newBufferedChannel[String](1) - ch1.send(42) - val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) - result shouldBe Integer.valueOf(42) - - it should "receive from the second channel if first is empty" in: - val ch1 = Channel.newRendezvousChannel[Int]() - val ch2 = Channel.newBufferedChannel[String](1) - ch2.send("hello") - val result = Select.selectOrClosed(ch1.receiveClause(), ch2.receiveClause()) - result shouldBe "hello" - - it should "select default when no channel is ready" in: - val ch1 = Channel.newRendezvousChannel[Int]() - val ch2 = Channel.newRendezvousChannel[String]() - val result = Select.selectOrClosed( - ch1.receiveClause(), - ch2.receiveClause(), - Select.defaultClause("default_value") - ) - result shouldBe "default_value" - - it should "return closed when channel is in error" in: - val ch1 = Channel.newRendezvousChannel[Int]() - val ex = new RuntimeException("error") - ch1.error(ex) - val result = Select.selectOrClosed(ch1.receiveClause()) - result shouldBe a[ChannelError] - - it should "support send clauses" in: - val ch = Channel.newBufferedChannel[Int](1) - val result = Select.selectOrClosed(ch.sendClause(42)) - result shouldBe null // sent successfully - ch.receive() shouldBe 42 From 358c0340797deec6f0b9e73cfa3d795b6646bf94 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 09:22:21 +0000 Subject: [PATCH 04/17] Fix native test execution: use %%% for ScalaTest, exclude shared tests from native link - Use ScalaTest %%% (cross-platform) dependency for core cross-project - Set Test/fork := false for native (required by Scala Native test runner) - Exclude shared test sources from native (they use JVM-only APIs: java.time, java.net) - Reduce many-forks test count from 1000 to 100 for native scheduling reliability - All 51 native tests pass; all 899 JVM tests pass Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 7 ++++--- .../test/scala/ox/channels/jox/ChannelBufferedTest.scala | 8 +++++--- .../scala/ox/channels/jox/ChannelRendezvousTest.scala | 8 +++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index fb8265b5..87479e43 100644 --- a/build.sbt +++ b/build.sbt @@ -60,9 +60,7 @@ lazy val core = crossProject(JVMPlatform, NativePlatform) .settings(commonSettings) .settings( name := "core", - libraryDependencies ++= Seq( - scalaTest - ), + libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.20" % Test, Test / fork := true ) .jvmSettings( @@ -74,6 +72,9 @@ lazy val core = crossProject(JVMPlatform, NativePlatform) ) .jvmSettings(enableMimaSettings) .nativeSettings( + Test / fork := false, + // Only include native-specific tests; shared Ox tests use JVM-only APIs (java.time, java.net, etc.) + Test / unmanagedSourceDirectories := Seq((Test / sourceDirectory).value / "scala"), nativeConfig ~= { _.withMultithreading(true) } diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala index bdebff56..7ab03f3c 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala @@ -33,14 +33,16 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: ch.receive() shouldBe 3 } + // Java version uses 1000; reduced for Scala Native virtual thread scheduling reliability it should "send and receive in many forks" in scoped { scope => + val n = 100 val ch = Channel.newBufferedChannel[Int](16) val s = new ConcurrentSkipListSet[Int]() - for i <- 1 to 1000 do forkVoid(scope, () => ch.send(i)) - val fs = (1 to 1000).map(_ => forkVoid(scope, () => { s.add(ch.receive()); () })) + for i <- 1 to n do forkVoid(scope, () => ch.send(i)) + val fs = (1 to n).map(_ => forkVoid(scope, () => { s.add(ch.receive()); () })) fs.foreach(_.get()) - s.size() shouldBe 1000 + s.size() shouldBe n } it should "handle done with buffered values" in: diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala index 6da577eb..a3aebebe 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala @@ -16,14 +16,16 @@ class ChannelRendezvousTest extends AnyFlatSpec with Matchers: t2.get() shouldBe "x" } + // Java version uses 1000; reduced for Scala Native virtual thread scheduling reliability it should "send and receive in many forks" in scoped { scope => + val n = 100 val channel = Channel.newRendezvousChannel[Int]() val s = new ConcurrentSkipListSet[Int]() - for i <- 1 to 1000 do forkVoid(scope, () => channel.send(i)) - val fs = (1 to 1000).map(_ => forkVoid(scope, () => { s.add(channel.receive()); () })) + for i <- 1 to n do forkVoid(scope, () => channel.send(i)) + val fs = (1 to n).map(_ => forkVoid(scope, () => { s.add(channel.receive()); () })) fs.foreach(_.get()) - s.size() shouldBe 1000 + s.size() shouldBe n } it should "send and receive many elements in two forks" in scoped { scope => From d5d76fbebfc2dd8fe9de5260107fd24c9b016aac Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 10:51:42 +0000 Subject: [PATCH 05/17] Add virtual threads benchmark example (JVM vs Native) Spawns 10,000 virtual threads in an Ox supervised scope, measures wall-clock time. Includes README with instructions for packaging and running standalone binaries on both platforms. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 19 +++++ example/README.md | 69 +++++++++++++++++++ .../main/scala/VirtualThreadsBenchmark.scala | 26 +++++++ 3 files changed, 114 insertions(+) create mode 100644 example/README.md create mode 100644 example/src/main/scala/VirtualThreadsBenchmark.scala diff --git a/build.sbt b/build.sbt index 87479e43..bbe192d1 100644 --- a/build.sbt +++ b/build.sbt @@ -80,6 +80,25 @@ lazy val core = crossProject(JVMPlatform, NativePlatform) } ) +lazy val example = crossProject(JVMPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("example")) + .settings(commonSettings) + .settings( + name := "example", + publishArtifact := false + ) + .jvmSettings( + Compile / mainClass := Some("VirtualThreadsBenchmark") + ) + .nativeSettings( + Compile / mainClass := Some("VirtualThreadsBenchmark"), + nativeConfig ~= { + _.withMultithreading(true) + } + ) + .dependsOn(core) + lazy val kafka: Project = (project in file("kafka")) .settings(commonSettings) .settings( diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..1acd9e20 --- /dev/null +++ b/example/README.md @@ -0,0 +1,69 @@ +# Virtual Threads Benchmark: JVM vs Scala Native + +Spawns 10,000 virtual threads in an Ox supervised scope, joins all, and +measures wall-clock time. Use this to compare JVM and Scala Native virtual +thread performance. + +## Prerequisites + +- JDK 21+ (for JVM) +- clang / LLVM 16+ (for Scala Native) + +## Run directly via sbt + +```bash +# JVM +sbt exampleJVM/run + +# Native (first run includes compilation + linking, ~60s) +sbt exampleNative/run +``` + +## Package standalone binaries + +### JVM (uber-jar via sbt-assembly, or just stage) + +```bash +# Create a runnable script + lib directory +sbt exampleJVM/stage + +# Run +./example/jvm/target/universal/stage/bin/example +``` + +Or run directly with `java`: + +```bash +sbt exampleJVM/package +java -jar example/jvm/target/scala-3.3.7/example_3-*.jar +``` + +### Native (produces a single static binary) + +```bash +# Build the native binary +sbt exampleNative/nativeLink + +# Run +./example/native/target/scala-3.3.7/example + +# Compare both (run 3 times each for stability): +echo "=== JVM ===" && for i in 1 2 3; do java -jar example/jvm/target/scala-3.3.7/example_3-*.jar; done +echo "=== Native ===" && for i in 1 2 3; do ./example/native/target/scala-3.3.7/example; done +``` + +## What it does + +```scala +supervised { + for _ <- 1 to 10_000 do + fork { + counter.incrementAndGet() + } +} +// all 10,000 forks joined here +``` + +This exercises the core virtual thread lifecycle: creation, scheduling, +execution, and join — with no I/O or blocking, so it isolates the raw +thread management overhead. diff --git a/example/src/main/scala/VirtualThreadsBenchmark.scala b/example/src/main/scala/VirtualThreadsBenchmark.scala new file mode 100644 index 00000000..e8876c00 --- /dev/null +++ b/example/src/main/scala/VirtualThreadsBenchmark.scala @@ -0,0 +1,26 @@ +import ox.* + +import java.util.concurrent.atomic.AtomicLong + +/** Spawns 10,000 virtual threads in a supervised scope, each incrementing a shared counter, then + * joins all. Measures wall-clock time to compare JVM vs Scala Native virtual thread performance. + */ +object VirtualThreadsBenchmark: + def main(args: Array[String]): Unit = + val n = 10_000 + val counter = new AtomicLong(0L) + + val start = System.nanoTime() + + supervised { + for _ <- 1 to n do + fork { + // simulate minimal work per thread + counter.incrementAndGet() + } + } + + val elapsed = (System.nanoTime() - start) / 1_000_000 + + assert(counter.get() == n, s"Expected $n, got ${counter.get()}") + println(s"Spawned and joined $n virtual threads in ${elapsed}ms") From 8f7f0c68bf9a2bc140ac4112307842f9a481fa81 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 11:06:00 +0000 Subject: [PATCH 06/17] =?UTF-8?q?Rename=20example=E2=86=92examples,=20bump?= =?UTF-8?q?=20to=20100k=20threads,=20fix=20paths,=20add=20fat-jar=20packag?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename to VirtualThreadsNativeJvmBenchmark, module to examples - Bump from 10k to 100k virtual threads - Add sbt-assembly for fat-jar packaging - Move README content into class scaladoc with verified paths - Retain only fat-jar (JVM) and native-binary methods Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 12 ++-- example/README.md | 69 ------------------- .../main/scala/VirtualThreadsBenchmark.scala | 26 ------- .../VirtualThreadsNativeJvmBenchmark.scala | 42 +++++++++++ project/plugins.sbt | 1 + 5 files changed, 49 insertions(+), 101 deletions(-) delete mode 100644 example/README.md delete mode 100644 example/src/main/scala/VirtualThreadsBenchmark.scala create mode 100644 examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala diff --git a/build.sbt b/build.sbt index bbe192d1..56b3da08 100644 --- a/build.sbt +++ b/build.sbt @@ -80,19 +80,19 @@ lazy val core = crossProject(JVMPlatform, NativePlatform) } ) -lazy val example = crossProject(JVMPlatform, NativePlatform) +lazy val examples = crossProject(JVMPlatform, NativePlatform) .crossType(CrossType.Pure) - .in(file("example")) + .in(file("examples")) .settings(commonSettings) .settings( - name := "example", - publishArtifact := false + name := "examples", + publishArtifact := false, + Compile / mainClass := Some("VirtualThreadsNativeJvmBenchmark") ) .jvmSettings( - Compile / mainClass := Some("VirtualThreadsBenchmark") + assembly / assemblyJarName := "examples-assembly.jar" ) .nativeSettings( - Compile / mainClass := Some("VirtualThreadsBenchmark"), nativeConfig ~= { _.withMultithreading(true) } diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 1acd9e20..00000000 --- a/example/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Virtual Threads Benchmark: JVM vs Scala Native - -Spawns 10,000 virtual threads in an Ox supervised scope, joins all, and -measures wall-clock time. Use this to compare JVM and Scala Native virtual -thread performance. - -## Prerequisites - -- JDK 21+ (for JVM) -- clang / LLVM 16+ (for Scala Native) - -## Run directly via sbt - -```bash -# JVM -sbt exampleJVM/run - -# Native (first run includes compilation + linking, ~60s) -sbt exampleNative/run -``` - -## Package standalone binaries - -### JVM (uber-jar via sbt-assembly, or just stage) - -```bash -# Create a runnable script + lib directory -sbt exampleJVM/stage - -# Run -./example/jvm/target/universal/stage/bin/example -``` - -Or run directly with `java`: - -```bash -sbt exampleJVM/package -java -jar example/jvm/target/scala-3.3.7/example_3-*.jar -``` - -### Native (produces a single static binary) - -```bash -# Build the native binary -sbt exampleNative/nativeLink - -# Run -./example/native/target/scala-3.3.7/example - -# Compare both (run 3 times each for stability): -echo "=== JVM ===" && for i in 1 2 3; do java -jar example/jvm/target/scala-3.3.7/example_3-*.jar; done -echo "=== Native ===" && for i in 1 2 3; do ./example/native/target/scala-3.3.7/example; done -``` - -## What it does - -```scala -supervised { - for _ <- 1 to 10_000 do - fork { - counter.incrementAndGet() - } -} -// all 10,000 forks joined here -``` - -This exercises the core virtual thread lifecycle: creation, scheduling, -execution, and join — with no I/O or blocking, so it isolates the raw -thread management overhead. diff --git a/example/src/main/scala/VirtualThreadsBenchmark.scala b/example/src/main/scala/VirtualThreadsBenchmark.scala deleted file mode 100644 index e8876c00..00000000 --- a/example/src/main/scala/VirtualThreadsBenchmark.scala +++ /dev/null @@ -1,26 +0,0 @@ -import ox.* - -import java.util.concurrent.atomic.AtomicLong - -/** Spawns 10,000 virtual threads in a supervised scope, each incrementing a shared counter, then - * joins all. Measures wall-clock time to compare JVM vs Scala Native virtual thread performance. - */ -object VirtualThreadsBenchmark: - def main(args: Array[String]): Unit = - val n = 10_000 - val counter = new AtomicLong(0L) - - val start = System.nanoTime() - - supervised { - for _ <- 1 to n do - fork { - // simulate minimal work per thread - counter.incrementAndGet() - } - } - - val elapsed = (System.nanoTime() - start) / 1_000_000 - - assert(counter.get() == n, s"Expected $n, got ${counter.get()}") - println(s"Spawned and joined $n virtual threads in ${elapsed}ms") diff --git a/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala new file mode 100644 index 00000000..06f55853 --- /dev/null +++ b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala @@ -0,0 +1,42 @@ +import ox.* + +import java.util.concurrent.atomic.AtomicLong + +/** Spawns 100,000 virtual threads in a supervised scope, each incrementing a shared counter, + * then joins all. Measures wall-clock time to compare JVM vs Scala Native performance. + * + * Prerequisites: JDK 21+ (JVM), clang/LLVM 16+ (Native). + * + * To package & run: + * {{{ + * # JVM fat jar + * sbt examplesJVM/assembly + * java -jar examples/.jvm/target/scala-3.3.7/examples-assembly.jar + * + * # Native binary + * sbt examplesNative/nativeLink + * ./examples/.native/target/scala-3.3.7/examples + * + * # Compare (3 iterations each): + * for i in 1 2 3; do java -jar examples/.jvm/target/scala-3.3.7/examples-assembly.jar; done + * for i in 1 2 3; do ./examples/.native/target/scala-3.3.7/examples; done + * }}} + */ +object VirtualThreadsNativeJvmBenchmark: + def main(args: Array[String]): Unit = + val n = 100_000 + val counter = new AtomicLong(0L) + + val start = System.nanoTime() + + supervised { + for _ <- 1 to n do + fork { + counter.incrementAndGet() + } + } + + val elapsed = (System.nanoTime() - start) / 1_000_000 + + assert(counter.get() == n, s"Expected $n, got ${counter.get()}") + println(s"Spawned and joined $n virtual threads in ${elapsed}ms") diff --git a/project/plugins.sbt b/project/plugins.sbt index 682e5f18..0363833e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,3 +5,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.9.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.5") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") From 77fdb6ff839399a546501dfc76a8db180f1532c5 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 11:14:05 +0000 Subject: [PATCH 07/17] Format code --- .../src/main/scala/ox/channels/Channel.scala | 2 +- .../scala/ox/channels/ChannelClosed.scala | 2 +- .../scala/ox/channels/jox/CellState.scala | 1 + .../main/scala/ox/channels/jox/Channel.scala | 126 ++++++++++++------ .../ox/channels/jox/CloseableChannel.scala | 1 + .../scala/ox/channels/jox/Continuation.scala | 2 + .../main/scala/ox/channels/jox/Segment.scala | 12 +- .../main/scala/ox/channels/jox/Select.scala | 21 ++- .../scala/ox/channels/jox/SelectClause.scala | 3 + .../src/main/scala/ox/channels/jox/Sink.scala | 1 + .../main/scala/ox/channels/jox/Source.scala | 1 + .../ox/channels/jox/StoredSelectClause.scala | 1 + .../src/main/scala/ox/channels/select.scala | 2 +- 13 files changed, 122 insertions(+), 53 deletions(-) diff --git a/core/native/src/main/scala/ox/channels/Channel.scala b/core/native/src/main/scala/ox/channels/Channel.scala index 1d4609a2..1e417f37 100644 --- a/core/native/src/main/scala/ox/channels/Channel.scala +++ b/core/native/src/main/scala/ox/channels/Channel.scala @@ -1,6 +1,6 @@ package ox.channels -import ox.channels.{jox => j} +import ox.channels.jox as j import ox.channels.jox.{Channel as JChannel, Select as JSelect, SelectClause as JSelectClause, Sink as JSink, Source as JSource} import ChannelClosedUnion.orThrow diff --git a/core/native/src/main/scala/ox/channels/ChannelClosed.scala b/core/native/src/main/scala/ox/channels/ChannelClosed.scala index 3090baf4..64535622 100644 --- a/core/native/src/main/scala/ox/channels/ChannelClosed.scala +++ b/core/native/src/main/scala/ox/channels/ChannelClosed.scala @@ -1,6 +1,6 @@ package ox.channels -import ox.channels.{jox => j} +import ox.channels.jox as j /** Returned by channel methods (e.g. [[Source.receiveOrClosed]], [[Sink.sendOrClosed]], [[selectOrClosed]]) when the channel is closed. */ sealed trait ChannelClosed: diff --git a/core/native/src/main/scala/ox/channels/jox/CellState.scala b/core/native/src/main/scala/ox/channels/jox/CellState.scala index 578c9ae4..3f747dee 100644 --- a/core/native/src/main/scala/ox/channels/jox/CellState.scala +++ b/core/native/src/main/scala/ox/channels/jox/CellState.scala @@ -9,6 +9,7 @@ enum CellState: case IN_BUFFER // used to inform a potentially concurrent sender that the cell is now in the buffer case RESUMING // expandBuffer is resuming a sender case CLOSED +end CellState enum SendResult: case AWAITED, BUFFERED, RESUMED, FAILED, CLOSED diff --git a/core/native/src/main/scala/ox/channels/jox/Channel.scala b/core/native/src/main/scala/ox/channels/jox/Channel.scala index 27cc7fdf..5d4c8636 100644 --- a/core/native/src/main/scala/ox/channels/jox/Channel.scala +++ b/core/native/src/main/scala/ox/channels/jox/Channel.scala @@ -42,6 +42,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if rem == 0 then SEGMENT_SIZE else rem currentSegment.setup_markCellsProcessed(cellsToProcess) segmentId += 1 + end processInitialBuffer // ******* // Sending @@ -77,36 +78,38 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if seg.getId != id then sendersAndClosedFlag.compareAndSet(s, seg.getId * SEGMENT_SIZE) // continue - skipping interrupted cells - else if isClosed(scf) then - return closedReason.get().nn + else if isClosed(scf) then return closedReason.get().nn else val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) sendResult match case SendResult.BUFFERED | SendResult.AWAITED => return null - case SendResult.RESUMED => + case SendResult.RESUMED => seg.cleanPrev() return null case ss: StoredSelectClause => return ss - case SendResult.FAILED => + case SendResult.FAILED => seg.cleanPrev() // continue - trying with a new cell case SendResult.CLOSED => return closedReason.get().nn - case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") - else if isClosed(scf) then - return closedReason.get().nn + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") + end match + end if + else if isClosed(scf) then return closedReason.get().nn else val sendResult = updateCellSend(seg, i, s, value, select, selectClause, true) sendResult match case SendResult.BUFFERED | SendResult.AWAITED => return null - case SendResult.RESUMED => + case SendResult.RESUMED => seg.cleanPrev() return null case ss: StoredSelectClause => return ss - case SendResult.FAILED => + case SendResult.FAILED => seg.cleanPrev() // continue - trying with a new cell case SendResult.CLOSED => return closedReason.get().nn - case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult in channel: $this") + end match + end if end while throw new AssertionError("unreachable") end doSend @@ -129,8 +132,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if s >= r then return Channel.TRY_SEND_NOT_SENT else if s >= bufEnd && s >= r then return Channel.TRY_SEND_NOT_SENT - if !sendersAndClosedFlag.compareAndSet(scf, scf + 1) then - () // continue + if !sendersAndClosedFlag.compareAndSet(scf, scf + 1) then () // continue else val id = s / SEGMENT_SIZE val i = (s % SEGMENT_SIZE).toInt @@ -148,8 +150,11 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else val r = finishTrySend(seg, i, s, value) if r ne RETRY_SENTINEL then return r + end if + end if end while throw new AssertionError("unreachable") + end trySendOrClosed private val RETRY_SENTINEL: AnyRef = new AnyRef @@ -160,15 +165,17 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T catch case e: InterruptedException => throw new AssertionError("unreachable: non-blocking send", e) sendResult match case SendResult.BUFFERED => null - case SendResult.RESUMED => + case SendResult.RESUMED => segment.cleanPrev() null case SendResult.FAILED => segment.cleanPrev() RETRY_SENTINEL - case SendResult.CLOSED => closedReason.get() + case SendResult.CLOSED => closedReason.get() case r if r eq Channel.TRY_SEND_NOT_SENT => Channel.TRY_SEND_NOT_SENT - case _ => throw new IllegalStateException(s"Unexpected result: $sendResult") + case _ => throw new IllegalStateException(s"Unexpected result: $sendResult") + end match + end finishTrySend // Non-blocking receive override def tryReceiveOrClosed(): AnyRef = @@ -200,8 +207,11 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else val res = finishTryReceive(seg, i) if res ne RETRY_SENTINEL then return res + end if + end if end while throw new AssertionError("unreachable") + end tryReceiveOrClosed /** Returns result, null (nothing available), or RETRY_SENTINEL to indicate the caller should loop. */ private def finishTryReceive(segment: Segment, i: Int): AnyRef | Null = @@ -217,6 +227,8 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else segment.cleanPrev() result + end if + end finishTryReceive /** Core send logic. Returns SendResult, TRY_SEND_NOT_SENT, or StoredSelectClause. */ @throws[InterruptedException] @@ -270,8 +282,10 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T return SendResult.CLOSED case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + end if end while throw new AssertionError("unreachable") + end updateCellSend // ********* // Receiving @@ -309,14 +323,17 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else if !result.isInstanceOf[StoredSelectClause] then seg.cleanPrev() if result ne ReceiveResult.FAILED then return result + end if else val result = updateCellReceive(seg, i, r, select, selectClause, true) if result eq ReceiveResult.CLOSED then return closedReason.get().nn else if !result.isInstanceOf[StoredSelectClause] then seg.cleanPrev() if result ne ReceiveResult.FAILED then return result + end if end while throw new AssertionError("unreachable") + end doReceive @throws[InterruptedException] private def updateCellReceive( @@ -348,10 +365,9 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T val result = c.await(segment, i, isRendezvous) if result eq ChannelClosedMarker.CLOSED then return ReceiveResult.CLOSED else return result - else - if segment.casCell(i, state, BROKEN) then - expandBuffer() - return ReceiveResult.FAILED + else if segment.casCell(i, state, BROKEN) then + expandBuffer() + return ReceiveResult.FAILED else state match case c: Continuation => @@ -368,18 +384,21 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T expandBuffer() return ss.payload.asInstanceOf[AnyRef] else return ReceiveResult.FAILED - case cs: CellState => cs match - case CellState.INTERRUPTED_SEND => return ReceiveResult.FAILED - case CellState.RESUMING => Thread.onSpinWait() - case CellState.CLOSED => return ReceiveResult.CLOSED - case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case cs: CellState => + cs match + case CellState.INTERRUPTED_SEND => return ReceiveResult.FAILED + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return ReceiveResult.CLOSED + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") case _ => // buffered value segment.setCell(i, DONE) expandBuffer() return state.asInstanceOf[AnyRef] + end if end while throw new AssertionError("unreachable") + end updateCellReceive // **************** // Buffer expansion @@ -409,6 +428,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else if result == ExpandBufferResult.CLOSED then seg.cellProcessed_notInterruptedSender() () // continue to mark other closed cells as processed + end if else val result = updateCellExpandBuffer(seg, i) if result == ExpandBufferResult.DONE then @@ -417,6 +437,9 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T else if result == ExpandBufferResult.CLOSED then seg.cellProcessed_notInterruptedSender() () // continue + end if + end while + end expandBuffer private def updateCellExpandBuffer(segment: Segment, i: Int): ExpandBufferResult = while true do @@ -425,14 +448,14 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T if segment.casCell(i, null, IN_BUFFER) then return ExpandBufferResult.DONE else state match - case DONE => return ExpandBufferResult.DONE + case DONE => return ExpandBufferResult.DONE case c: Continuation if c.isSender => if segment.casCell(i, state, RESUMING) then if c.tryResume(0.asInstanceOf[AnyRef]) then segment.setCell(i, c.payload) return ExpandBufferResult.DONE else return ExpandBufferResult.FAILED - case _: Continuation => return ExpandBufferResult.DONE + case _: Continuation => return ExpandBufferResult.DONE case ss: StoredSelectClause if ss.isSender => if segment.casCell(i, state, RESUMING) then if ss.select.trySelect(ss) then @@ -440,18 +463,21 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T return ExpandBufferResult.DONE else return ExpandBufferResult.FAILED case _: StoredSelectClause => return ExpandBufferResult.DONE - case cs: CellState => cs match - case CellState.INTERRUPTED_SEND => return ExpandBufferResult.FAILED - case CellState.INTERRUPTED_RECEIVE => return ExpandBufferResult.DONE - case CellState.BROKEN => return ExpandBufferResult.DONE - case CellState.RESUMING => Thread.onSpinWait() - case CellState.CLOSED => return ExpandBufferResult.CLOSED - case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case cs: CellState => + cs match + case CellState.INTERRUPTED_SEND => return ExpandBufferResult.FAILED + case CellState.INTERRUPTED_RECEIVE => return ExpandBufferResult.DONE + case CellState.BROKEN => return ExpandBufferResult.DONE + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return ExpandBufferResult.CLOSED + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") case _ => // buffered value return ExpandBufferResult.DONE + end if end while throw new AssertionError("unreachable") + end updateCellExpandBuffer // ******* // Closing @@ -516,6 +542,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T i -= 1 closeCellsUntil(lastCellToClose, segment.getPrev) + end closeCellsUntil private def updateCellClose(segment: Segment, i: Int): Unit = while true do @@ -535,16 +562,18 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T case ss: StoredSelectClause => if ss.select.channelClosed(closedReason.get().nn) then return else Thread.onSpinWait() - case cs: CellState => cs match - case CellState.DONE | CellState.BROKEN => return - case CellState.INTERRUPTED_RECEIVE | CellState.INTERRUPTED_SEND => return - case CellState.RESUMING => Thread.onSpinWait() - case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case cs: CellState => + cs match + case CellState.DONE | CellState.BROKEN => return + case CellState.INTERRUPTED_RECEIVE | CellState.INTERRUPTED_SEND => return + case CellState.RESUMING => Thread.onSpinWait() + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") case _ => // buffered value: discarding if segment.casCell(i, state, CLOSED) then segment.cellInterruptedReceiver() return + end if override def closedForSend(): ChannelClosed | Null = if isClosed(sendersAndClosedFlag.get()) then closedReason.get() else null @@ -582,8 +611,10 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T seg.cleanPrev() if hasValueToReceive(seg, i) then return true else receivers.compareAndSet(r, r + 1) + end if end while false + end hasValuesToReceive private def hasValueToReceive(segment: Segment, i: Int): Boolean = while true do @@ -593,14 +624,18 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T state match case c: Continuation => return c.isSender case ss: StoredSelectClause => return ss.isSender - case cs: CellState => cs match - case CellState.INTERRUPTED_SEND | CellState.INTERRUPTED_RECEIVE => return false - case CellState.RESUMING => Thread.onSpinWait() - case CellState.CLOSED => return false - case CellState.DONE | CellState.BROKEN => return false - case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") + case cs: CellState => + cs match + case CellState.INTERRUPTED_SEND | CellState.INTERRUPTED_RECEIVE => return false + case CellState.RESUMING => Thread.onSpinWait() + case CellState.CLOSED => return false + case CellState.DONE | CellState.BROKEN => return false + case _ => throw new IllegalStateException(s"Unexpected state: $state in channel: $this") case _ => return true // buffered value + end if + end while false + end hasValueToReceive // ************** // Select clauses @@ -617,6 +652,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T catch case e: InterruptedException => throw new IllegalStateException(e) override private[jox] def transformedRawValue(rawValue: AnyRef): U = callback(rawValue.asInstanceOf[T]) + end receiveClause override def sendClause(value: T): SelectClause[Null] = sendClause(value, () => null) @@ -631,6 +667,8 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T catch case e: InterruptedException => throw new IllegalStateException(e) override private[jox] def transformedRawValue(rawValue: AnyRef): U = callback() + end new + end sendClause private[jox] def cleanupStoredSelectClause(segment: Segment, i: Int, isSender: Boolean): Unit = segment.setCell(i, if isSender then INTERRUPTED_SEND else INTERRUPTED_RECEIVE) diff --git a/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala b/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala index ddaaee6c..1439a707 100644 --- a/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala +++ b/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala @@ -11,3 +11,4 @@ trait CloseableChannel: def closedForSend(): ChannelClosed | Null def closedForReceive(): ChannelClosed | Null +end CloseableChannel diff --git a/core/native/src/main/scala/ox/channels/jox/Continuation.scala b/core/native/src/main/scala/ox/channels/jox/Continuation.scala index 5377e9ca..5589b6f9 100644 --- a/core/native/src/main/scala/ox/channels/jox/Continuation.scala +++ b/core/native/src/main/scala/ox/channels/jox/Continuation.scala @@ -34,9 +34,11 @@ final class Continuation(val payload: AnyRef): else segment.cellInterruptedReceiver() throw new InterruptedException() else Thread.currentThread().interrupt() + end if end while data.get() end await +end Continuation object Continuation: val RENDEZVOUS_SPINS: Int = diff --git a/core/native/src/main/scala/ox/channels/jox/Segment.scala b/core/native/src/main/scala/ox/channels/jox/Segment.scala index efb511cf..3ceb5213 100644 --- a/core/native/src/main/scala/ox/channels/jox/Segment.scala +++ b/core/native/src/main/scala/ox/channels/jox/Segment.scala @@ -56,8 +56,7 @@ final class Segment( val toAdd = -(1 << POINTERS_SHIFT) var currentP = pointersNotProcessedNotInterrupted.get() while true do - if pointersNotProcessedNotInterrupted.compareAndSet(currentP, currentP + toAdd) then - return (currentP + toAdd) == 0 + if pointersNotProcessedNotInterrupted.compareAndSet(currentP, currentP + toAdd) then return (currentP + toAdd) == 0 currentP = pointersNotProcessedNotInterrupted.get() false // unreachable @@ -97,6 +96,7 @@ final class Segment( else if _prev != null && _prev.isRemoved then () // continue loop else continue = false end while + end remove def close(): Segment = var s: Segment = this @@ -107,6 +107,7 @@ final class Segment( else if n eq CLOSED_SENTINEL then return s else s = n s // unreachable + end close private def aliveSegmentLeft(): Segment | Null = var s = prevRef.get() @@ -131,6 +132,7 @@ final class Segment( val nextStr = if n == null then "null" else if n eq CLOSED_SENTINEL then "closed" else n.id.toString val prevStr = if p == null then "null" else p.id.toString s"Segment{id=$id, next=$nextStr, prev=$prevStr, pointers=$ptrs, notProcessed=$notProcessed, notInterrupted=$notInterrupted}" + end toString end Segment @@ -163,10 +165,10 @@ object Segment: if n eq CLOSED_SENTINEL then return null else if n == null then val newSegment = new Segment(current.getId + 1, current, 0, start.isRendezvousOrUnlimited) - if current.setNextIfNull(newSegment) then - if current.isRemoved then current.remove() + if current.setNextIfNull(newSegment) then if current.isRemoved then current.remove() else current = n.nn current + end findSegment private def moveForward(ref: AtomicReference[Segment], to: Segment): Boolean = while true do @@ -177,5 +179,7 @@ object Segment: if current.decPointers() then current.remove() return true else if to.decPointers() then to.remove() + end while false // unreachable + end moveForward end Segment diff --git a/core/native/src/main/scala/ox/channels/jox/Select.scala b/core/native/src/main/scala/ox/channels/jox/Select.scala index 03e6a647..b3fa1c85 100644 --- a/core/native/src/main/scala/ox/channels/jox/Select.scala +++ b/core/native/src/main/scala/ox/channels/jox/Select.scala @@ -20,6 +20,7 @@ object Select: val r = doSelectOrClosed(clauses*) if r ne RestartSelectMarker.RESTART then return r throw new AssertionError("unreachable") + end selectOrClosed @throws[InterruptedException] def defaultClause[T](value: T): SelectClause[T] = new DefaultClauseValue(value) @@ -44,6 +45,7 @@ object Select: case _ => if !si.register(clause) then done = true i += 1 + end while si.checkStateAndWait(allRendezvous) end doSelectOrClosed @@ -59,7 +61,9 @@ object Select: j += 1 allRendezvous = allRendezvous && (chi == null || chi.isRendezvous) i += 1 + end while allRendezvous + end verifyChannelsUnique_getAreAllRendezvous private def getAnyChannelInError(clauses: Seq[SelectClause[?]]): ChannelError | Null = for clause <- clauses do @@ -69,6 +73,7 @@ object Select: case ce: ChannelError => return ce case _ => null + end getAnyChannelInError end Select private[jox] final class SelectInstance(clausesCount: Int): @@ -90,6 +95,8 @@ private[jox] final class SelectInstance(clausesCount: Int): resultSelectedDuringRegistration = result state.set(clause) false + end match + end register @throws[InterruptedException] def checkStateAndWait(allRendezvous: Boolean): AnyRef = @@ -111,6 +118,8 @@ private[jox] final class SelectInstance(clausesCount: Int): cleanup(null) throw new InterruptedException() else Thread.currentThread().interrupt() + end while + end if case clausesToReRegister: java.util.List[?] => if state.compareAndSet(currentState, SelectState.REGISTERING) then @@ -131,6 +140,7 @@ private[jox] final class SelectInstance(clausesCount: Int): storedClauses ++= newStored if !register(clause) then done = true + end while case selectedClause: SelectClause[?] @unchecked => cleanup(selectedClause) @@ -147,12 +157,13 @@ private[jox] final class SelectInstance(clausesCount: Int): case _ => throw new IllegalStateException(s"Unknown state: $currentState") + end match end while throw new AssertionError("unreachable") + end checkStateAndWait private def cleanup(selected: SelectClause[?] | Null): Unit = - for stored <- storedClauses do - if !(stored.clause eq selected) then stored.cleanup() + for stored <- storedClauses do if !(stored.clause eq selected) then stored.cleanup() storedClauses.clear() /** Called by another thread to try selecting this clause. */ @@ -183,7 +194,10 @@ private[jox] final class SelectInstance(clausesCount: Int): return false case _ => throw new IllegalStateException(s"Unknown state: $currentState") + end match + end while false // unreachable + end trySelect /** Called when a channel is closed. */ def channelClosed(channelClosed: ChannelClosed): Boolean = @@ -204,5 +218,8 @@ private[jox] final class SelectInstance(clausesCount: Int): return false case _ => throw new IllegalStateException(s"Unknown state: $currentState") + end match + end while false // unreachable + end channelClosed end SelectInstance diff --git a/core/native/src/main/scala/ox/channels/jox/SelectClause.scala b/core/native/src/main/scala/ox/channels/jox/SelectClause.scala index 12c3df79..a3ae8cab 100644 --- a/core/native/src/main/scala/ox/channels/jox/SelectClause.scala +++ b/core/native/src/main/scala/ox/channels/jox/SelectClause.scala @@ -3,10 +3,13 @@ package ox.channels.jox /** A clause to use as part of `Select.select`. */ abstract class SelectClause[T]: private[jox] def getChannel: Channel[?] | Null = null + /** Returns a StoredSelectClause, ChannelClosed, or the selected value (not null). */ private[jox] def register(select: SelectInstance): AnyRef + /** Transforms the raw value using the transformation function provided when creating the clause. */ private[jox] def transformedRawValue(rawValue: AnyRef): T +end SelectClause private[jox] abstract class DefaultClause[T] extends SelectClause[T]: override private[jox] def register(select: SelectInstance): AnyRef = this diff --git a/core/native/src/main/scala/ox/channels/jox/Sink.scala b/core/native/src/main/scala/ox/channels/jox/Sink.scala index 68b7fe5c..64ee1d91 100644 --- a/core/native/src/main/scala/ox/channels/jox/Sink.scala +++ b/core/native/src/main/scala/ox/channels/jox/Sink.scala @@ -12,3 +12,4 @@ trait Sink[T] extends CloseableChannel: def sendClause(value: T): SelectClause[Null] def sendClause[U](value: T, callback: () => U): SelectClause[U] +end Sink diff --git a/core/native/src/main/scala/ox/channels/jox/Source.scala b/core/native/src/main/scala/ox/channels/jox/Source.scala index 46b1cb43..1e0e8dfe 100644 --- a/core/native/src/main/scala/ox/channels/jox/Source.scala +++ b/core/native/src/main/scala/ox/channels/jox/Source.scala @@ -12,3 +12,4 @@ trait Source[T] extends CloseableChannel: def receiveClause(): SelectClause[T] def receiveClause[U](callback: T => U): SelectClause[U] +end Source diff --git a/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala b/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala index a1f11a0a..c2880590 100644 --- a/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala +++ b/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala @@ -11,3 +11,4 @@ private[jox] final class StoredSelectClause( ): def cleanup(): Unit = clause.getChannel.nn.cleanupStoredSelectClause(segment, cellIndex, isSender) +end StoredSelectClause diff --git a/core/native/src/main/scala/ox/channels/select.scala b/core/native/src/main/scala/ox/channels/select.scala index e38c0c6e..d3c22911 100644 --- a/core/native/src/main/scala/ox/channels/select.scala +++ b/core/native/src/main/scala/ox/channels/select.scala @@ -1,6 +1,6 @@ package ox.channels -import ox.channels.jox.{Select as JSelect} +import ox.channels.jox.Select as JSelect import ox.channels.ChannelClosedUnion.{map, orThrow} import ox.{discard, forkUnsupervised, sleep, unsupervised} From 09d476095992d13ed852e8147c1c7fddbc98e4cc Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 13:54:47 +0000 Subject: [PATCH 08/17] Apply scalafmt to native test sources Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ox/channels/jox/ChannelBufferedTest.scala | 15 +++++- .../ox/channels/jox/ChannelClosedTest.scala | 1 + .../jox/ChannelInterruptionTest.scala | 1 + .../channels/jox/ChannelRendezvousTest.scala | 39 +++++++++++----- .../jox/ChannelTrySendReceiveTest.scala | 1 + .../channels/jox/ChannelUnlimitedTest.scala | 1 + .../scala/ox/channels/jox/SelectTest.scala | 1 + .../test/scala/ox/channels/jox/TestUtil.scala | 46 +++++++++++++------ 8 files changed, 77 insertions(+), 28 deletions(-) diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala index 7ab03f3c..1e94a868 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala @@ -23,7 +23,11 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: ch.send(1) ch.send(2) @volatile var sent = false - forkVoid(scope, () => { ch.send(3); sent = true }) + forkVoid( + scope, + () => + ch.send(3); sent = true + ) Thread.sleep(50) sent shouldBe false ch.receive() shouldBe 1 @@ -40,7 +44,13 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: val s = new ConcurrentSkipListSet[Int]() for i <- 1 to n do forkVoid(scope, () => ch.send(i)) - val fs = (1 to n).map(_ => forkVoid(scope, () => { s.add(ch.receive()); () })) + val fs = (1 to n).map(_ => + forkVoid( + scope, + () => + s.add(ch.receive()); () + ) + ) fs.foreach(_.get()) s.size() shouldBe n } @@ -61,3 +71,4 @@ class ChannelBufferedTest extends AnyFlatSpec with Matchers: val ex = new RuntimeException("test error") ch.error(ex) ch.receiveOrClosed() shouldBe a[ChannelError] +end ChannelBufferedTest diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala index 8c097e0e..1b7da5f6 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala @@ -49,3 +49,4 @@ class ChannelClosedTest extends AnyFlatSpec with Matchers: c.error(new RuntimeException()) c.isClosedForReceive shouldBe true c.isClosedForSend shouldBe true +end ChannelClosedTest diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala index 5f30bba4..5e71afd7 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala @@ -69,3 +69,4 @@ class ChannelInterruptionTest extends AnyFlatSpec with Matchers: Thread.sleep(50) ch.receive() shouldBe 3 } +end ChannelInterruptionTest diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala index a3aebebe..17493bfa 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala @@ -23,7 +23,13 @@ class ChannelRendezvousTest extends AnyFlatSpec with Matchers: val s = new ConcurrentSkipListSet[Int]() for i <- 1 to n do forkVoid(scope, () => channel.send(i)) - val fs = (1 to n).map(_ => forkVoid(scope, () => { s.add(channel.receive()); () })) + val fs = (1 to n).map(_ => + forkVoid( + scope, + () => + s.add(channel.receive()); () + ) + ) fs.foreach(_.get()) s.size() shouldBe n } @@ -41,16 +47,26 @@ class ChannelRendezvousTest extends AnyFlatSpec with Matchers: val channel = Channel.newRendezvousChannel[Int]() val trail = new ConcurrentLinkedQueue[String]() - forkVoid(scope, () => { channel.send(1); trail.add("S"); () }) - forkVoid(scope, () => { channel.send(2); trail.add("S"); () }) - forkVoid(scope, () => { - Thread.sleep(100L) - trail.add("R1") - channel.receive() - Thread.sleep(100L) - trail.add("R2") - channel.receive() - }).get() + forkVoid( + scope, + () => + channel.send(1); trail.add("S"); () + ) + forkVoid( + scope, + () => + channel.send(2); trail.add("S"); () + ) + forkVoid( + scope, + () => + Thread.sleep(100L) + trail.add("R1") + channel.receive() + Thread.sleep(100L) + trail.add("R2") + channel.receive() + ).get() Thread.sleep(100L) import scala.jdk.CollectionConverters.* @@ -76,3 +92,4 @@ class ChannelRendezvousTest extends AnyFlatSpec with Matchers: f.get() shouldBe a[ChannelError] c.sendOrClosed(2) shouldBe a[ChannelError] } +end ChannelRendezvousTest diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala index e6b483b8..c8eaed4f 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala @@ -98,3 +98,4 @@ class ChannelTrySendReceiveTest extends AnyFlatSpec with Matchers: "trySend with null" should "throw NPE" in: val ch = Channel.newBufferedChannel[String](1) a[NullPointerException] should be thrownBy ch.trySendOrClosed(null.asInstanceOf[String]) +end ChannelTrySendReceiveTest diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala b/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala index 33f7af15..9ad69a68 100644 --- a/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala @@ -21,3 +21,4 @@ class ChannelUnlimitedTest extends AnyFlatSpec with Matchers: ch.receive() shouldBe 2 ch.receive() shouldBe 3 ch.receiveOrClosed() shouldBe a[ChannelDone] +end ChannelUnlimitedTest diff --git a/core/native/src/test/scala/ox/channels/jox/SelectTest.scala b/core/native/src/test/scala/ox/channels/jox/SelectTest.scala index 5a899428..d8254508 100644 --- a/core/native/src/test/scala/ox/channels/jox/SelectTest.scala +++ b/core/native/src/test/scala/ox/channels/jox/SelectTest.scala @@ -86,3 +86,4 @@ class SelectTest extends AnyFlatSpec with Matchers: val ch = Channel.newBufferedChannel[Int](1) an[IllegalArgumentException] should be thrownBy Select.selectOrClosed(ch.receiveClause(), ch.receiveClause()) +end SelectTest diff --git a/core/native/src/test/scala/ox/channels/jox/TestUtil.scala b/core/native/src/test/scala/ox/channels/jox/TestUtil.scala index c86252a7..1b838e02 100644 --- a/core/native/src/test/scala/ox/channels/jox/TestUtil.scala +++ b/core/native/src/test/scala/ox/channels/jox/TestUtil.scala @@ -8,32 +8,45 @@ import scala.collection.mutable object TestUtil: def scoped(f: Scope => Unit): Unit = val scope = new Scope - val mainTask = Thread.ofVirtual().start(() => - try f(scope) - catch case e: Exception => scope.completeExceptionally(e) - ) + val mainTask = Thread + .ofVirtual() + .start(() => + try f(scope) + catch case e: Exception => scope.completeExceptionally(e) + ) mainTask.join() scope.waitForCompletion() + end scoped def fork[T](scope: Scope, f: () => T): Future[T] = val cf = new CompletableFuture[T]() - Thread.ofVirtual().start(() => - try cf.complete(f()) - catch case ex: Exception => cf.completeExceptionally(ex) - ) + Thread + .ofVirtual() + .start(() => + try cf.complete(f()) + catch case ex: Exception => cf.completeExceptionally(ex) + ) scope.addFuture(cf) cf + end fork def forkVoid(scope: Scope, f: () => Unit): Future[Void] = - fork(scope, () => { f(); null }) + fork( + scope, + () => + f(); null + ) def forkCancelable[T](scope: Scope, f: () => T): CancelableFork[T] = val cf = new CompletableFuture[T]() - val t = Thread.ofVirtual().start(() => - try cf.complete(f()) - catch case ex: Exception => cf.completeExceptionally(ex) - ) + val t = Thread + .ofVirtual() + .start(() => + try cf.complete(f()) + catch case ex: Exception => cf.completeExceptionally(ex) + ) new CancelableFork(t, cf) + end forkCancelable class Scope: private val futures = mutable.ArrayBuffer.empty[CompletableFuture[?]] @@ -48,10 +61,13 @@ object TestUtil: synchronized { for f <- futures do try f.get() - catch case e: ExecutionException => - if exception == null then exception = e.getCause.asInstanceOf[Exception] + catch + case e: ExecutionException => + if exception == null then exception = e.getCause.asInstanceOf[Exception] } if exception != null then throw new ExecutionException(exception) + end waitForCompletion + end Scope class CancelableFork[T](thread: Thread, future: CompletableFuture[T]): def get(): T = future.get() From 0f2d10034f0bea7aae747a30ff2925acbcea0506 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 14:42:11 +0000 Subject: [PATCH 09/17] Add source back-pointers to all ported Jox files Each file now has a comment linking to the original Java source in the softwaremill/jox repo at the v1.1.2-channels tag. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/native/src/main/scala/ox/channels/jox/CellState.scala | 4 ++++ core/native/src/main/scala/ox/channels/jox/Channel.scala | 2 ++ .../src/main/scala/ox/channels/jox/ChannelClosed.scala | 5 +++++ .../src/main/scala/ox/channels/jox/CloseableChannel.scala | 2 ++ .../native/src/main/scala/ox/channels/jox/Continuation.scala | 3 +++ core/native/src/main/scala/ox/channels/jox/Segment.scala | 2 ++ core/native/src/main/scala/ox/channels/jox/Select.scala | 2 ++ .../native/src/main/scala/ox/channels/jox/SelectClause.scala | 2 ++ core/native/src/main/scala/ox/channels/jox/Sink.scala | 2 ++ core/native/src/main/scala/ox/channels/jox/Source.scala | 2 ++ .../src/main/scala/ox/channels/jox/StoredSelectClause.scala | 3 +++ 11 files changed, 29 insertions(+) diff --git a/core/native/src/main/scala/ox/channels/jox/CellState.scala b/core/native/src/main/scala/ox/channels/jox/CellState.scala index 3f747dee..7ab3d092 100644 --- a/core/native/src/main/scala/ox/channels/jox/CellState.scala +++ b/core/native/src/main/scala/ox/channels/jox/CellState.scala @@ -1,5 +1,9 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Channel.java +// (inner enums/classes: CellState, SendResult, ReceiveResult, ExpandBufferResult, ContinuationMarker, ChannelClosedMarker, SentClauseMarker; +// plus RestartSelectMarker, SelectState, TimeoutMarker from Select.java) + // Possible states of a cell: one of these enum constants, Continuation, StoredSelectClause, or a buffered value enum CellState: case DONE diff --git a/core/native/src/main/scala/ox/channels/jox/Channel.scala b/core/native/src/main/scala/ox/channels/jox/Channel.scala index 5d4c8636..c1d58b80 100644 --- a/core/native/src/main/scala/ox/channels/jox/Channel.scala +++ b/core/native/src/main/scala/ox/channels/jox/Channel.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Channel.java + import java.util.concurrent.atomic.{AtomicLong, AtomicReference} import java.util.concurrent.locks.LockSupport diff --git a/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala b/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala index ddbabaf1..bdd261b9 100644 --- a/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala +++ b/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala @@ -1,5 +1,10 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/ChannelClosed.java +// https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/ChannelDone.java +// https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/ChannelError.java +// https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/ChannelClosedException.java + sealed trait ChannelClosed: def toException(): ChannelClosedException def channel: Channel[?] diff --git a/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala b/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala index 1439a707..1a1cf8a4 100644 --- a/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala +++ b/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/CloseableChannel.java + trait CloseableChannel: def done(): Unit def doneOrClosed(): AnyRef diff --git a/core/native/src/main/scala/ox/channels/jox/Continuation.scala b/core/native/src/main/scala/ox/channels/jox/Continuation.scala index 5589b6f9..8155b9e4 100644 --- a/core/native/src/main/scala/ox/channels/jox/Continuation.scala +++ b/core/native/src/main/scala/ox/channels/jox/Continuation.scala @@ -1,5 +1,8 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Channel.java +// (inner class Continuation, lines 1508-1622) + import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.LockSupport diff --git a/core/native/src/main/scala/ox/channels/jox/Segment.scala b/core/native/src/main/scala/ox/channels/jox/Segment.scala index 3ceb5213..e0f0c0e0 100644 --- a/core/native/src/main/scala/ox/channels/jox/Segment.scala +++ b/core/native/src/main/scala/ox/channels/jox/Segment.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Segment.java + import java.util.concurrent.atomic.{AtomicInteger, AtomicReference, AtomicReferenceArray} final class Segment( diff --git a/core/native/src/main/scala/ox/channels/jox/Select.scala b/core/native/src/main/scala/ox/channels/jox/Select.scala index b3fa1c85..a8cc6617 100644 --- a/core/native/src/main/scala/ox/channels/jox/Select.scala +++ b/core/native/src/main/scala/ox/channels/jox/Select.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Select.java + import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.LockSupport import scala.collection.mutable diff --git a/core/native/src/main/scala/ox/channels/jox/SelectClause.scala b/core/native/src/main/scala/ox/channels/jox/SelectClause.scala index a3ae8cab..49ac5a21 100644 --- a/core/native/src/main/scala/ox/channels/jox/SelectClause.scala +++ b/core/native/src/main/scala/ox/channels/jox/SelectClause.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/SelectClause.java + /** A clause to use as part of `Select.select`. */ abstract class SelectClause[T]: private[jox] def getChannel: Channel[?] | Null = null diff --git a/core/native/src/main/scala/ox/channels/jox/Sink.scala b/core/native/src/main/scala/ox/channels/jox/Sink.scala index 64ee1d91..eb6d66d2 100644 --- a/core/native/src/main/scala/ox/channels/jox/Sink.scala +++ b/core/native/src/main/scala/ox/channels/jox/Sink.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Sink.java + trait Sink[T] extends CloseableChannel: @throws[InterruptedException] def send(value: T): Unit diff --git a/core/native/src/main/scala/ox/channels/jox/Source.scala b/core/native/src/main/scala/ox/channels/jox/Source.scala index 1e0e8dfe..e755e727 100644 --- a/core/native/src/main/scala/ox/channels/jox/Source.scala +++ b/core/native/src/main/scala/ox/channels/jox/Source.scala @@ -1,5 +1,7 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Source.java + trait Source[T] extends CloseableChannel: @throws[InterruptedException] def receive(): T diff --git a/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala b/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala index c2880590..eb2401d1 100644 --- a/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala +++ b/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala @@ -1,5 +1,8 @@ package ox.channels.jox +// Ported from: https://github.com/softwaremill/jox/blob/v1.1.2-channels/channels/src/main/java/com/softwaremill/jox/Select.java +// (inner class StoredSelectClause, lines 597-643) + /** Keeps information about a select instance stored in a channel cell, awaiting completion. */ private[jox] final class StoredSelectClause( val select: SelectInstance, From e7dff70274bfc45f9e2c0963d2c322893c2b1a3c Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 15:17:45 +0000 Subject: [PATCH 10/17] Migrate from sbt-crossproject to sbt-projectmatrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace sbt-scala-native-crossproject with sbt-projectmatrix (0.11.0), matching the convention used in other SoftwareMill projects (sttp, sttp-model). Source layout changes: - core/shared/src/ → core/src/main/scala/, core/src/test/scala/ - core/jvm/src/ → core/src/main/scalajvm/, core/src/test/scalajvm/ - core/native/src/ → core/src/main/scalanative/, core/src/test/scalanative/ Project names: core3 (JVM), coreNative3 (Native), examples3, examplesNative3. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 76 ++++++++++--------- .../src/main/scala/ox/Chunk.scala | 0 .../src/main/scala/ox/ErrorMode.scala | 0 core/{shared => }/src/main/scala/ox/Ox.scala | 0 .../src/main/scala/ox/OxApp.scala | 0 .../scala/ox/channels/BufferCapacity.scala | 0 .../ox/channels/ChannelClosedUnion.scala | 0 .../ox/channels/SourceCompanionOps.scala | 0 .../scala/ox/channels/SourceDrainOps.scala | 0 .../main/scala/ox/channels/SourceOps.scala | 0 .../src/main/scala/ox/channels/actor.scala | 0 .../scala/ox/channels/forkPropagate.scala | 0 .../src/main/scala/ox/collections.scala | 0 .../src/main/scala/ox/control.scala | 0 .../src/main/scala/ox/either.scala | 0 .../src/main/scala/ox/flow/Flow.scala | 0 .../scala/ox/flow/FlowCompanionIOOps.scala | 0 .../main/scala/ox/flow/FlowCompanionOps.scala | 0 .../ox/flow/FlowCompanionReactiveOps.scala | 0 .../src/main/scala/ox/flow/FlowIOOps.scala | 0 .../src/main/scala/ox/flow/FlowOps.scala | 0 .../main/scala/ox/flow/FlowReactiveOps.scala | 0 .../src/main/scala/ox/flow/FlowRunOps.scala | 0 .../src/main/scala/ox/flow/FlowTextOps.scala | 0 .../scala/ox/flow/internal/WeightedHeap.scala | 0 .../scala/ox/flow/internal/groupByImpl.scala | 0 .../{shared => }/src/main/scala/ox/fork.scala | 0 .../src/main/scala/ox/inScopeRunner.scala | 0 .../main/scala/ox/internal/ScopeContext.scala | 0 .../main/scala/ox/internal/ThreadHerd.scala | 0 .../src/main/scala/ox/local.scala | 0 .../src/main/scala/ox/oxThreadFactory.scala | 0 core/{shared => }/src/main/scala/ox/par.scala | 0 .../{shared => }/src/main/scala/ox/race.scala | 0 .../scala/ox/resilience/AdaptiveRetry.scala | 0 .../scala/ox/resilience/CircuitBreaker.scala | 0 .../ox/resilience/CircuitBreakerConfig.scala | 0 .../CircuitBreakerStateMachine.scala | 0 .../DurationRateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/RateLimiter.scala | 0 .../ox/resilience/RateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/ResultPolicy.scala | 0 .../scala/ox/resilience/RetryConfig.scala | 0 .../StartTimeRateLimiterAlgorithm.scala | 0 .../scala/ox/resilience/TokenBucket.scala | 0 .../src/main/scala/ox/resilience/retry.scala | 0 .../src/main/scala/ox/resource.scala | 0 .../src/main/scala/ox/scheduling/Jitter.scala | 0 .../scala/ox/scheduling/RepeatConfig.scala | 0 .../main/scala/ox/scheduling/Schedule.scala | 0 .../src/main/scala/ox/scheduling/repeat.scala | 0 .../main/scala/ox/scheduling/scheduled.scala | 0 .../src/main/scala/ox/supervised.scala | 0 .../src/main/scala/ox/unsupervised.scala | 0 .../{shared => }/src/main/scala/ox/util.scala | 0 .../main/scalajvm}/ox/channels/Channel.scala | 0 .../scalajvm}/ox/channels/ChannelClosed.scala | 0 .../main/scalajvm}/ox/channels/select.scala | 0 .../scalanative}/ox/channels/Channel.scala | 0 .../ox/channels/ChannelClosed.scala | 0 .../ox/channels/jox/CellState.scala | 0 .../ox/channels/jox/Channel.scala | 0 .../ox/channels/jox/ChannelClosed.scala | 0 .../ox/channels/jox/CloseableChannel.scala | 0 .../ox/channels/jox/Continuation.scala | 0 .../scalanative}/ox/channels/jox/README.md | 0 .../ox/channels/jox/Segment.scala | 0 .../scalanative}/ox/channels/jox/Select.scala | 0 .../ox/channels/jox/SelectClause.scala | 0 .../scalanative}/ox/channels/jox/Sink.scala | 0 .../scalanative}/ox/channels/jox/Source.scala | 0 .../ox/channels/jox/StoredSelectClause.scala | 0 .../scalanative}/ox/channels/select.scala | 0 .../src/test/scala/ox/AppErrorTest.scala | 0 .../src/test/scala/ox/CancelTest.scala | 0 .../src/test/scala/ox/ChunkTest.scala | 0 .../src/test/scala/ox/CollectParTest.scala | 0 .../src/test/scala/ox/ControlTest.scala | 0 .../src/test/scala/ox/EitherTest.scala | 0 .../src/test/scala/ox/ExceptionTest.scala | 0 .../src/test/scala/ox/FilterParTest.scala | 0 .../src/test/scala/ox/ForeachParTest.scala | 0 .../src/test/scala/ox/ForkTest.scala | 0 .../src/test/scala/ox/LocalTest.scala | 0 .../src/test/scala/ox/MapParTest.scala | 0 .../src/test/scala/ox/OxAppTest.scala | 0 .../src/test/scala/ox/ParTest.scala | 0 .../src/test/scala/ox/RaceTest.scala | 0 .../src/test/scala/ox/ResourceTest.scala | 0 .../src/test/scala/ox/SupervisedTest.scala | 0 .../src/test/scala/ox/UtilTest.scala | 0 .../test/scala/ox/channels/ActorTest.scala | 0 .../test/scala/ox/channels/ChannelTest.scala | 0 .../scala/ox/channels/ChannelTryTest.scala | 0 .../channels/SelectOrClosedWithinTest.scala | 0 .../scala/ox/channels/SelectWithinTest.scala | 0 .../ox/channels/SourceOpsEmptyTest.scala | 0 .../SourceOpsFactoryMethodsTest.scala | 0 .../ox/channels/SourceOpsFailedTest.scala | 0 .../ox/channels/SourceOpsForeachTest.scala | 0 .../channels/SourceOpsFutureSourceTest.scala | 0 .../ox/channels/SourceOpsFutureTest.scala | 0 .../scala/ox/channels/SourceOpsTest.scala | 0 .../ox/channels/SourceOpsTransformTest.scala | 0 .../ox/flow/FlowCompanionIOOpsTest.scala | 0 .../scala/ox/flow/FlowCompanionOpsTest.scala | 0 .../test/scala/ox/flow/FlowIOOpsTest.scala | 0 .../scala/ox/flow/FlowOpsAlsoToTapTest.scala | 0 .../scala/ox/flow/FlowOpsAlsoToTest.scala | 0 .../test/scala/ox/flow/FlowOpsBatchTest.scala | 0 .../ox/flow/FlowOpsBatchWeightedTest.scala | 0 .../scala/ox/flow/FlowOpsBufferTest.scala | 0 .../scala/ox/flow/FlowOpsCollectTest.scala | 0 .../ox/flow/FlowOpsConcatPrependTest.scala | 0 .../scala/ox/flow/FlowOpsConcatTest.scala | 0 .../scala/ox/flow/FlowOpsConflateTest.scala | 0 .../scala/ox/flow/FlowOpsDebounceByTest.scala | 0 .../scala/ox/flow/FlowOpsDebounceTest.scala | 0 .../test/scala/ox/flow/FlowOpsDrainTest.scala | 0 .../test/scala/ox/flow/FlowOpsDropTest.scala | 0 .../test/scala/ox/flow/FlowOpsEmptyTest.scala | 0 .../scala/ox/flow/FlowOpsExpandTest.scala | 0 .../ox/flow/FlowOpsExtrapolateTest.scala | 0 .../ox/flow/FlowOpsFactoryMethodsTest.scala | 0 .../scala/ox/flow/FlowOpsFailedTest.scala | 0 .../scala/ox/flow/FlowOpsFilterTest.scala | 0 .../scala/ox/flow/FlowOpsFlatMapTest.scala | 0 .../scala/ox/flow/FlowOpsFlattenParTest.scala | 0 .../scala/ox/flow/FlowOpsFlattenTest.scala | 0 .../test/scala/ox/flow/FlowOpsFoldTest.scala | 0 .../scala/ox/flow/FlowOpsForeachTest.scala | 0 .../ox/flow/FlowOpsFutureSourceTest.scala | 0 .../scala/ox/flow/FlowOpsFutureTest.scala | 0 .../scala/ox/flow/FlowOpsGroupByTest.scala | 0 .../scala/ox/flow/FlowOpsGroupedTest.scala | 0 .../ox/flow/FlowOpsInterleaveAllTest.scala | 0 .../scala/ox/flow/FlowOpsInterleaveTest.scala | 0 .../ox/flow/FlowOpsIntersperseTest.scala | 0 .../scala/ox/flow/FlowOpsLastOptionTest.scala | 0 .../test/scala/ox/flow/FlowOpsLastTest.scala | 0 .../scala/ox/flow/FlowOpsMapConcatTest.scala | 0 .../scala/ox/flow/FlowOpsMapParTest.scala | 0 .../ox/flow/FlowOpsMapParUnorderedTest.scala | 0 .../flow/FlowOpsMapStatefulConcatTest.scala | 0 .../ox/flow/FlowOpsMapStatefulTest.scala | 0 .../test/scala/ox/flow/FlowOpsMapTest.scala | 0 .../ox/flow/FlowOpsMapUsingSinkTest.scala | 0 .../ox/flow/FlowOpsMapWithResourceTest.scala | 0 .../test/scala/ox/flow/FlowOpsMergeTest.scala | 0 .../scala/ox/flow/FlowOpsOnCompleteTest.scala | 0 .../ox/flow/FlowOpsOnErrorCompleteTest.scala | 0 .../ox/flow/FlowOpsOnErrorRecoverTest.scala | 0 .../scala/ox/flow/FlowOpsOrElseTest.scala | 0 .../scala/ox/flow/FlowOpsPipeToTest.scala | 0 .../scala/ox/flow/FlowOpsRecoverTest.scala | 0 .../ox/flow/FlowOpsRecoverWithRetryTest.scala | 0 .../ox/flow/FlowOpsRecoverWithTest.scala | 0 .../scala/ox/flow/FlowOpsReduceTest.scala | 0 .../scala/ox/flow/FlowOpsRepeatEvalTest.scala | 0 .../test/scala/ox/flow/FlowOpsRetryTest.scala | 0 .../ox/flow/FlowOpsRunToChannelTest.scala | 0 .../scala/ox/flow/FlowOpsRunToMapTest.scala | 0 .../scala/ox/flow/FlowOpsRunToSetTest.scala | 0 .../scala/ox/flow/FlowOpsSampleTest.scala | 0 .../test/scala/ox/flow/FlowOpsScanTest.scala | 0 .../scala/ox/flow/FlowOpsSlidingTest.scala | 0 .../scala/ox/flow/FlowOpsSplitOnTest.scala | 0 .../test/scala/ox/flow/FlowOpsSplitTest.scala | 0 .../scala/ox/flow/FlowOpsTakeLastTest.scala | 0 .../test/scala/ox/flow/FlowOpsTakeTest.scala | 0 .../scala/ox/flow/FlowOpsTakeWhileTest.scala | 0 .../test/scala/ox/flow/FlowOpsTapTest.scala | 0 .../scala/ox/flow/FlowOpsThrottleTest.scala | 0 .../test/scala/ox/flow/FlowOpsTickTest.scala | 0 .../scala/ox/flow/FlowOpsTimeoutTest.scala | 0 .../ox/flow/FlowOpsUsingChannelTest.scala | 0 .../test/scala/ox/flow/FlowOpsUsingSink.scala | 0 .../scala/ox/flow/FlowOpsZipAllTest.scala | 0 .../test/scala/ox/flow/FlowOpsZipTest.scala | 0 .../ox/flow/FlowOpsZipWithIndexTest.scala | 0 .../test/scala/ox/flow/FlowTextOpsTest.scala | 0 .../ox/flow/internal/WeightedHeapTest.scala | 0 .../ox/resilience/AfterAttemptTest.scala | 0 .../ox/resilience/BackoffRetryTest.scala | 0 .../CircuitBreakerStateMachineTest.scala | 0 .../ox/resilience/CircuitBreakerTest.scala | 0 .../resilience/FixedIntervalRetryTest.scala | 0 .../ox/resilience/ImmediateRetryTest.scala | 0 .../resilience/RateLimiterInterfaceTest.scala | 0 .../scala/ox/resilience/RateLimiterTest.scala | 0 .../ScheduleFallingBackRetryTest.scala | 0 .../ox/scheduling/FixedRateRepeatTest.scala | 0 .../ox/scheduling/ImmediateRepeatTest.scala | 0 .../test/scala/ox/scheduling/JitterTest.scala | 0 .../src/test/scala/ox/util/ElapsedTime.scala | 0 .../src/test/scala/ox/util/MaxCounter.scala | 0 .../src/test/scala/ox/util/Trail.scala | 0 .../reactive/FlowPublisherPekkoTest.scala | 0 .../flow/reactive/FlowPublisherTckTest.scala | 0 .../ox/channels/jox/ChannelBufferedTest.scala | 0 .../ox/channels/jox/ChannelClosedTest.scala | 0 .../jox/ChannelInterruptionTest.scala | 0 .../channels/jox/ChannelRendezvousTest.scala | 0 .../jox/ChannelTrySendReceiveTest.scala | 0 .../channels/jox/ChannelUnlimitedTest.scala | 0 .../ox/channels/jox/SelectTest.scala | 0 .../ox/channels/jox/TestUtil.scala | 0 project/plugins.sbt | 2 +- 208 files changed, 41 insertions(+), 37 deletions(-) rename core/{shared => }/src/main/scala/ox/Chunk.scala (100%) rename core/{shared => }/src/main/scala/ox/ErrorMode.scala (100%) rename core/{shared => }/src/main/scala/ox/Ox.scala (100%) rename core/{shared => }/src/main/scala/ox/OxApp.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/BufferCapacity.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/ChannelClosedUnion.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/SourceCompanionOps.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/SourceDrainOps.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/SourceOps.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/actor.scala (100%) rename core/{shared => }/src/main/scala/ox/channels/forkPropagate.scala (100%) rename core/{shared => }/src/main/scala/ox/collections.scala (100%) rename core/{shared => }/src/main/scala/ox/control.scala (100%) rename core/{shared => }/src/main/scala/ox/either.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/Flow.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowCompanionIOOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowCompanionOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowIOOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowReactiveOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowRunOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/FlowTextOps.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/internal/WeightedHeap.scala (100%) rename core/{shared => }/src/main/scala/ox/flow/internal/groupByImpl.scala (100%) rename core/{shared => }/src/main/scala/ox/fork.scala (100%) rename core/{shared => }/src/main/scala/ox/inScopeRunner.scala (100%) rename core/{shared => }/src/main/scala/ox/internal/ScopeContext.scala (100%) rename core/{shared => }/src/main/scala/ox/internal/ThreadHerd.scala (100%) rename core/{shared => }/src/main/scala/ox/local.scala (100%) rename core/{shared => }/src/main/scala/ox/oxThreadFactory.scala (100%) rename core/{shared => }/src/main/scala/ox/par.scala (100%) rename core/{shared => }/src/main/scala/ox/race.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/AdaptiveRetry.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/CircuitBreaker.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/CircuitBreakerConfig.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/RateLimiter.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/ResultPolicy.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/RetryConfig.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/TokenBucket.scala (100%) rename core/{shared => }/src/main/scala/ox/resilience/retry.scala (100%) rename core/{shared => }/src/main/scala/ox/resource.scala (100%) rename core/{shared => }/src/main/scala/ox/scheduling/Jitter.scala (100%) rename core/{shared => }/src/main/scala/ox/scheduling/RepeatConfig.scala (100%) rename core/{shared => }/src/main/scala/ox/scheduling/Schedule.scala (100%) rename core/{shared => }/src/main/scala/ox/scheduling/repeat.scala (100%) rename core/{shared => }/src/main/scala/ox/scheduling/scheduled.scala (100%) rename core/{shared => }/src/main/scala/ox/supervised.scala (100%) rename core/{shared => }/src/main/scala/ox/unsupervised.scala (100%) rename core/{shared => }/src/main/scala/ox/util.scala (100%) rename core/{jvm/src/main/scala => src/main/scalajvm}/ox/channels/Channel.scala (100%) rename core/{jvm/src/main/scala => src/main/scalajvm}/ox/channels/ChannelClosed.scala (100%) rename core/{jvm/src/main/scala => src/main/scalajvm}/ox/channels/select.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/Channel.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/ChannelClosed.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/CellState.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Channel.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/ChannelClosed.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/CloseableChannel.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Continuation.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/README.md (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Segment.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Select.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/SelectClause.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Sink.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/Source.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/jox/StoredSelectClause.scala (100%) rename core/{native/src/main/scala => src/main/scalanative}/ox/channels/select.scala (100%) rename core/{shared => }/src/test/scala/ox/AppErrorTest.scala (100%) rename core/{shared => }/src/test/scala/ox/CancelTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ChunkTest.scala (100%) rename core/{shared => }/src/test/scala/ox/CollectParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ControlTest.scala (100%) rename core/{shared => }/src/test/scala/ox/EitherTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ExceptionTest.scala (100%) rename core/{shared => }/src/test/scala/ox/FilterParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ForeachParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ForkTest.scala (100%) rename core/{shared => }/src/test/scala/ox/LocalTest.scala (100%) rename core/{shared => }/src/test/scala/ox/MapParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/OxAppTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/RaceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/ResourceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/SupervisedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/UtilTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/ActorTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/ChannelTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/ChannelTryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SelectWithinTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsEmptyTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsFailedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsForeachTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsFutureTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/channels/SourceOpsTransformTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowCompanionOpsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowIOOpsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsBatchTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsBufferTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsCollectTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsConcatTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsConflateTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsDebounceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsDrainTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsDropTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsEmptyTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsExpandTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFailedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFilterTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFlattenTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFoldTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsForeachTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsFutureTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsGroupByTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsGroupedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsLastTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapParTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsMergeTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsOrElseTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsPipeToTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRecoverTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsReduceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsSampleTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsScanTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsSlidingTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsSplitTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTakeTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsThrottleTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTickTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsUsingSink.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsZipAllTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsZipTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/FlowTextOpsTest.scala (100%) rename core/{shared => }/src/test/scala/ox/flow/internal/WeightedHeapTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/AfterAttemptTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/BackoffRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/CircuitBreakerTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/ImmediateRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/RateLimiterTest.scala (100%) rename core/{shared => }/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala (100%) rename core/{shared => }/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala (100%) rename core/{shared => }/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala (100%) rename core/{shared => }/src/test/scala/ox/scheduling/JitterTest.scala (100%) rename core/{shared => }/src/test/scala/ox/util/ElapsedTime.scala (100%) rename core/{shared => }/src/test/scala/ox/util/MaxCounter.scala (100%) rename core/{shared => }/src/test/scala/ox/util/Trail.scala (100%) rename core/{jvm/src/test/scala => src/test/scalajvm}/ox/flow/reactive/FlowPublisherPekkoTest.scala (100%) rename core/{jvm/src/test/scala => src/test/scalajvm}/ox/flow/reactive/FlowPublisherTckTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelBufferedTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelClosedTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelInterruptionTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelRendezvousTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelTrySendReceiveTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/ChannelUnlimitedTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/SelectTest.scala (100%) rename core/{native/src/test/scala => src/test/scalanative}/ox/channels/jox/TestUtil.scala (100%) diff --git a/build.sbt b/build.sbt index 56b3da08..9468ccda 100644 --- a/build.sbt +++ b/build.sbt @@ -2,12 +2,13 @@ import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings import com.softwaremill.Publish.{ossPublishSettings, updateDocs} import com.softwaremill.UpdateVersionInDocs import com.typesafe.tools.mima.core.{MissingClassProblem, ProblemFilters} -import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} import scalanative.build._ +lazy val scala3 = "3.3.7" + lazy val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( organization := "com.softwaremill.ox", - scalaVersion := "3.3.7", + scalaVersion := scala3, updateDocs := Def.taskDyn { val files1 = UpdateVersionInDocs(sLog.value, organization.value, version.value) Def.task { @@ -52,50 +53,53 @@ compileDocumentation := { lazy val rootProject = (project in file(".")) .settings(commonSettings) .settings(publishArtifact := false, name := "ox") - .aggregate(core.jvm, core.native, kafka, mdcLogback, flowReactiveStreams, cron, otelContext) + .aggregate(core.projectRefs ++ examples.projectRefs ++ Seq[ProjectReference](kafka, mdcLogback, flowReactiveStreams, cron, otelContext): _*) -lazy val core = crossProject(JVMPlatform, NativePlatform) - .crossType(CrossType.Full) - .in(file("core")) +lazy val core = (projectMatrix in file("core")) .settings(commonSettings) .settings( name := "core", - libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.20" % Test, - Test / fork := true + libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.20" % Test ) - .jvmSettings( - libraryDependencies ++= Seq( - "com.softwaremill.jox" % "channels" % "1.1.2", - "org.apache.pekko" %% "pekko-stream" % "1.6.0" % Test, - "org.reactivestreams" % "reactive-streams-tck-flow" % "1.0.4" % Test + .jvmPlatform( + scalaVersions = Seq(scala3), + settings = enableMimaSettings ++ Seq( + Test / fork := true, + libraryDependencies ++= Seq( + "com.softwaremill.jox" % "channels" % "1.1.2", + "org.apache.pekko" %% "pekko-stream" % "1.6.0" % Test, + "org.reactivestreams" % "reactive-streams-tck-flow" % "1.0.4" % Test + ) ) ) - .jvmSettings(enableMimaSettings) - .nativeSettings( - Test / fork := false, - // Only include native-specific tests; shared Ox tests use JVM-only APIs (java.time, java.net, etc.) - Test / unmanagedSourceDirectories := Seq((Test / sourceDirectory).value / "scala"), - nativeConfig ~= { - _.withMultithreading(true) - } + .nativePlatform( + scalaVersions = Seq(scala3), + settings = Seq( + Test / fork := false, + // Only include native-specific tests; shared Ox tests use JVM-only APIs (java.time, java.net, etc.) + Test / unmanagedSourceDirectories := Seq((Test / sourceDirectory).value / "scalanative"), + nativeConfig ~= { _.withMultithreading(true) } + ) ) -lazy val examples = crossProject(JVMPlatform, NativePlatform) - .crossType(CrossType.Pure) - .in(file("examples")) +lazy val examples = (projectMatrix in file("examples")) .settings(commonSettings) .settings( name := "examples", publishArtifact := false, Compile / mainClass := Some("VirtualThreadsNativeJvmBenchmark") ) - .jvmSettings( - assembly / assemblyJarName := "examples-assembly.jar" + .jvmPlatform( + scalaVersions = Seq(scala3), + settings = Seq( + assembly / assemblyJarName := "examples-assembly.jar" + ) ) - .nativeSettings( - nativeConfig ~= { - _.withMultithreading(true) - } + .nativePlatform( + scalaVersions = Seq(scala3), + settings = Seq( + nativeConfig ~= { _.withMultithreading(true) } + ) ) .dependsOn(core) @@ -113,7 +117,7 @@ lazy val kafka: Project = (project in file("kafka")) scalaTest ) ) - .dependsOn(core.jvm) + .dependsOn(core.jvm(scala3)) lazy val mdcLogback: Project = (project in file("mdc-logback")) .settings(commonSettings) @@ -124,7 +128,7 @@ lazy val mdcLogback: Project = (project in file("mdc-logback")) scalaTest ) ) - .dependsOn(core.jvm) + .dependsOn(core.jvm(scala3)) lazy val flowReactiveStreams: Project = (project in file("flow-reactive-streams")) .settings(commonSettings) @@ -135,7 +139,7 @@ lazy val flowReactiveStreams: Project = (project in file("flow-reactive-streams" scalaTest ) ) - .dependsOn(core.jvm) + .dependsOn(core.jvm(scala3)) lazy val cron: Project = (project in file("cron")) .settings(commonSettings) @@ -146,7 +150,7 @@ lazy val cron: Project = (project in file("cron")) scalaTest ) ) - .dependsOn(core.jvm % "test->test;compile->compile") + .dependsOn(core.jvm(scala3) % "test->test;compile->compile") lazy val otelContext: Project = (project in file("otel-context")) .settings(commonSettings) @@ -157,7 +161,7 @@ lazy val otelContext: Project = (project in file("otel-context")) scalaTest ) ) - .dependsOn(core.jvm % "test->test;compile->compile") + .dependsOn(core.jvm(scala3) % "test->test;compile->compile") lazy val documentation: Project = (project in file("generated-doc")) // important: it must not be doc/ .enablePlugins(MdocPlugin) @@ -175,7 +179,7 @@ lazy val documentation: Project = (project in file("generated-doc")) // importan libraryDependencies ++= Seq(logback % Test) ) .dependsOn( - core.jvm, + core.jvm(scala3), kafka, mdcLogback, flowReactiveStreams, diff --git a/core/shared/src/main/scala/ox/Chunk.scala b/core/src/main/scala/ox/Chunk.scala similarity index 100% rename from core/shared/src/main/scala/ox/Chunk.scala rename to core/src/main/scala/ox/Chunk.scala diff --git a/core/shared/src/main/scala/ox/ErrorMode.scala b/core/src/main/scala/ox/ErrorMode.scala similarity index 100% rename from core/shared/src/main/scala/ox/ErrorMode.scala rename to core/src/main/scala/ox/ErrorMode.scala diff --git a/core/shared/src/main/scala/ox/Ox.scala b/core/src/main/scala/ox/Ox.scala similarity index 100% rename from core/shared/src/main/scala/ox/Ox.scala rename to core/src/main/scala/ox/Ox.scala diff --git a/core/shared/src/main/scala/ox/OxApp.scala b/core/src/main/scala/ox/OxApp.scala similarity index 100% rename from core/shared/src/main/scala/ox/OxApp.scala rename to core/src/main/scala/ox/OxApp.scala diff --git a/core/shared/src/main/scala/ox/channels/BufferCapacity.scala b/core/src/main/scala/ox/channels/BufferCapacity.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/BufferCapacity.scala rename to core/src/main/scala/ox/channels/BufferCapacity.scala diff --git a/core/shared/src/main/scala/ox/channels/ChannelClosedUnion.scala b/core/src/main/scala/ox/channels/ChannelClosedUnion.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/ChannelClosedUnion.scala rename to core/src/main/scala/ox/channels/ChannelClosedUnion.scala diff --git a/core/shared/src/main/scala/ox/channels/SourceCompanionOps.scala b/core/src/main/scala/ox/channels/SourceCompanionOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/SourceCompanionOps.scala rename to core/src/main/scala/ox/channels/SourceCompanionOps.scala diff --git a/core/shared/src/main/scala/ox/channels/SourceDrainOps.scala b/core/src/main/scala/ox/channels/SourceDrainOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/SourceDrainOps.scala rename to core/src/main/scala/ox/channels/SourceDrainOps.scala diff --git a/core/shared/src/main/scala/ox/channels/SourceOps.scala b/core/src/main/scala/ox/channels/SourceOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/SourceOps.scala rename to core/src/main/scala/ox/channels/SourceOps.scala diff --git a/core/shared/src/main/scala/ox/channels/actor.scala b/core/src/main/scala/ox/channels/actor.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/actor.scala rename to core/src/main/scala/ox/channels/actor.scala diff --git a/core/shared/src/main/scala/ox/channels/forkPropagate.scala b/core/src/main/scala/ox/channels/forkPropagate.scala similarity index 100% rename from core/shared/src/main/scala/ox/channels/forkPropagate.scala rename to core/src/main/scala/ox/channels/forkPropagate.scala diff --git a/core/shared/src/main/scala/ox/collections.scala b/core/src/main/scala/ox/collections.scala similarity index 100% rename from core/shared/src/main/scala/ox/collections.scala rename to core/src/main/scala/ox/collections.scala diff --git a/core/shared/src/main/scala/ox/control.scala b/core/src/main/scala/ox/control.scala similarity index 100% rename from core/shared/src/main/scala/ox/control.scala rename to core/src/main/scala/ox/control.scala diff --git a/core/shared/src/main/scala/ox/either.scala b/core/src/main/scala/ox/either.scala similarity index 100% rename from core/shared/src/main/scala/ox/either.scala rename to core/src/main/scala/ox/either.scala diff --git a/core/shared/src/main/scala/ox/flow/Flow.scala b/core/src/main/scala/ox/flow/Flow.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/Flow.scala rename to core/src/main/scala/ox/flow/Flow.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowCompanionIOOps.scala b/core/src/main/scala/ox/flow/FlowCompanionIOOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowCompanionIOOps.scala rename to core/src/main/scala/ox/flow/FlowCompanionIOOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowCompanionOps.scala b/core/src/main/scala/ox/flow/FlowCompanionOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowCompanionOps.scala rename to core/src/main/scala/ox/flow/FlowCompanionOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala b/core/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala rename to core/src/main/scala/ox/flow/FlowCompanionReactiveOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowIOOps.scala b/core/src/main/scala/ox/flow/FlowIOOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowIOOps.scala rename to core/src/main/scala/ox/flow/FlowIOOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowOps.scala b/core/src/main/scala/ox/flow/FlowOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowOps.scala rename to core/src/main/scala/ox/flow/FlowOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowReactiveOps.scala b/core/src/main/scala/ox/flow/FlowReactiveOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowReactiveOps.scala rename to core/src/main/scala/ox/flow/FlowReactiveOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowRunOps.scala b/core/src/main/scala/ox/flow/FlowRunOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowRunOps.scala rename to core/src/main/scala/ox/flow/FlowRunOps.scala diff --git a/core/shared/src/main/scala/ox/flow/FlowTextOps.scala b/core/src/main/scala/ox/flow/FlowTextOps.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/FlowTextOps.scala rename to core/src/main/scala/ox/flow/FlowTextOps.scala diff --git a/core/shared/src/main/scala/ox/flow/internal/WeightedHeap.scala b/core/src/main/scala/ox/flow/internal/WeightedHeap.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/internal/WeightedHeap.scala rename to core/src/main/scala/ox/flow/internal/WeightedHeap.scala diff --git a/core/shared/src/main/scala/ox/flow/internal/groupByImpl.scala b/core/src/main/scala/ox/flow/internal/groupByImpl.scala similarity index 100% rename from core/shared/src/main/scala/ox/flow/internal/groupByImpl.scala rename to core/src/main/scala/ox/flow/internal/groupByImpl.scala diff --git a/core/shared/src/main/scala/ox/fork.scala b/core/src/main/scala/ox/fork.scala similarity index 100% rename from core/shared/src/main/scala/ox/fork.scala rename to core/src/main/scala/ox/fork.scala diff --git a/core/shared/src/main/scala/ox/inScopeRunner.scala b/core/src/main/scala/ox/inScopeRunner.scala similarity index 100% rename from core/shared/src/main/scala/ox/inScopeRunner.scala rename to core/src/main/scala/ox/inScopeRunner.scala diff --git a/core/shared/src/main/scala/ox/internal/ScopeContext.scala b/core/src/main/scala/ox/internal/ScopeContext.scala similarity index 100% rename from core/shared/src/main/scala/ox/internal/ScopeContext.scala rename to core/src/main/scala/ox/internal/ScopeContext.scala diff --git a/core/shared/src/main/scala/ox/internal/ThreadHerd.scala b/core/src/main/scala/ox/internal/ThreadHerd.scala similarity index 100% rename from core/shared/src/main/scala/ox/internal/ThreadHerd.scala rename to core/src/main/scala/ox/internal/ThreadHerd.scala diff --git a/core/shared/src/main/scala/ox/local.scala b/core/src/main/scala/ox/local.scala similarity index 100% rename from core/shared/src/main/scala/ox/local.scala rename to core/src/main/scala/ox/local.scala diff --git a/core/shared/src/main/scala/ox/oxThreadFactory.scala b/core/src/main/scala/ox/oxThreadFactory.scala similarity index 100% rename from core/shared/src/main/scala/ox/oxThreadFactory.scala rename to core/src/main/scala/ox/oxThreadFactory.scala diff --git a/core/shared/src/main/scala/ox/par.scala b/core/src/main/scala/ox/par.scala similarity index 100% rename from core/shared/src/main/scala/ox/par.scala rename to core/src/main/scala/ox/par.scala diff --git a/core/shared/src/main/scala/ox/race.scala b/core/src/main/scala/ox/race.scala similarity index 100% rename from core/shared/src/main/scala/ox/race.scala rename to core/src/main/scala/ox/race.scala diff --git a/core/shared/src/main/scala/ox/resilience/AdaptiveRetry.scala b/core/src/main/scala/ox/resilience/AdaptiveRetry.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/AdaptiveRetry.scala rename to core/src/main/scala/ox/resilience/AdaptiveRetry.scala diff --git a/core/shared/src/main/scala/ox/resilience/CircuitBreaker.scala b/core/src/main/scala/ox/resilience/CircuitBreaker.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/CircuitBreaker.scala rename to core/src/main/scala/ox/resilience/CircuitBreaker.scala diff --git a/core/shared/src/main/scala/ox/resilience/CircuitBreakerConfig.scala b/core/src/main/scala/ox/resilience/CircuitBreakerConfig.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/CircuitBreakerConfig.scala rename to core/src/main/scala/ox/resilience/CircuitBreakerConfig.scala diff --git a/core/shared/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala b/core/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala rename to core/src/main/scala/ox/resilience/CircuitBreakerStateMachine.scala diff --git a/core/shared/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala b/core/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala rename to core/src/main/scala/ox/resilience/DurationRateLimiterAlgorithm.scala diff --git a/core/shared/src/main/scala/ox/resilience/RateLimiter.scala b/core/src/main/scala/ox/resilience/RateLimiter.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/RateLimiter.scala rename to core/src/main/scala/ox/resilience/RateLimiter.scala diff --git a/core/shared/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala b/core/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala rename to core/src/main/scala/ox/resilience/RateLimiterAlgorithm.scala diff --git a/core/shared/src/main/scala/ox/resilience/ResultPolicy.scala b/core/src/main/scala/ox/resilience/ResultPolicy.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/ResultPolicy.scala rename to core/src/main/scala/ox/resilience/ResultPolicy.scala diff --git a/core/shared/src/main/scala/ox/resilience/RetryConfig.scala b/core/src/main/scala/ox/resilience/RetryConfig.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/RetryConfig.scala rename to core/src/main/scala/ox/resilience/RetryConfig.scala diff --git a/core/shared/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala b/core/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala rename to core/src/main/scala/ox/resilience/StartTimeRateLimiterAlgorithm.scala diff --git a/core/shared/src/main/scala/ox/resilience/TokenBucket.scala b/core/src/main/scala/ox/resilience/TokenBucket.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/TokenBucket.scala rename to core/src/main/scala/ox/resilience/TokenBucket.scala diff --git a/core/shared/src/main/scala/ox/resilience/retry.scala b/core/src/main/scala/ox/resilience/retry.scala similarity index 100% rename from core/shared/src/main/scala/ox/resilience/retry.scala rename to core/src/main/scala/ox/resilience/retry.scala diff --git a/core/shared/src/main/scala/ox/resource.scala b/core/src/main/scala/ox/resource.scala similarity index 100% rename from core/shared/src/main/scala/ox/resource.scala rename to core/src/main/scala/ox/resource.scala diff --git a/core/shared/src/main/scala/ox/scheduling/Jitter.scala b/core/src/main/scala/ox/scheduling/Jitter.scala similarity index 100% rename from core/shared/src/main/scala/ox/scheduling/Jitter.scala rename to core/src/main/scala/ox/scheduling/Jitter.scala diff --git a/core/shared/src/main/scala/ox/scheduling/RepeatConfig.scala b/core/src/main/scala/ox/scheduling/RepeatConfig.scala similarity index 100% rename from core/shared/src/main/scala/ox/scheduling/RepeatConfig.scala rename to core/src/main/scala/ox/scheduling/RepeatConfig.scala diff --git a/core/shared/src/main/scala/ox/scheduling/Schedule.scala b/core/src/main/scala/ox/scheduling/Schedule.scala similarity index 100% rename from core/shared/src/main/scala/ox/scheduling/Schedule.scala rename to core/src/main/scala/ox/scheduling/Schedule.scala diff --git a/core/shared/src/main/scala/ox/scheduling/repeat.scala b/core/src/main/scala/ox/scheduling/repeat.scala similarity index 100% rename from core/shared/src/main/scala/ox/scheduling/repeat.scala rename to core/src/main/scala/ox/scheduling/repeat.scala diff --git a/core/shared/src/main/scala/ox/scheduling/scheduled.scala b/core/src/main/scala/ox/scheduling/scheduled.scala similarity index 100% rename from core/shared/src/main/scala/ox/scheduling/scheduled.scala rename to core/src/main/scala/ox/scheduling/scheduled.scala diff --git a/core/shared/src/main/scala/ox/supervised.scala b/core/src/main/scala/ox/supervised.scala similarity index 100% rename from core/shared/src/main/scala/ox/supervised.scala rename to core/src/main/scala/ox/supervised.scala diff --git a/core/shared/src/main/scala/ox/unsupervised.scala b/core/src/main/scala/ox/unsupervised.scala similarity index 100% rename from core/shared/src/main/scala/ox/unsupervised.scala rename to core/src/main/scala/ox/unsupervised.scala diff --git a/core/shared/src/main/scala/ox/util.scala b/core/src/main/scala/ox/util.scala similarity index 100% rename from core/shared/src/main/scala/ox/util.scala rename to core/src/main/scala/ox/util.scala diff --git a/core/jvm/src/main/scala/ox/channels/Channel.scala b/core/src/main/scalajvm/ox/channels/Channel.scala similarity index 100% rename from core/jvm/src/main/scala/ox/channels/Channel.scala rename to core/src/main/scalajvm/ox/channels/Channel.scala diff --git a/core/jvm/src/main/scala/ox/channels/ChannelClosed.scala b/core/src/main/scalajvm/ox/channels/ChannelClosed.scala similarity index 100% rename from core/jvm/src/main/scala/ox/channels/ChannelClosed.scala rename to core/src/main/scalajvm/ox/channels/ChannelClosed.scala diff --git a/core/jvm/src/main/scala/ox/channels/select.scala b/core/src/main/scalajvm/ox/channels/select.scala similarity index 100% rename from core/jvm/src/main/scala/ox/channels/select.scala rename to core/src/main/scalajvm/ox/channels/select.scala diff --git a/core/native/src/main/scala/ox/channels/Channel.scala b/core/src/main/scalanative/ox/channels/Channel.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/Channel.scala rename to core/src/main/scalanative/ox/channels/Channel.scala diff --git a/core/native/src/main/scala/ox/channels/ChannelClosed.scala b/core/src/main/scalanative/ox/channels/ChannelClosed.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/ChannelClosed.scala rename to core/src/main/scalanative/ox/channels/ChannelClosed.scala diff --git a/core/native/src/main/scala/ox/channels/jox/CellState.scala b/core/src/main/scalanative/ox/channels/jox/CellState.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/CellState.scala rename to core/src/main/scalanative/ox/channels/jox/CellState.scala diff --git a/core/native/src/main/scala/ox/channels/jox/Channel.scala b/core/src/main/scalanative/ox/channels/jox/Channel.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Channel.scala rename to core/src/main/scalanative/ox/channels/jox/Channel.scala diff --git a/core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala b/core/src/main/scalanative/ox/channels/jox/ChannelClosed.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/ChannelClosed.scala rename to core/src/main/scalanative/ox/channels/jox/ChannelClosed.scala diff --git a/core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala b/core/src/main/scalanative/ox/channels/jox/CloseableChannel.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/CloseableChannel.scala rename to core/src/main/scalanative/ox/channels/jox/CloseableChannel.scala diff --git a/core/native/src/main/scala/ox/channels/jox/Continuation.scala b/core/src/main/scalanative/ox/channels/jox/Continuation.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Continuation.scala rename to core/src/main/scalanative/ox/channels/jox/Continuation.scala diff --git a/core/native/src/main/scala/ox/channels/jox/README.md b/core/src/main/scalanative/ox/channels/jox/README.md similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/README.md rename to core/src/main/scalanative/ox/channels/jox/README.md diff --git a/core/native/src/main/scala/ox/channels/jox/Segment.scala b/core/src/main/scalanative/ox/channels/jox/Segment.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Segment.scala rename to core/src/main/scalanative/ox/channels/jox/Segment.scala diff --git a/core/native/src/main/scala/ox/channels/jox/Select.scala b/core/src/main/scalanative/ox/channels/jox/Select.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Select.scala rename to core/src/main/scalanative/ox/channels/jox/Select.scala diff --git a/core/native/src/main/scala/ox/channels/jox/SelectClause.scala b/core/src/main/scalanative/ox/channels/jox/SelectClause.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/SelectClause.scala rename to core/src/main/scalanative/ox/channels/jox/SelectClause.scala diff --git a/core/native/src/main/scala/ox/channels/jox/Sink.scala b/core/src/main/scalanative/ox/channels/jox/Sink.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Sink.scala rename to core/src/main/scalanative/ox/channels/jox/Sink.scala diff --git a/core/native/src/main/scala/ox/channels/jox/Source.scala b/core/src/main/scalanative/ox/channels/jox/Source.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/Source.scala rename to core/src/main/scalanative/ox/channels/jox/Source.scala diff --git a/core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala b/core/src/main/scalanative/ox/channels/jox/StoredSelectClause.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/jox/StoredSelectClause.scala rename to core/src/main/scalanative/ox/channels/jox/StoredSelectClause.scala diff --git a/core/native/src/main/scala/ox/channels/select.scala b/core/src/main/scalanative/ox/channels/select.scala similarity index 100% rename from core/native/src/main/scala/ox/channels/select.scala rename to core/src/main/scalanative/ox/channels/select.scala diff --git a/core/shared/src/test/scala/ox/AppErrorTest.scala b/core/src/test/scala/ox/AppErrorTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/AppErrorTest.scala rename to core/src/test/scala/ox/AppErrorTest.scala diff --git a/core/shared/src/test/scala/ox/CancelTest.scala b/core/src/test/scala/ox/CancelTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/CancelTest.scala rename to core/src/test/scala/ox/CancelTest.scala diff --git a/core/shared/src/test/scala/ox/ChunkTest.scala b/core/src/test/scala/ox/ChunkTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ChunkTest.scala rename to core/src/test/scala/ox/ChunkTest.scala diff --git a/core/shared/src/test/scala/ox/CollectParTest.scala b/core/src/test/scala/ox/CollectParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/CollectParTest.scala rename to core/src/test/scala/ox/CollectParTest.scala diff --git a/core/shared/src/test/scala/ox/ControlTest.scala b/core/src/test/scala/ox/ControlTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ControlTest.scala rename to core/src/test/scala/ox/ControlTest.scala diff --git a/core/shared/src/test/scala/ox/EitherTest.scala b/core/src/test/scala/ox/EitherTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/EitherTest.scala rename to core/src/test/scala/ox/EitherTest.scala diff --git a/core/shared/src/test/scala/ox/ExceptionTest.scala b/core/src/test/scala/ox/ExceptionTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ExceptionTest.scala rename to core/src/test/scala/ox/ExceptionTest.scala diff --git a/core/shared/src/test/scala/ox/FilterParTest.scala b/core/src/test/scala/ox/FilterParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/FilterParTest.scala rename to core/src/test/scala/ox/FilterParTest.scala diff --git a/core/shared/src/test/scala/ox/ForeachParTest.scala b/core/src/test/scala/ox/ForeachParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ForeachParTest.scala rename to core/src/test/scala/ox/ForeachParTest.scala diff --git a/core/shared/src/test/scala/ox/ForkTest.scala b/core/src/test/scala/ox/ForkTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ForkTest.scala rename to core/src/test/scala/ox/ForkTest.scala diff --git a/core/shared/src/test/scala/ox/LocalTest.scala b/core/src/test/scala/ox/LocalTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/LocalTest.scala rename to core/src/test/scala/ox/LocalTest.scala diff --git a/core/shared/src/test/scala/ox/MapParTest.scala b/core/src/test/scala/ox/MapParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/MapParTest.scala rename to core/src/test/scala/ox/MapParTest.scala diff --git a/core/shared/src/test/scala/ox/OxAppTest.scala b/core/src/test/scala/ox/OxAppTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/OxAppTest.scala rename to core/src/test/scala/ox/OxAppTest.scala diff --git a/core/shared/src/test/scala/ox/ParTest.scala b/core/src/test/scala/ox/ParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ParTest.scala rename to core/src/test/scala/ox/ParTest.scala diff --git a/core/shared/src/test/scala/ox/RaceTest.scala b/core/src/test/scala/ox/RaceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/RaceTest.scala rename to core/src/test/scala/ox/RaceTest.scala diff --git a/core/shared/src/test/scala/ox/ResourceTest.scala b/core/src/test/scala/ox/ResourceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/ResourceTest.scala rename to core/src/test/scala/ox/ResourceTest.scala diff --git a/core/shared/src/test/scala/ox/SupervisedTest.scala b/core/src/test/scala/ox/SupervisedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/SupervisedTest.scala rename to core/src/test/scala/ox/SupervisedTest.scala diff --git a/core/shared/src/test/scala/ox/UtilTest.scala b/core/src/test/scala/ox/UtilTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/UtilTest.scala rename to core/src/test/scala/ox/UtilTest.scala diff --git a/core/shared/src/test/scala/ox/channels/ActorTest.scala b/core/src/test/scala/ox/channels/ActorTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/ActorTest.scala rename to core/src/test/scala/ox/channels/ActorTest.scala diff --git a/core/shared/src/test/scala/ox/channels/ChannelTest.scala b/core/src/test/scala/ox/channels/ChannelTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/ChannelTest.scala rename to core/src/test/scala/ox/channels/ChannelTest.scala diff --git a/core/shared/src/test/scala/ox/channels/ChannelTryTest.scala b/core/src/test/scala/ox/channels/ChannelTryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/ChannelTryTest.scala rename to core/src/test/scala/ox/channels/ChannelTryTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala b/core/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala rename to core/src/test/scala/ox/channels/SelectOrClosedWithinTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SelectWithinTest.scala b/core/src/test/scala/ox/channels/SelectWithinTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SelectWithinTest.scala rename to core/src/test/scala/ox/channels/SelectWithinTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsEmptyTest.scala b/core/src/test/scala/ox/channels/SourceOpsEmptyTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsEmptyTest.scala rename to core/src/test/scala/ox/channels/SourceOpsEmptyTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala b/core/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala rename to core/src/test/scala/ox/channels/SourceOpsFactoryMethodsTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsFailedTest.scala b/core/src/test/scala/ox/channels/SourceOpsFailedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsFailedTest.scala rename to core/src/test/scala/ox/channels/SourceOpsFailedTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsForeachTest.scala b/core/src/test/scala/ox/channels/SourceOpsForeachTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsForeachTest.scala rename to core/src/test/scala/ox/channels/SourceOpsForeachTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala b/core/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala rename to core/src/test/scala/ox/channels/SourceOpsFutureSourceTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsFutureTest.scala b/core/src/test/scala/ox/channels/SourceOpsFutureTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsFutureTest.scala rename to core/src/test/scala/ox/channels/SourceOpsFutureTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsTest.scala b/core/src/test/scala/ox/channels/SourceOpsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsTest.scala rename to core/src/test/scala/ox/channels/SourceOpsTest.scala diff --git a/core/shared/src/test/scala/ox/channels/SourceOpsTransformTest.scala b/core/src/test/scala/ox/channels/SourceOpsTransformTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/channels/SourceOpsTransformTest.scala rename to core/src/test/scala/ox/channels/SourceOpsTransformTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala b/core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala rename to core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowCompanionOpsTest.scala b/core/src/test/scala/ox/flow/FlowCompanionOpsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowCompanionOpsTest.scala rename to core/src/test/scala/ox/flow/FlowCompanionOpsTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowIOOpsTest.scala b/core/src/test/scala/ox/flow/FlowIOOpsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowIOOpsTest.scala rename to core/src/test/scala/ox/flow/FlowIOOpsTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala b/core/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala rename to core/src/test/scala/ox/flow/FlowOpsAlsoToTapTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala b/core/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala rename to core/src/test/scala/ox/flow/FlowOpsAlsoToTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsBatchTest.scala b/core/src/test/scala/ox/flow/FlowOpsBatchTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsBatchTest.scala rename to core/src/test/scala/ox/flow/FlowOpsBatchTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala b/core/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala rename to core/src/test/scala/ox/flow/FlowOpsBatchWeightedTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsBufferTest.scala b/core/src/test/scala/ox/flow/FlowOpsBufferTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsBufferTest.scala rename to core/src/test/scala/ox/flow/FlowOpsBufferTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsCollectTest.scala b/core/src/test/scala/ox/flow/FlowOpsCollectTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsCollectTest.scala rename to core/src/test/scala/ox/flow/FlowOpsCollectTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala b/core/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala rename to core/src/test/scala/ox/flow/FlowOpsConcatPrependTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsConcatTest.scala b/core/src/test/scala/ox/flow/FlowOpsConcatTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsConcatTest.scala rename to core/src/test/scala/ox/flow/FlowOpsConcatTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsConflateTest.scala b/core/src/test/scala/ox/flow/FlowOpsConflateTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsConflateTest.scala rename to core/src/test/scala/ox/flow/FlowOpsConflateTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala b/core/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala rename to core/src/test/scala/ox/flow/FlowOpsDebounceByTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsDebounceTest.scala b/core/src/test/scala/ox/flow/FlowOpsDebounceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsDebounceTest.scala rename to core/src/test/scala/ox/flow/FlowOpsDebounceTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsDrainTest.scala b/core/src/test/scala/ox/flow/FlowOpsDrainTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsDrainTest.scala rename to core/src/test/scala/ox/flow/FlowOpsDrainTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsDropTest.scala b/core/src/test/scala/ox/flow/FlowOpsDropTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsDropTest.scala rename to core/src/test/scala/ox/flow/FlowOpsDropTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsEmptyTest.scala b/core/src/test/scala/ox/flow/FlowOpsEmptyTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsEmptyTest.scala rename to core/src/test/scala/ox/flow/FlowOpsEmptyTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsExpandTest.scala b/core/src/test/scala/ox/flow/FlowOpsExpandTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsExpandTest.scala rename to core/src/test/scala/ox/flow/FlowOpsExpandTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala b/core/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala rename to core/src/test/scala/ox/flow/FlowOpsExtrapolateTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala b/core/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFactoryMethodsTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFailedTest.scala b/core/src/test/scala/ox/flow/FlowOpsFailedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFailedTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFailedTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFilterTest.scala b/core/src/test/scala/ox/flow/FlowOpsFilterTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFilterTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFilterTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala b/core/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFlatMapTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala b/core/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFlattenParTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFlattenTest.scala b/core/src/test/scala/ox/flow/FlowOpsFlattenTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFlattenTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFlattenTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFoldTest.scala b/core/src/test/scala/ox/flow/FlowOpsFoldTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFoldTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFoldTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsForeachTest.scala b/core/src/test/scala/ox/flow/FlowOpsForeachTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsForeachTest.scala rename to core/src/test/scala/ox/flow/FlowOpsForeachTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala b/core/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFutureSourceTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsFutureTest.scala b/core/src/test/scala/ox/flow/FlowOpsFutureTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsFutureTest.scala rename to core/src/test/scala/ox/flow/FlowOpsFutureTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsGroupByTest.scala b/core/src/test/scala/ox/flow/FlowOpsGroupByTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsGroupByTest.scala rename to core/src/test/scala/ox/flow/FlowOpsGroupByTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsGroupedTest.scala b/core/src/test/scala/ox/flow/FlowOpsGroupedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsGroupedTest.scala rename to core/src/test/scala/ox/flow/FlowOpsGroupedTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala b/core/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala rename to core/src/test/scala/ox/flow/FlowOpsInterleaveAllTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala b/core/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala rename to core/src/test/scala/ox/flow/FlowOpsInterleaveTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala b/core/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala rename to core/src/test/scala/ox/flow/FlowOpsIntersperseTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala b/core/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala rename to core/src/test/scala/ox/flow/FlowOpsLastOptionTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsLastTest.scala b/core/src/test/scala/ox/flow/FlowOpsLastTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsLastTest.scala rename to core/src/test/scala/ox/flow/FlowOpsLastTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapConcatTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapParTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapParTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapParTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapParTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapParUnorderedTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapStatefulConcatTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapStatefulTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapUsingSinkTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala b/core/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMapWithResourceTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsMergeTest.scala b/core/src/test/scala/ox/flow/FlowOpsMergeTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsMergeTest.scala rename to core/src/test/scala/ox/flow/FlowOpsMergeTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala b/core/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala rename to core/src/test/scala/ox/flow/FlowOpsOnCompleteTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala b/core/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala rename to core/src/test/scala/ox/flow/FlowOpsOnErrorCompleteTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala b/core/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala rename to core/src/test/scala/ox/flow/FlowOpsOnErrorRecoverTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsOrElseTest.scala b/core/src/test/scala/ox/flow/FlowOpsOrElseTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsOrElseTest.scala rename to core/src/test/scala/ox/flow/FlowOpsOrElseTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsPipeToTest.scala b/core/src/test/scala/ox/flow/FlowOpsPipeToTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsPipeToTest.scala rename to core/src/test/scala/ox/flow/FlowOpsPipeToTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRecoverTest.scala b/core/src/test/scala/ox/flow/FlowOpsRecoverTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRecoverTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRecoverTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala b/core/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRecoverWithRetryTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala b/core/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRecoverWithTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsReduceTest.scala b/core/src/test/scala/ox/flow/FlowOpsReduceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsReduceTest.scala rename to core/src/test/scala/ox/flow/FlowOpsReduceTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala b/core/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRepeatEvalTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRetryTest.scala b/core/src/test/scala/ox/flow/FlowOpsRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRetryTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRetryTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala b/core/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRunToChannelTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala b/core/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRunToMapTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala b/core/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala rename to core/src/test/scala/ox/flow/FlowOpsRunToSetTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsSampleTest.scala b/core/src/test/scala/ox/flow/FlowOpsSampleTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsSampleTest.scala rename to core/src/test/scala/ox/flow/FlowOpsSampleTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsScanTest.scala b/core/src/test/scala/ox/flow/FlowOpsScanTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsScanTest.scala rename to core/src/test/scala/ox/flow/FlowOpsScanTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsSlidingTest.scala b/core/src/test/scala/ox/flow/FlowOpsSlidingTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsSlidingTest.scala rename to core/src/test/scala/ox/flow/FlowOpsSlidingTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala b/core/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala rename to core/src/test/scala/ox/flow/FlowOpsSplitOnTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsSplitTest.scala b/core/src/test/scala/ox/flow/FlowOpsSplitTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsSplitTest.scala rename to core/src/test/scala/ox/flow/FlowOpsSplitTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala b/core/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTakeLastTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTakeTest.scala b/core/src/test/scala/ox/flow/FlowOpsTakeTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTakeTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTakeTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala b/core/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTakeWhileTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTapTest.scala b/core/src/test/scala/ox/flow/FlowOpsTapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTapTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTapTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsThrottleTest.scala b/core/src/test/scala/ox/flow/FlowOpsThrottleTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsThrottleTest.scala rename to core/src/test/scala/ox/flow/FlowOpsThrottleTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTickTest.scala b/core/src/test/scala/ox/flow/FlowOpsTickTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTickTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTickTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala b/core/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala rename to core/src/test/scala/ox/flow/FlowOpsTimeoutTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala b/core/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala rename to core/src/test/scala/ox/flow/FlowOpsUsingChannelTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsUsingSink.scala b/core/src/test/scala/ox/flow/FlowOpsUsingSink.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsUsingSink.scala rename to core/src/test/scala/ox/flow/FlowOpsUsingSink.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsZipAllTest.scala b/core/src/test/scala/ox/flow/FlowOpsZipAllTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsZipAllTest.scala rename to core/src/test/scala/ox/flow/FlowOpsZipAllTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsZipTest.scala b/core/src/test/scala/ox/flow/FlowOpsZipTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsZipTest.scala rename to core/src/test/scala/ox/flow/FlowOpsZipTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala b/core/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala rename to core/src/test/scala/ox/flow/FlowOpsZipWithIndexTest.scala diff --git a/core/shared/src/test/scala/ox/flow/FlowTextOpsTest.scala b/core/src/test/scala/ox/flow/FlowTextOpsTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/FlowTextOpsTest.scala rename to core/src/test/scala/ox/flow/FlowTextOpsTest.scala diff --git a/core/shared/src/test/scala/ox/flow/internal/WeightedHeapTest.scala b/core/src/test/scala/ox/flow/internal/WeightedHeapTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/flow/internal/WeightedHeapTest.scala rename to core/src/test/scala/ox/flow/internal/WeightedHeapTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/AfterAttemptTest.scala b/core/src/test/scala/ox/resilience/AfterAttemptTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/AfterAttemptTest.scala rename to core/src/test/scala/ox/resilience/AfterAttemptTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/BackoffRetryTest.scala b/core/src/test/scala/ox/resilience/BackoffRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/BackoffRetryTest.scala rename to core/src/test/scala/ox/resilience/BackoffRetryTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala b/core/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala rename to core/src/test/scala/ox/resilience/CircuitBreakerStateMachineTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/CircuitBreakerTest.scala b/core/src/test/scala/ox/resilience/CircuitBreakerTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/CircuitBreakerTest.scala rename to core/src/test/scala/ox/resilience/CircuitBreakerTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala b/core/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala rename to core/src/test/scala/ox/resilience/FixedIntervalRetryTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/ImmediateRetryTest.scala b/core/src/test/scala/ox/resilience/ImmediateRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/ImmediateRetryTest.scala rename to core/src/test/scala/ox/resilience/ImmediateRetryTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala b/core/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala rename to core/src/test/scala/ox/resilience/RateLimiterInterfaceTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/RateLimiterTest.scala b/core/src/test/scala/ox/resilience/RateLimiterTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/RateLimiterTest.scala rename to core/src/test/scala/ox/resilience/RateLimiterTest.scala diff --git a/core/shared/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala b/core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala rename to core/src/test/scala/ox/resilience/ScheduleFallingBackRetryTest.scala diff --git a/core/shared/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala b/core/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala rename to core/src/test/scala/ox/scheduling/FixedRateRepeatTest.scala diff --git a/core/shared/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala b/core/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala rename to core/src/test/scala/ox/scheduling/ImmediateRepeatTest.scala diff --git a/core/shared/src/test/scala/ox/scheduling/JitterTest.scala b/core/src/test/scala/ox/scheduling/JitterTest.scala similarity index 100% rename from core/shared/src/test/scala/ox/scheduling/JitterTest.scala rename to core/src/test/scala/ox/scheduling/JitterTest.scala diff --git a/core/shared/src/test/scala/ox/util/ElapsedTime.scala b/core/src/test/scala/ox/util/ElapsedTime.scala similarity index 100% rename from core/shared/src/test/scala/ox/util/ElapsedTime.scala rename to core/src/test/scala/ox/util/ElapsedTime.scala diff --git a/core/shared/src/test/scala/ox/util/MaxCounter.scala b/core/src/test/scala/ox/util/MaxCounter.scala similarity index 100% rename from core/shared/src/test/scala/ox/util/MaxCounter.scala rename to core/src/test/scala/ox/util/MaxCounter.scala diff --git a/core/shared/src/test/scala/ox/util/Trail.scala b/core/src/test/scala/ox/util/Trail.scala similarity index 100% rename from core/shared/src/test/scala/ox/util/Trail.scala rename to core/src/test/scala/ox/util/Trail.scala diff --git a/core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala b/core/src/test/scalajvm/ox/flow/reactive/FlowPublisherPekkoTest.scala similarity index 100% rename from core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherPekkoTest.scala rename to core/src/test/scalajvm/ox/flow/reactive/FlowPublisherPekkoTest.scala diff --git a/core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala b/core/src/test/scalajvm/ox/flow/reactive/FlowPublisherTckTest.scala similarity index 100% rename from core/jvm/src/test/scala/ox/flow/reactive/FlowPublisherTckTest.scala rename to core/src/test/scalajvm/ox/flow/reactive/FlowPublisherTckTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelBufferedTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelBufferedTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelBufferedTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelClosedTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelClosedTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelClosedTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelInterruptionTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelInterruptionTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelInterruptionTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelRendezvousTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelRendezvousTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelRendezvousTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelTrySendReceiveTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelTrySendReceiveTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelTrySendReceiveTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala b/core/src/test/scalanative/ox/channels/jox/ChannelUnlimitedTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/ChannelUnlimitedTest.scala rename to core/src/test/scalanative/ox/channels/jox/ChannelUnlimitedTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/SelectTest.scala b/core/src/test/scalanative/ox/channels/jox/SelectTest.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/SelectTest.scala rename to core/src/test/scalanative/ox/channels/jox/SelectTest.scala diff --git a/core/native/src/test/scala/ox/channels/jox/TestUtil.scala b/core/src/test/scalanative/ox/channels/jox/TestUtil.scala similarity index 100% rename from core/native/src/test/scala/ox/channels/jox/TestUtil.scala rename to core/src/test/scalanative/ox/channels/jox/TestUtil.scala diff --git a/project/plugins.sbt b/project/plugins.sbt index 0363833e..c0490e4b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,5 +4,5 @@ addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.9.0") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.5") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") From 0ff933bb9ac5569bea5eca787bda3a2ddd5e6f15 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 17:34:25 +0000 Subject: [PATCH 11/17] Format code --- .../src/main/scala/VirtualThreadsNativeJvmBenchmark.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala index 06f55853..be442ce3 100644 --- a/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala +++ b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala @@ -2,8 +2,8 @@ import ox.* import java.util.concurrent.atomic.AtomicLong -/** Spawns 100,000 virtual threads in a supervised scope, each incrementing a shared counter, - * then joins all. Measures wall-clock time to compare JVM vs Scala Native performance. +/** Spawns 100,000 virtual threads in a supervised scope, each incrementing a shared counter, then joins all. Measures wall-clock time to + * compare JVM vs Scala Native performance. * * Prerequisites: JDK 21+ (JVM), clang/LLVM 16+ (Native). * @@ -40,3 +40,5 @@ object VirtualThreadsNativeJvmBenchmark: assert(counter.get() == n, s"Expected $n, got ${counter.get()}") println(s"Spawned and joined $n virtual threads in ${elapsed}ms") + end main +end VirtualThreadsNativeJvmBenchmark From 739a47efbdb5c25bfbcf31a62aad15871ea107e7 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 18:57:52 +0000 Subject: [PATCH 12/17] Update example --- .../scala/VirtualThreadsNativeJvmBenchmark.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala index be442ce3..4818c6da 100644 --- a/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala +++ b/examples/src/main/scala/VirtualThreadsNativeJvmBenchmark.scala @@ -10,16 +10,16 @@ import java.util.concurrent.atomic.AtomicLong * To package & run: * {{{ * # JVM fat jar - * sbt examplesJVM/assembly - * java -jar examples/.jvm/target/scala-3.3.7/examples-assembly.jar + * sbt examples3/assembly + * java -jar examples/target/jvm-3/examples-assembly.jar * * # Native binary - * sbt examplesNative/nativeLink - * ./examples/.native/target/scala-3.3.7/examples + * sbt examplesNative3/nativeLink + * ./examples/target/native-3/example * * # Compare (3 iterations each): - * for i in 1 2 3; do java -jar examples/.jvm/target/scala-3.3.7/examples-assembly.jar; done - * for i in 1 2 3; do ./examples/.native/target/scala-3.3.7/examples; done + * for i in 1 2 3; do java -jar examples/target/jvm-3/examples-assembly.jar; done + * for i in 1 2 3; do ./examples/target/native-3/example; done * }}} */ object VirtualThreadsNativeJvmBenchmark: From 0aa5d3632f345ad1f4581cdb7353b9c2995b9fb2 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 19:06:25 +0000 Subject: [PATCH 13/17] Update docs --- README.md | 12 +++++++----- doc/index.md | 5 +++-- doc/info/dependency.md | 8 +++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8c5e867a..3cc9585f 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ [![Maven Central](https://img.shields.io/maven-central/v/com.softwaremill.ox/core_3)](https://central.sonatype.com/artifact/com.softwaremill.ox/core_3) [![ScalaDoc](https://javadoc.io/badge2/com.softwaremill.ox/core_3/ScalaDoc.svg)](https://javadoc.io/doc/com.softwaremill.ox/core_3) -Safe direct-style streaming, concurrency and resiliency for Scala on the JVM. Requires JDK 21+ & Scala 3. Ox covers -the following areas: +Safe direct-style streaming, concurrency and resiliency for Scala on the JVM. +Requires JDK 21+ & Scala 3. Experimental support for Scala Native. + +Ox covers the following areas: * streaming: push-based backpressured streaming designed for direct-style, with a rich set of stream transformations, flexible stream source & sink definitions and reactive streams integration @@ -22,13 +24,13 @@ preserving developer-friendly stack traces, and without compromising performance To use Ox, add the following dependency, using either [sbt](https://www.scala-sbt.org): ```scala -"com.softwaremill.ox" %% "core" % "1.0.4" +"com.softwaremill.ox" %%% "core" % "1.0.4" ``` Or [scala-cli](https://scala-cli.virtuslab.org): ```scala -//> using dep "com.softwaremill.ox::core:1.0.4" +//> using dep "com.softwaremill.ox:::core:1.0.4" ``` Documentation is available at [https://ox.softwaremill.com](https://ox.softwaremill.com), ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox). @@ -265,4 +267,4 @@ We offer commercial development services. [Contact us](https://softwaremill.com) ## Copyright -Copyright (C) 2023-2025 SoftwareMill [https://softwaremill.com](https://softwaremill.com). +Copyright (C) 2023-2026 SoftwareMill [https://softwaremill.com](https://softwaremill.com). diff --git a/doc/index.md b/doc/index.md index c489321d..e61cd4ed 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,8 +1,9 @@ # Ox -Safe direct-style streaming, concurrency and resiliency for Scala on the JVM. Requires JDK 21+ & Scala 3. +Safe direct-style streaming, concurrency and resiliency for Scala on the JVM. +Requires JDK 21+ & Scala 3. Experimental support for Scala Native. -To start using Ox, add the `com.softwaremill.ox::core:@VERSION@` [dependency](info/dependency.md) to your project. +To start using Ox, add the `com.softwaremill.ox:::core:@VERSION@` [dependency](info/dependency.md) to your project. Then, take a look at the tour of Ox, or follow one of the topics listed in the menu to get to know Ox's API! In addition to this documentation, ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox). diff --git a/doc/info/dependency.md b/doc/info/dependency.md index 034c4583..63c5ee99 100644 --- a/doc/info/dependency.md +++ b/doc/info/dependency.md @@ -4,12 +4,14 @@ To use ox core in your project, add: ```scala // sbt dependency -"com.softwaremill.ox" %% "core" % "@VERSION@" +"com.softwaremill.ox" %%% "core" % "@VERSION@" // scala-cli dependency -//> using dep com.softwaremill.ox::core:@VERSION@ +//> using dep com.softwaremill.ox:::core:@VERSION@ ``` -Ox core depends only on the Java [jox](https://github.com/softwaremill/jox) project, where channels are implemented. There are no other direct or transitive dependencies. +On the JVM, Ox core depends only on the Java [jox](https://github.com/softwaremill/jox) project, where channels are implemented. There are no other direct or transitive dependencies. + +For Scala Native, only the the `core` module is available. It contains a reimplementation of Jox Channels in pure Scala. Integration modules have separate dependencies. \ No newline at end of file From 181e9ecc4714030abccfa45cd9241c0756630e6a Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Mon, 25 May 2026 20:45:39 +0000 Subject: [PATCH 14/17] Run shared tests on Native: fix Trail.scala, move IO tests to scalajvm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace java.time.Clock in Trail.scala with System.currentTimeMillis() (Clock not available on Scala Native) - Move FlowCompanionIOOpsTest and FlowIOOpsTest to scalajvm/ (use java.net.URL, Class.getResource not available on Native) - Remove native test source restriction — all shared tests now compile and link on Native (~873 tests run successfully, 11 failures from timing/select issues) Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sbt | 2 -- core/src/test/scala/ox/util/Trail.scala | 3 +-- .../{scala => scalajvm}/ox/flow/FlowCompanionIOOpsTest.scala | 0 core/src/test/{scala => scalajvm}/ox/flow/FlowIOOpsTest.scala | 0 4 files changed, 1 insertion(+), 4 deletions(-) rename core/src/test/{scala => scalajvm}/ox/flow/FlowCompanionIOOpsTest.scala (100%) rename core/src/test/{scala => scalajvm}/ox/flow/FlowIOOpsTest.scala (100%) diff --git a/build.sbt b/build.sbt index 9468ccda..f292a83b 100644 --- a/build.sbt +++ b/build.sbt @@ -76,8 +76,6 @@ lazy val core = (projectMatrix in file("core")) scalaVersions = Seq(scala3), settings = Seq( Test / fork := false, - // Only include native-specific tests; shared Ox tests use JVM-only APIs (java.time, java.net, etc.) - Test / unmanagedSourceDirectories := Seq((Test / sourceDirectory).value / "scalanative"), nativeConfig ~= { _.withMultithreading(true) } ) ) diff --git a/core/src/test/scala/ox/util/Trail.scala b/core/src/test/scala/ox/util/Trail.scala index 134b7a70..a4a36676 100644 --- a/core/src/test/scala/ox/util/Trail.scala +++ b/core/src/test/scala/ox/util/Trail.scala @@ -2,12 +2,11 @@ package ox.util import ox.discard -import java.time.Clock import java.util.concurrent.atomic.AtomicReference class Trail(trail: AtomicReference[Vector[String]] = AtomicReference(Vector.empty)): def add(s: String): Unit = - println(s"[${Clock.systemUTC().instant()}] [${Thread.currentThread().threadId()}] $s") + println(s"[${System.currentTimeMillis()}] [${Thread.currentThread().threadId()}] $s") trail.updateAndGet(_ :+ s).discard def get: Vector[String] = trail.get diff --git a/core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala b/core/src/test/scalajvm/ox/flow/FlowCompanionIOOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowCompanionIOOpsTest.scala rename to core/src/test/scalajvm/ox/flow/FlowCompanionIOOpsTest.scala diff --git a/core/src/test/scala/ox/flow/FlowIOOpsTest.scala b/core/src/test/scalajvm/ox/flow/FlowIOOpsTest.scala similarity index 100% rename from core/src/test/scala/ox/flow/FlowIOOpsTest.scala rename to core/src/test/scalajvm/ox/flow/FlowIOOpsTest.scala From 6a559ce8df8c3f677ec5f0cfaf65bb349614587e Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Tue, 26 May 2026 11:03:08 +0000 Subject: [PATCH 15/17] Fix Trail to use java.util.Date, use Integer.valueOf(0) in Jox port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trail.scala: use java.util.Date for human-readable timestamps (java.time.Clock not available on Scala Native) - Channel.scala: replace 0.asInstanceOf[AnyRef] with Integer.valueOf(0) to ensure non-null boxed value on all platforms Investigation: ActorTest hangs on Native due to Scala Native 0.5.12 virtual thread scalability limitation — 1000+ virtual threads concurrently blocking on CompletableFuture.get() causes starvation. Works at N<=100. Not a Jox port bug. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/src/main/scalanative/ox/channels/jox/Channel.scala | 4 ++-- core/src/test/scala/ox/util/Trail.scala | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/scalanative/ox/channels/jox/Channel.scala b/core/src/main/scalanative/ox/channels/jox/Channel.scala index c1d58b80..6a980457 100644 --- a/core/src/main/scalanative/ox/channels/jox/Channel.scala +++ b/core/src/main/scalanative/ox/channels/jox/Channel.scala @@ -374,7 +374,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T state match case c: Continuation => if segment.casCell(i, state, RESUMING) then - if c.tryResume(0.asInstanceOf[AnyRef]) then + if c.tryResume(Integer.valueOf(0)) then segment.setCell(i, DONE) expandBuffer() return c.payload @@ -453,7 +453,7 @@ final class Channel[T] private (val capacity: Int) extends Source[T] with Sink[T case DONE => return ExpandBufferResult.DONE case c: Continuation if c.isSender => if segment.casCell(i, state, RESUMING) then - if c.tryResume(0.asInstanceOf[AnyRef]) then + if c.tryResume(Integer.valueOf(0)) then segment.setCell(i, c.payload) return ExpandBufferResult.DONE else return ExpandBufferResult.FAILED diff --git a/core/src/test/scala/ox/util/Trail.scala b/core/src/test/scala/ox/util/Trail.scala index a4a36676..ff4b8210 100644 --- a/core/src/test/scala/ox/util/Trail.scala +++ b/core/src/test/scala/ox/util/Trail.scala @@ -2,11 +2,12 @@ package ox.util import ox.discard +import java.util.Date import java.util.concurrent.atomic.AtomicReference class Trail(trail: AtomicReference[Vector[String]] = AtomicReference(Vector.empty)): def add(s: String): Unit = - println(s"[${System.currentTimeMillis()}] [${Thread.currentThread().threadId()}] $s") + println(s"[${new Date()}] [${Thread.currentThread().threadId()}] $s") trail.updateAndGet(_ :+ s).discard def get: Vector[String] = trail.get From 3d2701ade8fe3e779a1fc234b61b7ffd49b63ba7 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Tue, 26 May 2026 11:08:50 +0000 Subject: [PATCH 16/17] Add repro for Scala Native virtual thread scalability issue NativeVirtualThreadScalabilityIssue.scala demonstrates that N>=500 virtual threads concurrently blocking on CompletableFuture.get() causes a livelock on Scala Native 0.5.12 (works on JVM at any N). This explains the ActorTest hang on Native. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../NativeVirtualThreadScalabilityIssue.scala | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala diff --git a/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala b/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala new file mode 100644 index 00000000..1fe8bc32 --- /dev/null +++ b/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala @@ -0,0 +1,56 @@ +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicInteger + +/** Reproduces a Scala Native 0.5.12 virtual thread scalability issue. + * + * Pattern: N virtual threads each block on CompletableFuture.get() while a single "actor" virtual thread processes + * requests sequentially. At N=100 this works. At N=1000 it livelocks on Native (works on JVM). + * + * This is the same pattern used by ox.channels.Actor.ask under high concurrency. + * + * To reproduce: + * {{{ + * sbt examples3/run # JVM — prints "OK" in ~1s + * sbt examplesNative3/run # Native — hangs at "Running with N=1000" + * }}} + */ +object NativeVirtualThreadScalabilityIssue: + def main(args: Array[String]): Unit = + for n <- List(10, 100, 500, 1000) do + println(s"Running with N=$n...") + run(n) + println(s" N=$n OK") + println("All OK") + + private def run(n: Int): Unit = + val done = new CompletableFuture[Unit]() + + Thread.ofVirtual().start { () => + val queue = new java.util.concurrent.LinkedBlockingQueue[() => Unit]() + val counter = new AtomicInteger(0) + + // single "actor" thread processing requests sequentially + val actor = Thread.ofVirtual().start { () => + while true do + val msg = queue.take() + msg() + } + + // N concurrent virtual threads, each sending a request and blocking on the response + val forks = (1 to n).map { _ => + val f = new CompletableFuture[Unit]() + Thread.ofVirtual().start { () => + val result = new CompletableFuture[Int]() + queue.put { () => result.complete(counter.incrementAndGet()): Unit } + result.get() + f.complete(()): Unit + } + f + } + + forks.foreach(_.get()) + actor.interrupt() + done.complete(()): Unit + } + + done.get() From 71bb9d977a985c1dbba4a24cac590957953515e4 Mon Sep 17 00:00:00 2001 From: Adam Warski Date: Tue, 26 May 2026 11:28:23 +0000 Subject: [PATCH 17/17] Simplify virtual thread scalability repro Use Thread.join() instead of CompletableFuture for fork tracking, removing one layer of indirection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../NativeVirtualThreadScalabilityIssue.scala | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala b/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala index 1fe8bc32..f5a548ee 100644 --- a/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala +++ b/examples/src/main/scala/NativeVirtualThreadScalabilityIssue.scala @@ -4,14 +4,14 @@ import java.util.concurrent.atomic.AtomicInteger /** Reproduces a Scala Native 0.5.12 virtual thread scalability issue. * * Pattern: N virtual threads each block on CompletableFuture.get() while a single "actor" virtual thread processes - * requests sequentially. At N=100 this works. At N=1000 it livelocks on Native (works on JVM). + * requests sequentially. At N=100 this works. At N>=500 it livelocks on Native (works on JVM). * * This is the same pattern used by ox.channels.Actor.ask under high concurrency. * * To reproduce: * {{{ - * sbt examples3/run # JVM — prints "OK" in ~1s - * sbt examplesNative3/run # Native — hangs at "Running with N=1000" + * sbt examples3/run # JVM — prints "All OK" + * sbt examplesNative3/run # Native — hangs at N>=500 * }}} */ object NativeVirtualThreadScalabilityIssue: @@ -23,34 +23,24 @@ object NativeVirtualThreadScalabilityIssue: println("All OK") private def run(n: Int): Unit = - val done = new CompletableFuture[Unit]() + val queue = new java.util.concurrent.LinkedBlockingQueue[() => Unit]() + val counter = new AtomicInteger(0) - Thread.ofVirtual().start { () => - val queue = new java.util.concurrent.LinkedBlockingQueue[() => Unit]() - val counter = new AtomicInteger(0) - - // single "actor" thread processing requests sequentially - val actor = Thread.ofVirtual().start { () => - while true do - val msg = queue.take() - msg() - } + // single "actor" thread processing requests sequentially + val actor = Thread.ofVirtual().start { () => + while true do + val msg = queue.take() + msg() + } - // N concurrent virtual threads, each sending a request and blocking on the response - val forks = (1 to n).map { _ => - val f = new CompletableFuture[Unit]() - Thread.ofVirtual().start { () => - val result = new CompletableFuture[Int]() - queue.put { () => result.complete(counter.incrementAndGet()): Unit } - result.get() - f.complete(()): Unit - } - f + // N concurrent virtual threads, each sending a request and blocking on the response + val threads = (1 to n).map { _ => + Thread.ofVirtual().start { () => + val result = new CompletableFuture[Int]() + queue.put { () => result.complete(counter.incrementAndGet()): Unit } + result.get(): Unit } - - forks.foreach(_.get()) - actor.interrupt() - done.complete(()): Unit } - done.get() + threads.foreach(_.join()) + actor.interrupt()