Skip to content

Commit c027d00

Browse files
authored
4.x: Streamable + doOnError, forEach(StreamerInput) (#8218)
* 4.x: Streamable + doOnError, forEach(StreamerInput) * fix tests * Improve API, fix bugs, add coverage * fix style * Fix flaky DispatchStreamProcessorTest > normalMulti() * improve coverage of withCancellation, rename to StreamSink * Remove ThrowableWrapper, fix attempt at flaky tests * flaky forEachInputCancelUpfront fix?
1 parent db62338 commit c027d00

27 files changed

Lines changed: 851 additions & 312 deletions

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@
2121
import java.util.function.Consumer;
2222

2323
import io.reactivex.rxjava4.annotations.NonNull;
24-
import io.reactivex.rxjava4.disposables.*;
25-
import io.reactivex.rxjava4.exceptions.ThrowableWrapper;
26-
import io.reactivex.rxjava4.internal.util.*;
24+
import io.reactivex.rxjava4.disposables.Disposable;
25+
import io.reactivex.rxjava4.internal.util.ExceptionHelper;
2726
import io.reactivex.rxjava4.plugins.RxJavaPlugins;
2827

2928
/**
@@ -88,14 +87,14 @@ public CompletionStageDisposable(@NonNull CompletionStage<T> stage, @NonNull Dis
8887
* <p>
8988
* Rethrows any original unchecked exceptions as is.
9089
* @throws CancellationException if the computation was cancelled
91-
* @throws ThrowableWrapper if the original exception was a checked exception
90+
* @throws CompletionException if the original exception was a checked exception
9291
*/
9392
public void await() {
9493
state.lazySet(true);
9594
try {
9695
stage.toCompletableFuture().join();
9796
} catch (CompletionException ce) {
98-
throw ExceptionHelper.wrapOrThrow(ce.getCause());
97+
throw ExceptionHelper.unwrapOrThrow(ce);
9998
}
10099
}
101100

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,26 @@
2020

2121
/**
2222
* A {@link Processor}-like interface combining the {@code Streamable} interface and the
23-
* {@link StreamerInput} interface to establish a push-pull bridge based on {@link CompletionStage}-based
23+
* {@link StreamSink} interface to establish a push-pull bridge based on {@link CompletionStage}-based
2424
* asynchronous processing and dispatching of values and errors.
2525
* @param <In> the element type of the input side
2626
* @param <Out> the element type of the output side
2727
* @since 4.0.0
2828
*/
29-
public interface StreamProcessor<@NonNull In, @NonNull Out> extends Streamable<Out>, StreamerInput<In> {
29+
public interface StreamProcessor<@NonNull In, @NonNull Out> extends Streamable<Out>, StreamSink<In> {
3030

3131
/**
3232
* Returns {@code true} if this {@link StreamProcessor} has {@link Streamer}s.
3333
* @return {@code true} if this {@link StreamProcessor} has {@link Streamer}s.
3434
*/
3535
boolean hasStreamers();
3636

37+
/**
38+
* Returns the current number of {@link Streamer}s subscribed to this {@link StreamProcessor}
39+
* @return the current number of {@link Streamer}s subscribed to this {@link StreamProcessor}
40+
*/
41+
int streamerCount();
42+
3743
/**
3844
* Returns {@code true} if this {@code StreamProcessor} was completed normally via {@link #finish(Throwable)}.
3945
* @return {@code true} if this {@code StreamProcessor} was completed normally via {@link #finish(Throwable)}.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2016-present, RxJava Contributors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
5+
* compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is
10+
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
11+
* the License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package io.reactivex.rxjava4.core;
15+
16+
import java.util.Objects;
17+
import java.util.concurrent.*;
18+
import java.util.concurrent.Flow.Subscriber;
19+
20+
import io.reactivex.rxjava4.annotations.*;
21+
import io.reactivex.rxjava4.disposables.*;
22+
import io.reactivex.rxjava4.functions.Function;
23+
import io.reactivex.rxjava4.internal.operators.streamable.*;
24+
25+
/**
26+
* An interface to submit items and terminal events to a consumer that indacates when the processing of
27+
* said item or terminal event has completed, similar to how {@link Subscriber} can receive events.
28+
* <p>
29+
* The general contract is to call {@link #next(Object)} zero or more times, then
30+
* call {@link #finish(Throwable)} at most once, all in a non-overlapping fashion and only if the
31+
* returned {@link CompletionStage} has completed in some fashion.
32+
* @param <T> the item type to be offered
33+
* @since 4.0.0
34+
*/
35+
public interface StreamSink<@NonNull T> {
36+
37+
/**
38+
* Offer the next item.
39+
* @param item the item being offered
40+
* @return a {@link CompletionStage} that completes with {@code true} if the value was successfully consumed,
41+
* {@code false} if the value was rejected or exceptionally on error
42+
*/
43+
@NonNull
44+
CompletionStage<Boolean> next(T item);
45+
46+
/**
47+
* Offer the final, terminal event.
48+
* @param throwable the optional throwable to signal error, {@code null} to signal normal completion
49+
* @return a {@link CompletionStage} that completes with {@code null} if the call succeeded
50+
* or exceptionally on error
51+
*/
52+
@NonNull
53+
CompletionStage<Void> finish(@Nullable Throwable throwable);
54+
55+
/**
56+
* Returns the {@link DisposableContainer} to use to detect if the consumer has indicated no more
57+
* items it is willing to accept.
58+
* <p>
59+
* The default implementation returns a fresh {@link CompositeDisposable}.
60+
* @return the {@code DisposableContainer}
61+
*/
62+
@NonNull
63+
default DisposableContainer cancellation() {
64+
return new CompositeDisposable();
65+
}
66+
67+
/**
68+
* Returns a new {@link StreamSink} that returns the given {@link DisposableContainer}
69+
* in its {@link #cancellation()}, allowing overriding the cancellation management
70+
* of this {@code StreamSink}
71+
* @param cancellation the {@link DisposableContainer} to use as cancellation management
72+
* @return the new {@code StreamSink} instance
73+
* @throws NullPointerException if {@code cancellation} is {@code null}
74+
*/
75+
@NonNull
76+
default StreamSink<T> withCancellation(DisposableContainer cancellation) {
77+
Objects.requireNonNull(cancellation, "cancellation is null");
78+
return new StreamSinkWithCancellation<>(this, cancellation);
79+
}
80+
81+
/**
82+
* Creates a {@link StreamSink} via lambda callbacks for {@link #next(Object)} and
83+
* {@link #finish(Throwable)}.
84+
* <p>
85+
* Non-fatal exceptions thrown by the callbacks are turned into failed
86+
* {@link CompletableFuture#failedFuture(Throwable)}s.
87+
* @param <T> the element type of the stream
88+
* @param onNext the callback for the {@code next} method
89+
* @param onFinish the callback for the {@code finish} method
90+
* @return the new {@link StreamSink} instance
91+
*/
92+
@NonNull
93+
static <T> StreamSink<T> create(
94+
@NonNull Function<? super T, ? extends CompletionStage<Boolean>> onNext,
95+
@NonNull Function<? super Throwable, ? extends CompletionStage<Void>> onFinish
96+
) {
97+
Objects.requireNonNull(onNext, "onNext is null");
98+
Objects.requireNonNull(onFinish, "onFinish is null");
99+
return new StreamSinkLambda<>(onNext, onFinish);
100+
}
101+
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,17 @@ default Streamable<T> delay(long time, TimeUnit unit, Scheduler scheduler) {
589589
return RxJavaPlugins.onAssembly(new StreamableDelay<>(this, time, unit, scheduler));
590590
}
591591

592+
/**
593+
* Calls the specific {@link Consumer} if there is an error from the upstream.
594+
* @param consumer the consumer to call with the Throwable
595+
* @return the new {@code Streamable} instance
596+
* @throws NullPointerException if {@code consumer} is {@code null}
597+
*/
598+
default Streamable<T> doOnError(Consumer<? super Throwable> consumer) {
599+
Objects.requireNonNull(consumer, "consumer is null");
600+
return intercept(StreamableHelper.createOnError(consumer));
601+
}
602+
592603
/**
593604
* Calls the given consumer whenever an upstream item becomes available.
594605
* @param consumer the callback to invoke with the next item from upstream
@@ -983,6 +994,34 @@ default void subscribe(@NonNull Flow.Subscriber<? super T> subscriber) {
983994
subscribe(subscriber, Executors.newVirtualThreadPerTaskExecutor());
984995
}
985996

997+
/**
998+
* Relays the events of the upstream into a {@link StreamSink} consumer
999+
* via the help of the standard {@link Executors#newVirtualThreadPerTaskExecutor()}
1000+
* as a mediator for pull-to-push.
1001+
* @param consumer the consumer to relay events into
1002+
* @return the stage that gets completed normally or with an exception when this
1003+
* {@code Streamable} terminates
1004+
* @throws NullPointerException if {@code consumer} is {@code null}
1005+
*/
1006+
default CompletionStage<Void> subscribe(@NonNull StreamSink<? super T> consumer) {
1007+
return subscribe(consumer, Executors.newVirtualThreadPerTaskExecutor());
1008+
}
1009+
1010+
/**
1011+
* Relays the events of the upstream into a {@link StreamSink} consumer
1012+
* via the help of the given {@link ExecutorService} as a mediator for pull-to-push.
1013+
* @param consumer the consumer to relay events into
1014+
* @param executor the {@link ExecutorService} to run the blocking consume and emissions
1015+
* @return the stage that gets completed normally or with an exception when this
1016+
* {@code Streamable} terminates
1017+
* @throws NullPointerException if {@code consumer} or {@code executor} is {@code null}
1018+
*/
1019+
default CompletionStage<Void> subscribe(@NonNull StreamSink<? super T> consumer, ExecutorService executor) {
1020+
Objects.requireNonNull(consumer, "consumer is null");
1021+
Objects.requireNonNull(executor, "executor is null");
1022+
return StreamableForEach.forEach(this, consumer, executor);
1023+
}
1024+
9861025
/**
9871026
* Creates a new {@link TestSubscriber} and subscribes it to this {@code Streamable}.
9881027
* @return the created test subscriber

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
package io.reactivex.rxjava4.core;
1515

16+
import java.util.NoSuchElementException;
1617
import java.util.concurrent.*;
1718

1819
import io.reactivex.rxjava4.annotations.NonNull;
@@ -50,6 +51,7 @@ public interface Streamer<@NonNull T> {
5051
* Calling it during an ongoing [#next()] or [#finish()] call, or beyond the lifecycle of the `Streamer`
5152
* is an undefined behavior. It may yield `null` or throw.
5253
* @return the current item
54+
* @throws NoSuchElementException if there are no items to return
5355
*/
5456
@NonNull
5557
T current();
@@ -76,25 +78,42 @@ public interface Streamer<@NonNull T> {
7678
* @return true if there are more items, false if no more items are coming, or crashes
7779
*/
7880
default boolean awaitNext() {
79-
var s = next();
80-
if (s == NEXT_TRUE) {
81+
return awaitBoolean(next());
82+
}
83+
84+
/**
85+
* Convenience method to blockingly await the CompletionStage returned by the {@link #finish()} method.
86+
*/
87+
default void awaitFinish() {
88+
awaitVoid(finish());
89+
}
90+
91+
/**
92+
* Convenience method to await the completion of a boolean stage, optimized
93+
* for handling {@value #NEXT_TRUE} and {@value #NEXT_FALSE} directly.
94+
* @param stage the stage to await
95+
* @return the result of the stage
96+
*/
97+
static boolean awaitBoolean(CompletionStage<Boolean> stage) {
98+
if (stage == NEXT_TRUE) {
8199
return true;
82100
} else
83-
if (s == NEXT_FALSE) {
101+
if (stage == NEXT_FALSE) {
84102
return false;
85103
}
86-
return s.toCompletableFuture().join();
104+
return stage.toCompletableFuture().join();
87105
}
88106

89107
/**
90-
* Convenience method to blockingly await the CompletionStage returned by the {@link #finish()} method.
108+
* Convenience method to await the completion of a stage, optimized
109+
* for handling {@value #FINISHED} directly.
110+
* @param stage the stage to await
91111
*/
92-
default void awaitFinish() {
93-
var s = finish();
94-
if (s == FINISHED) {
112+
static void awaitVoid(CompletionStage<Void> stage) {
113+
if (stage == FINISHED) {
95114
return;
96115
}
97-
s.toCompletableFuture().join();
116+
stage.toCompletableFuture().join();
98117
}
99118

100119
/**

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

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/main/java/io/reactivex/rxjava4/exceptions/Exceptions.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
package io.reactivex.rxjava4.exceptions;
1515

16+
import java.util.concurrent.CompletionException;
17+
1618
import io.reactivex.rxjava4.annotations.NonNull;
1719
import io.reactivex.rxjava4.internal.util.ExceptionHelper;
1820

@@ -28,7 +30,7 @@ private Exceptions() {
2830
}
2931
/**
3032
* Convenience method to throw a {@code RuntimeException} and {@code Error} directly
31-
* or wrap any other exception type into a {@link ThrowableWrapper}.
33+
* or wrap any other exception type into a {@link CompletionException}.
3234
* @param t the exception to throw directly or wrapped
3335
* @return because {@code propagate} itself throws an exception or error, this is a sort of phantom return
3436
* value; {@code propagate} does not actually return anything

src/main/java/io/reactivex/rxjava4/exceptions/ThrowableWrapper.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)