2023-05-03 06:26:35 +00:00
|
|
|
/*
|
|
|
|
mprpc is a simple bidirectional RPC library using the MessagePack-RPC spec.
|
|
|
|
|
|
|
|
It fully implements the spec and additionally provides Go `error“ handling by
|
|
|
|
converting the error to a standard format for other clients.
|
|
|
|
|
|
|
|
mprpc does not have a typical server/client designation - both use "handlers",
|
|
|
|
which expose methods to be called over the network. A "client" would be an
|
|
|
|
RPCConn which doesn't expose any services, and a "server" would be an RPCConn
|
|
|
|
that doesn't make any `Call`s to the other side.
|
|
|
|
|
|
|
|
This lack of discrete server and client enables mprpc to implement a basic
|
|
|
|
"streaming" architecture on top of the MessagePack-RPC spec, which does not
|
|
|
|
include streaming primitives. Instead, we can provide simple "service handlers"
|
|
|
|
as a callback/destination for streaming data.
|
|
|
|
|
|
|
|
For example, a "client" could subscribe to events from the "server", by
|
|
|
|
providing a callback service to point events to. Then, the "server" would
|
|
|
|
Notify() the callback service with the new event as an argument every time it
|
|
|
|
occured. While this may be less optimal than protocol-level streaming, it is
|
|
|
|
far simpler.
|
|
|
|
|
2023-05-03 15:38:37 +00:00
|
|
|
# Generic Helper Functions
|
|
|
|
|
2023-05-03 06:26:35 +00:00
|
|
|
The idiomatic way to use mprpc is to use the generic functions that are provided
|
2023-05-03 15:38:37 +00:00
|
|
|
as helpers. They allow the programmer to easily wrap existing functions in a
|
|
|
|
closure that automatically encodes and decodes the parameters and results to
|
|
|
|
their MessagePack representations. See the Make* generic functions for more
|
|
|
|
information.
|
2023-05-03 06:26:35 +00:00
|
|
|
|
|
|
|
// Assume myParam and myResult are MessagePack-enabled structs.
|
|
|
|
// Use `msgp` to generate the required functions for them.
|
|
|
|
|
|
|
|
// this is our plain function - we can call it locally to test.
|
|
|
|
func myPlainFunction(p myParam) (r myResult, err error)
|
|
|
|
|
|
|
|
// wrapped is a ServiceFunc that can be passed to rpcConn.RegisterHandler
|
|
|
|
var wrapped := MakeService(myPlainFunction)
|
|
|
|
|
|
|
|
The generic functions allow for flexiblity and elegant code while still keeping
|
|
|
|
the underlying implementation reflect-free. For more complex functions (i.e
|
|
|
|
multiple parameters or return types), a second layer of indirection can be used.
|
2023-05-03 15:38:37 +00:00
|
|
|
|
|
|
|
There is also a `MakeCaller` function that can make a stub function that handles
|
|
|
|
encoding the arguments and decoding the response for a remote procedure.
|
2023-05-03 06:26:35 +00:00
|
|
|
*/
|
|
|
|
package mprpc
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
import (
|
2023-05-20 19:53:34 +00:00
|
|
|
"errors"
|
|
|
|
"io"
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
"github.com/tinylib/msgp/msgp"
|
2023-05-03 00:16:20 +00:00
|
|
|
"golang.org/x/exp/slog"
|
2023-05-01 14:49:47 +00:00
|
|
|
)
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-03 06:26:35 +00:00
|
|
|
// ServiceFunc is a RPC service handler.
|
|
|
|
// It can be created manually, or by using the generic MakeService function on a
|
|
|
|
//
|
2023-05-07 05:00:46 +00:00
|
|
|
// func(msgp.Encoder) (msgp.Decoder, error)
|
2023-05-03 06:26:35 +00:00
|
|
|
//
|
|
|
|
// type.
|
2023-05-02 07:46:39 +00:00
|
|
|
type ServiceFunc func(params msgp.Raw) (res msgp.Raw, err error)
|
2023-05-01 14:49:47 +00:00
|
|
|
|
2023-05-03 06:26:35 +00:00
|
|
|
// RPCConn is a single RPC communication pair.
|
|
|
|
// It is used by both the
|
|
|
|
// "server" aka listener, and client.
|
2023-05-01 14:49:47 +00:00
|
|
|
type RPCConn struct {
|
|
|
|
// TODO: use io.readwritecloser?
|
2023-05-20 19:53:34 +00:00
|
|
|
rwc io.ReadWriteCloser
|
2023-05-01 14:49:47 +00:00
|
|
|
handlers map[string]ServiceFunc
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-02 07:46:39 +00:00
|
|
|
ct rpcConnTrack
|
2023-05-03 00:16:20 +00:00
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
logger slog.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// creates a new RPC connection on top of an io.ReadWriteCloser. Can be
|
|
|
|
// pre-seeded with handlers.
|
|
|
|
func NewRPC(rwc io.ReadWriteCloser, logger *slog.Logger, initialHandlers map[string]ServiceFunc) (rpc *RPCConn, err error) {
|
|
|
|
|
|
|
|
rpc = &RPCConn{
|
|
|
|
rwc: rwc,
|
|
|
|
handlers: make(map[string]ServiceFunc),
|
|
|
|
ct: NewRPCConnTrack(),
|
|
|
|
}
|
|
|
|
if initialHandlers != nil {
|
|
|
|
for k,v := range initialHandlers {
|
|
|
|
rpc.handlers[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
// Call intiates an RPC call to a remote method and returns the
|
2023-05-03 06:26:35 +00:00
|
|
|
// response, or the error, if any. To make calling easier, you can
|
|
|
|
// construct a "Caller" with MakeCaller
|
2023-05-03 00:16:20 +00:00
|
|
|
func (rpc *RPCConn) Call(method string, params msgp.Raw) (msgp.Raw, error) {
|
2023-05-02 07:46:39 +00:00
|
|
|
|
|
|
|
// TODO: error handling.
|
|
|
|
|
|
|
|
id, cb := rpc.ct.Claim()
|
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
req := NewRequest(id, method, params)
|
2023-05-01 14:49:47 +00:00
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
w := msgp.NewWriter(rpc.rwc)
|
2023-05-02 07:46:39 +00:00
|
|
|
req.EncodeMsg(w)
|
|
|
|
|
|
|
|
// block and wait for response.
|
|
|
|
resp := <-cb
|
|
|
|
|
|
|
|
return resp.Result, &resp.Error
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
// Notify initiates a notification to a remote method. It does not
|
|
|
|
// return any information. There is no response from the server.
|
2023-05-20 19:53:34 +00:00
|
|
|
// This method will not block nor will it inform the caller if any errors occur.
|
2023-05-03 00:16:20 +00:00
|
|
|
func (rpc *RPCConn) Notify(method string, params msgp.Raw) {
|
2023-05-01 14:49:47 +00:00
|
|
|
// TODO: return an error if there's a local problem?
|
2023-05-02 07:46:39 +00:00
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
req := NewNotification(method, params)
|
2023-05-02 07:46:39 +00:00
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
w := msgp.NewWriter(rpc.rwc)
|
2023-05-02 07:46:39 +00:00
|
|
|
req.EncodeMsg(w)
|
2023-05-01 14:49:47 +00:00
|
|
|
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
// Register a new handler to be called by the remote side. An error
|
|
|
|
// is returned if the handler name is already in use.
|
|
|
|
func (rpc *RPCConn) RegisterHandler(name string, fn ServiceFunc) error {
|
|
|
|
// TODO: check if name in use.
|
|
|
|
// TODO: mutex lock for sync (or use sync.map?
|
|
|
|
rpc.handlers[name] = fn
|
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
rpc.logger.Info("registered a new handler", "name", name, "fn", fn)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
// Removes a handler, if it exists. Never errors. No-op if the name
|
|
|
|
// is not a registered handler.
|
|
|
|
func (rpc *RPCConn) RemoveHandler(name string) error {
|
|
|
|
delete(rpc.handlers, name)
|
2023-05-01 14:49:47 +00:00
|
|
|
return nil
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
// Serve runs the server. It will dispatch goroutines to handle each method
|
|
|
|
// call. This can (and should in most cases) be run in the background to allow
|
|
|
|
// for sending and receving on the same connection.
|
2023-05-01 14:49:47 +00:00
|
|
|
func (rpc *RPCConn) Serve() {
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
// construct a stream reader.
|
2023-05-20 19:53:34 +00:00
|
|
|
msgReader := msgp.NewReader(rpc.rwc)
|
2023-05-01 14:49:47 +00:00
|
|
|
|
|
|
|
// read a request/notification from the connection.
|
|
|
|
|
|
|
|
var rawmsg msgp.Raw = make(msgp.Raw, 0, 4)
|
|
|
|
|
2023-05-02 07:46:39 +00:00
|
|
|
for {
|
2023-05-20 19:53:34 +00:00
|
|
|
err := rawmsg.DecodeMsg(msgReader)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
rpc.logger.Info("reached EOF, stopping server")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rpc.logger.Warn("error decoding message", "err", err)
|
|
|
|
continue
|
|
|
|
}
|
2023-05-02 07:46:39 +00:00
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
rpcIntf, err := parseRPC(rawmsg)
|
|
|
|
|
|
|
|
if err != nil {
|
2023-05-20 19:53:34 +00:00
|
|
|
rpc.logger.Warn("Could not parse RPC message", "err", err)
|
2023-05-03 00:16:20 +00:00
|
|
|
continue
|
|
|
|
}
|
2023-05-02 07:46:39 +00:00
|
|
|
|
|
|
|
switch rpcObject := rpcIntf.(type) {
|
|
|
|
case Request:
|
|
|
|
// the object is a request - we must dispatch a goroutine
|
|
|
|
// that will call the handler and also send a return value.
|
|
|
|
go rpc.dispatch(rpcObject)
|
|
|
|
case Notification:
|
|
|
|
go rpc.dispatchNotif(rpcObject)
|
|
|
|
case Response:
|
|
|
|
cbCh, err := rpc.ct.Clear(rpcObject.MsgId)
|
|
|
|
if err != nil {
|
2023-05-20 19:53:34 +00:00
|
|
|
rpc.logger.Warn("could not get rpc callback", "msgid", rpcObject.MsgId, "err", err)
|
2023-05-03 00:16:20 +00:00
|
|
|
continue
|
2023-05-02 07:46:39 +00:00
|
|
|
}
|
|
|
|
cbCh <- rpcObject
|
2023-05-20 19:53:34 +00:00
|
|
|
default:
|
|
|
|
panic("invalid rpcObject!")
|
2023-05-02 07:46:39 +00:00
|
|
|
}
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
2023-05-01 14:49:47 +00:00
|
|
|
}
|
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// INTERNAL functions for rpcConn
|
|
|
|
|
2023-05-03 15:38:37 +00:00
|
|
|
// dispatch is an internal method used to execute a Request sent by the remote:w
|
2023-05-01 14:49:47 +00:00
|
|
|
func (rpc *RPCConn) dispatch(req Request) {
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
result, err := rpc.handlers[req.Method](req.Params)
|
2023-04-30 19:49:18 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2023-05-20 19:53:34 +00:00
|
|
|
rpc.logger.Warn("error dispatching rpc function", "method", req.Method, "err", err)
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
2023-05-01 14:49:47 +00:00
|
|
|
// construct the response frame.
|
|
|
|
var rpcE *RPCError = MakeRPCError(err)
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-20 19:53:34 +00:00
|
|
|
w := msgp.NewWriter(rpc.rwc)
|
2023-05-01 14:49:47 +00:00
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
response := NewResponse(req.MsgId, *rpcE, result)
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
response.EncodeMsg(w)
|
|
|
|
|
|
|
|
}
|
2023-05-03 15:38:37 +00:00
|
|
|
|
|
|
|
// dispatchNotif is like dispatch, but for Notifications. This means that it never replies,
|
|
|
|
// even if there is an error.
|
2023-05-01 14:49:47 +00:00
|
|
|
func (rpc *RPCConn) dispatchNotif(req Notification) {
|
2023-04-30 19:49:18 +00:00
|
|
|
|
2023-05-01 14:49:47 +00:00
|
|
|
_, err := rpc.handlers[req.Method](req.Params)
|
|
|
|
|
|
|
|
if err != nil {
|
2023-05-03 00:16:20 +00:00
|
|
|
// log the error, but don't do anything about it.
|
2023-05-20 19:53:34 +00:00
|
|
|
rpc.logger.Warn("error dispatching rpc function", "method", req.Method, "err", err)
|
2023-04-30 19:49:18 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-02 07:46:39 +00:00
|
|
|
|
|
|
|
// Next, we define some helper generic functions that can be used to make
|
|
|
|
// implementing a msg wrapper easier.
|
|
|
|
|
|
|
|
type msgpackObject interface {
|
|
|
|
msgp.Decodable
|
|
|
|
msgp.Encodable
|
|
|
|
msgp.MarshalSizer
|
|
|
|
msgp.Unmarshaler
|
|
|
|
}
|
|
|
|
|
|
|
|
// MakeService is a generic wrapper function. It takes a function with the signature
|
|
|
|
// of func(T msgpObject)(R msgpObject, error) where T and R can be *concrete* types.
|
|
|
|
// and returns a new function that handles conversion to/from msgp.Raw.
|
|
|
|
// the function returned can be used by the RPCConn as a handler function.
|
2023-05-02 08:04:56 +00:00
|
|
|
// This function can typically have it's paramters inferred.
|
2023-05-03 00:16:20 +00:00
|
|
|
func MakeService[T, R msgpackObject](fn func(T) (R, error)) ServiceFunc {
|
2023-05-02 07:46:39 +00:00
|
|
|
return func(p msgp.Raw) (msgp.Raw, error) {
|
|
|
|
// decode the raw data into a new underlying type.
|
|
|
|
var params T
|
2023-05-03 15:38:37 +00:00
|
|
|
|
2023-05-02 07:46:39 +00:00
|
|
|
_, err := params.UnmarshalMsg(p)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// now, call the function fn with the given params, and record the value.
|
|
|
|
|
|
|
|
resp, err := fn(params)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp.MarshalMsg([]byte{})
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2023-05-02 08:04:56 +00:00
|
|
|
|
|
|
|
// should the RPCConn/method name be baked into the function or should they be
|
|
|
|
// part of the returned function paramters?
|
|
|
|
|
|
|
|
// MakeCaller creates a simple wrapper around a parameter of call. The method name
|
|
|
|
// and RPC connection can be given to the returned function to make a RPC call on that
|
|
|
|
// function with the given type parameters.
|
|
|
|
//
|
|
|
|
// This function is slightly obtuse compared to MakeBoundCaller but is more flexible
|
|
|
|
// since you can reuse the same function across multiple connections and method names.
|
|
|
|
//
|
|
|
|
// This generic function must always have it's type paratmers declared explicitly.
|
|
|
|
// They cannot be inferred from the given parameters.
|
|
|
|
func MakeCaller[T, R msgpackObject]() func(string, T, *RPCConn) (R, error) {
|
|
|
|
return func(method string, param T, rpc *RPCConn) (R, error) {
|
2023-05-03 00:16:20 +00:00
|
|
|
|
2023-05-03 15:38:37 +00:00
|
|
|
rawParam, err := param.MarshalMsg([]byte{})
|
|
|
|
if err != nil {
|
|
|
|
var emtpyR R
|
|
|
|
return emtpyR, err
|
|
|
|
}
|
2023-05-03 00:16:20 +00:00
|
|
|
rawResponse, err := rpc.Call(method, rawParam)
|
2023-05-03 15:38:37 +00:00
|
|
|
|
2023-05-02 08:04:56 +00:00
|
|
|
if err != nil {
|
|
|
|
var emtpyR R
|
|
|
|
return emtpyR, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resp R
|
|
|
|
|
|
|
|
_, err = resp.UnmarshalMsg(rawResponse)
|
|
|
|
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MakeBoundCaller is like MakeCaller, except the RPC connection and method name are
|
|
|
|
// fixed and cannot be adjusted later. This function is more elegant but less flexible
|
2023-05-03 00:16:20 +00:00
|
|
|
// than MakeCaller and should be used when performance is not critical.
|
2023-05-02 08:04:56 +00:00
|
|
|
//
|
|
|
|
// This generic function must always have it's type paratmers declared explicitly.
|
|
|
|
// They cannot be inferred from the given parameters.
|
|
|
|
func MakeBoundCaller[T, R msgpackObject](rpc *RPCConn, method string) func(T) (R, error) {
|
|
|
|
|
|
|
|
return func(param T) (R, error) {
|
|
|
|
// encode parameters
|
|
|
|
// invoke rpc.Call
|
|
|
|
// await response
|
|
|
|
// unpack values.
|
2023-05-03 00:16:20 +00:00
|
|
|
rawParam, _ := param.MarshalMsg([]byte{})
|
2023-05-02 08:04:56 +00:00
|
|
|
|
2023-05-03 00:16:20 +00:00
|
|
|
rawResponse, err := rpc.Call(method, rawParam)
|
2023-05-02 08:04:56 +00:00
|
|
|
if err != nil {
|
|
|
|
var emtpyR R
|
|
|
|
return emtpyR, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resp R
|
|
|
|
|
|
|
|
_, err = resp.UnmarshalMsg(rawResponse)
|
|
|
|
|
|
|
|
return resp, err
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2023-05-03 00:16:20 +00:00
|
|
|
|
|
|
|
func MakeNotifier[T msgpackObject]() func(string, T, *RPCConn) {
|
|
|
|
return func(method string, param T, rpc *RPCConn) {
|
|
|
|
rawParam, _ := param.MarshalMsg([]byte{})
|
|
|
|
rpc.Notify(method, rawParam)
|
|
|
|
}
|
|
|
|
}
|