Skip to content

Commit 9d4631b

Browse files
authored
3.x: Add onDropped callback to throttleLatest operator (#7457)
* 3.x: Add onDropped callback to throttleLatest operator * Reorganize imports
1 parent 2edba23 commit 9d4631b

File tree

8 files changed

+1193
-53
lines changed

8 files changed

+1193
-53
lines changed

src/main/java/io/reactivex/rxjava3/core/Flowable.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17327,7 +17327,61 @@ public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @N
1732717327
public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) {
1732817328
Objects.requireNonNull(unit, "unit is null");
1732917329
Objects.requireNonNull(scheduler, "scheduler is null");
17330-
return RxJavaPlugins.onAssembly(new FlowableThrottleLatest<>(this, timeout, unit, scheduler, emitLast));
17330+
return RxJavaPlugins.onAssembly(new FlowableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, null));
17331+
}
17332+
17333+
/**
17334+
* Throttles items from the upstream {@code Flowable} by first emitting the next
17335+
* item from upstream, then periodically emitting the latest item (if any) when
17336+
* the specified timeout elapses between them, invoking the consumer for any dropped item.
17337+
* <p>
17338+
* <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt="">
17339+
* <p>
17340+
* If no items were emitted from the upstream during this timeout phase, the next
17341+
* upstream item is emitted immediately and the timeout window starts from then.
17342+
* <dl>
17343+
* <dt><b>Backpressure:</b></dt>
17344+
* <dd>This operator does not support backpressure as it uses time to control data flow.
17345+
* If the downstream is not ready to receive items, a
17346+
* {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException}
17347+
* will be signaled.</dd>
17348+
* <dt><b>Scheduler:</b></dt>
17349+
* <dd>You specify which {@link Scheduler} this operator will use.</dd>
17350+
* <dt><b>Error handling:</b></dt>
17351+
* <dd>
17352+
* If the upstream signals an {@code onError} or {@code onDropped} callback crashes,
17353+
* the error is delivered immediately to the downstream. If both happen, a {@link CompositeException}
17354+
* is created, containing both the upstream and the callback error.
17355+
* If the {@code onDropped} callback crashes during cancellation, the exception is forwarded
17356+
* to the global error handler via {@link RxJavaPlugins#onError(Throwable)}.
17357+
* </dd>
17358+
* </dl>
17359+
* @param timeout the time to wait after an item emission towards the downstream
17360+
* before trying to emit the latest item from upstream again
17361+
* @param unit the time unit
17362+
* @param scheduler the {@code Scheduler} where the timed wait and latest item
17363+
* emission will be performed
17364+
* @param emitLast If {@code true}, the very last item from the upstream will be emitted
17365+
* immediately when the upstream completes, regardless if there is
17366+
* a timeout window active or not. If {@code false}, the very last
17367+
* upstream item is ignored and the flow terminates.
17368+
* @param onDropped called when an item is replaced by a newer item that doesn't get delivered
17369+
* to the downstream, including the very last item if {@code emitLast} is {@code false}
17370+
* and the current undelivered item when the sequence gets canceled.
17371+
* @return the new {@code Flowable} instance
17372+
* @throws NullPointerException if {@code unit}, {@code scheduler} or {@code onDropped} is {@code null}
17373+
* @since 3.1.6 - Experimental
17374+
*/
17375+
@CheckReturnValue
17376+
@NonNull
17377+
@BackpressureSupport(BackpressureKind.ERROR)
17378+
@SchedulerSupport(SchedulerSupport.CUSTOM)
17379+
@Experimental
17380+
public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) {
17381+
Objects.requireNonNull(unit, "unit is null");
17382+
Objects.requireNonNull(scheduler, "scheduler is null");
17383+
Objects.requireNonNull(onDropped, "onDropped is null");
17384+
return RxJavaPlugins.onAssembly(new FlowableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, onDropped));
1733117385
}
1733217386

