Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/src/main/java/io/grpc/internal/ServerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import io.grpc.ServerServiceDefinition;
import io.grpc.ServerTransportFilter;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.perfmark.Link;
import io.perfmark.PerfMark;
import io.perfmark.Tag;
Expand Down Expand Up @@ -811,7 +812,13 @@ void setListener(ServerStreamListener listener) {
private void internalClose(Throwable t) {
// TODO(ejona86): this is not thread-safe :)
String description = "Application error processing RPC";
stream.close(Status.UNKNOWN.withDescription(description).withCause(t), new Metadata());
Status statusToPropagate = Status.UNKNOWN.withDescription(description).withCause(t);
if (t instanceof StatusRuntimeException) {
if (((StatusRuntimeException) t).getStatus().getCode() == Status.Code.RESOURCE_EXHAUSTED) {
statusToPropagate = ((StatusRuntimeException) t).getStatus().withCause(t);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there no other cases where we can get "RESOURCE_EXHAUSTED" ? As, this is not tied specifically to marshaling the intended behavior can change if and when new "RESOURCE_EXHAUSTED" are added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AgraVator , I've addressed the review comments and changed the logic to handle both StatusRuntimeException and StatusException , I also maintained the existing exception propagation as same for other status code except RESOURCE_EXHAUSTED, Please review and share your feedback.

}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be refactored to use a single if. But, personally I would extract out the explicit conversion of t into a variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, @AgraVator . I've addressed the comments.

stream.close(statusToPropagate, new Metadata());
}

@Override
Expand Down
94 changes: 94 additions & 0 deletions core/src/test/java/io/grpc/internal/ServerImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import io.grpc.ServiceDescriptor;
import io.grpc.Status;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import io.grpc.StringMarshaller;
import io.grpc.internal.ServerImpl.JumpToApplicationThreadServerStreamListener;
import io.grpc.internal.ServerImplBuilder.ClientTransportServersBuilder;
Expand Down Expand Up @@ -1542,6 +1543,99 @@ public void channelz_transport_membershp() throws Exception {
assertTrue(after.end);
}

@Test
public void testInternalClose_nonProtocolStatusRuntimeExceptionBecomesUnknown() {
JumpToApplicationThreadServerStreamListener listener
= new JumpToApplicationThreadServerStreamListener(
executor.getScheduledExecutorService(),
executor.getScheduledExecutorService(),
stream,
Context.ROOT.withCancellation(),
PerfMark.createTag());
ServerStreamListener mockListener = mock(ServerStreamListener.class);
listener.setListener(mockListener);

StatusRuntimeException statusRuntimeException
= new StatusRuntimeException(Status.PERMISSION_DENIED.withDescription("denied"));
doThrow(statusRuntimeException).when(mockListener).onReady();
listener.onReady();
try {
executor.runDueTasks();
fail("Expected exception");
} catch (RuntimeException t) {
assertSame(statusRuntimeException, t);
ensureServerStateNotLeaked();
}
verify(stream).close(statusCaptor.capture(), metadataCaptor.capture());
Status status = statusCaptor.getValue();
assertEquals(Code.UNKNOWN, status.getCode());
assertEquals("Application error processing RPC", status.getDescription());
assertEquals(statusRuntimeException, status.getCause());
assertTrue(metadataCaptor.getValue().keys().isEmpty());
}

@Test
public void testInternalClose_otherExceptionBecomesUnknown() {
JumpToApplicationThreadServerStreamListener listener
= new JumpToApplicationThreadServerStreamListener(
executor.getScheduledExecutorService(),
executor.getScheduledExecutorService(),
stream,
Context.ROOT.withCancellation(),
PerfMark.createTag());
ServerStreamListener mockListener = mock(ServerStreamListener.class);
listener.setListener(mockListener);

RuntimeException expectedT = new RuntimeException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectedT ?

Copy link
Contributor Author

@vimanikag vimanikag Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AgraVator , The new test cases are inspired by the existing junit's like onReady_runtimeExceptionCancelsCall()/halfClosed_runtimeExceptionCancelsCall , following the same naming format. Please advise if you would prefer to change the variable to expected if We suspect it's a typo or if the current naming was intentional.

doThrow(expectedT).when(mockListener)
.messagesAvailable(any(StreamListener.MessageProducer.class));
listener.messagesAvailable(mock(StreamListener.MessageProducer.class));
try {
executor.runDueTasks();
fail("Expected exception");
} catch (RuntimeException t) {
assertSame(expectedT, t);
ensureServerStateNotLeaked();
}
verify(stream).close(statusCaptor.capture(), metadataCaptor.capture());
Status status = statusCaptor.getValue();
assertEquals(Code.UNKNOWN, status.getCode());
assertEquals("Application error processing RPC", status.getDescription());
assertEquals(expectedT, status.getCause());
assertTrue(metadataCaptor.getValue().keys().isEmpty());
}

@Test
public void testInternalClose_propagatesResourceExhausted() {
JumpToApplicationThreadServerStreamListener listener
= new JumpToApplicationThreadServerStreamListener(
executor.getScheduledExecutorService(),
executor.getScheduledExecutorService(),
stream,
Context.ROOT.withCancellation(),
PerfMark.createTag());
ServerStreamListener mockListener = mock(ServerStreamListener.class);
listener.setListener(mockListener);

StatusRuntimeException statusRuntimeException
= new StatusRuntimeException(Status.RESOURCE_EXHAUSTED.withDescription("exhausted"));
doThrow(statusRuntimeException).when(mockListener)
.messagesAvailable(any(StreamListener.MessageProducer.class));
listener.messagesAvailable(mock(StreamListener.MessageProducer.class));
try {
executor.runDueTasks();
fail("Expected exception");
} catch (RuntimeException t) {
assertSame(statusRuntimeException, t);
}
verify(stream).close(statusCaptor.capture(), metadataCaptor.capture());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.RESOURCE_EXHAUSTED, status.getCode());
assertEquals("exhausted", status.getDescription());
assertEquals(statusRuntimeException, status.getCause());
assertTrue(metadataCaptor.getValue().keys().isEmpty());
}

private void createAndStartServer() throws IOException {
createServer();
server.start();
Expand Down