WIP mrpc/jstreamrpc make jstreamrpc simpler and implement mrpc.Debug

This commit is contained in:
Brian Picciano 2018-04-27 08:03:39 +00:00
parent bdfd7500fb
commit dfa32aa6d9
4 changed files with 105 additions and 40 deletions

64
mrpc/debug.go Normal file
View File

@ -0,0 +1,64 @@
package mrpc
import "context"
// Debug data is arbitrary data embedded in a Call's request by the Client or in
// its Response by the Server. Debug data is organized into namespaces to help
// avoid conflicts while still preserving serializability.
//
// Debug data is intended to be used for debugging purposes only, and should
// never be used to effect the path-of-action a Call takes. Put another way:
// when implementing a Call always assume that the Debug info has been
// accidentally removed from the Call's request/response.
type Debug map[string]map[string]interface{}
// Copy returns an identical copy of the Debug being called upon
func (d Debug) Copy() Debug {
d2 := make(Debug, len(d))
for ns, kv := range d {
d2[ns] = make(map[string]interface{}, len(kv))
for k, v := range kv {
d2[ns][k] = v
}
}
return d2
}
// Set returns a copy of the Debug instance with the key set to the value within
// the given namespace.
func (d Debug) Set(ns, key string, val interface{}) Debug {
d = d.Copy()
if d[ns] == nil {
d[ns] = map[string]interface{}{}
}
d[ns][key] = val
return d
}
// Get returns the value for the key within the namespace. Also returns whether
// or not the key was set. This method will never panic.
func (d Debug) Get(ns, key string) (interface{}, bool) {
if d == nil || d[ns] == nil {
return nil, false
}
val, ok := d[ns][key]
return val, ok
}
type debugKey int
// CtxWithDebug returns a new Context with the given Debug embedded in it,
// overwriting any previously embedded Debug.
func CtxWithDebug(ctx context.Context, d Debug) context.Context {
return context.WithValue(ctx, debugKey(0), d)
}
// CtxDebug returns the Debug instance embedded in the Context, or nil if none
// has been embedded.
func CtxDebug(ctx context.Context) Debug {
d := ctx.Value(debugKey(0))
if d == nil {
return Debug(nil)
}
return d.(Debug)
}

View File

@ -14,20 +14,30 @@ import (
)
// TODO Debug
// - ReqHead
// - client encodes into context
// - handler decodes from context
// - ResTail
// - handler ?
// - client ?
// 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 {
type debug struct {
Debug map[string]map[string]json.RawMessage `json:"debug,omitempty"`
}
type reqHead struct {
headTail
debug
Method string `json:"method"`
}
type resTail struct {
debug
Error interface{} `json:"err,omitempty"`
}
type ctxVal int
const (
@ -105,9 +115,7 @@ func HandleCall(
// 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 {
if err := marshalBody(w, ret); err != nil {
return err
}
@ -119,12 +127,11 @@ func HandleCall(
// 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?
// TODO what if this errors?
r.Next().Discard()
}
if err := r.Next().Discard(); err != nil {
return err
}
// TODO write response tail
return nil
}

View File

@ -4,6 +4,9 @@
// This package contains a few fundamental types: Handler, Call, and
// Client. Together these form the components needed to implement nearly any RPC
// system.
//
// TODO document examples
// TODO document Debug?
package mrpc
import (

View File

@ -21,8 +21,7 @@ this spec:
* 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.
`""` for strings, `0` for numbers, `{}` for objects, `null` for any-JSON
types).
`""` for strings, `0` for numbers, `{}` for objects).
## Debug
@ -45,11 +44,11 @@ client.
## Call request
A call request is defined as being three jstream elements read off the pipe by
the server. Once all three elements have been read the request is considered to
be completely consumed and the pipe may be used for a new request.
A call request is defined as being two jstream elements read off the pipe by the
server. Once both elements have been read the request is considered to be
completely consumed and the pipe may be used for a new request.
The three elements of the request stream are specified as follows:
The two elements of the request stream are specified as follows:
* The first element, the head, is a JSON value with an object containing a
`name` field, which identifies the call being made, and optionally a `debug`
@ -60,32 +59,23 @@ The three elements of the request stream are specified as follows:
the call. It's up to the client and server to coordinate beforehand what to
expect here.
* The third element, the tail, is a JSON value with an object optionally
containing a `debug` field.
## Call response
A call response almost the same as the call request. The only difference is the
lack of `name` field in the head, and the addition of the `err` field in the
tail.
A call response is defined as being two jstream elements read off the pipe by
the client. Once both elements have been read the response is considered to be
completely consumed and the pipe may be used for a new request.
A call response is defined as being three jstream elements read off the pipe by
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.
The two elements of the response stream are specified as follows:
The three elements of the response stream are specified as follows:
* The first element, the head, is a JSON value with an object containing
optionally containing a `debug` field.
* The second element is the response from the call. This may be a JSON value, a
* The first 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
the call. It's up to the client and server to coordinate beforehand what to
expect here.
* The third element, the tail, is a JSON value with an object optionally
containing an `err` field, and optionally containing `debug` field. The value
of `err` may be any JSON value which is meaningful to the client and server.
* The second element, the tail, is a JSON value with an object optionally
containing an `err` field, and optionally containing a `debug` field. The
value of `err` may be any JSON value which is meaningful to the client and
server. This element is required even if there's no `err` or `debug` data.
## Pipelining
@ -95,8 +85,9 @@ effect this means that the server can be sending back response data while the
client is still sending request data.
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
read the three request elements off the pipe in order to use it again).
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
assume the call has been completed.
successfully, and the server will ignore all subsequent request data (though it
must still fully read the request body off the pipe in order to use the pipe
again for a new call).
From the client's perspective once the the response tail has been received it
can cancel whatever request body data it's in the process of sending.