WIP jstreamrpc began implementation, found lots of weird holes in the spec that need to be addressed

This commit is contained in:
Brian Picciano 2018-04-14 10:45:58 +00:00
parent 2e8ef0e12a
commit bdfd7500fb
2 changed files with 139 additions and 9 deletions

View File

@ -0,0 +1,130 @@
// Package jstreamrpc implements the mrpc interfaces using the jstream protocol.
// This implementation makes a few design decisions which are not specified in
// the protocol, but which are good best practices none-the-less.
package jstreamrpc
import (
"context"
"encoding/json"
"errors"
"io"
"github.com/mediocregopher/mediocre-go-lib/jstream"
"github.com/mediocregopher/mediocre-go-lib/mrpc"
)
// TODO Debug
// TODO Error?
// TODO SizeHints
// TODO it seems like request tail and response head aren't useful nor
// convenient to use, might be better to leave them out
type headTail struct {
Debug map[string]map[string]json.RawMessage `json:"debug,omitempty"`
}
type reqHead struct {
headTail
Method string `json:"method"`
}
type ctxVal int
const (
ctxValR ctxVal = iota
ctxValW
)
func unmarshalBody(i interface{}, el jstream.Element) error {
switch iT := i.(type) {
case func(*jstream.StreamReader) error:
stream, err := el.Stream()
if err != nil {
return err
}
return iT(stream)
case *io.Reader:
ioR, err := el.Bytes()
if err != nil {
return err
}
*iT = ioR
return nil
default:
return el.Value(i)
}
}
func marshalBody(w *jstream.StreamWriter, i interface{}) error {
switch iT := i.(type) {
case func(*jstream.StreamWriter) error:
return w.EncodeStream(0, iT)
case io.Reader:
return w.EncodeBytes(0, iT)
default:
return w.EncodeValue(iT)
}
}
// HandleCall TODO
//
// If this returns an error then both r and w should be discarded and no longer
// used.
func HandleCall(
ctx context.Context,
r *jstream.StreamReader,
w *jstream.StreamWriter,
h mrpc.Handler,
) error {
var head reqHead
if err := r.Next().Value(&head); err != nil {
return err
} else if head.Method == "" {
return errors.New("request head missing 'method' field")
}
var didReadBody bool
ctx = context.WithValue(ctx, ctxValR, r)
ctx = context.WithValue(ctx, ctxValW, w)
ret, err := h.ServeRPC(mrpc.Call{
Context: ctx,
Method: head.Method,
UnmarshalArgs: func(i interface{}) error {
didReadBody = true
return unmarshalBody(i, r.Next())
},
})
// TODO that error is ignored, need a way to differentiate a recoverable
// error from a non-recoverable one
// TODO the writing and reading of the next section could be done in
// parallel?
// TODO if ret is a byte blob or stream there may be user-spawned
// go-routines waiting to write to it, but if this errors out before
// marshalBody is called they will block forever. Probably need to cancel
// the context to let them know?
if err := w.EncodeValue(headTail{}); err != nil {
return err
} else if err := marshalBody(w, ret); err != nil {
return err
}
// TODO to reduce chance of user error maybe Discard should discard any
// remaining data on the Element, not on Elements which haven't been read
// from yet. Then it could always be called on the request body at this
// point.
// Reading the tail (and maybe discarding the body) should only be done once
// marshalBody has finished
if !didReadBody {
// TODO if this errors then presumably reading the tail will too?
r.Next().Discard()
}
if err := r.Next().Discard(); err != nil {
return err
}
return nil
}

View File

@ -15,7 +15,7 @@ this spec:
* An "RPC call", or just "call", is composed of two events: a "request" and a * An "RPC call", or just "call", is composed of two events: a "request" and a
"response". "response".
* The entity which initiates the call by sending a request is the "caller". * The entity which initiates the call by sending a request is the "client".
* The entity which serves the call by responding to a request is the "server". * The entity which serves the call by responding to a request is the "server".
@ -30,7 +30,7 @@ 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 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 `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 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 error, and request/response timings from both the client and server sides
(useful for determining RTT). (useful for determining RTT).
When determining if some piece of data should be considered debug data or part When determining if some piece of data should be considered debug data or part
@ -41,7 +41,7 @@ should run _identically_ in that scenario as in real-life.
In other words: if some field in `debug` effects the behavior of a call directly 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 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 between `debug` and the request/response proper, e.g. the IP address of the
caller. client.
## Call request ## Call request
@ -57,7 +57,7 @@ The three elements of the request stream are specified as follows:
* The second element is the argument to the call. This may be a JSON value, a * 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 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 the call. It's up to the client and server to coordinate beforehand what to
expect here. expect here.
* The third element, the tail, is a JSON value with an object optionally * The third element, the tail, is a JSON value with an object optionally
@ -70,7 +70,7 @@ lack of `name` field in the head, and the addition of the `err` field in the
tail. tail.
A call response is defined as being three jstream elements read off the pipe by 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 the client. 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. be completely consumed and the pipe may be used for a new request.
The three elements of the response stream are specified as follows: The three elements of the response stream are specified as follows:
@ -80,23 +80,23 @@ The three elements of the response stream are specified as follows:
* The second element is the response from the call. This may be a JSON value, a * The second element is the response from the call. This may be a JSON value, a
byte blob, or an embedded stream containing even more elements, depending on 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 the call. It's up to the client and server to coordinate beforehand what to
expect here. expect here.
* The third element, the tail, is a JSON value with an object optionally * The third element, the tail, is a JSON value with an object optionally
containing an `err` field, and optionally containing `debug` field. The value containing an `err` field, and optionally containing `debug` field. The value
of `err` may be any JSON value which is meaningful to the caller and server. of `err` may be any JSON value which is meaningful to the client and server.
## Pipelining ## Pipelining
The protocol allows for the server to begin sending back a response, and even to The protocol allows for the server to begin sending back a response, and even to
send back a complete response, _as soon as_ it receives the request head. In 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 effect this means that the server can be sending back response data while the
caller is still sending request data. client is still sending request data.
Once the server has sent the response tail it can assume the call has completed Once the server has sent the response tail it can assume the call has completed
successfully and ignore all subsequent request data (though it must still fully successfully and ignore all subsequent request data (though it must still fully
read the three request elements off the pipe in order to use it again). read the three request elements off the pipe in order to use it again).
Likewise, once a caller receives the response tail it can cancel whatever it's Likewise, once a client receives the response tail it can cancel whatever it's
doing, finish sending the request argument and tail as soon as possible, and doing, finish sending the request argument and tail as soon as possible, and
assume the call has been completed. assume the call has been completed.