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

Introduce GsonGrpcJsonMarshaller and catch JsonMarshaller initialization failure #4033

Merged
merged 14 commits into from
Jan 25, 2022

Conversation

jrhee17
Copy link
Contributor

@jrhee17 jrhee17 commented Jan 16, 2022

Motivation:

Currently, armeria server fails to start up while trying to create a MessageMarshaller for proto2 files containing Enum definitions.

It seems like protobuf-jackson doesn't support proto2.
However, it seems like upstream at least partially supports proto2.

https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/JsonFormat

The JSON format follows Proto3 JSON specification and only proto3 features are supported. Proto2 only features (e.g., extensions and unknown fields) will be discarded in the conversion. That is, when converting proto2 messages to JSON format, extensions and unknown fields will be treated as if they do not exist. This applies to proto2 messages embedded in proto3 messages as well.

I propose that we fallback to use the upstream implementation as the default json marshaller when a service includes any proto2 defined message.
I realized that it is difficult to support such default implementation since

  1. It isn't possible to detect if proto2 specific features are used. Even though proto2 files are included, users may not be using proto2 specific features. These users will incur the penalty of using the gson implementation unnecessarily.
  2. Since GrpcJsonMarshallerBuilder#jsonMarshallerCustomizer is exposed, it is difficult to introduce a new default json marshalling implementation without breaking changes. (we will need to add a mapping from MessageMarshaller#Builder to JsonFormat options which probably isn't generic/worth it)

Considering the above, I believe it is easier just provide an option so that users can opt-in for gson serde

Modifications:

  • Introduce a GsonGrpcJsonMarshaller which performs json marshalling using google's apis. Also introduce a corresponding builder class GrpcJsonMarshallerBuilder
  • Add apis GrpcJsonMarshaller#ofJackson, GrpcJsonMarshaller#builderForJackson, GrpcJsonMarshaller#ofGson, GrpcJsonMarshaller#builderForGson

Result:

@codecov
Copy link

codecov bot commented Jan 16, 2022

Codecov Report

Merging #4033 (c6b082d) into master (f60ab06) will increase coverage by 0.03%.
The diff coverage is 50.87%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #4033      +/-   ##
============================================
+ Coverage     73.36%   73.40%   +0.03%     
- Complexity    15893    15947      +54     
============================================
  Files          1385     1388       +3     
  Lines         60755    60883     +128     
  Branches       7701     7714      +13     
============================================
+ Hits          44575    44689     +114     
- Misses        12283    12291       +8     
- Partials       3897     3903       +6     
Impacted Files Coverage Δ
...ria/common/grpc/GsonGrpcJsonMarshallerBuilder.java 18.18% <18.18%> (ø)
...rmeria/internal/client/grpc/GrpcClientFactory.java 84.53% <50.00%> (-3.38%) ⬇️
...inecorp/armeria/server/grpc/FramedGrpcService.java 82.06% <63.63%> (-2.33%) ⬇️
...rp/armeria/common/grpc/GsonGrpcJsonMarshaller.java 85.71% <85.71%> (ø)
...necorp/armeria/common/grpc/GrpcJsonMarshaller.java 100.00% <100.00%> (ø)
...rmeria/spring/actuate/WebOperationServiceUtil.java 82.60% <0.00%> (-17.40%) ⬇️
...ecorp/armeria/server/LengthBasedServiceNaming.java 75.00% <0.00%> (-16.67%) ⬇️
.../armeria/client/circuitbreaker/CircuitBreaker.java 60.00% <0.00%> (-15.00%) ⬇️
.../linecorp/armeria/client/retrofit2/PipeBuffer.java 79.06% <0.00%> (-9.31%) ⬇️
.../linecorp/armeria/server/tomcat/TomcatService.java 57.95% <0.00%> (-2.45%) ⬇️
... and 42 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 f60ab06...c6b082d. Read the comment docs.

@jrhee17 jrhee17 marked this pull request as ready for review January 17, 2022 07:34
@jrhee17 jrhee17 marked this pull request as draft January 17, 2022 08:09
} else if (grpcJsonMarshallType == GrpcJsonMarshallType.PROTOBUF_JACKSON) {
return GrpcJsonUtil.protobufJacksonJsonMarshaller(serviceDescriptor, jsonMarshallerCustomizer);
} else if (grpcJsonMarshallType == GrpcJsonMarshallType.UPSTREAM) {
return UpstreamJsonMarshaller.INSTANCE;
Copy link
Contributor

Choose a reason for hiding this comment

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

GrpcJsonMarshallType.UPSTREAM and jsonMarshallerCustomizer are mutually exclusive.
Originally, this builder is designed for protobuf-jackson.
How about adding some factory methods instead of GrpcJsonMarshallType?

// Choose the JSON codec depending on a proto syntax.
GrpcJsonMarshaller.of();

// Always uses protobuf-jackon
GrpcJsonMarshaller.ofJackson(...);
GrpcJsonMarshaller.builderForJackson()
                  .jsonMarshallerCustomizer(...)
                  .build(ServiceDescriptor);

// Always uses protobuf-util's JsonFormat
GrpcJsonMarshaller.ofGson(...);

Copy link
Contributor Author

@jrhee17 jrhee17 Jan 18, 2022

Choose a reason for hiding this comment

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

Good point.

I've updated my thoughts on the default implementation in the PR description.

I'm thinking it might be better to leave the default as-is, and just providing a gson implementation which users can opt-in. This can be slightly inconvenient since users need to add compatibility for both client and server-side, but at least there will be no breaking changes.

GrpcService.builder()
           .jsonMarshallerFactory(sd -> GrpcJsonMarshaller.ofGson())
           .build();
...
GrpcClients.builder(server.httpUri(GrpcSerializationFormats.JSON))
           .jsonMarshallerFactory(sd -> GrpcJsonMarshaller.ofGson())
           .build(Proto2ServiceBlockingStub.class);

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking it might be better to leave the default as-is

Sounds good. 👍

@jrhee17 jrhee17 force-pushed the bugfix/grpc-json-type branch 2 times, most recently from 5bf6f55 to f24c861 Compare January 18, 2022 05:54
@jrhee17 jrhee17 marked this pull request as ready for review January 18, 2022 08:34
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

Nice work, @jrhee17!

Comment on lines 44 to 45
* Sets a {@link Consumer} that can customize the {@link JsonFormat.Parser} for {@link Message}
* used when handling JSON payloads in the service.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Sets a {@link Consumer} that can customize the {@link JsonFormat.Parser} for {@link Message}
* used when handling JSON payloads in the service.
* Adds a {@link Consumer} that can customize the {@link JsonFormat.Parser}
* used when deserializing a JSON payload into a {@link Message}.

@jrhee17
Copy link
Contributor Author

jrhee17 commented Jan 19, 2022

Note: also added a warning log

Sample ⬇️

14:17:46.728 [Test worker] WARN  c.l.armeria.common.grpc.GrpcJsonUtil - Failed to instantiate a json marshaller for [MethodDescriptor{fullMethodName=example.grpc.protoversion.Proto2Service/echo, type=UNARY, idempotent=false, safe=false, sampledToLocalTracing=true, requestMarshaller=io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller@4e3f2908, responseMarshaller=io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller@7e87ef9e, schemaDescriptor=example.armeria.grpc.Proto2ServiceGrpc$Proto2ServiceMethodDescriptorSupplier@e3b0369}]. Consider using GrpcJsonMarshaller.ofGson instead.

@jrhee17
Copy link
Contributor Author

jrhee17 commented Jan 19, 2022

Updated Version of the warning log

14:34:41.525 [Test worker] WARN  c.l.armeria.common.grpc.GrpcJsonUtil - Failed to instantiate a json marshaller for [MethodDescriptor{fullMethodName=example.grpc.protoversion.Proto2Service/echo, type=UNARY, idempotent=false, safe=false, sampledToLocalTracing=true, requestMarshaller=io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller@67e0ff3a, responseMarshaller=io.grpc.protobuf.lite.ProtoLiteUtils$MessageMarshaller@7b2bf745, schemaDescriptor=example.armeria.grpc.Proto2ServiceGrpc$Proto2ServiceMethodDescriptorSupplier@6fe595dc}]. Consider using com.linecorp.armeria.common.grpc.GrpcJsonMarshaller.ofGson() instead.
java.lang.IllegalStateException: Could not find setter.
	at org.curioswitch.common.protobuf.json.ProtoFieldInfo.setValueMethod(ProtoFieldInfo.java:230)
	at org.curioswitch.common.protobuf.json.DoParse.setFieldValue(DoParse.java:452)
...

return new GrpcJsonMarshallerBuilder();
}

/**
* Returns a newly-created {@link GrpcJsonMarshaller} which serializes and deserializes {@link Message}.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should add notes that the performance is much lower and should only be used if the default has problems, for example enum values in proto2 files. In practical terms, ofGson is not fast enough for production workloads - actually I don't know if having these factories solves the user's reported issue since it still requires configuration, as opposed to configuring the serialization formats. AFAICT, the user didn't need JSON, but they were having issues because the default serialization formats includes it. Requiring a different option may not actually help them.

Copy link
Contributor Author

@jrhee17 jrhee17 Jan 19, 2022

Choose a reason for hiding this comment

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

Should add notes that the performance is much lower

Thanks for looking though this PR! Left an additional note in the javadocs that addresses this.

AFAICT, the user didn't need JSON, but they were having issues because the default serialization formats includes it.

I still think this PR has meaning in that users who are committed to using armeria still have an option to use the upstream marshaller implementation if they'd like.

Having said this, if we want to avoid breaking changes and avoid user-side extra configuration, I guess the other option we have is to just swallow the exception and warn when building a MessageMarshaller fails. Let me also consider this.

@jrhee17
Copy link
Contributor Author

jrhee17 commented Jan 19, 2022

Note to reviewers: Updated behavior to swallow an exception and warn if constructing jsonMarshallers failed

@jrhee17 jrhee17 changed the title Use upstream json marshall implementation for proto2 Introduce GsonGrpcJsonMarshaller, catch jsonMarshaller initialization failure Jan 19, 2022
@jrhee17 jrhee17 changed the title Introduce GsonGrpcJsonMarshaller, catch jsonMarshaller initialization failure Introduce GsonGrpcJsonMarshaller and catch JsonMarshaller initialization failure Jan 19, 2022
.collect(toImmutableMap(ServiceDescriptor::getName, jsonMarshallerFactory));
} catch (Exception e) {
logger.warn("Failed to instantiate a JSON marshaller. Consider " +
"disabling gRPC-JSON serialization or using {}.ofGson() instead.",
Copy link
Member

Choose a reason for hiding this comment

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

disabling gRPC-JSON serialization with <method reference> or using ...

Copy link
Member

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

Thanks a lot, @jrhee17!

* and {@code Gson} to serialize and deserialize messages.
* Note:
* <a href="https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/util/JsonFormat">JsonFormat</a>
* has a known performance issue. Use this method only when
Copy link
Member

Choose a reason for hiding this comment

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

Could you leave a link to the performance issue? 😄

@@ -1,5 +1,5 @@
/*
* Copyright 2020 LINE Corporation
* Copyright 2022 LINE Corporation
Copy link
Member

Choose a reason for hiding this comment

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

revert?

@jrhee17
Copy link
Contributor Author

jrhee17 commented Jan 25, 2022

Sorry about the last minute change, but I realized that client-side could also benefit from richer exception messages.
I'd appreciate if reviewers can take a look at the following commit 58acdbc

try {
jsonMarshaller = options.get(GrpcClientOptions.GRPC_JSON_MARSHALLER_FACTORY)
.apply(serviceDescriptor);
} catch (RuntimeException e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about catching Exception like FramedGrpcService?

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.

Nice work, @jrhee17 🙇

@trustin trustin merged commit 8df42aa into line:master Jan 25, 2022
dolgodolah added a commit to dolgodolah/armeria-practice that referenced this pull request Aug 16, 2023
- grpc + armeria 사용 시 JsonMarshaller 초기화 실패하는 버그가 있는 듯 함.
- line/armeria#4033
- jsonMarshallerFactory 이용해서 JsonMarshaller 초기화
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Server fails to start with a grpc service for a proto2 proto that contains an enum.
5 participants