
io.grpc.stub.BlockingClientCall Maven / Gradle / Ivy
/*
* Copyright 2023 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.grpc.stub;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import io.grpc.ClientCall;
import io.grpc.ExperimentalApi;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.stub.ClientCalls.ThreadSafeThreadlessExecutor;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents a bidirectional streaming call from a client. Allows in a blocking manner, sending
* over the stream and receiving from the stream. Also supports terminating the call.
* Wraps a ClientCall and converts from async communication to the sync paradigm used by the
* various blocking stream methods in {@link ClientCalls} which are used by the generated stubs.
*
* Supports separate threads for reads and writes, but only 1 of each
*
*
Read methods consist of:
*
* - {@link #read()}
*
- {@link #read(long timeout, TimeUnit unit)}
*
- {@link #hasNext()}
*
- {@link #cancel(String, Throwable)}
*
*
* Write methods consist of:
*
* - {@link #write(Object)}
*
- {@link #write(Object, long timeout, TimeUnit unit)}
*
- {@link #halfClose()}
*
*
* @param Type of the Request Message
* @param Type of the Response Message
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918")
public final class BlockingClientCall {
private static final Logger logger = Logger.getLogger(BlockingClientCall.class.getName());
private final BlockingQueue buffer;
private final ClientCall call;
private final ThreadSafeThreadlessExecutor executor;
private boolean writeClosed;
private volatile Status closedStatus; // null if not closed
BlockingClientCall(ClientCall call, ThreadSafeThreadlessExecutor executor) {
this.call = call;
this.executor = executor;
buffer = new ArrayBlockingQueue<>(1);
}
/**
* Wait if necessary for a value to be available from the server. If there is an available value
* return it immediately, if the stream is closed return a null. Otherwise, wait for a value to be
* available or the stream to be closed
*
* @return value from server or null if stream has been closed
* @throws StatusException If the stream has closed in an error state
*/
public RespT read() throws InterruptedException, StatusException {
try {
return read(true, 0, TimeUnit.NANOSECONDS);
} catch (TimeoutException e) {
throw new AssertionError("should never happen", e);
}
}
/**
* Wait with timeout, if necessary, for a value to be available from the server. If there is an
* available value, return it immediately. If the stream is closed return a null. Otherwise, wait
* for a value to be available, the stream to be closed or the timeout to expire.
*
* @param timeout how long to wait before giving up. Values <= 0 are no wait
* @param unit a TimeUnit determining how to interpret the timeout parameter
* @return value from server or null (if stream has been closed)
* @throws TimeoutException if no read becomes ready before the specified timeout expires
* @throws StatusException If the stream has closed in an error state
*/
public RespT read(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException,
StatusException {
return read(false, timeout, unit);
}
private RespT read(boolean waitForever, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, StatusException {
long start = System.nanoTime();
long end = start + unit.toNanos(timeout);
Predicate> predicate = BlockingClientCall::skipWaitingForRead;
executor.waitAndDrainWithTimeout(waitForever, end, predicate, this);
RespT bufferedValue = buffer.poll();
if (logger.isLoggable(Level.FINER)) {
logger.finer("Client Blocking read had value: " + bufferedValue);
}
Status currentClosedStatus;
if (bufferedValue != null) {
call.request(1);
return bufferedValue;
} else if ((currentClosedStatus = closedStatus) == null) {
throw new IllegalStateException(
"The message disappeared... are you reading from multiple threads?");
} else if (!currentClosedStatus.isOk()) {
throw currentClosedStatus.asException();
} else {
return null;
}
}
boolean skipWaitingForRead() {
return closedStatus != null || !buffer.isEmpty();
}
/**
* Wait for a value to be available from the server. If there is an
* available value, return true immediately. If the stream was closed with Status.OK, return
* false. If the stream was closed with an error status, throw a StatusException. Otherwise, wait
* for a value to be available or the stream to be closed.
*
* @return True when there is a value to read. Return false if stream closed cleanly.
* @throws StatusException If the stream was closed in an error state
*/
public boolean hasNext() throws InterruptedException, StatusException {
executor.waitAndDrain((x) -> !x.buffer.isEmpty() || x.closedStatus != null, this);
Status currentClosedStatus = closedStatus;
if (currentClosedStatus != null && !currentClosedStatus.isOk()) {
throw currentClosedStatus.asException();
}
return !buffer.isEmpty();
}
/**
* Send a value to the stream for sending to server, wait if necessary for the grpc stream to be
* ready.
*
* If write is not legal at the time of call, immediately returns false
*
*
NOTE: This method will return as soon as it passes the request to the grpc stream
* layer. It will not block while the message is being sent on the wire and returning true does
* not guarantee that the server gets the message.
*
*
WARNING: Doing only writes without reads can lead to deadlocks. This is because
* flow control, imposed by networks to protect intermediary routers and endpoints that are
* operating under resource constraints, requires reads to be done in order to progress writes.
* Furthermore, the server closing the stream will only be identified after
* the last sent value is read.
*
* @param request Message to send to the server
* @return true if the request is sent to stream, false if skipped
* @throws StatusException If the stream has closed in an error state
*/
public boolean write(ReqT request) throws InterruptedException, StatusException {
try {
return write(true, request, Integer.MAX_VALUE, TimeUnit.DAYS);
} catch (TimeoutException e) {
throw new RuntimeException(e); // should never happen
}
}
/**
* Send a value to the stream for sending to server, wait if necessary for the grpc stream to be
* ready up to specified timeout.
*
*
If write is not legal at the time of call, immediately returns false
*
*
NOTE: This method will return as soon as it passes the request to the grpc stream
* layer. It will not block while the message is being sent on the wire and returning true does
* not guarantee that the server gets the message.
*
*
WARNING: Doing only writes without reads can lead to deadlocks as a result of
* flow control. Furthermore, the server closing the stream will only be identified after the
* last sent value is read.
*
* @param request Message to send to the server
* @param timeout How long to wait before giving up. Values <= 0 are no wait
* @param unit A TimeUnit determining how to interpret the timeout parameter
* @return true if the request is sent to stream, false if skipped
* @throws TimeoutException if write does not become ready before the specified timeout expires
* @throws StatusException If the stream has closed in an error state
*/
public boolean write(ReqT request, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, StatusException {
return write(false, request, timeout, unit);
}
private boolean write(boolean waitForever, ReqT request, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, StatusException {
if (writeClosed) {
throw new IllegalStateException("Writes cannot be done after calling halfClose or cancel");
}
long end = System.nanoTime() + unit.toNanos(timeout);
Predicate> predicate =
(x) -> x.call.isReady() || x.closedStatus != null;
executor.waitAndDrainWithTimeout(waitForever, end, predicate, this);
Status savedClosedStatus = closedStatus;
if (savedClosedStatus == null) {
call.sendMessage(request);
return true;
} else if (savedClosedStatus.isOk()) {
return false;
} else {
// Propagate any errors returned from the server
throw savedClosedStatus.asException();
}
}
void sendSingleRequest(ReqT request) {
call.sendMessage(request);
}
/**
* Cancel stream and stop any further writes. Note that some reads that are in flight may still
* happen after the cancel.
*
* @param message if not {@code null}, will appear as the description of the CANCELLED status
* @param cause if not {@code null}, will appear as the cause of the CANCELLED status
*/
public void cancel(String message, Throwable cause) {
writeClosed = true;
call.cancel(message, cause);
}
/**
* Indicate that no more writes will be done and the stream will be closed from the client side.
*
* @see ClientCall#halfClose()
*/
public void halfClose() {
if (writeClosed) {
throw new IllegalStateException(
"halfClose cannot be called after already half closed or cancelled");
}
writeClosed = true;
call.halfClose();
}
/**
* Status that server sent when closing channel from its side.
*
* @return null if stream not closed by server, otherwise Status sent by server
*/
@VisibleForTesting
Status getClosedStatus() {
drainQuietly();
return closedStatus;
}
/**
* Check for whether some action is ready.
*
* @return True if legal to write and writeOrRead can run without blocking
*/
@VisibleForTesting
boolean isEitherReadOrWriteReady() {
return (isWriteLegal() && isWriteReady()) || isReadReady();
}
/**
* Check whether there are any values waiting to be read.
*
* @return true if read will not block
*/
@VisibleForTesting
boolean isReadReady() {
drainQuietly();
return !buffer.isEmpty();
}
/**
* Check that write hasn't been marked complete and stream is ready to receive a write (so will
* not block).
*
* @return true if legal to write and write will not block
*/
@VisibleForTesting
boolean isWriteReady() {
drainQuietly();
return isWriteLegal() && call.isReady();
}
/**
* Check whether we'll ever be able to do writes or should terminate.
* @return True if writes haven't been closed and the server hasn't closed the stream
*/
private boolean isWriteLegal() {
return !writeClosed && closedStatus == null;
}
ClientCall.Listener getListener() {
return new QueuingListener();
}
private void drainQuietly() {
try {
executor.drain();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private final class QueuingListener extends ClientCall.Listener {
@Override
public void onMessage(RespT value) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed");
buffer.add(value);
}
@Override
public void onClose(Status status, Metadata trailers) {
Preconditions.checkState(closedStatus == null, "ClientCall already closed");
closedStatus = status;
}
}
}