Skip to content

Commit fc87d8c

Browse files
authored
4.x: Streamable/Streamer API rewamp, dropping next/finish args (#8212)
1 parent 601662e commit fc87d8c

24 files changed

Lines changed: 380 additions & 514 deletions

src/main/java/io/reactivex/rxjava4/core/CompletionStageDisposable.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,9 @@ public CompletionStageDisposable(@NonNull CompletionStage<T> stage, @NonNull Dis
9191
* @throws ThrowableWrapper if the original exception was a checked exception
9292
*/
9393
public void await() {
94-
await(null);
95-
}
96-
97-
/**
98-
* Await the completion of the current stage.
99-
* <p>
100-
* Rethrows any original unchecked exceptions as is.
101-
* @param canceller the canceller link
102-
* @throws CancellationException if the computation was cancelled
103-
* @throws ThrowableWrapper if the original exception was a checked exception
104-
*/
105-
public void await(DisposableContainer canceller) {
10694
state.lazySet(true);
10795
try {
108-
AwaitCoordinatorStatic.await(stage, canceller);
96+
stage.toCompletableFuture().join();
10997
} catch (CompletionException ce) {
11098
throw ExceptionHelper.wrapOrThrow(ce.getCause());
11199
}

src/main/java/io/reactivex/rxjava4/core/Streamable.java

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,46 @@
2525
import io.reactivex.rxjava4.functions.*;
2626
import io.reactivex.rxjava4.internal.functions.ObjectHelper;
2727
import io.reactivex.rxjava4.internal.operators.streamable.*;
28-
import io.reactivex.rxjava4.internal.util.*;
28+
import io.reactivex.rxjava4.internal.util.ExceptionHelper;
2929
import io.reactivex.rxjava4.plugins.RxJavaPlugins;
3030
import io.reactivex.rxjava4.schedulers.Schedulers;
3131
import io.reactivex.rxjava4.subscribers.TestSubscriber;
3232

33-
/**
34-
* The holographically emergent {@code IAsyncEnumerable} of the Java world.
35-
* Runs best with Virtual Threads.
36-
* TODO proper docs
37-
* @param <T> the element type of the stream.
38-
* @since 4.0.0
39-
*/
33+
/// Represents a virtual-thread capable, multi-valued (a)synchronous sequence of values that
34+
/// builds upon the Java [CompletionStage]-based concurrency-coordination model.
35+
///
36+
/// The lifecycle of the sequence is as follows:
37+
///
38+
/// - consumer calls {@link #stream(DisposableContainer)}
39+
/// - consumer calls {@link Streamer#next()} in a loop and consumer checks if what
40+
/// the {@link CompletionStage} outcome is:
41+
/// - if it succeeded with a boolean {@code true}, it is safe to call {@link Streamer#current()}.
42+
/// - if it succeeded with a boolean {@code false}, no further values are coming.
43+
/// - if it failed with a {@code Throwable}, no further values are coming and the error can be propagated further
44+
/// - consumer calls {@link Streamer#finish()}
45+
///
46+
/// It is always necessary to have the consumer call {@code finish} because that is responsible for cleaning up
47+
/// resources of the upstream.
48+
///
49+
/// Downstream cancellations are signaled via the [DisposableContainer], where operators can register their own
50+
/// [Disposable]s that get disposed. Because dispose can happen at any time and asynchronously to the consumption loop,
51+
/// the sensitive sources must complete their waiting `CompletionStage` returned by `next` exceptionally via a
52+
/// [CancellationException]. This will unblock the loops and invoke the `finish` method of the lifecycle at
53+
/// the consumer thread. Depending on the operator, the `CancellationException` may not be propagated further.
54+
///
55+
/// If a source wishes to fail, it must signal the [Throwable] via the returned {@code CompletionStage} of {@code next}.
56+
/// If the `finish` also throws, its `Throwable` should be added as suppressed exception to the original `Throwable`.
57+
///
58+
/// The `Streamer` methods must be invoked sequentially and non-overlappingly, similar to the
59+
/// <a href='https://github.com/reactive-streams/reactive-streams-jvm#1.3'>Reactive Streams rule §1.3</a>.
60+
///
61+
/// This reactive type was modeled after the C# `IAsyncEnumerable` and `IAsyncEnumerator` interfaces. Unfortunately,
62+
/// Java never added any `async`/`await` infrastructure, plus `CompletionStage` doesn't even have any native way to blockingly
63+
/// join to it. Therefore, when running in a (virtual) blocking fashion, one may use the {@link Streamer#awaitNext()}
64+
/// or {@link Streamer#awaitFinish()} helper methods.
65+
///
66+
/// @param <T> the element type of the {@code Streamable} sequence.
67+
/// @since 4.0.0
4068
@FunctionalInterface
4169
public interface Streamable<@NonNull T> {
4270

@@ -258,32 +286,6 @@ static <T> Streamable<T> fromPublisher(@NonNull Flow.Publisher<T> source, @NonNu
258286
.toStreamable();
259287
}
260288

261-
/**
262-
* Generates a sequence in order which the stages complete in any form.
263-
* @param <T> the common element type
264-
* @param stages the iterable of stages to be relayed in the order they complete
265-
* @param executor the executor to run the blocking operator
266-
* @return the new Streamable instance
267-
*/
268-
@SuppressWarnings("unchecked")
269-
@CheckReturnValue
270-
@NonNull
271-
static <@NonNull T> Streamable<CompletionStage<T>> fromStages(
272-
@NonNull Iterable<? extends CompletionStage<? extends T>> stages, ExecutorService executor) {
273-
Objects.requireNonNull(stages, "stages is null");
274-
Objects.requireNonNull(executor, "executor is null");
275-
return create(emitter -> {
276-
var list = new ArrayList<CompletionStage<? extends T>>();
277-
for(var stage : stages) {
278-
list.add(stage);
279-
}
280-
while (!list.isEmpty()) {
281-
var winner = AwaitCoordinatorStatic.awaitFirstIndex(list, emitter.canceller());
282-
emitter.emit((CompletionStage<T>)list.remove(winner));
283-
}
284-
}, executor);
285-
}
286-
287289
/**
288290
* Defers the creation of the actual {@code Streamable}, allowing a per streamer
289291
* state to be created along with it.
@@ -330,10 +332,10 @@ static <T> Streamable<T> fromPublisher(@NonNull Flow.Publisher<T> source, @NonNu
330332
return create(emitter -> {
331333
try (var mainSource = sources.forEach(item -> {
332334
try (var innerSource = item.forEach(emitter::emit, emitter.canceller().derive(), executor)) {
333-
innerSource.await(emitter.canceller());
335+
innerSource.await();
334336
}
335337
}, emitter.canceller(), executor)) {
336-
mainSource.await(emitter.canceller());
338+
mainSource.await();
337339
}
338340
}, executor);
339341
}
@@ -562,7 +564,7 @@ default Flowable<T> toFlowable() {
562564
default Flowable<T> toFlowable(@NonNull ExecutorService executor) {
563565
Objects.requireNonNull(executor, "executir is null");
564566
var me = this;
565-
return Flowable.virtualCreate(emitter -> me.forEach(emitter::emit).await(emitter.canceller()), executor);
567+
return Flowable.virtualCreate(emitter -> me.forEach(emitter::emit).await(), executor);
566568
}
567569

568570
/**
@@ -595,7 +597,7 @@ default Flowable<T> toFlowable(@NonNull ExecutorService executor) {
595597
// System.out.println("item " + item);
596598
transformer.transform(item, emitter, stopper);
597599
}, emitter.canceller(), executor)
598-
.await(emitter.canceller()), executor);
600+
.await(), executor);
599601
}
600602

601603
// oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
@@ -658,7 +660,7 @@ default CompletionStageDisposable<Void> forEach(@NonNull Consumer<? super T> con
658660
try {
659661
try {
660662
while (!canceller.isDisposed()) {
661-
if (str.awaitNext(canceller)) {
663+
if (str.awaitNext()) {
662664
// System.out.println("Received " + str.current());
663665
consumer.accept(Objects.requireNonNull(str.current(), "The upstream Streamable " + me.getClass() + " produced a null element!"));
664666
} else {
@@ -667,7 +669,7 @@ default CompletionStageDisposable<Void> forEach(@NonNull Consumer<? super T> con
667669
}
668670
}
669671
} finally {
670-
str.awaitFinish(canceller);
672+
str.awaitFinish();
671673
}
672674
} catch (final Throwable crash) {
673675
Exceptions.throwIfFatal(crash);
@@ -705,7 +707,7 @@ default CompletionStageDisposable<Void> forEach(
705707
try {
706708
var stopper = Disposable.empty();
707709
while (!canceller.isDisposed() && !stopper.isDisposed()) {
708-
if (str.awaitNext(canceller)) {
710+
if (str.awaitNext()) {
709711
// System.out.println("Received " + str.current());
710712
var v = Objects.requireNonNull(str.current(), "The upstream Streamable " + me.getClass() + " produced a null element!");
711713
consumer.accept(v, stopper);
@@ -715,7 +717,7 @@ default CompletionStageDisposable<Void> forEach(
715717
}
716718
}
717719
} finally {
718-
str.awaitFinish(canceller);
720+
str.awaitFinish();
719721
}
720722
} catch (final Throwable crash) {
721723
Exceptions.throwIfFatal(crash);
@@ -739,7 +741,7 @@ default void subscribe(@NonNull Flow.Subscriber<? super T> subscriber, @NonNull
739741
final Streamable<T> me = this;
740742
Flowable.<T>virtualCreate(emitter -> {
741743
me.forEach(emitter::emit, emitter.canceller().derive(), executor)
742-
.await(emitter.canceller());
744+
.await();
743745
}, executor)
744746
.subscribe(subscriber);
745747
}
@@ -751,7 +753,7 @@ default void subscribe(@NonNull Flow.Subscriber<? super T> subscriber, @NonNull
751753
default void subscribe(@NonNull Flow.Subscriber<? super T> subscriber) {
752754
final Streamable<T> me = this;
753755
Flowable.<T>virtualCreate(emitter -> {
754-
me.forEach(emitter::emit).await(emitter.canceller());
756+
me.forEach(emitter::emit).await();
755757
})
756758
.subscribe(subscriber);
757759
}

src/main/java/io/reactivex/rxjava4/core/Streamer.java

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,94 +13,104 @@
1313

1414
package io.reactivex.rxjava4.core;
1515

16-
import java.util.*;
1716
import java.util.concurrent.*;
1817

1918
import io.reactivex.rxjava4.annotations.NonNull;
20-
import io.reactivex.rxjava4.disposables.*;
21-
import io.reactivex.rxjava4.internal.util.AwaitCoordinator;
2219

23-
/**
24-
* A realized stream which can then be consumed asynchronously in steps.
25-
* Think of it as the {@IAsyncEnumerator} of the Java world. Runs best on Virtual Threads.
26-
* <p>
27-
* To make sure you can run finish, use {@link DisposableContainer#clear()} or {@link DisposableContainer#reset()}
28-
* to get rid of all previous registered disposables. finish() will create its own, and if that
29-
* gets stuck, just call clear()/dispose() on the container to get rid of this sequence for good.
30-
* @param <T> the element type.
31-
* TODO proper docs
32-
* @since 4.0.0
33-
*/
34-
public interface Streamer<@NonNull T> extends AwaitCoordinator {
20+
/// A realized stream which can then be consumed asynchronously in steps.
21+
/// Think of it as the `IAsyncEnumerator` from C# ported to the clumsy Java world.
22+
///
23+
/// The `Streamer` methods must be invoked sequentially and non-overlappingly, similar to the
24+
/// <a href='https://github.com/reactive-streams/reactive-streams-jvm#1.3'>Reactive Streams rule §1.3</a>.
25+
///
26+
/// For an optimized synchronous operation, please consider using the {@link #NEXT_TRUE}, {@link #NEXT_FALSE}
27+
/// and {@link #FINISHED} constant CompletionStages.
28+
/// @param <T> the element type.
29+
/// @since 4.0.0
30+
public interface Streamer<@NonNull T> {
3531

3632
// oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
3733
// API
3834
// oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
3935

4036
/**
4137
* Determine if there are more elements available from the source.
42-
* @param cancellation ability to perform cancellation on a per-virtual-pull request.
43-
* @return eventually true or false, indicating availability or termination
38+
* @return a `CompletionStage` with 3 outcomes
39+
* <ul>
40+
* <li>`true` indicates there is an item available for consumption via {@link #current()}
41+
* <li>`false` indicates there are no more items available
42+
*< li>`Throwable` indicates there was an upstream error
43+
* </ul>
4444
*/
4545
@NonNull
46-
CompletionStage<Boolean> next(@NonNull DisposableContainer cancellation);
46+
CompletionStage<Boolean> next();
4747

4848
/**
49-
* Returns the current element if {@link #next(DisposableContainer)} yielded {@code true}.
50-
* Can be called multiple times between {@link #next(DisposableContainer)} calls.
51-
* @return the current element
52-
* @throws NoSuchElementException before the very first {@link #next(DisposableContainer)}
53-
* or after {@link #next(DisposableContainer)} returned {@code false}
49+
* Returns the currently available item synchronously if the previous call to [#next()] yielded `true`.
50+
* Calling it during an ongoing [#next()] or [#finish()] call, or beyond the lifecycle of the `Streamer`
51+
* is an undefined behavior. It may yield `null` or throw.
52+
* @return the current item
5453
*/
5554
@NonNull
5655
T current();
5756

5857
/**
59-
* Called when the stream ends or gets cancelled. Should be always invoked.
60-
* TODO, this is inherited from {@code IAsyncDisposable} in C#...
61-
* @param cancellation to cancel a stuck finish operation, just in case.
62-
* @return the stage you can await to cleanups to happen
58+
* Finish the sequence once all processing has been done to it either via exhaustion or via cancellation.
59+
* <p>
60+
* Usually involves resource cleanup, so this method must be always called.
61+
* <p>
62+
* If the cleanup crashes and the [#next()] crashed too, the cleanup `Throwable` will be added as suppressed
63+
* to the main crash `Throwable` from `next`.
64+
*
65+
* @return a `CompletionStage` that completes when the resource cleanup completes normally or exceptionally
6366
*/
6467
@NonNull
65-
CompletionStage<Void> finish(@NonNull DisposableContainer cancellation);
68+
CompletionStage<Void> finish();
6669

6770
// oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
6871
// HELPERS
6972
// oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
7073

7174
/**
72-
* Moves and awaits the sequence's next element, returns false if there are no more
73-
* data.
74-
* @param cancellation to efficiently cancel this await if necessary
75-
* @return true if the next element via {@link #current()} can be read, or false if
76-
* the stream ended.
75+
* Convenience method to blockingly await the CompletionStage returned by the {@link #next()} method.
76+
* @return true if there are more items, false if no more items are coming, or crashes
7777
*/
78-
default boolean awaitNext(@NonNull DisposableContainer cancellation) {
79-
return await(next(cancellation), cancellation);
78+
default boolean awaitNext() {
79+
var s = next();
80+
if (s == NEXT_TRUE) {
81+
return true;
82+
} else
83+
if (s == NEXT_FALSE) {
84+
return false;
85+
}
86+
return s.toCompletableFuture().join();
8087
}
8188

8289
/**
83-
* Who cancels the cancellation attempt? Another cancellation attempt!
84-
* @param cancellation the token to cancel and ongoing cancel attempt
90+
* Convenience method to blockingly await the CompletionStage returned by the {@link #finish()} method.
8591
*/
86-
default void awaitFinish(@NonNull DisposableContainer cancellation) {
87-
await(finish(cancellation), cancellation);
92+
default void awaitFinish() {
93+
var s = finish();
94+
if (s == FINISHED) {
95+
return;
96+
}
97+
s.toCompletableFuture().join();
8898
}
8999

90100
/**
91-
* Use this constant in {@link #next(DisposableContainer)} to indicate
101+
* Use this constant in {@link #next()} to indicate
92102
* the next value is available, synchronously.
93103
*/
94104
CompletionStage<Boolean> NEXT_TRUE = CompletableFuture.completedStage(true);
95105

96106
/**
97-
* Use this constant in {@link #next(DisposableContainer)} to indicate
107+
* Use this constant in {@link #next()} to indicate
98108
* no more values will be available, synchronously.
99109
*/
100110
CompletionStage<Boolean> NEXT_FALSE = CompletableFuture.completedStage(false);
101111

102112
/**
103-
* Use this constant in {@link #finish(DisposableContainer)} to indicate
113+
* Use this constant in {@link #finish()} to indicate
104114
* the cleanup was done synchronously.
105115
*/
106116
CompletionStage<Void> FINISHED = CompletableFuture.completedStage(null);

src/main/java/io/reactivex/rxjava4/core/config/StreamableInterceptConfig.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
* Configuration record the intercept() operator with various lifecylce-stage transforming callbacks
2626
* @param <T> the element type of the sequence
2727
* @param onStream called when the {@link Streamable#stream(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
28-
* @param onNext called when the {@link Streamer#next(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
28+
* @param onNext called when the {@link Streamer#next()} is invoked
2929
* @param onCurrent called when the {@link Streamer#current()} is invoked
30-
* @param onFinish called when the {@link Streamer#finish(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
30+
* @param onFinish called when the {@link Streamer#finish()} is invoked
3131
* @since 4.0.0
3232
*/
3333
public record StreamableInterceptConfig<T>(
@@ -56,9 +56,9 @@ public StreamableInterceptConfig(@NonNull Function<? super T, ? extends T> onCur
5656
/**
5757
* Constructs a fully configured record.
5858
* @param onStream called when the {@link Streamable#stream(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
59-
* @param onNext called when the {@link Streamer#next(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
59+
* @param onNext called when the {@link Streamer#next()} is invoked
6060
* @param onCurrent called when the {@link Streamer#current()} is invoked
61-
* @param onFinish called when the {@link Streamer#finish(io.reactivex.rxjava4.disposables.DisposableContainer)} is invoked
61+
* @param onFinish called when the {@link Streamer#finish()} is invoked
6262
*/
6363
public StreamableInterceptConfig {
6464
Objects.requireNonNull(onStream, "onStream is null");

src/main/java/io/reactivex/rxjava4/internal/operators/streamable/StreamableEmpty.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ enum EmptyStreamer implements Streamer<Object> {
3434
INSTANCE;
3535

3636
@Override
37-
public @NonNull CompletionStage<Boolean> next(DisposableContainer cancellation) {
37+
public @NonNull CompletionStage<Boolean> next() {
3838
return NEXT_FALSE;
3939
}
4040

@@ -44,7 +44,7 @@ enum EmptyStreamer implements Streamer<Object> {
4444
}
4545

4646
@Override
47-
public @NonNull CompletionStage<Void> finish(DisposableContainer canceller) {
47+
public @NonNull CompletionStage<Void> finish() {
4848
return FINISHED;
4949
}
5050
}

0 commit comments

Comments
 (0)