From dfa32aa6d99365ff511281ca89b2d25246561c7b Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Fri, 27 Apr 2018 08:03:39 +0000 Subject: [PATCH] WIP mrpc/jstreamrpc make jstreamrpc simpler and implement mrpc.Debug --- mrpc/debug.go | 64 +++++++++++++++++++++++++++++++++++ mrpc/jstreamrpc/jstreamrpc.go | 29 ++++++++++------ mrpc/mrpc.go | 3 ++ mrpc/proto.md | 49 +++++++++++---------------- 4 files changed, 105 insertions(+), 40 deletions(-) create mode 100644 mrpc/debug.go diff --git a/mrpc/debug.go b/mrpc/debug.go new file mode 100644 index 0000000..cce4d96 --- /dev/null +++ b/mrpc/debug.go @@ -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) +} diff --git a/mrpc/jstreamrpc/jstreamrpc.go b/mrpc/jstreamrpc/jstreamrpc.go index dd3f85b..714bb83 100644 --- a/mrpc/jstreamrpc/jstreamrpc.go +++ b/mrpc/jstreamrpc/jstreamrpc.go @@ -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 } diff --git a/mrpc/mrpc.go b/mrpc/mrpc.go index af77355..1cbf196 100644 --- a/mrpc/mrpc.go +++ b/mrpc/mrpc.go @@ -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 ( diff --git a/mrpc/proto.md b/mrpc/proto.md index 50473d2..d49d471 100644 --- a/mrpc/proto.md +++ b/mrpc/proto.md @@ -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.