question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

gRPC Server Streaming: Client Only Receives #onNext Response After Server Calls #onCompleted

See original GitHub issue

Describe the bug

The server’s responseObserver#onNext calls seem to be collected and only sent to the client after the server calls responseObserver#onComplete.

Expected behavior

The server’s responseObserver#onNext calls are sent to the client immediately when the server calls them and not only after responseObserver#onComplete was called.

Actual behavior

The server’s responseObserver#onNext calls seem to be collected and only sent to the client after the server calls responseObserver#onComplete.

How to Reproduce?

Create a Maven project and follow these steps (note that I put client and server in the same Maven project)

Generate code for this protobuf:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "examples";
option java_outer_classname = "HelloWorldProto";

package helloworld;

service Greeter {
    rpc SayHelloServerStr (HelloRequest) returns (stream HelloReply) {}
}

message HelloRequest {
    repeated int32 duration = 1;
}

message HelloReply {
    string message = 1;
}

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.acme</groupId>
    <artifactId>grpc-plain-text-quickstart</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
        <quarkus.platform.version>2.14.1.Final</quarkus.platform.version>
        <surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
        <compiler-plugin.version>3.8.0</compiler-plugin.version>
        <assertj.version>3.22.0</assertj.version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
            <dependency>
                <groupId>org.assertj</groupId>
                <artifactId>assertj-core</artifactId>
                <version>${assertj.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-resteasy-reactive</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-grpc</artifactId>
        </dependency>
        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.awaitility</groupId>
            <artifactId>awaitility</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
	<plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>quarkus-maven-plugin</artifactId>
                <version>${quarkus.platform.version}</version>
                <executions>
                    <execution>
                        <goals>
			    <goal>build</goal>
			    <goal>generate-code</goal>
		            <goal>generate-code-tests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <systemPropertyVariables>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    </systemPropertyVariables>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <quarkus.package.type>native</quarkus.package.type>
            </properties>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-failsafe-plugin</artifactId>
                        <version>${surefire-plugin.version}</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>integration-test</goal>
                                    <goal>verify</goal>
                                </goals>
                                <configuration>
                                    <systemPropertyVariables>
                                        <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                                    </systemPropertyVariables>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Java server service impl.:

import examples.GreeterGrpc.GreeterImplBase;
import examples.HelloReply;
import examples.HelloRequest;
import io.grpc.stub.StreamObserver;
import io.quarkus.grpc.GrpcService;

@GrpcService
public class HelloWorldService extends GreeterImplBase {
	
	@Override
	public void sayHelloServerStr(HelloRequest request, StreamObserver<HelloReply> responseObserver) {

		request.getDurationList().parallelStream().forEach(duration -> {
			System.out.println("server: starting " + duration);
			sleep(duration);
			responseObserver.onNext(HelloReply.newBuilder().setMessage("Slept for " + duration + " ms").build());
			System.out.println("server: finished " + duration);
		});

		System.out.println("server: reached #onCompleted");
		sleep(2000);
		responseObserver.onCompleted();
		System.out.println("server: finished #onCompleted");
	}

	public static void sleep(long duration) {
		try {
			Thread.sleep(duration);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}

}

Java client code:

import static java.util.Arrays.asList;

import examples.GreeterGrpc;
import examples.HelloRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class MainServerStreaming {
	public static void main(String[] args) {
		ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9000).usePlaintext().build();

		try {
			final var durations = asList(1000, 1500, 50);

			final var request = HelloRequest.newBuilder().addAllDuration(durations).build();

			final var stub = GreeterGrpc.newBlockingStub(channel);

			stub.sayHelloServerStr(request).forEachRemaining(response -> {
				System.out.println("client:" + response.getMessage());
			});
			System.out.println("client: done");

		} finally {
			channel.shutdown();
		}
	}
}

Steps

  • Run the server (e.g. with mvn compile quarkus:dev)
  • Run the client

You’ll notice that the client output

client:Slept for 50 ms
client:Slept for 1000 ms
client:Slept for 1500 ms
client: done

only starts after the server calls responseObserver.onCompleted();.

Output of uname -a or ver

MSYS_NT-10.0-19044 LT-000X9DJ1M2 3.3.3-341.x86_64 2022-01-17 11:45 UTC x86_64 Msys

Output of java -version

openjdk version “17.0.3” 2022-04-19 OpenJDK Runtime Environment Temurin-17.0.3+7 (build 17.0.3+7) OpenJDK 64-Bit Server VM Temurin-17.0.3+7 (build 17.0.3+7, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

2.14.1.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.5

Additional information

I implemented a gRPC server with vanilla gRPC with the Maven dependencies described on https://github.com/grpc/grpc-java/blob/master/README.md. I used the very same service implementation and client code. I only had to start the server on my own like

public static void main(String[] args) throws Exception {
	System.out.println("Server started on 9000");

	Server server = ServerBuilder.forPort(9000).addService(new HelloWorldService()).build();
	server.start();

	Runtime.getRuntime().addShutdownHook(new Thread(() -> {
		server.shutdown();
		System.out.println("Successfully stopped the server");
	}));

	server.awaitTermination();
}

-> The vanilla gRPC server works as specified: The #onNext responses are sent to the client immediately. The client starts printing its output already before the server calls #onCompleted on the observer.

I.e. with vanilla gRPC I can achieve the desired behavior I cannot achieve with Quarkus gRPC.

Issue Analytics

  • State:closed
  • Created 10 months ago
  • Reactions:9
  • Comments:14 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
cescoffiercommented, Nov 28, 2022

Yes, it works as expected, see my comment where I explain the monopolization of the event loop.

1reaction
cescoffiercommented, Nov 23, 2022

Well, that was interesting and the test is wrong, not the Quarkus code 😄

Let me explain. The sayHelloServerStr runs on the event loop, the same event loop used underneath by netty to write the frames into the wire. When calling onNext, the message gets serialized and enqueued to a write queue. A flush action is scheduled on the same event loop. So, the flush happens later (to avoid writing too much).

However, the code shown above is monopolizing that thread, meaning that while the messages are written on the write queue, the flush doesn’t have the chance to run, as the event loop is never released.

How to fix the code: add @Blocking, and it will work.

Note that if you have kept monopolizing/blocking the event loop for more than 2s, you would have seen a warning in the log.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Neither OnCancelHandler nor onComplete gets invoked after ...
After server sends completion event to the client, i.e., half-closes the call, client doesn't dispatch any other events (although request ...
Read more >
Can someone explain to me what's the proper usage of gRPC ...
The method doc says: "Receives a terminating error from the stream. May only be called once and if called it must be the...
Read more >
StreamObserver (grpc-all 1.51.0 API)
Method Detail​​ Receives a value from the stream. Can be called many times but is never called after onError(Throwable) or onCompleted() are called....
Read more >
server doesn't detect when client disappears - Google Groups
When the server shuts down, this is easy - it calls StreamObserver.onCompleted() and the client is then notified that the stream is ending....
Read more >
grpc/grpc - Gitter
Client sends serverResponseObserver.onCompleted() to indicate that client is done sending requests. Server receives half-close from client and does cleanup and ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found