Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract grpc protocol to separate artifact and add AbstractUnaryGrpcService. #1727

Merged
merged 4 commits into from
Apr 23, 2019

Conversation

anuraaga
Copy link
Collaborator

@anuraaga anuraaga commented Apr 19, 2019

For #1703

Tried extracting a package within the armeria-grpc artifact that only contains wire format logic, but it is fragile due to accidental dependencies and classpath magic like service loaders, which we use. So now we extract to a separate artifact entirely.

AbstractUnaryGrpcService allows defining unary gRPC services that are provided the direct bytes of a single gRPC message and return the bytes of a single gRPC message, and the base class takes cafe of the gRPC wire protocol. As this is an advanced use case, it doesn't attempt to implement everything in gRPC but should hopefully provide enough for most armeria-grpc users that are concerned about having dependencies on stub libraries.

@adriancole can you check whether this works for you or provide any suggestions?

Copy link
Contributor

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

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

I think this will work. need to test as you mentioned

@codecov
Copy link

codecov bot commented Apr 21, 2019

Codecov Report

Merging #1727 into master will increase coverage by 0.03%.
The diff coverage is 86.79%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #1727      +/-   ##
============================================
+ Coverage      72.9%   72.94%   +0.03%     
- Complexity     8140     8151      +11     
============================================
  Files           734      736       +2     
  Lines         32419    32465      +46     
  Branches       3986     3989       +3     
============================================
+ Hits          23636    23680      +44     
- Misses         6751     6755       +4     
+ Partials       2032     2030       -2
Impacted Files Coverage Δ Complexity Δ
...ecorp/armeria/server/grpc/UnframedGrpcService.java 82.72% <ø> (ø) 19 <0> (ø) ⬇️
...a/common/grpc/protocol/ArmeriaStatusException.java 100% <ø> (ø) 3 <0> (?)
.../com/linecorp/armeria/server/grpc/GrpcService.java 88.46% <ø> (ø) 29 <0> (ø) ⬇️
...inecorp/armeria/client/grpc/ArmeriaClientCall.java 81.67% <ø> (ø) 37 <0> (ø) ⬇️
...ria/common/grpc/protocol/StatusMessageEscaper.java 92.3% <ø> (ø) 22 <0> (?)
...a/common/grpc/protocol/ArmeriaMessageDeframer.java 70.81% <ø> (ø) 47 <0> (?)
...ria/common/grpc/protocol/ArmeriaMessageFramer.java 91.48% <ø> (ø) 19 <0> (?)
...necorp/armeria/internal/grpc/HttpStreamReader.java 81.18% <ø> (ø) 28 <0> (ø) ⬇️
...armeria/common/grpc/protocol/GrpcTrailersUtil.java 100% <100%> (ø) 3 <3> (?)
...inecorp/armeria/server/grpc/ArmeriaServerCall.java 86.16% <100%> (-0.33%) 81 <2> (-2)
... and 11 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a6c9fcc...792dcc8. Read the comment docs.

Copy link
Member

@trustin trustin left a comment

Choose a reason for hiding this comment

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

Very nice. Great job, @anuraaga 👍

@trustin trustin added this to the 0.84.0 milestone Apr 22, 2019
Copy link
Contributor

@hyangtack hyangtack left a comment

Choose a reason for hiding this comment

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

Great job! Left one nit. :-)

@trustin trustin removed the request for review from minwoox April 22, 2019 02:49
@codefromthecrypt
Copy link
Contributor

wonder if you need to rebase this in order to get the gradle + JITPACK goodness or not..

@codefromthecrypt
Copy link
Contributor

at any rate I can just use snapshot to test :P

@anuraaga
Copy link
Collaborator Author

Jitpack is set up by the depending project (e.g, zipkin), don't think we'd need to do anything here for you to try it

@codefromthecrypt
Copy link
Contributor

@anuraaga sorry.. didn't tag you on this reference line/gradle-scripts#55

@codefromthecrypt
Copy link
Contributor

looks like still a dep on grpc's utilities, used for the following header

    /**
     * {@code "armeria.grpc.ThrowableProto-bin"}.
     */
    public static final AsciiString ARMERIA_GRPC_THROWABLEPROTO_BIN =
            HttpHeaderNames.of(ProtoUtils.keyForProto(ThrowableProto.getDefaultInstance()).name());
