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 // TODO Debug
// - ReqHead
// - client encodes into context
// - handler decodes from context
// - ResTail
// - handler ?
// - client ?
// TODO Error? // TODO Error?
// TODO SizeHints // 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"` Debug map[string]map[string]json.RawMessage `json:"debug,omitempty"`
} }
type reqHead struct { type reqHead struct {
headTail debug
Method string `json:"method"` Method string `json:"method"`
} }
type resTail struct {
debug
Error interface{} `json:"err,omitempty"`
}
type ctxVal int type ctxVal int
const ( const (
@ -105,9 +115,7 @@ func HandleCall(
// marshalBody is called they will block forever. Probably need to cancel // marshalBody is called they will block forever. Probably need to cancel
// the context to let them know? // the context to let them know?
if err := w.EncodeValue(headTail{}); err != nil { if err := marshalBody(w, ret); err != nil {
return err
} else if err := marshalBody(w, ret); err != nil {
return err return err
} }
@ -119,12 +127,11 @@ func HandleCall(
// Reading the tail (and maybe discarding the body) should only be done once // Reading the tail (and maybe discarding the body) should only be done once
// marshalBody has finished // marshalBody has finished
if !didReadBody { if !didReadBody {
// TODO if this errors then presumably reading the tail will too? // TODO what if this errors?
r.Next().Discard() r.Next().Discard()
} }
if err := r.Next().Discard(); err != nil {
return err // TODO write response tail
}
return nil return nil
} }

View File

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

View File

@ -21,8 +21,7 @@ this spec:
* 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).
types).
## Debug ## Debug
@ -45,11 +44,11 @@ client.
## Call request ## Call request
A call request is defined as being three jstream elements read off the pipe by A call request is defined as being two jstream elements read off the pipe by the
the server. Once all three elements have been read the request is considered to server. Once both elements have been read the request is considered to be
be completely consumed and the pipe may be used for a new request. 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 * 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` `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 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
containing a `debug` field.
## Call response ## Call response
A call response almost the same as the call request. The only difference is the A call response is defined as being two jstream elements read off the pipe by
lack of `name` field in the head, and the addition of the `err` field in the the client. Once both elements have been read the response is considered to be
tail. 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 two elements of the response stream are specified as follows:
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 three elements of the response stream are specified as follows: * The first element is the response from the call. This may be a JSON value, a
* 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
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 client 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 second 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 a `debug` field. The
of `err` may be any JSON value which is meaningful to the client and server. 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 ## 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. 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 the server will ignore all subsequent request data (though it
read the three request elements off the pipe in order to use it again). must still fully read the request body off the pipe in order to use the pipe
Likewise, once a client receives the response tail it can cancel whatever it's again for a new call).
doing, finish sending the request argument and tail as soon as possible, and
assume the call has been completed. 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.