-
Notifications
You must be signed in to change notification settings - Fork 4
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
Transport Agnostic RPC implementation #498
Conversation
The plan right now is to create some test utils using fast-check and the transform stream to convert the raw stream data into distinct JSONRPC messages. Looking over the docs we can use generators to transform the stream pretty easily. But the example provided doesn't return a new stream. https://exploringjs.com/nodejs-shell-scripting/ch_web-streams.html#transforming-web-streams-via-generators. So we won't be able to compose it with other transform streams. If we don't care about that then it is fine. Otherwise we would have to do it the usual way. https://exploringjs.com/nodejs-shell-scripting/ch_web-streams.html#implementing-custom-transformstreams. After creating the transformer stream I can look into parsing the JSONRPC messages. We need a good way of enforcing the structure of these using the type system. Looking over the JSON RPC spec there isn't a good way to tell the type of a message. For example, a request message contains the fields The parameters can be any valid |
These web streams don't play very nice with strict typing. While interfaces with proper types are provided. I can't get proper type inference when implementing a transformer class. For example. when implementing the class transform(chunk, controller) {
chunk //any
controller //any
}; To get proper types on the method I need to implemented as transform: TransformerTransformCallback<Buffer, string> = async (chunk, controller) => {
chunk // Buffer
controller // TransformStreamDefaultController<string>
} While this will work it is not ideal. We do still get a warning if the type annotation is wrong. @CMCDragonkai You've worked with this. Do you have a good way of enforcing the types here? |
Slicing out the JSON messages from the stream is going to be a little tricky. I first thought that I could just keep a tally of We may be able to delimit the messages by using the provided ASCII seperators. That being the codes |
I think there are off the shelf JSON stream-parsers, can we make use of those? It should be possible to repurpose them to specifically do a "partial parse" that is parse me a JSON message, expect that there may be left over data in the buffer, which can be left to the next iteration of the parsing loop. |
I remember referencing this one: https://github.com/juanjoDiaz/streamparser-json |
No thoughts yet on the type inference. |
Just took a look at this. It's useful but not ideal here. The parser expects a properly formatted JSON, so it can parse a stream of I'm going to try 3 methods of slicing the messages from the stream. Using transform streams should make them interchangeable so we can pick the one we think works the best. Right the 3 approaches are.
I'll prototype transform streams for each of these methods. |
I've been thinking about how to handle bad stream data. Do we even bother with this? A single stream would come from a quic stream. Theses should handle any problems with the data over the socket so I don't expect to see bad or mangled data within the stream. We should be able to handle bad or mangled data within the stream but I don't think we should bother with that. Any failure to pass data should just throw an error back down the stream and close it out. This would be part of the normal error handling but at a slightly lower level. |
Regarding streaming, based on what we talked about on the whiteboard I want to keep it as vanilla as possible. Try to get the json stream parser to "get you the next token", and then keep the remaining data. So if the string input is See if you can configure the streamparser-json to do that. If it can't do that out of the box, consider forking it and making upstream changes to be capable of this. Finally regarding bad stream data, that should be considered a lexing failure, upon a lexing failure that should result in bad stream data, and while the initial JSON message has been passed onto the downstream consumer, an error should be thrown on the stream, and that means the error gets thrown to the downstream system. The downstream consumer can then decide what to do, which means it could rethrow that exception or close the stream... etc. There should be no automatic decisions made during the mapping operation with respect to the upstream producer. Visually speaking that means something like:
And errors in the transformer is propagated as errors to the |
It is possible to separate the messages using the original bracket counting method using the This is an easy fix, I just need to work out the number of escaped characters and add that to the offset. |
Make sure to fuzz test your technique so that it works in all situations of sending JSON frames, or sending invalid data or sending mixed JSON frames and invalid data. |
Are you using |
51a043d
to
a39fa93
Compare
@tegefaulkes can you start speccing out the tasks for this PR? |
I've fleshed out the PR some more. There is still some stuff to add but I need to settle some structure in my head first. |
The class can be given a stream and then handle all the control and service handling. The thing is, while this RPC call is open we have state that should be running in the background. Given that we need to shut down these connections gracefully at any time. we need the ability to track these active calls. We can keep a set of active calls. When they are done they should remove themselves from the set. We can enumerate the set to destroy any active connections when we need to stop the RPC. These active calls might need some amount of state such as what service handler is needed when processing messages. This could be |
Can you provide a file structure of all the new modules be developed here. |
The #249 issue should be updated with the desired specification of this JSON RPC system. Cross out the old GRPC tasks, the old spec can just be separated with The new spec should have subsections with Refer to the issues in #495 for extra detail, I've also updated a comment on each of those issues relating to the migration to JSON RPC. Make sure to consider:
|
Based on some discussion:
|
We should confirm how to do explicit stream closing for both readable and writable. In the case of readable, there is way to "cancel" the stream using https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader/cancel as well as the This allows you to provide a reason for the cancellation. But in the case of readable stream the cancel reason I believe I will translate to a stream close. So on the readable side, this means: https://docs.rs/quiche/latest/quiche/struct.Connection.html#method.stream_shutdown. Which I can pass a But on the writable side, there's actually 2 ways to do this, the This is more on the QUIC side though, you need to ensure that you're explicitly signalling a close on the readable stream when you are finished reading (for unary, server streaming), and when you are finished writing (for unary, client streaming, server streaming, duplex streaming). However at the end of the handling, no matter what, both readable and writable sides should be explicitly closed. |
Ok so basically now we just need to fix up the above, and also prepare some middleware for authentication. |
The incremental JSON parser has an inefficient way of figuring the exact source offset. We need to raise an upstream issue to indicate that when we have escaped strings, the offset is not accurate. The offset is only useful if it is correctly indexed against the source input. It will be quite slow if we have to use This seems like a bug/oversight that upstream should be correcting. |
Furthermore another inefficiency is that we have to build up the chunks as an accumulating buffer before we do a We should be able to use the TokenParser to build up the tree internally and not have to do our own buffering. However the problem is that the parser currently throws an error when we write a string into it that has a concatenated message without a delimiter. What we need to do is to raise an issue upstream as well, to make the parser actually truly incremental, in which case it should not throw an error immediately on writing the string, but upon acquiring the next token. We can then ignore this error, and restart the parsing for the new message. |
@tegefaulkes it's possible that if you just ignore the error on the |
Some small notes.
|
Just remember that it should be |
I've created an upstream issue for the tokenizer problem at juanjoDiaz/streamparser-json#24. |
50ff0fa
to
19b4471
Compare
Squashed and re-based on staging |
19b4471
to
71833a0
Compare
Enabling CI |
There seems to be some issues with all of the tests and building. But that's something coming from the staging branch so I'll have to fix that there. I should be good to merge now. |
Description
This PR addresses adding our own custom transport agnostic RPC implementation.
The transport agnostic RPC needs to cover everything from the byte stream to the service handlers. This includes
Stage 1 - Converting the byte stream
Each RPC message is a single JSON RPC message object. There are 4 forms to this, a request, notification, response result and a response error. These messages are specified in the JSON-RPC 2.0 spec at https://www.jsonrpc.org/specification.
We need convert a continuous stream of these messages into distinct messages. The stream is made up of JSON-RPC objects that are
JSON.stringify()
ed and converted to buffers. The messages are structured like{JsonRpcMessage}{JsonRpcMessage}
but in buffer form.These object strings are delimited by their top level
{}
bracket pair. No delimiter character exists between each message. So to extract a single message out we need to find the top level matching{}
take the sub-array of that part of the incoming buffer.Ultimately the resulting transformation stream takes in arbitrary sized buffers of the incoming messages and outputs buffers of single complete message buffers. E.G.
{jso
|nMessa
|ge}{JsonMess
|age}
=>{JsonRpcMessage}
|{JsonRpcMessage}
This can then be piped through a message validation stream.Stage 2 - parsing and message validation
In this stage we are taking single JSON-RPC messages and converting them into objects and validating them. If the message is still in the buffer form then the conversion is done with
Json.parse(Buffer.toString('utf-8'))
.These objects need to be valid JSON-RPC message objects. So we need to create validation functions for each type of message. There are 4 kinds of JSON-RPC messages specified in https://www.jsonrpc.org/specification. Each one will need it's own validation function.
Solid types for each type of message need to be created. These message types need to be generic on the data so we can enforce a strict message schema for each service handler. The message data can't be validated at this stage. This should be left for the handlers to validate.
On top of the JSON-RPC spec, we need to add some more parameters to the messages.
A
type
parameter is needed to distinguish message types from each other for simple pattern matching. We also need some kind of sub type to distinguish control messages from data messages.Stage 3 - service and client handlers
We need a structured way of implementing client call and service handlers the the RPC. The data type of the requests and responses need to be identical for both and enforced by the typescript type system.
The request and response data structure isn't set in stone yet. Right now it's a
POJO
, there was discussion about it being anArray
but the JSON-RPC spec allows for anyJSONValue
. When implementing the handler we need to specify the request and response types. This is to enforce the type on input and output data in the handler.request and response data validation needs to be handled within the handlers. We will need to create a parser function for our message data.
The handler themselves need to be generators that implement our handler interface. The interface will look something like this..
This will be the most generic form of the duplex stream. Some prototyping needs to be done but it should be possible to have higher order functions that converts the unary, server stream and client stream handlers to this duplex stream form.
Remaining spec
Todo:
Issues Fixed
RPCServer
specification #500RPCClient
specification #501Tasks
Buffer
streams toUint8Array
streamsRPCServer
toCreateDestroy
RPCClient
needs to take a callback to create a streamFinal checklist