Caused by: java.lang.ClassNotFoundException: io.grpc.protobuf.ProtoUtils
	at java.net.URLClassLoader.findClass(URLClassLoader.java:471) ~[?:?]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:588) ~[?:?]
	at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:93) ~[zipkin-server-2.13.0-SNAPSHOT-exec.jar:?]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[?:?]
	at com.linecorp.armeria.common.grpc.GrpcHeaderNames.<clinit>(GrpcHeaderNames.java:52) ~[armeria-grpc-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.common.grpc.protocol.AbstractUnaryGrpcService.<clinit>(AbstractUnaryGrpcService.java:47) ~[armeria-grpc-0.84.0-SNAPSHOT.jar!/:?]

@codefromthecrypt
Copy link
Contributor

testing by inlining "armeria.grpc.ThrowableProto-bin" vs using code to generate it

@codefromthecrypt
Copy link
Contributor

right now I am building with a small patch. I still have the exception above, just trying to figure out what to do about it.

diff --git a/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcHeaderNames.java b/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcHeaderNames.java
index b14f5442..0c0bb342 100644
--- a/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcHeaderNames.java
+++ b/grpc/src/main/java/com/linecorp/armeria/common/grpc/GrpcHeaderNames.java
@@ -17,8 +17,6 @@
 package com.linecorp.armeria.common.grpc;
 
 import com.linecorp.armeria.common.HttpHeaderNames;
-
-import io.grpc.protobuf.ProtoUtils;
 import io.netty.util.AsciiString;
 
 /**
@@ -49,7 +47,7 @@ public final class GrpcHeaderNames {
      * {@code "armeria.grpc.ThrowableProto-bin"}.
      */
     public static final AsciiString ARMERIA_GRPC_THROWABLEPROTO_BIN =
-            HttpHeaderNames.of(ProtoUtils.keyForProto(ThrowableProto.getDefaultInstance()).name());
+            HttpHeaderNames.of("armeria.grpc.ThrowableProto-bin");
 
     private GrpcHeaderNames() {}
 }

@codefromthecrypt
Copy link
Contributor

this also crashes the server.. looking into it now.

java.lang.NoClassDefFoundError: io/grpc/ServerServiceDefinition
	at com.linecorp.armeria.server.grpc.GrpcDocServicePlugin.loadDocStrings(GrpcDocServicePlugin.java:195) ~[armeria-grpc-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.server.docs.DocService.lambda$addDocStrings$4(DocService.java:181) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:271) ~[?:?]
	at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) ~[?:?]
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[?:?]
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474) ~[?:?]
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913) ~[?:?]
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?]
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578) ~[?:?]
	at com.linecorp.armeria.server.docs.DocService.addDocStrings(DocService.java:183) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.server.docs.DocService.access$200(DocService.java:80) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.server.docs.DocService$1.serverStarting(DocService.java:161) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.server.Server$ServerStartStopSupport.notifyStarting(Server.java:494) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.server.Server$ServerStartStopSupport.notifyStarting(Server.java:266) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.common.util.StartStopSupport.notifyListeners(StartStopSupport.java:342) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at com.linecorp.armeria.common.util.StartStopSupport.lambda$start$3(StartStopSupport.java:171) ~[armeria-0.84.0-SNAPSHOT.jar!/:?]
	at io.netty.util.concurrent.GlobalEventExecutor$TaskRunner.run(GlobalEventExecutor.java:248) [netty-common-4.1.34.Final.jar!/:4.1.34.Final]
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.34.Final.jar!/:4.1.34.Final]
	at java.lang.Thread.run(Thread.java:834) [?:?]
Caused by: java.lang.ClassNotFoundException: io.grpc.ServerServiceDefinition
	at java.net.URLClassLoader.findClass(URLClassLoader.java:471) ~[?:?]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:588) ~[?:?]
	at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:93) ~[zipkin-server-2.13.0-SNAPSHOT-exec.jar:?]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[?:?]
	... 19 more

@anuraaga
Copy link
Collaborator Author

Ah I think the doc service stuff will get triggered as long as armeria-grpc is on the classpath, using service loader.

This would be a reason to extract a separate artifact for grpc-protocol. Otherwise, I can just add a try / catch there which feels yucky but might be fine. @trustin any thoughts?

@codefromthecrypt
Copy link
Contributor

I've a try/catch in progress.. just testing now

--- a/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java
+++ b/core/src/main/java/com/linecorp/armeria/server/docs/DocService.java
@@ -32,6 +32,7 @@ import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
@@ -178,8 +179,14 @@ public class DocService extends AbstractCompositeService<HttpRequest, HttpRespon
     private static ServiceSpecification addDocStrings(ServiceSpecification spec, List<ServiceConfig> services) {
         final Map<String, String> docStrings =
                 plugins.stream()
-                       .flatMap(plugin -> plugin.loadDocStrings(findSupportedServices(plugin, services))
-                                                .entrySet().stream())
+                       .flatMap(plugin -> {
+                           try { // Guard when optional classpath dependencies are unmet
+                               return plugin.loadDocStrings(findSupportedServices(plugin, services))
+                                   .entrySet().stream();
+                           } catch (NoClassDefFoundError e) {
+                               return Stream.empty();
+                           }
+                       })
                        .collect(toImmutableMap(Entry::getKey, Entry::getValue, (a, b) -> a));
 
         return new ServiceSpecification(

@codefromthecrypt
Copy link
Contributor

ok with the two diffs I posted the server starts. the response is different, which I'm looking into now. Seems like a missing content type header.

before

$ curl -X POST localhost:9411/zipkin.proto3.SpanService/Report -H'Content-Type: application/grpc' -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9411 (#0)
> POST /zipkin.proto3.SpanService/Report HTTP/1.1
> Host: localhost:9411
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/grpc
> 
< HTTP/1.1 200 OK
< content-type: application/grpc+proto
< grpc-status: 13
< grpc-message: Half-closed without a request
< content-length: 0
< 
* Connection #0 to host localhost left intact

now (plus the two patches)

$ curl -X POST localhost:9411/zipkin.proto3.SpanService/Report -H'Content-Type: application/grpc' -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9411 (#0)
> POST /zipkin.proto3.SpanService/Report HTTP/1.1
> Host: localhost:9411
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/grpc
> 
< HTTP/1.1 200 OK
< grpc-encoding: identity
< transfer-encoding: chunked
< 
* Connection #0 to host localhost left intact

@codefromthecrypt
Copy link
Contributor

ps hold a tick as I think the path I'm looking at isn't actually required..

@codefromthecrypt
Copy link
Contributor

here's my entire code now.. same thing with the difference in curl result (also with grpc client fail). Not sure if there's a smoking gun here.

service SpanService {
  rpc Report(zipkin.proto3.ListOfSpans) returns (google.protobuf.Empty) {}
}
@ConditionalOnProperty(name = "zipkin.collector.grpc.enabled") // disabled by default
final class ZipkinGrpcCollector {
  @Bean ArmeriaServerConfigurator grpcCollectorConfigurator(StorageComponent storage,
    CollectorSampler sampler, CollectorMetrics metrics) {
    CollectorMetrics grpcMetrics = metrics.forTransport("grpc");
    Collector collector = Collector.newBuilder(getClass())
      .storage(storage)
      .sampler(sampler)
      .metrics(grpcMetrics)
      .build();

    return sb ->
      sb.service("/zipkin.proto3.SpanService/Report", new SpanService(collector, grpcMetrics));
  }

  static final class SpanService extends AbstractUnaryGrpcService {

    final Collector collector;
    final CollectorMetrics metrics;

    SpanService(Collector collector, CollectorMetrics metrics) {
      this.collector = collector;
      this.metrics = metrics;
    }

    @Override protected CompletableFuture<byte[]> handleMessage(byte[] bytes) {
      metrics.incrementMessages();
      CompletableFutureCallback result = new CompletableFutureCallback();
      collector.acceptSpans(bytes, SpanBytesDecoder.PROTO3, result);
      return result;
    }
  }

  static final class CompletableFutureCallback extends CompletableFuture<byte[]>
    implements Callback<Void> {

    @Override public void onSuccess(Void value) {
      complete(new byte[0] /* empty result */);
    }

    @Override public void onError(Throwable t) {
      completeExceptionally(t);
    }
  }
}

@codefromthecrypt
Copy link
Contributor

it seems the thing grpc's client dislikes most is the missing content-type in the response

io.grpc.StatusRuntimeException: UNKNOWN: HTTP status code 200
invalid content-type: null
headers: Metadata(:status=200,grpc-encoding=identity)
DATA-----------------------------

@anuraaga
Copy link
Collaborator Author

Yeah will add that in, that was not intended. Will also update the test to use upstream client instead of armeria client to catch that, since it's more important to test that anyways. Thanks for checking out the code!

@codefromthecrypt
Copy link
Contributor

actually there's a bit tougher issue trying to exclude protobuf (which reduces the jar cost of this integration to near nothing). serviceloader pukes on .hasNext()

@trustin
Copy link
Member

trustin commented Apr 22, 2019

@anuraaga wrote:

This would be a reason to extract a separate artifact for grpc-protocol. Otherwise, I can just add a try / catch there which feels yucky but might be fine. @trustin any thoughts?

+1 for a separate module, because otherwise it will be fairly easy to break this again and again.

@anuraaga
Copy link
Collaborator Author

Ready for another look.

Extracted a module, which solves doc service issue and ensures no more accidental usage of gRPC dependencies. And fixed content type header, upstream client went from not working to working in a unit test.

@adriancole think you should be good. I merged master so jitpack might work too

@codefromthecrypt
Copy link
Contributor

trying now thanks!

@codefromthecrypt
Copy link
Contributor

Screen Shot 2019-04-22 at 8 19 16 PM

the anticipation! cc @bsideup

@codefromthecrypt
Copy link
Contributor

@codefromthecrypt
Copy link
Contributor

ignore the jitpack related comments.. will follow-up on these elsewhere

@codefromthecrypt
Copy link
Contributor

shipit! this works https://github.com/apache/incubator-zipkin/pull/2328/files

@trustin
Copy link
Member

trustin commented Apr 22, 2019

I think we can merge this and do a follow-up on armeria-grpc-protocol later. Sounds good, @anuraaga and @adriancole ?

@anuraaga
Copy link
Collaborator Author

Yeah I'm good but just to clarify, what do you mean by follow up? Do you mean the name of the artifact?

@trustin
Copy link
Member

trustin commented Apr 22, 2019

@anuraaga That's because I didn't know you finished the separation already. That was fast. 👍

@trustin
Copy link
Member

trustin commented Apr 22, 2019

Could you update the PR description and commit message finally so I can merge this PR?

@anuraaga anuraaga changed the title Add AbstractUnaryGrpcService. Extract grpc protocol to separate artifact and add AbstractUnaryGrpcService. Apr 22, 2019
@anuraaga
Copy link
Collaborator Author

Updated description :)

@trustin
Copy link
Member

trustin commented Apr 23, 2019

@hyangtack Could you take another look since it was updated since your last review?

Copy link
Contributor

@hyangtack hyangtack left a comment

Choose a reason for hiding this comment

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

Still LGTM :-)

@trustin trustin merged commit 743b97f into line:master Apr 23, 2019
@trustin
Copy link
Member

trustin commented Apr 23, 2019

Thanks, @anuraaga and @adriancole !

@anuraaga anuraaga mentioned this pull request Apr 30, 2019
trustin pushed a commit that referenced this pull request May 2, 2019
Fixes #1703

This is the client version of #1727. Advanced users can use UnaryGrpcClient to issue gRPC-framed requests to a gRPC server given they handle serialization / deserialization of messages themselves, either by depending on protobuf (without needing to depend on grpc) or reimplementing the logic themselves.

As this is an advanced API, I think it's good to keep it small since it won't be as well tested as normal APIs. I've only exposed the most flexible APIs without providing any extra helpers as a result.
fmguerreiro pushed a commit to fmguerreiro/armeria that referenced this pull request Sep 19, 2020
…ervice. (line#1727)

For line#1703 

Tried extracting a package within the armeria-grpc artifact that only contains wire format logic, but it is fragile due to accidental dependencies and classpath magic like service loaders, which we use. So now we extract to a separate artifact entirely.

`AbstractUnaryGrpcService` allows defining unary gRPC services that are provided the direct bytes of a single gRPC message and return the bytes of a single gRPC message, and the base class takes cafe of the gRPC wire protocol. As this is an advanced use case, it doesn't attempt to implement everything in gRPC but should hopefully provide enough for most armeria-grpc users that are concerned about having dependencies on stub libraries.
fmguerreiro pushed a commit to fmguerreiro/armeria that referenced this pull request Sep 19, 2020
Fixes line#1703

This is the client version of line#1727. Advanced users can use UnaryGrpcClient to issue gRPC-framed requests to a gRPC server given they handle serialization / deserialization of messages themselves, either by depending on protobuf (without needing to depend on grpc) or reimplementing the logic themselves.

As this is an advanced API, I think it's good to keep it small since it won't be as well tested as normal APIs. I've only exposed the most flexible APIs without providing any extra helpers as a result.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants