WIP mrpc update proto.md to use jstream
This commit is contained in:
parent
fc853e739c
commit
5626f9171b
248
mrpc/proto.md
248
mrpc/proto.md
@ -1,208 +1,102 @@
|
|||||||
# Protocol
|
# Protocol
|
||||||
|
|
||||||
The mediocre-rpc protocol is designed to operate over nearly any network
|
The mediocre-rpc protocol is an RPC protocol with support for streaming
|
||||||
protocol. It exists entirely in the data layer, and only relies on being carried
|
arbitrary numbers of both request and response objects, byte blobs of unknown
|
||||||
by a protocol which supports a request/response paradaigm, and garauntees
|
length, and managing request/response debug data.
|
||||||
order/reception. This includes HTTP and raw TCP sockets, and likely many others.
|
|
||||||
|
|
||||||
RPC calls and responses are JSON encoded objects. The protocol supports single
|
The protocol itself is carried via the jstream protocol, which is specified and
|
||||||
object calls/responses, as well as streaming multiple objects for either. There
|
implemented in this repo.
|
||||||
is also support for streaming raw byte blobs.
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
A couple common rules which apply across all subsequent documentation for this
|
Common rules and terminology which apply across all subsequent documentation for
|
||||||
spec:
|
this spec:
|
||||||
|
|
||||||
|
* An "RPC call", or just "call", is composed of two events: a "request" and a
|
||||||
|
"response".
|
||||||
|
|
||||||
|
* The entity which initiates the call by sending a request is the "caller".
|
||||||
|
|
||||||
|
* The entity which serves the call by responding to a request is the "server".
|
||||||
|
|
||||||
* In all JSON object specs, a field which is not required can be omitted
|
* In all JSON object specs, a field which is not required can be omitted
|
||||||
entirely, and its value is assumed to be the expected type's empty value (e.g.
|
entirely, and its value is assumed to be the expected type's empty value (e.g.
|
||||||
`""` for strings, `0` for numbers, `{}` for objects, `null` for any-JSON
|
`""` for strings, `0` for numbers, `{}` for objects, `null` for any-JSON
|
||||||
types).
|
types).
|
||||||
|
|
||||||
* For multiple JSON objects appearing back-to-back on the wire there may or may
|
## Debug
|
||||||
not be white-space separating them.
|
|
||||||
|
|
||||||
## Calls
|
Many components of this RPC protocol carry a `debug` field, whose value may be
|
||||||
|
some arbitrary set of data as desired by the user. The use and purpose of the
|
||||||
|
`debug` field will be different for everyone, but some example use-cases would
|
||||||
|
be a unique ID useful for tracing, metadata useful for logging in case of an
|
||||||
|
error, and request/response timings from both the caller and server sides
|
||||||
|
(useful for determining RTT).
|
||||||
|
|
||||||
### Single call
|
When determining if some piece of data should be considered debug data or part
|
||||||
|
of the request/response proper, imagine that some piece of code was completely
|
||||||
|
removing the `debug` field in random places at random times. Your application
|
||||||
|
should run _identically_ in that scenario as in real-life.
|
||||||
|
|
||||||
A single call looks like the following:
|
In other words: if some field in `debug` effects the behavior of a call directly
|
||||||
|
then it should not be carried via `debug`. This could mean duplicating data
|
||||||
|
between `debug` and the request/response proper, e.g. the IP address of the
|
||||||
|
caller.
|
||||||
|
|
||||||
{
|
## Call request
|
||||||
"method":"methodName (required)",
|
|
||||||
"args":"anyJSON",
|
|
||||||
"debug":{ "foo":"anyJSON" }
|
|
||||||
}
|
|
||||||
|
|
||||||
`method` is required and indicates the name of the method being called. Its
|
A call request is defined as being three jstream elements read off the pipe by
|
||||||
value has no restrictions on the protocol level, it's up to the caller and
|
the server. Once all three elements have been read the request is considered to
|
||||||
handler to know ahead of time which methods are available.
|
be completely consumed and the pipe may be used for a new request.
|
||||||
|
|
||||||
`args` are the arguments to the method, and can be anything.
|
The three elements of the request stream are specified as follows:
|
||||||
|
|
||||||
`debug` is metadata about the call which can be made accessible to the handler
|
* The first element, the head, is a JSON value with an object containing a
|
||||||
for purposes of tracing, logging, etc... `debug` must be a JSON object. A good
|
`name` field, which identifies the call being made, and optionally a `debug`
|
||||||
rule to know if something should be debug or part of the arguments is that if
|
field.
|
||||||
any `debug` field were to be deleted on any request the response wouldn't
|
|
||||||
change. If the response were to change then that field should be in `args`.
|
|
||||||
|
|
||||||
### Stream call
|
* The second element is the argument to the call. This may be a JSON value, a
|
||||||
|
byte blob, or an embedded stream containing even more elements, depending on
|
||||||
|
the call. It's up to the caller and server to coordinate beforehand what to
|
||||||
|
expect here.
|
||||||
|
|
||||||
A stream call consists of a leading JSON object which looks like a single call,
|
* The third element, the tail, is a JSON value with an object optionally
|
||||||
a body of zero or more JSON objects containing single elements of a stream, and
|
containing a `debug` field.
|
||||||
a tail which indicates the end of the stream.
|
|
||||||
|
|
||||||
A stream method call whose body is all JSON strings might look this (newlines
|
## Call response
|
||||||
added for clarity, they are optional in the protocol):
|
|
||||||
|
|
||||||
{
|
A call response almost the same as the call request. The only difference is the
|
||||||
"method":"methodName (required)",
|
lack of `name` field in the head, and the addition of the `err` field in the
|
||||||
"args":"anyJSON",
|
tail.
|
||||||
"debug":{ "foo":"anyJSON" },
|
|
||||||
"streamStart":true,
|
|
||||||
"streamLen":3
|
|
||||||
}
|
|
||||||
|
|
||||||
{ "el":"anyJSON" }
|
A call response is defined as being three jstream elements read off the pipe by
|
||||||
|
the caller. Once all three elements have been read the response is considered to
|
||||||
|
be completely consumed and the pipe may be used for a new request.
|
||||||
|
|
||||||
{ "elBytesFrame":"RANDFRAME" }
|
The three elements of the response stream are specified as follows:
|
||||||
RANDFRAMEsome very cool arbitrary bytes
|
|
||||||
which may contain whitespace or anythingRANDFRAME
|
|
||||||
|
|
||||||
{
|
* The first element, the head, is a JSON value with an object containing
|
||||||
"elBytesFrame":"RANDFRAME",
|
optionally containing a `debug` field.
|
||||||
"elBytesLen":10
|
|
||||||
}
|
|
||||||
RANDFRAMEsome-bytesRANDFRAME
|
|
||||||
|
|
||||||
{
|
* The second element is the response from the call. This may be a JSON value, a
|
||||||
"args":"anyJSON",
|
byte blob, or an embedded stream containing even more elements, depending on
|
||||||
"debug":{ "foo":"anyJSON" },
|
the call. It's up to the caller and server to coordinate beforehand what to
|
||||||
"streamEnd":true
|
expect here.
|
||||||
}
|
|
||||||
|
|
||||||
The head is the first JSON value in the stream. It looks and operates much like a
|
* The third element, the tail, is a JSON value with an object optionally
|
||||||
single call's head, but with the added `streamStart` field. The `streamLen`
|
containing an `err` field, and optionally containing `debug` field. The value
|
||||||
field is optional, but may be used if the number of elements in the stream is
|
of `err` may be any JSON value which is meaningful to the caller and server.
|
||||||
known beforehand.
|
|
||||||
|
|
||||||
Each element in the stream is a JSON object, and can be either a single JSON
|
## Pipelining
|
||||||
value or a blob of arbitrary bytes.
|
|
||||||
|
|
||||||
If the element is a JSON value the JSON object will have an `el` field whose
|
The protocol allows for the server to begin sending back a response, and even to
|
||||||
value is the JSON value.
|
send back a complete response, _as soon as_ it receives the request head. In
|
||||||
|
effect this means that the server can be sending back response data while the
|
||||||
|
caller is still sending request data.
|
||||||
|
|
||||||
If the element is a blob of arbitrary bytes the JSON object will have an
|
Once the server has sent the response tail it can assume the call has completed
|
||||||
`elBytesFrame` field whose value is a set of random alphanumeric characters
|
successfully and ignore all subsequent request data (though it must still fully
|
||||||
which will be used to prefix and suffix the bytes. Following the JSON object may
|
read the three request elements off the pipe in order to use it again).
|
||||||
be some whitespace, and then the arbitrary bytes with the `elBytesFrame`
|
Likewise, once a caller receives the response tail it can cancel whatever it's
|
||||||
prefixed and suffixed immediately around it. The JSON object for the element may
|
doing, finish sending the request argument and tail as soon as possible, and
|
||||||
optionally have the `elBytesLen` field if the length of the blob is known
|
assume the call has been completed.
|
||||||
beforehand. The arbitrary bytes _must_ be prefixed/suffixed by the frame even if
|
|
||||||
`elBytesLen` is given.
|
|
||||||
|
|
||||||
TODO elBytesFrame size recommendation
|
|
||||||
|
|
||||||
The tail is the last JSON value in the stream and indicates the stream
|
|
||||||
has ended. It is required even if `streamLen` was given in the head. The tail
|
|
||||||
can also have its own `args` and `debug`, independent of the head's but subject
|
|
||||||
to the same rules.
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
### Single response
|
|
||||||
|
|
||||||
A single response looks like the following:
|
|
||||||
|
|
||||||
{
|
|
||||||
"res":"anyJSON",
|
|
||||||
"err":{
|
|
||||||
"msg":"some presumably helpful string",
|
|
||||||
"meta":"anyJSON"
|
|
||||||
},
|
|
||||||
"debug":{ "foo":"anyJSON" }
|
|
||||||
}
|
|
||||||
|
|
||||||
`res` is the response from the call, and can be anything.
|
|
||||||
|
|
||||||
`err` is mutually exclusive with `res` (if one is set the other should be `null`
|
|
||||||
or unset). The `msg` is be any arbitrary string. `meta` is optional and contains
|
|
||||||
any extra information which might be actionable by the client receiving the
|
|
||||||
error.
|
|
||||||
|
|
||||||
`debug` is metadata about the response which can be made accessible to the
|
|
||||||
caller for purposes of tracing, logging, etc... `debug` must be a JSON object. A
|
|
||||||
good rule to know if something should be debug or part of the results is that
|
|
||||||
if any `debug` field were to be deleted on any response the client would act in
|
|
||||||
the same way. If the client's subsequent actions were to change then that field
|
|
||||||
should be in `res`.
|
|
||||||
|
|
||||||
### Stream response
|
|
||||||
|
|
||||||
A stream response looks and acts very similar to a stream call, and the
|
|
||||||
documentation for the stream call can be referenced for details on the
|
|
||||||
following:
|
|
||||||
|
|
||||||
{
|
|
||||||
"res":"anyJSON",
|
|
||||||
"debug":{ "foo":"anyJSON" },
|
|
||||||
"streamStart":true,
|
|
||||||
"streamLen":3
|
|
||||||
}
|
|
||||||
|
|
||||||
{ "el":"anyJSON" }
|
|
||||||
|
|
||||||
{ "elBytesFrame":"RANDFRAME" }
|
|
||||||
RANDFRAMEsome very cool arbitrary bytes
|
|
||||||
which may contain whitespace or anythingRANDFRAME
|
|
||||||
|
|
||||||
{
|
|
||||||
"elBytesFrame":"RANDFRAME",
|
|
||||||
"elBytesLen":10
|
|
||||||
}
|
|
||||||
RANDFRAMEsome-bytesRANDFRAME
|
|
||||||
|
|
||||||
{
|
|
||||||
"args":"anyJSON",
|
|
||||||
"debug":{ "foo":"anyJSON" },
|
|
||||||
"streamEnd":true
|
|
||||||
}
|
|
||||||
|
|
||||||
The one note specific to stream responses is that a stream response _cannot_
|
|
||||||
contain an `err` field.
|
|
||||||
|
|
||||||
## Network details
|
|
||||||
|
|
||||||
In the case of a stream call the handler may send back a response before the
|
|
||||||
stream has been completely sent. In this case the client should ignore the fact
|
|
||||||
that the stream wasn't completely sent and return the response as it receives
|
|
||||||
it.
|
|
||||||
|
|
||||||
In the case of a stream call and a stream response both the client and handler
|
|
||||||
can be sending their respective streams to the other simultaneously. As in the
|
|
||||||
previous case, if the handler ends the stream with the tail object the caller
|
|
||||||
should treat the call as successfully completed.
|
|
||||||
|
|
||||||
The general rule is: If the client receives either a single response or the tail
|
|
||||||
of a stream response then the call should be treated as completed.
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
* In the case of pipelining then short-circuiting ought to be implemented for
|
|
||||||
the case of the stream response having been completed but the stream call
|
|
||||||
hasn't. In that case the stream call should short-circuit the stream so the
|
|
||||||
connection can be reused asap
|
|
||||||
|
|
||||||
* Figure out the naming
|
|
||||||
|
|
||||||
* Len is weird, cause if a stream is short-circuited then the length will have
|
|
||||||
just been a hint
|
|
||||||
|
|
||||||
* Maybe just make bytes a thing which can be bolted onto any of the JSON
|
|
||||||
objects, either the single, head, elements, or tail.
|
|
||||||
|
|
||||||
* Maybe instead of merely defining a single-level stream, do something where all
|
|
||||||
elements in the stream are the same (either a json value or bytes), and
|
|
||||||
additionally each can declare that it is the beginning of a stream of further
|
|
||||||
objects. That gets weird with both `method` and `err`, but it's kinda nice in
|
|
||||||
that it would potentially simplify the code interface greatly.
|
|
||||||
|
Loading…
Reference in New Issue
Block a user