diff --git a/api/src/main/java/io/grpc/Contexts.java b/api/src/main/java/io/grpc/Contexts.java index c62ffc80a38..9c3697dd6d4 100644 --- a/api/src/main/java/io/grpc/Contexts.java +++ b/api/src/main/java/io/grpc/Contexts.java @@ -118,6 +118,16 @@ public void onReady() { context.detach(previous); } } + + @Override + public void onEvent(Object event) { + Context previous = context.attach(); + try { + super.onEvent(event); + } finally { + context.detach(previous); + } + } } /** diff --git a/api/src/main/java/io/grpc/PartialForwardingServerCall.java b/api/src/main/java/io/grpc/PartialForwardingServerCall.java index a313407b23e..8c8f53cf93c 100644 --- a/api/src/main/java/io/grpc/PartialForwardingServerCall.java +++ b/api/src/main/java/io/grpc/PartialForwardingServerCall.java @@ -87,6 +87,11 @@ public SecurityLevel getSecurityLevel() { return delegate().getSecurityLevel(); } + @Override + public void triggerEvent(Object event) { + delegate().triggerEvent(event); + } + @Override public String toString() { return MoreObjects.toStringHelper(this).add("delegate", delegate()).toString(); diff --git a/api/src/main/java/io/grpc/PartialForwardingServerCallListener.java b/api/src/main/java/io/grpc/PartialForwardingServerCallListener.java index ca2fd0058c9..23e93bb065e 100644 --- a/api/src/main/java/io/grpc/PartialForwardingServerCallListener.java +++ b/api/src/main/java/io/grpc/PartialForwardingServerCallListener.java @@ -50,6 +50,11 @@ public void onReady() { delegate().onReady(); } + @Override + public void onEvent(Object event) { + delegate().onEvent(event); + } + @Override public String toString() { return MoreObjects.toStringHelper(this).add("delegate", delegate()).toString(); diff --git a/api/src/main/java/io/grpc/ServerCall.java b/api/src/main/java/io/grpc/ServerCall.java index 3db8ac30e83..2e6ee07a23f 100644 --- a/api/src/main/java/io/grpc/ServerCall.java +++ b/api/src/main/java/io/grpc/ServerCall.java @@ -100,6 +100,20 @@ public void onComplete() {} * another {@code onReady()} callback. */ public void onReady() {} + + /** + * A custom event has been triggered by the call. + * + *

This callback is guaranteed to run on the call's executor, serialized with other + * callbacks (like {@link #onMessage}, {@link #onHalfClose}). This means the implementation + * does not need internal synchronization to access call-specific state. + * + * @param event the triggered event. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12979") + public void onEvent(Object event) { + // Default no-op + } } /** @@ -262,6 +276,20 @@ public String getAuthority() { return null; } + /** + * Triggers a custom event to be processed by the listener. + * The event will be delivered to {@link Listener#onEvent(Object)} on the call's executor. + * + *

This method is thread-safe and can be called from any thread. No events will be delivered + * after the RPC is cancelled or completed. + * + * @param event the event to trigger. + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12979") + public void triggerEvent(Object event) { + // Default no-op + } + /** * The {@link MethodDescriptor} for the call. */ diff --git a/api/src/test/java/io/grpc/ContextsTest.java b/api/src/test/java/io/grpc/ContextsTest.java index ec9dc3929a2..974b1aff0c2 100644 --- a/api/src/test/java/io/grpc/ContextsTest.java +++ b/api/src/test/java/io/grpc/ContextsTest.java @@ -82,6 +82,11 @@ public void interceptCall_basic() { assertSame(uniqueContext, Context.current()); methodCalls.add(5); } + + @Override public void onEvent(Object event) { + assertSame(uniqueContext, Context.current()); + methodCalls.add(6); + } }; ServerCall.Listener wrapped = interceptCall(uniqueContext, call, headers, new ServerCallHandler() { @@ -101,7 +106,8 @@ public ServerCall.Listener startCall( wrapped.onCancel(); wrapped.onComplete(); wrapped.onReady(); - assertEquals(Arrays.asList(1, 2, 3, 4, 5), methodCalls); + wrapped.onEvent(new Object()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), methodCalls); assertSame(origContext, Context.current()); } @@ -145,6 +151,10 @@ public void interceptCall_restoresIfListenerThrows() { @Override public void onReady() { throw new RuntimeException(); } + + @Override public void onEvent(Object event) { + throw new RuntimeException(); + } }; ServerCall.Listener wrapped = interceptCall(uniqueContext, call, headers, new ServerCallHandler() { @@ -180,6 +190,11 @@ public ServerCall.Listener startCall( fail("Exception expected"); } catch (RuntimeException expected) { } + try { + wrapped.onEvent(new Object()); + fail("Exception expected"); + } catch (RuntimeException expected) { + } assertSame(origContext, Context.current()); } diff --git a/binder/src/main/java/io/grpc/binder/internal/Inbound.java b/binder/src/main/java/io/grpc/binder/internal/Inbound.java index 83fc8273d6f..5671d808114 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Inbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Inbound.java @@ -668,6 +668,16 @@ protected void deliverCloseAbnormal(Status status) { listener.closed(status); } + void triggerEvent(Object event) { + ServerStreamListener localListener; + synchronized (this) { + localListener = listener; + } + if (localListener != null) { + localListener.triggerEvent(event); + } + } + @GuardedBy("this") void onCloseSent(Status status) { if (!isClosed()) { diff --git a/binder/src/main/java/io/grpc/binder/internal/MultiMessageServerStream.java b/binder/src/main/java/io/grpc/binder/internal/MultiMessageServerStream.java index f54769caefa..7a57138ce22 100644 --- a/binder/src/main/java/io/grpc/binder/internal/MultiMessageServerStream.java +++ b/binder/src/main/java/io/grpc/binder/internal/MultiMessageServerStream.java @@ -175,6 +175,11 @@ public void setDecompressor(Decompressor decompressor) { // Ignore. } + @Override + public void triggerEvent(Object event) { + inbound.triggerEvent(event); + } + @Override public void optimizeForDirectExecutor() { // Ignore. diff --git a/binder/src/main/java/io/grpc/binder/internal/SingleMessageServerStream.java b/binder/src/main/java/io/grpc/binder/internal/SingleMessageServerStream.java index 383bd7a2593..5f1dd511f73 100644 --- a/binder/src/main/java/io/grpc/binder/internal/SingleMessageServerStream.java +++ b/binder/src/main/java/io/grpc/binder/internal/SingleMessageServerStream.java @@ -167,6 +167,11 @@ public void setDecompressor(Decompressor decompressor) { // Ignore. } + @Override + public void triggerEvent(Object event) { + inbound.triggerEvent(event); + } + @Override public void optimizeForDirectExecutor() { // Ignore. diff --git a/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java index b0d84f1be74..e556954fd40 100644 --- a/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java @@ -289,6 +289,7 @@ public ListenableFuture checkAuthorizationAsync(int uid) { ListenableFuture authFuture = asyncPolicy.checkAuthorizationAsync(SOME_UID); assertThat(awaitResult(settableUid)).isEqualTo(SOME_UID); authFuture.cancel(false); + executor.submit(() -> {}).get(10, TimeUnit.SECONDS); assertThat(delegateAuthFuture.isCancelled()).isTrue(); } diff --git a/core/src/main/java/io/grpc/internal/AbstractServerStream.java b/core/src/main/java/io/grpc/internal/AbstractServerStream.java index c468cba978a..67dfdc93d42 100644 --- a/core/src/main/java/io/grpc/internal/AbstractServerStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractServerStream.java @@ -173,6 +173,16 @@ public final void setListener(ServerStreamListener serverStreamListener) { transportState().setListener(serverStreamListener); } + @Override + public final void triggerEvent(final Object event) { + transportState().runOnTransportThread(new Runnable() { + @Override + public void run() { + transportState().triggerEvent(event); + } + }); + } + @Override public StatsTraceContext statsTraceContext() { return statsTraceCtx; @@ -259,6 +269,13 @@ public void deframerClosed(boolean hasPartialMessage) { + public final void triggerEvent(Object event) { + if (listenerClosed) { + return; + } + listener().triggerEvent(event); + } + @Override protected ServerStreamListener listener() { return listener; diff --git a/core/src/main/java/io/grpc/internal/ServerCallImpl.java b/core/src/main/java/io/grpc/internal/ServerCallImpl.java index e224384ce8f..6e894371be1 100644 --- a/core/src/main/java/io/grpc/internal/ServerCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerCallImpl.java @@ -254,6 +254,11 @@ public MethodDescriptor getMethodDescriptor() { return method; } + @Override + public void triggerEvent(Object event) { + stream.triggerEvent(event); + } + @Override public SecurityLevel getSecurityLevel() { final Attributes attributes = getAttributes(); @@ -395,5 +400,13 @@ public void onReady() { listener.onReady(); } } + + @Override + public void triggerEvent(Object event) { + if (call.cancelled) { + return; + } + listener.onEvent(event); + } } } diff --git a/core/src/main/java/io/grpc/internal/ServerImpl.java b/core/src/main/java/io/grpc/internal/ServerImpl.java index d9f64c2d473..767d85f443b 100644 --- a/core/src/main/java/io/grpc/internal/ServerImpl.java +++ b/core/src/main/java/io/grpc/internal/ServerImpl.java @@ -781,6 +781,9 @@ public void closed(Status status) {} @Override public void onReady() {} + + @Override + public void triggerEvent(Object event) {} } /** @@ -960,6 +963,34 @@ public void runInContext() { callExecutor.execute(new OnReady()); } } + + @Override + public void triggerEvent(final Object event) { + try (TaskCloseable ignore = PerfMark.traceTask("ServerStreamListener.triggerEvent")) { + PerfMark.attachTag(tag); + final Link link = PerfMark.linkOut(); + + final class TriggerEvent extends ContextRunnable { + TriggerEvent() { + super(context); + } + + @Override + public void runInContext() { + try (TaskCloseable ignore = PerfMark.traceTask("ServerCallListener(app).onEvent")) { + PerfMark.attachTag(tag); + PerfMark.linkIn(link); + getListener().triggerEvent(event); + } catch (Throwable t) { + internalClose(t); + throw t; + } + } + } + + callExecutor.execute(new TriggerEvent()); + } + } } @VisibleForTesting diff --git a/core/src/main/java/io/grpc/internal/ServerStream.java b/core/src/main/java/io/grpc/internal/ServerStream.java index aa5ba10329c..4c88e94e4d7 100644 --- a/core/src/main/java/io/grpc/internal/ServerStream.java +++ b/core/src/main/java/io/grpc/internal/ServerStream.java @@ -87,6 +87,12 @@ public interface ServerStream extends Stream { */ void setListener(ServerStreamListener serverStreamListener); + /** + * Triggers a custom event. Implementations must ensure this is propagated to the + * listener on the transport thread. + */ + void triggerEvent(Object event); + /** * The context for recording stats and traces for this stream. */ diff --git a/core/src/main/java/io/grpc/internal/ServerStreamListener.java b/core/src/main/java/io/grpc/internal/ServerStreamListener.java index e55217ab422..74de0f2079e 100644 --- a/core/src/main/java/io/grpc/internal/ServerStreamListener.java +++ b/core/src/main/java/io/grpc/internal/ServerStreamListener.java @@ -42,4 +42,9 @@ public interface ServerStreamListener extends StreamListener { * @param status details about the remote closure */ void closed(Status status); + + /** + * Propagates a custom event to the listener. Must be called on the transport thread. + */ + void triggerEvent(Object event); } diff --git a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java index 137ba19bfea..5defd17fdd0 100644 --- a/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractServerStreamTest.java @@ -361,6 +361,31 @@ public void close_sendTrailersClearsReservedFields() { assertEquals("bad", metadataCaptor.getValue().get(InternalStatus.MESSAGE_KEY)); } + @Test + public void triggerEvent_propagatesToListener() { + ServerStreamListener listener = mock(ServerStreamListener.class); + stream.transportState().setListener(listener); + + Object event = new Object(); + stream.triggerEvent(event); + + verify(listener).triggerEvent(event); + } + + @Test + public void triggerEvent_ignoredAfterClose() { + ServerStreamListener listener = mock(ServerStreamListener.class); + stream.transportState().setListener(listener); + + stream.close(Status.OK, new Metadata()); + stream.transportState().complete(); + + Object event = new Object(); + stream.triggerEvent(event); + + verify(listener, never()).triggerEvent(any()); + } + @Test public void changeOnReadyThreshold() { stream.setListener(new ServerStreamListenerBase()); @@ -391,6 +416,9 @@ public void halfClosed() {} @Override public void closed(Status status) {} + + @Override + public void triggerEvent(Object event) {} } private static class AbstractServerStreamBase extends AbstractServerStream { diff --git a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java index 7394c83eab2..4a2de9f3936 100644 --- a/core/src/test/java/io/grpc/internal/ServerCallImplTest.java +++ b/core/src/test/java/io/grpc/internal/ServerCallImplTest.java @@ -493,6 +493,32 @@ public void streamListener_unexpectedRuntimeException() { assertThat(e).hasMessageThat().isEqualTo("unexpected exception"); } + @Test + public void triggerEvent_propagatesToStream() { + Object event = new Object(); + call.triggerEvent(event); + verify(stream).triggerEvent(event); + } + + @Test + public void streamListener_triggerEvent() { + ServerStreamListenerImpl streamListener = + new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); + Object event = new Object(); + streamListener.triggerEvent(event); + verify(callListener).onEvent(event); + } + + @Test + public void streamListener_triggerEvent_cancelled() { + ServerStreamListenerImpl streamListener = + new ServerCallImpl.ServerStreamListenerImpl<>(call, callListener, context); + Object event = new Object(); + streamListener.closed(Status.CANCELLED); + streamListener.triggerEvent(event); + verify(callListener, never()).onEvent(event); + } + private static class LongMarshaller implements Marshaller { @Override public InputStream stream(Long value) { diff --git a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java index 5d07de32df9..5127c7c2b0f 100644 --- a/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java +++ b/core/src/testFixtures/java/io/grpc/internal/AbstractTransportTest.java @@ -2088,6 +2088,35 @@ public void clientChecksInboundMetadataSize_trailer() throws Exception { assertNull(metadata.get(tellTaleKey)); } + @Test + public void serverStream_triggerEvent() throws Exception { + server.start(serverListener); + client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + MockServerTransportListener serverTransportListener + = serverListener.takeListenerOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS); + serverTransport = serverTransportListener.transport; + + ClientStream clientStream = client.newStream( + methodDescriptor, new Metadata(), callOptions, noopTracers); + ClientStreamListenerBase clientStreamListener = new ClientStreamListenerBase(); + clientStream.start(clientStreamListener); + + StreamCreation serverStreamCreation + = serverTransportListener.takeStreamOrFail(TIMEOUT_MS, TimeUnit.MILLISECONDS); + ServerStream serverStream = serverStreamCreation.stream; + ServerStreamListenerBase serverStreamListener = serverStreamCreation.listener; + + Object event = new Object(); + serverStream.triggerEvent(event); + + Object receivedEvent = serverStreamListener.eventQueue.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals(event, receivedEvent); + + // Cleanup + clientStream.cancel(Status.CANCELLED); + } + /** * Helper that simply does an RPC. It can be used similar to a sleep for negative testing: to give * time for actions _not_ to happen. Since it is based on doing an actual RPC with actual diff --git a/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java index aaa70600542..e4ac01912e4 100644 --- a/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java +++ b/core/src/testFixtures/java/io/grpc/internal/ServerStreamListenerBase.java @@ -89,6 +89,8 @@ public void halfClosed() { halfClosedLatch.countDown(); } + public final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + @Override public void closed(Status status) { if (this.status.isDone()) { @@ -96,4 +98,12 @@ public void closed(Status status) { } this.status.set(status); } + + @Override + public void triggerEvent(Object event) { + if (this.status.isDone()) { + fail("triggerEvent invoked after closed"); + } + eventQueue.add(event); + } } diff --git a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index a92f10fd5c5..57820b396ad 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -429,6 +429,11 @@ public void setListener(ServerStreamListener serverStreamListener) { clientStream.setListener(serverStreamListener); } + @Override + public void triggerEvent(Object event) { + clientStream.triggerServerEvent(event); + } + @Override public void request(int numMessages) { boolean onReady = clientStream.serverRequested(numMessages); @@ -732,6 +737,20 @@ private synchronized void setListener(ServerStreamListener listener) { this.serverStreamListener = listener; } + void triggerServerEvent(final Object event) { + synchronized (this) { + if (!closed) { + syncContext.executeLater(new Runnable() { + @Override + public void run() { + serverStreamListener.triggerEvent(event); + } + }); + } + } + syncContext.drain(); + } + @Override public void request(int numMessages) { boolean onReady = serverStream.clientRequested(numMessages); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index ef8d2e5efda..b22be2460f1 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -1343,6 +1343,10 @@ public void halfClosed() { @Override public void closed(Status status) { } + + @Override + public void triggerEvent(Object event) { + } } private final class EchoServerListener implements ServerListener { diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java index 00db6e1d339..d3e50cf3821 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java @@ -1519,6 +1519,10 @@ public void closed(Status status) { public void onReady() { } + @Override + public void triggerEvent(Object event) { + } + static String getContent(InputStream message) throws IOException { try { return new String(ByteStreams.toByteArray(message), UTF_8);