1733317387
/**

src/main/java/io/reactivex/rxjava3/core/Observable.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14362,7 +14362,55 @@ public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit,
1436214362
public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) {
1436314363
Objects.requireNonNull(unit, "unit is null");
1436414364
Objects.requireNonNull(scheduler, "scheduler is null");
14365-
return RxJavaPlugins.onAssembly(new ObservableThrottleLatest<>(this, timeout, unit, scheduler, emitLast));
14365+
return RxJavaPlugins.onAssembly(new ObservableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, null));
14366+
}
14367+
14368+
/**
14369+
* Throttles items from the current {@code Observable} by first emitting the next
14370+
* item from upstream, then periodically emitting the latest item (if any) when
14371+
* the specified timeout elapses between them, invoking the consumer for any dropped item.
14372+
* <p>
14373+
* <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt="">
14374+
* <p>
14375+
* If no items were emitted from the upstream during this timeout phase, the next
14376+
* upstream item is emitted immediately and the timeout window starts from then.
14377+
* <dl>
14378+
* <dt><b>Scheduler:</b></dt>
14379+
* <dd>You specify which {@link Scheduler} this operator will use.</dd>
14380+
* <dt><b>Error handling:</b></dt>
14381+
* <dd>
14382+
* If the upstream signals an {@code onError} or {@code onDropped} callback crashes,
14383+
* the error is delivered immediately to the downstream. If both happen, a {@link CompositeException}
14384+
* is created, containing both the upstream and the callback error.
14385+
* If the {@code onDropped} callback crashes when the sequence gets disposed, the exception is forwarded
14386+
* to the global error handler via {@link RxJavaPlugins#onError(Throwable)}.
14387+
* </dd>
14388+
* </dl>
14389+
* @param timeout the time to wait after an item emission towards the downstream
14390+
* before trying to emit the latest item from upstream again
14391+
* @param unit the time unit
14392+
* @param scheduler the {@code Scheduler} where the timed wait and latest item
14393+
* emission will be performed
14394+
* @param emitLast If {@code true}, the very last item from the upstream will be emitted
14395+
* immediately when the upstream completes, regardless if there is
14396+
* a timeout window active or not. If {@code false}, the very last
14397+
* upstream item is ignored and the flow terminates.
14398+
* @param onDropped called when an item is replaced by a newer item that doesn't get delivered
14399+
* to the downstream, including the very last item if {@code emitLast} is {@code false}
14400+
* and the current undelivered item when the sequence gets disposed.
14401+
* @return the new {@code Observable} instance
14402+
* @throws NullPointerException if {@code unit}, {@code scheduler} or {@code onDropped} is {@code null}
14403+
* @since 3.1.6 - Experimental
14404+
*/
14405+
@CheckReturnValue
14406+
@SchedulerSupport(SchedulerSupport.CUSTOM)
14407+
@NonNull
14408+
@Experimental
14409+
public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) {
14410+
Objects.requireNonNull(unit, "unit is null");
14411+
Objects.requireNonNull(scheduler, "scheduler is null");
14412+
Objects.requireNonNull(onDropped, "onDropped is null");
14413+
return RxJavaPlugins.onAssembly(new ObservableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, onDropped));
1436614414
}
1436714415

