All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.salesforce.reactivegrpc.common.ReactivePublisherBackpressureOnReadyHandler Maven / Gradle / Ivy

/*
 *  Copyright (c) 2017, salesforce.com, inc.
 *  All rights reserved.
 *  Licensed under the BSD 3-Clause license.
 *  For full license text, see LICENSE.txt file in the repo root  or https://opensource.org/licenses/BSD-3-Clause
 */

package com.salesforce.reactivegrpc.common;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Runnables;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.CallStreamObserver;
import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ServerCallStreamObserver;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * ReactivePublisherBackpressureOnReadyHandler bridges the manual flow control idioms of Reactive Streams and gRPC. This class takes
 * messages off of a {@link org.reactivestreams.Publisher} and feeds them into a {@link CallStreamObserver}
 * while respecting backpressure. This class is the inverse of {@link ReactiveStreamObserverPublisher}.
 * 

* When a gRPC publisher's transport wants more data to transmit, the {@link CallStreamObserver}'s onReady handler is * signaled. This handler must keep transmitting messages until {@link CallStreamObserver#isReady()} ceases to be true. *

* When a {@link org.reactivestreams.Publisher} is subscribed to by a {@link Subscriber}, the * {@code Publisher} hands the {@code Subscriber} a {@link Subscription}. When the {@code Subscriber} * wants more messages from the {@code Publisher}, the {@code Subscriber} calls {@link Subscription#request(long)}. *

* To bridge the two idioms: when gRPC wants more messages, the {@code onReadyHandler} is called and {@link #run()} * calls the {@code Subscription}'s {@code request()} method, asking the {@code Publisher} to produce another message. * Since this class is also registered as the {@code Publisher}'s {@code Subscriber}, the {@link #onNext(Object)} * method is called. {@code onNext()} passes the message to gRPC's {@link CallStreamObserver#onNext(Object)} method, * and then calls {@code request()} again if {@link CallStreamObserver#isReady()} is true. The loop of * request->pass->check is repeated until {@code isReady()} returns false, indicating that the outbound transmit buffer * is full and that backpressure must be applied. * * @param */ public class ReactivePublisherBackpressureOnReadyHandler implements Subscriber, Runnable { private CallStreamObserver requestStream; private Subscription subscription; private AtomicBoolean canceled = new AtomicBoolean(false); private CountDownLatch subscribed = new CountDownLatch(1); private Runnable cancelRequestStream = Runnables.doNothing(); public ReactivePublisherBackpressureOnReadyHandler(final ClientCallStreamObserver requestStream) { this.requestStream = Preconditions.checkNotNull(requestStream); requestStream.setOnReadyHandler(this); cancelRequestStream = new Runnable() { @Override public void run() { requestStream.cancel("Cancelled", Status.CANCELLED.asException()); } }; } public ReactivePublisherBackpressureOnReadyHandler(ServerCallStreamObserver requestStream) { this.requestStream = Preconditions.checkNotNull(requestStream); requestStream.setOnReadyHandler(this); requestStream.setOnCancelHandler(new Runnable() { @Override public void run() { subscription.cancel(); } }); } @Override public void run() { try { subscribed.await(); } catch (InterruptedException e) { } Preconditions.checkState(subscription != null, "onSubscribe() not yet called"); if (!isCanceled()) { // restart the pump subscription.request(1); } } public void cancel() { canceled.set(true); if (subscription != null) { subscription.cancel(); subscription = null; } cancelRequestStream.run(); } public boolean isCanceled() { return canceled.get(); } @Override public void onSubscribe(Subscription subscription) { if (this.subscription != null) { subscription.cancel(); } else { this.subscription = Preconditions.checkNotNull(subscription); subscribed.countDown(); } } @Override public void onNext(T t) { if (!isCanceled()) { requestStream.onNext(Preconditions.checkNotNull(t)); if (requestStream.isReady()) { // keep the pump going subscription.request(1); } } } @Override public void onError(Throwable throwable) { requestStream.onError(prepareError(Preconditions.checkNotNull(throwable))); } @Override public void onComplete() { if (!isCanceled()) { requestStream.onCompleted(); } } private static Throwable prepareError(Throwable throwable) { if (throwable instanceof StatusException || throwable instanceof StatusRuntimeException) { return throwable; } else { return Status.fromThrowable(throwable).asException(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy