- 
                Notifications
    
You must be signed in to change notification settings  - Fork 965
 
Add Implement JsonRpcService #6289
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
base: json-rpc
Are you sure you want to change the base?
Conversation
| 
           If you have a moment, I would appreciate a review.  | 
    
        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core/src/main/java/com/linecorp/armeria/common/jsonrpc/JsonRpcError.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core/src/main/java/com/linecorp/armeria/common/jsonrpc/JsonRpcRequest.java
          
            Show resolved
            Hide resolved
        
      | /** | ||
| * Returns the JSON-RPC version. | ||
| */ | ||
| String version(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we define the version values as an enum type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a JsonRpcVersion enum.
Could you please check if this aligns with what you intended?
| /** | ||
| * Returns the parameters for the JSON-RPC method. | ||
| */ | ||
| List<Object> params(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<Object> would not express by-name parameters. We may introduce JsonRpcParameter to abstract by-position and by-name.
https://www.jsonrpc.org/specification#parameter_structures
by-name: params MUST be an Object,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have implemented the suggestion to introduce JsonRpcParameter to abstract both by-position and by-name parameters.
Please review the changes.
        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
          
            Show resolved
            Hide resolved
        
              
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
              
                Outdated
          
            Show resolved
            Hide resolved
        
      | // Notification | ||
| if (req.id() == null) { | ||
| if (handler != null) { | ||
| handler.handle(ctx, req); | ||
| } | ||
| return UnmodifiableFuture.completedFuture(null); | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we return the future when handler.handle(ctx, req) completes.
In addition, it would be worth returning the status code defined in the MCP specification for compatibility.
- Should we return 
202 Acceptedinstead if the handler accepts the notification? - Should we return 
400 Bad Requestif the handler raises an exception? 
If the input is a JSON-RPC response or notification:
- If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body.
 - If the server cannot accept the input, it MUST return an HTTP error status code (e.g., 400 Bad Request). The HTTP response body MAY comprise a JSON-RPC error response that has no id.
 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we return the future when handler.handle(ctx, req) completes.
I don't think this comment is addressed. My suggestion was:
if (handler != null) {
    return handler.handle(ctx, req)
                  .thenApply(res -> null);
}
return UnmodifiableFuture.completedFuture(null);
          Codecov Report❌ Patch coverage is  Additional details and impacted files@@             Coverage Diff             @@
##             json-rpc    #6289   +/-   ##
===========================================
  Coverage            ?   74.11%           
  Complexity          ?    23087           
===========================================
  Files               ?     2073           
  Lines               ?    86374           
  Branches            ?    11341           
===========================================
  Hits                ?    64012           
  Misses              ?    16930           
  Partials            ?     5432           ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
  | 
    
          🔍 Build Scan® (commit: e4c355b) | 
    
| import com.linecorp.armeria.common.jsonrpc.JsonRpcError; | ||
| import com.linecorp.armeria.common.jsonrpc.JsonRpcVersion; | ||
| 
               | 
          ||
| @UnstableApi | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@UnstableApi is used in public and protected API.
| @UnstableApi | 
| static JsonRpcRequest of(JsonNode node) throws JsonProcessingException { | ||
| requireNonNull(node, "node"); | ||
| checkArgument(node.isObject(), "node.isObject(): %s (expected: true)", node.isObject()); | ||
| return JacksonUtil.newDefaultObjectMapper().treeToValue(node, DefaultJsonRpcRequest.class); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's declare JacksonUtil.newDefaultObjectMapper() in DefaultJsonRpcRequest as a static value to avoid additional allocations.
| import com.linecorp.armeria.common.annotation.Nullable; | ||
| import com.linecorp.armeria.common.annotation.UnstableApi; | ||
| 
               | 
          ||
| @UnstableApi | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| @UnstableApi | 
| .thenApply(JsonRpcService::parseRequestContentAsJson) | ||
| .thenApply(json -> dispatchRequest(ctx, json)) | ||
| .exceptionally(e -> { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Micro optimization) Should we consolidate the three callbacks into CompletableFuture.handle() method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've refactored the code to consolidate the three callbacks into a single CompletableFuture.handle() method as you recommended.
Could you please take another look to see if this implementation aligns with what you had in mind?
| if (rawRequest.isObject()) { | ||
| return handleUnaryRequest(ctx, rawRequest); | ||
| } else { | ||
| return HttpResponse.ofJson(HttpStatus.BAD_REQUEST, JsonRpcError.PARSE_ERROR); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, should we return the following message if the request is an array?
JsonRpcError.INVALID_REQUEST.withData("Batch requests are not supported by this server.");
| 
               | 
          ||
| private HttpResponse toHttpResponse(DefaultJsonRpcResponse response) { | ||
| if (response == null) { | ||
| return HttpResponse.of(HttpStatus.ACCEPTED, MediaType.PLAIN_TEXT_UTF_8, ""); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may not need a content-type and an empty string for accepted status.
| return HttpResponse.of(HttpStatus.ACCEPTED, MediaType.PLAIN_TEXT_UTF_8, ""); | |
| return HttpResponse.of(HttpStatus.ACCEPTED); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the empty string because the MCP documentation specifies that the response for this case should have no body. If we simply return HttpResponse.of(HttpStatus.ACCEPTED), the response body will contain the string "202 Accepted".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood. We could make the response body empty by specifying response headers only.
HttpResponse.of(ResponseHeaders.of(HttpStatus.ACCEPTED));        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
          
            Show resolved
            Hide resolved
        
      | // Notification | ||
| if (req.id() == null) { | ||
| if (handler != null) { | ||
| handler.handle(ctx, req); | ||
| } | ||
| return UnmodifiableFuture.completedFuture(null); | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we return the future when handler.handle(ctx, req) completes.
I don't think this comment is addressed. My suggestion was:
if (handler != null) {
    return handler.handle(ctx, req)
                  .thenApply(res -> null);
}
return UnmodifiableFuture.completedFuture(null);        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
          
            Show resolved
            Hide resolved
        
      | */ | ||
| @UnstableApi | ||
| public class JsonRpcServiceBuilder { | ||
| private final Map<String, JsonRpcHandler> methodHandlers = new HashMap<>(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer immutable collections. Should we use ImmutableMap.Builder to collect the handlers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good.
| /** | ||
| * Creates a new instance with result. | ||
| */ | ||
| public AbstractJsonRpcResponse(Object result) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| public AbstractJsonRpcResponse(Object result) { | |
| protected AbstractJsonRpcResponse(Object result) { | 
| /** | ||
| * Creates a new instance with error. | ||
| */ | ||
| public AbstractJsonRpcResponse(JsonRpcError error) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| public AbstractJsonRpcResponse(JsonRpcError error) { | |
| protected AbstractJsonRpcResponse(JsonRpcError error) { | 
| final JsonRpcRequest that = (JsonRpcRequest) obj; | ||
| return Objects.equals(id, that.id()) && | ||
| Objects.equals(method, that.method()) && | ||
| Objects.equals(params, that.params()); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question) Is JsonRpcVersion excluded intentionally from equals and hashCode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally omitted it because the version was a constant,
Now that it's an enum, it definitely belongs in the equals() and hashCode() methods.
I've pushed a fix to include JsonRpcVersion in both equals() and hashCode(). Thanks
| * Returns {@code true} if this Parameter is a Positional. | ||
| */ | ||
| public boolean isPositional() { | ||
| return value instanceof List; | ||
| } | ||
| 
               | 
          ||
| /** | ||
| * Returns {@code true} if this Parameter is a Named. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Returns {@code true} if this Parameter is a Positional. | |
| */ | |
| public boolean isPositional() { | |
| return value instanceof List; | |
| } | |
| /** | |
| * Returns {@code true} if this Parameter is a Named. | |
| * Returns {@code true} if this parameter is a positional. | |
| */ | |
| public boolean isPositional() { | |
| return value instanceof List; | |
| } | |
| /** | |
| * Returns {@code true} if this parameter is a pamed. | 
        
          
                core/src/main/java/com/linecorp/armeria/common/jsonrpc/JsonRpcParameter.java
          
            Show resolved
            Hide resolved
        
      | public static JsonRpcParameter of(Object parsedParams) { | ||
| checkArgument(parsedParams instanceof List || parsedParams instanceof Map, | ||
| "params type: %s (expected: List or Map)", | ||
| parsedParams != null ? parsedParams.getClass().getName() : "null"); | ||
| return new JsonRpcParameter(parsedParams); | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add overload methods instead of Object type for type-safety?
public static JsonRpcParameter of(List<Object> ...) { }
public static JsonRpcParameter of(Map<String, Object> ...) { }| 
               | 
          ||
| @VisibleForTesting | ||
| CompletableFuture<DefaultJsonRpcResponse> invokeMethod(ServiceRequestContext ctx, | ||
| JsonRpcRequest req) { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indent?
        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcService.java
          
            Show resolved
            Hide resolved
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left only minor comments. 😉
| public static boolean jsonRpcServiceContentLogging() { | ||
| return JSON_RPC_SERVICE_CONTENT_LOGGING; | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see we need this flag at the moment since annotatedServiceContentLogging was added to preserve the original behavior. Let's consider adding this option when there is  a request for it.
| "id type: %s (expected: Null or Number or String)", | ||
| Optional.ofNullable(id).map(Object::getClass).orElse(null)); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Optional might be a bit too much for this case. The intention will be fully conveyed by the id itself. If you prefer type, let's use a ternary operator.
| "id type: %s (expected: Null or Number or String)", | |
| Optional.ofNullable(id).map(Object::getClass).orElse(null)); | |
| "id: %s (expected: Null or Number or String)", id); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using id directly as suggested "id: %s (expected: Null or Number or String)", id) triggers a nullable warning.
As you also suggested, I've updated it to use a ternary operator instead. Thanks!
        
          
                core/src/main/java/com/linecorp/armeria/common/jsonrpc/DefaultJsonRpcRequest.java
          
            Show resolved
            Hide resolved
        
      | static JsonRpcRequest of(@Nullable Object id, String method, Map<String, Object> parameter) { | ||
| return new DefaultJsonRpcRequest(id, | ||
| requireNonNull(method, "method"), | ||
| requireNonNull(parameter, "param")); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| requireNonNull(parameter, "param")); | |
| requireNonNull(parameter, "parameter")); | 
| 
               | 
          ||
| /** | ||
| * Returns the ID of the JSON-RPC request. | ||
| * type must be Number or String | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * type must be Number or String | |
| * The type must be {@link Number} or {@link String}. | 
        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcServiceBuilder.java
          
            Show resolved
            Hide resolved
        
      | import com.linecorp.armeria.common.annotation.UnstableApi; | ||
| 
               | 
          ||
| /** | ||
| * A Json-RPC. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * A Json-RPC. | |
| * A JSON-RPC response. | 
| import com.linecorp.armeria.common.annotation.UnstableApi; | ||
| 
               | 
          ||
| /** | ||
| * A Json-RPC request. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * A Json-RPC request. | |
| * A JSON-RPC request. | 
| * Constructs a new {@link JsonRpcService}. | ||
| */ | ||
| public JsonRpcService build() { | ||
| return new JsonRpcService(methodHandlers.build()); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about raising an exception if methodHandlers.isEmpty() == true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your hard work, @patcher454! 🚀👍
        
          
                core/src/main/java/com/linecorp/armeria/server/jsonrpc/JsonRpcServiceBuilder.java
          
            Show resolved
            Hide resolved
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still LGTM
| 
          
 It looks like the :examples:athenz-example:test task is consistently failing in the build.  | 
    
| 
           #6473 Please merge main branch to resolve it.  | 
    
Related:
Motivation:
Modifications:
Result: