core: Use SyncContext for InProcessTransport listener callbacks to avoid deadlocks

Fixes deadlocks caused by client and server listeners being called in a synchronized block

Also support unary calls returning null values

Fixes #3084
This commit is contained in:
Larry Safran 2022-06-30 13:41:36 -07:00 committed by GitHub
parent c0790283ec
commit 74137b0978
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 120 deletions

View File

@ -40,6 +40,7 @@ import io.grpc.MethodDescriptor;
import io.grpc.SecurityLevel; import io.grpc.SecurityLevel;
import io.grpc.ServerStreamTracer; import io.grpc.ServerStreamTracer;
import io.grpc.Status; import io.grpc.Status;
import io.grpc.SynchronizationContext;
import io.grpc.internal.ClientStream; import io.grpc.internal.ClientStream;
import io.grpc.internal.ClientStreamListener; import io.grpc.internal.ClientStreamListener;
import io.grpc.internal.ClientStreamListener.RpcProgress; import io.grpc.internal.ClientStreamListener.RpcProgress;
@ -106,6 +107,18 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
private List<ServerStreamTracer.Factory> serverStreamTracerFactories; private List<ServerStreamTracer.Factory> serverStreamTracerFactories;
private final Attributes attributes; private final Attributes attributes;
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler =
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (e instanceof Error) {
throw new Error(e);
}
throw new RuntimeException(e);
}
};
@GuardedBy("this") @GuardedBy("this")
private final InUseStateAggregator<InProcessStream> inUseState = private final InUseStateAggregator<InProcessStream> inUseState =
new InUseStateAggregator<InProcessStream>() { new InUseStateAggregator<InProcessStream>() {
@ -407,8 +420,10 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
private class InProcessServerStream implements ServerStream { private class InProcessServerStream implements ServerStream {
final StatsTraceContext statsTraceCtx; final StatsTraceContext statsTraceCtx;
@GuardedBy("this") // All callbacks must run in syncContext to avoid possibility of deadlock in direct executors
private ClientStreamListener clientStreamListener; private ClientStreamListener clientStreamListener;
private final SynchronizationContext syncContext =
new SynchronizationContext(uncaughtExceptionHandler);
@GuardedBy("this") @GuardedBy("this")
private int clientRequested; private int clientRequested;
@GuardedBy("this") @GuardedBy("this")
@ -444,10 +459,11 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
if (onReady) { if (onReady) {
synchronized (this) { synchronized (this) {
if (!closed) { if (!closed) {
clientStreamListener.onReady(); syncContext.executeLater(() -> clientStreamListener.onReady());
} }
} }
} }
syncContext.drain();
} }
// This method is the only reason we have to synchronize field accesses. // This method is the only reason we have to synchronize field accesses.
@ -456,28 +472,36 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
* *
* @return whether onReady should be called on the server * @return whether onReady should be called on the server
*/ */
private synchronized boolean clientRequested(int numMessages) { private boolean clientRequested(int numMessages) {
boolean previouslyReady;
boolean nowReady;
synchronized (this) {
if (closed) { if (closed) {
return false; return false;
} }
boolean previouslyReady = clientRequested > 0;
previouslyReady = clientRequested > 0;
clientRequested += numMessages; clientRequested += numMessages;
while (clientRequested > 0 && !clientReceiveQueue.isEmpty()) { while (clientRequested > 0 && !clientReceiveQueue.isEmpty()) {
clientRequested--; clientRequested--;
clientStreamListener.messagesAvailable(clientReceiveQueue.poll()); StreamListener.MessageProducer producer = clientReceiveQueue.poll();
} syncContext.executeLater(() -> clientStreamListener.messagesAvailable(producer));
// Attempt being reentrant-safe
if (closed) {
return false;
} }
if (clientReceiveQueue.isEmpty() && clientNotifyStatus != null) { if (clientReceiveQueue.isEmpty() && clientNotifyStatus != null) {
closed = true; closed = true;
clientStream.statsTraceCtx.clientInboundTrailers(clientNotifyTrailers); clientStream.statsTraceCtx.clientInboundTrailers(clientNotifyTrailers);
clientStream.statsTraceCtx.streamClosed(clientNotifyStatus); clientStream.statsTraceCtx.streamClosed(clientNotifyStatus);
clientStreamListener.closed( Status notifyStatus = this.clientNotifyStatus;
clientNotifyStatus, RpcProgress.PROCESSED, clientNotifyTrailers); Metadata notifyTrailers = this.clientNotifyTrailers;
syncContext.executeLater(() ->
clientStreamListener.closed(notifyStatus, RpcProgress.PROCESSED, notifyTrailers));
} }
boolean nowReady = clientRequested > 0;
nowReady = clientRequested > 0;
}
syncContext.drain();
return !previouslyReady && nowReady; return !previouslyReady && nowReady;
} }
@ -486,7 +510,8 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
} }
@Override @Override
public synchronized void writeMessage(InputStream message) { public void writeMessage(InputStream message) {
synchronized (this) {
if (closed) { if (closed) {
return; return;
} }
@ -498,12 +523,15 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
StreamListener.MessageProducer producer = new SingleMessageProducer(message); StreamListener.MessageProducer producer = new SingleMessageProducer(message);
if (clientRequested > 0) { if (clientRequested > 0) {
clientRequested--; clientRequested--;
clientStreamListener.messagesAvailable(producer); syncContext.executeLater(() -> clientStreamListener.messagesAvailable(producer));
} else { } else {
clientReceiveQueue.add(producer); clientReceiveQueue.add(producer);
} }
} }
syncContext.drain();
}
@Override @Override
public void flush() {} public void flush() {}
@ -540,8 +568,9 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
} }
clientStream.statsTraceCtx.clientInboundHeaders(); clientStream.statsTraceCtx.clientInboundHeaders();
clientStreamListener.headersRead(headers); syncContext.executeLater(() -> clientStreamListener.headersRead(headers));
} }
syncContext.drain();
} }
@Override @Override
@ -585,13 +614,14 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
closed = true; closed = true;
clientStream.statsTraceCtx.clientInboundTrailers(trailers); clientStream.statsTraceCtx.clientInboundTrailers(trailers);
clientStream.statsTraceCtx.streamClosed(clientStatus); clientStream.statsTraceCtx.streamClosed(clientStatus);
clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, trailers); syncContext.executeLater(
() -> clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, trailers));
} else { } else {
clientNotifyStatus = clientStatus; clientNotifyStatus = clientStatus;
clientNotifyTrailers = trailers; clientNotifyTrailers = trailers;
} }
} }
syncContext.drain();
streamClosed(); streamClosed();
} }
@ -604,7 +634,8 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
streamClosed(); streamClosed();
} }
private synchronized boolean internalCancel(Status clientStatus) { private boolean internalCancel(Status clientStatus) {
synchronized (this) {
if (closed) { if (closed) {
return false; return false;
} }
@ -621,7 +652,11 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
} }
} }
clientStream.statsTraceCtx.streamClosed(clientStatus); clientStream.statsTraceCtx.streamClosed(clientStatus);
clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, new Metadata()); syncContext.executeLater(
() ->
clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, new Metadata()));
}
syncContext.drain();
return true; return true;
} }
@ -662,8 +697,10 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
private class InProcessClientStream implements ClientStream { private class InProcessClientStream implements ClientStream {
final StatsTraceContext statsTraceCtx; final StatsTraceContext statsTraceCtx;
final CallOptions callOptions; final CallOptions callOptions;
@GuardedBy("this") // All callbacks must run in syncContext to avoid possibility of deadlock in direct executors
private ServerStreamListener serverStreamListener; private ServerStreamListener serverStreamListener;
private final SynchronizationContext syncContext =
new SynchronizationContext(uncaughtExceptionHandler);
@GuardedBy("this") @GuardedBy("this")
private int serverRequested; private int serverRequested;
@GuardedBy("this") @GuardedBy("this")
@ -693,9 +730,10 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
if (onReady) { if (onReady) {
synchronized (this) { synchronized (this) {
if (!closed) { if (!closed) {
serverStreamListener.onReady(); syncContext.executeLater(() -> serverStreamListener.onReady());
} }
} }
syncContext.drain();
} }
} }
@ -705,21 +743,29 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
* *
* @return whether onReady should be called on the server * @return whether onReady should be called on the server
*/ */
private synchronized boolean serverRequested(int numMessages) { private boolean serverRequested(int numMessages) {
boolean previouslyReady;
boolean nowReady;
synchronized (this) {
if (closed) { if (closed) {
return false; return false;
} }
boolean previouslyReady = serverRequested > 0; previouslyReady = serverRequested > 0;
serverRequested += numMessages; serverRequested += numMessages;
while (serverRequested > 0 && !serverReceiveQueue.isEmpty()) { while (serverRequested > 0 && !serverReceiveQueue.isEmpty()) {
serverRequested--; serverRequested--;
serverStreamListener.messagesAvailable(serverReceiveQueue.poll()); StreamListener.MessageProducer producer = serverReceiveQueue.poll();
syncContext.executeLater(() -> serverStreamListener.messagesAvailable(producer));
} }
if (serverReceiveQueue.isEmpty() && serverNotifyHalfClose) { if (serverReceiveQueue.isEmpty() && serverNotifyHalfClose) {
serverNotifyHalfClose = false; serverNotifyHalfClose = false;
serverStreamListener.halfClosed(); syncContext.executeLater(() -> serverStreamListener.halfClosed());
} }
boolean nowReady = serverRequested > 0; nowReady = serverRequested > 0;
}
syncContext.drain();
return !previouslyReady && nowReady; return !previouslyReady && nowReady;
} }
@ -728,7 +774,8 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
} }
@Override @Override
public synchronized void writeMessage(InputStream message) { public void writeMessage(InputStream message) {
synchronized (this) {
if (closed) { if (closed) {
return; return;
} }
@ -740,11 +787,13 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
StreamListener.MessageProducer producer = new SingleMessageProducer(message); StreamListener.MessageProducer producer = new SingleMessageProducer(message);
if (serverRequested > 0) { if (serverRequested > 0) {
serverRequested--; serverRequested--;
serverStreamListener.messagesAvailable(producer); syncContext.executeLater(() -> serverStreamListener.messagesAvailable(producer));
} else { } else {
serverReceiveQueue.add(producer); serverReceiveQueue.add(producer);
} }
} }
syncContext.drain();
}
@Override @Override
public void flush() {} public void flush() {}
@ -768,8 +817,9 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
streamClosed(); streamClosed();
} }
private synchronized boolean internalCancel( private boolean internalCancel(
Status serverListenerStatus, Status serverTracerStatus) { Status serverListenerStatus, Status serverTracerStatus) {
synchronized (this) {
if (closed) { if (closed) {
return false; return false;
} }
@ -787,21 +837,26 @@ final class InProcessTransport implements ServerTransport, ConnectionClientTrans
} }
} }
serverStream.statsTraceCtx.streamClosed(serverTracerStatus); serverStream.statsTraceCtx.streamClosed(serverTracerStatus);
serverStreamListener.closed(serverListenerStatus); syncContext.executeLater(() -> serverStreamListener.closed(serverListenerStatus));
}
syncContext.drain();
return true; return true;
} }
@Override @Override
public synchronized void halfClose() { public void halfClose() {
synchronized (this) {
if (closed) { if (closed) {
return; return;
} }
if (serverReceiveQueue.isEmpty()) { if (serverReceiveQueue.isEmpty()) {
serverStreamListener.halfClosed(); syncContext.executeLater(() -> serverStreamListener.halfClosed());
} else { } else {
serverNotifyHalfClose = true; serverNotifyHalfClose = true;
} }
} }
syncContext.drain();
}
@Override @Override
public void setMessageCompression(boolean enable) {} public void setMessageCompression(boolean enable) {}

View File

@ -40,8 +40,6 @@ import org.mockito.ArgumentMatchers;
* Not intended to provide a high code coverage or to test every major usecase. * Not intended to provide a high code coverage or to test every major usecase.
* *
* directExecutor() makes it easier to have deterministic tests. * directExecutor() makes it easier to have deterministic tests.
* However, if your implementation uses another thread and uses streaming it is better to use
* the default executor, to avoid hitting bug #3084.
* *
* <p>For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and * <p>For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and
* {@link io.grpc.examples.routeguide.RouteGuideServerTest}. * {@link io.grpc.examples.routeguide.RouteGuideServerTest}.

View File

@ -33,8 +33,6 @@ import org.junit.runners.JUnit4;
* Not intended to provide a high code coverage or to test every major usecase. * Not intended to provide a high code coverage or to test every major usecase.
* *
* directExecutor() makes it easier to have deterministic tests. * directExecutor() makes it easier to have deterministic tests.
* However, if your implementation uses another thread and uses streaming it is better to use
* the default executor, to avoid hitting bug #3084.
* *
* <p>For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and * <p>For more unit test examples see {@link io.grpc.examples.routeguide.RouteGuideClientTest} and
* {@link io.grpc.examples.routeguide.RouteGuideServerTest}. * {@link io.grpc.examples.routeguide.RouteGuideServerTest}.