1436814416
/**

src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatest.java

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import org.reactivestreams.*;
2020

2121
import io.reactivex.rxjava3.core.*;
22-
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
22+
import io.reactivex.rxjava3.exceptions.*;
23+
import io.reactivex.rxjava3.functions.Consumer;
2324
import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper;
2425
import io.reactivex.rxjava3.internal.util.BackpressureHelper;
26+
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
2527

2628
/**
2729
* Emits the next or latest item when the given time elapses.
@@ -44,19 +46,24 @@ public final class FlowableThrottleLatest<T> extends AbstractFlowableWithUpstrea
4446

4547
final boolean emitLast;
4648

49+
final Consumer<? super T> onDropped;
50+
4751
public FlowableThrottleLatest(Flowable<T> source,
48-
long timeout, TimeUnit unit, Scheduler scheduler,
49-
boolean emitLast) {
52+
long timeout, TimeUnit unit,
53+
Scheduler scheduler,
54+
boolean emitLast,
55+
Consumer<? super T> onDropped) {
5056
super(source);
5157
this.timeout = timeout;
5258
this.unit = unit;
5359
this.scheduler = scheduler;
5460
this.emitLast = emitLast;
61+
this.onDropped = onDropped;
5562
}
5663

5764
@Override
5865
protected void subscribeActual(Subscriber<? super T> s) {
59-
source.subscribe(new ThrottleLatestSubscriber<>(s, timeout, unit, scheduler.createWorker(), emitLast));
66+
source.subscribe(new ThrottleLatestSubscriber<>(s, timeout, unit, scheduler.createWorker(), emitLast, onDropped));
6067
}
6168

6269
static final class ThrottleLatestSubscriber<T>
@@ -79,6 +86,8 @@ static final class ThrottleLatestSubscriber<T>
7986

8087
final AtomicLong requested;
8188

89+
final Consumer<? super T> onDropped;
90+
8291
Subscription upstream;
8392

8493
volatile boolean done;
@@ -93,15 +102,18 @@ static final class ThrottleLatestSubscriber<T>
93102
boolean timerRunning;
94103

95104
ThrottleLatestSubscriber(Subscriber<? super T> downstream,
96-
long timeout, TimeUnit unit, Scheduler.Worker worker,
97-
boolean emitLast) {
105+
long timeout, TimeUnit unit,
106+
Scheduler.Worker worker,
107+
boolean emitLast,
108+
Consumer<? super T> onDropped) {
98109
this.downstream = downstream;
99110
this.timeout = timeout;
100111
this.unit = unit;
101112
this.worker = worker;
102113
this.emitLast = emitLast;
103114
this.latest = new AtomicReference<>();
104115
this.requested = new AtomicLong();
116+
this.onDropped = onDropped;
105117
}
106118

107119
@Override
@@ -115,7 +127,17 @@ public void onSubscribe(Subscription s) {
115127

116128
@Override
117129
public void onNext(T t) {
118-
latest.set(t);
130+
T old = latest.getAndSet(t);
131+
if (onDropped != null && old != null) {
132+
try {
133+
onDropped.accept(old);
134+
} catch (Throwable ex) {
135+
Exceptions.throwIfFatal(ex);
136+
upstream.cancel();
137+
error = ex;
138+
done = true;
139+
}
140+
}
119141
drain();
120142
}
121143

@@ -145,6 +167,22 @@ public void cancel() {
145167
upstream.cancel();
146168
worker.dispose();
147169
if (getAndIncrement() == 0) {
170+
clear();
171+
}
172+
}
173+
174+
void clear() {
175+
if (onDropped != null) {
176+
T v = latest.getAndSet(null);
177+
if (v != null) {
178+
try {
179+
onDropped.accept(v);
180+
} catch (Throwable ex) {
181+
Exceptions.throwIfFatal(ex);
182+
RxJavaPlugins.onError(ex);
183+
}
184+
}
185+
} else {
148186
latest.lazySet(null);
149187
}
150188
}
@@ -170,14 +208,27 @@ void drain() {
170208

171209
for (;;) {
172210
if (cancelled) {
173-
latest.lazySet(null);
211+
clear();
174212
return;
175213
}
176214

177215
boolean d = done;
216+
Throwable error = this.error;
178217

179218
if (d && error != null) {
180-
latest.lazySet(null);
219+
if (onDropped != null) {
220+
T v = latest.getAndSet(null);
221+
if (v != null) {
222+
try {
223+
onDropped.accept(v);
224+
} catch (Throwable ex) {
225+
Exceptions.throwIfFatal(ex);
226+
error = new CompositeException(error, ex);
227+
}
228+
}
229+
} else {
230+
latest.lazySet(null);
231+
}
181232
downstream.onError(error);
182233
worker.dispose();
183234
return;
@@ -187,19 +238,31 @@ void drain() {
187238
boolean empty = v == null;
188239

189240
if (d) {
190-
if (!empty && emitLast) {
241+
if (!empty) {
191242
v = latest.getAndSet(null);
192-
long e = emitted;
193-
if (e != requested.get()) {
194-
emitted = e + 1;
195-
downstream.onNext(v);
196-
downstream.onComplete();
243+
if (emitLast) {
244+
long e = emitted;
245+
if (e != requested.get()) {
246+
emitted = e + 1;
247+
downstream.onNext(v);
248+
downstream.onComplete();
249+
} else {
250+
tryDropAndSignalMBE(v);
251+
}
197252
} else {
198-
downstream.onError(new MissingBackpressureException(
199-
"Could not emit final value due to lack of requests"));
253+
if (onDropped != null) {
254+
try {
255+
onDropped.accept(v);
256+
} catch (Throwable ex) {
257+
Exceptions.throwIfFatal(ex);
258+
downstream.onError(ex);
259+
worker.dispose();
260+
return;
261+
}
262+
}
263+
downstream.onComplete();
200264
}
201265
} else {
202-
latest.lazySet(null);
203266
downstream.onComplete();
204267
}
205268
worker.dispose();
@@ -222,8 +285,7 @@ void drain() {
222285
emitted = e + 1;
223286
} else {
224287
upstream.cancel();
225-
downstream.onError(new MissingBackpressureException(
226-
"Could not emit value due to lack of requests"));
288+
tryDropAndSignalMBE(v);
227289
worker.dispose();
228290
return;
229291
}
@@ -242,5 +304,19 @@ void drain() {
242304
}
243305
}
244306
}
307+
308+
void tryDropAndSignalMBE(T valueToDrop) {
309+
Throwable errorToSignal = new MissingBackpressureException(
310+
"Could not emit value due to lack of requests");
311+
if (onDropped != null) {
312+
try {
313+
onDropped.accept(valueToDrop);
314+
} catch (Throwable ex) {
315+
Exceptions.throwIfFatal(ex);
316+
errorToSignal = new CompositeException(errorToSignal, ex);
317+
}
318+
}
319+
downstream.onError(errorToSignal);
320+
}
245321
}
246322
}

0 commit comments

Comments
 (0)