View File

@ -53,8 +53,6 @@ import org.mockito.ArgumentCaptor;
* Not intended to provide a high code coverage or to test every major usecase. * Not intended to provide a high code coverage or to test every major usecase.
* *
* directExecutor() makes it easier to have deterministic tests. * directExecutor() makes it easier to have deterministic tests.
* However, if your implementation uses another thread and uses streaming it is better to use
* the default executor, to avoid hitting bug #3084.
* *
* <p>For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and * <p>For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and
* {@link io.grpc.examples.helloworld.HelloWorldServerTest}. * {@link io.grpc.examples.helloworld.HelloWorldServerTest}.

View File

@ -50,8 +50,6 @@ import org.mockito.ArgumentCaptor;
* Not intended to provide a high code coverage or to test every major usecase. * Not intended to provide a high code coverage or to test every major usecase.
* *
* directExecutor() makes it easier to have deterministic tests. * directExecutor() makes it easier to have deterministic tests.
* However, if your implementation uses another thread and uses streaming it is better to use
* the default executor, to avoid hitting bug #3084.
* *
* <p>For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and * <p>For basic unit test examples see {@link io.grpc.examples.helloworld.HelloWorldClientTest} and
* {@link io.grpc.examples.helloworld.HelloWorldServerTest}. * {@link io.grpc.examples.helloworld.HelloWorldServerTest}.

View File

@ -509,6 +509,7 @@ public final class ClientCalls {
private static final class UnaryStreamToFuture<RespT> extends StartableListener<RespT> { private static final class UnaryStreamToFuture<RespT> extends StartableListener<RespT> {
private final GrpcFuture<RespT> responseFuture; private final GrpcFuture<RespT> responseFuture;
private RespT value; private RespT value;
private boolean isValueReceived = false;
// Non private to avoid synthetic class // Non private to avoid synthetic class
UnaryStreamToFuture(GrpcFuture<RespT> responseFuture) { UnaryStreamToFuture(GrpcFuture<RespT> responseFuture) {
@ -521,17 +522,18 @@ public final class ClientCalls {
@Override @Override
public void onMessage(RespT value) { public void onMessage(RespT value) {
if (this.value != null) { if (this.isValueReceived) {
throw Status.INTERNAL.withDescription("More than one value received for unary call") throw Status.INTERNAL.withDescription("More than one value received for unary call")
.asRuntimeException(); .asRuntimeException();
} }
this.value = value; this.value = value;
this.isValueReceived = true;
} }
@Override @Override
public void onClose(Status status, Metadata trailers) { public void onClose(Status status, Metadata trailers) {
if (status.isOk()) { if (status.isOk()) {
if (value == null) { if (!isValueReceived) {
// No value received so mark the future as an error // No value received so mark the future as an error
responseFuture.setException( responseFuture.setException(
Status.INTERNAL.withDescription("No value received for unary call") Status.INTERNAL.withDescription("No value received for unary call")