hirpc

package module
v0.0.0-...-5bf7427 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 20, 2020 License: MIT Imports: 7 Imported by: 0

README

hirpc - dynamic RPC service dispatcher for net/http handler interface

Package hirpc provide Endpoint type - simple dynamic dispatch service registry for user defined structs. Most of it was inspired by github.com/gorilla/rpc. It allows user to expose exported struct methods matching specific signature pattern as a set of named RPC services using standard library net/http request handler interface and specific protocol codec adapter.

Key differences from gorilla/rpc, net/rpc and other implementations are compact implementation with minimal api and middleware interface modeled after net/http api but exposing method call context to user callbacks. This allows users to implement constraining patterns like rate-limiting or per-service and per-method access authorization in simple declarative manner.

Godoc reference

Usage

Simple JSON-RPC 2.0 server

To build a simple Echo RPC service handler define handler service and parameter types

type EchoService struct {
	ops uint64
} 

// EchoRequest - Echo method input parameter type
type EchoRequest struct {
	Value string `json:"value"`
}

// EchoReply - Echo method output parameter type
type EchoReply struct {
	Echo string `json:"echo"`
}

type Count struct {
	Value uint64 `json:"value"`
}

Define handler methods


func (es *EchoService) Echo(ctx context.Context, req *EchoRequest, res *EchoReply) error {
	defer atomic.AddUint64(&es.ops, 1)
	res.Echo = req.Value
	return nil
}

func (es *EchoService) Count(ctx context.Context, _ *struct{}, res *Count) error {
	atomic.AddUint64(&es.ops, 1)
	res.Value = atomic.LoadUint64(&es.ops)
	return nil
}

Create Endpoint using jsonrpc protocol codec and start http server

func main() {
	es := &EchoService{}
	ep := hirpc.NewEndpoint(jsonrpc.Codec, nil)
	ep.Register("echo", es)
	srv := &http.Server{
		Addr:    ":8000",
		Handler: ep,
	}
	if err := srv.ListenAndServe(); err != nil {
		log.Println(err)
	}
}

This is basically all required bits to get JSON-RPC 2.0 compatible server up and running, ready to serve procedure calls over http:

$ curl -H 'Content-Type: application/json;' --data-binary \
'[{"jsonrpc":"2.0","id":"12345","method":"echo.Echo","params": {"value": "123"}}, {"jsonrpc":"2.0","id":"23456","method":"echo.Count","params": {}}]' \
http://localhost:8000/
[{"id":"12345","jsonrpc":"2.0","result":{"value":"123"}},{"id":"23456","jsonrpc":"2.0","result":{"value":1}}]

More complex example using middleware

To improve on previous example we'll add middleware to enforce validation of incoming parameter data before method call if parameter type implements specific interface.

Add Validator interface and implement Validate method for EchoRequest

// Validator - allows to validate underlying value
type Validator interface {
	Validate() error
}

// Validate - validate attribute values
func (e *EchoRequest) Validate() error {
	if len(e.Value) == 0 {
		return fmt.Errorf("value attribute is required")
	}
	return nil
}

Define middleware constructor

// validator - constructs middleware that validates method input parameter if it implements Validator interface
func validator(cc *hirpc.CallContext, h hirpc.CallHandler) hirpc.CallHandler {
	return func(ctx context.Context) (interface{}, error) {
		param := cc.Param.Interface()
		if val, ok := param.(Validator); ok {
			if err := val.Validate(); err != nil {
				return nil, err
			}
		}
		return h(ctx)
	}
}

Then pass middleware to endpoint or service

func main() {
	es := &EchoService{}
	ep := hirpc.NewEndpoint(jsonrpc.Codec, nil, validator)
	ep.Register("echo", es)
	srv := &http.Server{
		Addr:    ":8000",
		Handler: ep,
	}
	if err := srv.ListenAndServe(); err != nil {
		log.Println(err)
	}
}

This way input data validation is enforced just by implementing Validator interface on specific type

$ curl -H 'Content-Type: application/json;' \
--data-binary '[{"jsonrpc":"2.0","id":"12345","method":"echo.Count","params": {}}, {"jsonrpc":"2.0","id":"23456","method":"echo.Echo","params": {"value": ""}}]' \
http://localhost:8000/    
[{"id":"23456","jsonrpc":"2.0","result":{"value":1}},{"id":"23456","jsonrpc":"2.0","error":{"code":-32000,"message":"value attribute is required"}}]                                                                                   

Description

Endpoint is a simple HTTP handler dispatching RPC requests decoded by HTTPCodec to set of handler services defined by user defined struct object methods and encoding results into response using same codec. Endpoint stores service metadata collected using reflect package when user registers new object with Endpoint.Register method and uses it to find specific handler function and construct call context when handling request. All struct methods matching signature pattern of

func (*ExportedType) ExportedMethod(context.Context, *InType, *OutType) error

are treated as exported RPC handlers and registered as methods of this service. Input and output parameters should be pointers to types used protocol codec can (de)serialize. All other methods and properties are ignored.

Service registered with empty name or using Endpoint.Root method is a namespace root service, Endpoint uses it to handle method calls with empty service name if set. Registering new service under same name discards previously registered instance.

Endpoint public methods are synchronized using sync.RWMutex so it's safe to Register and Unregister services at runtime.

HTTP request handling

In order to handle incoming http requests, Endpoint requires HTTPCodec interface implementation to translate protocol data format into resolvable object and CallScheduler instance to invoke resolved method handlers concurrently or sequentially.

Endpoint starts by decoding request body into set of CallRequest objects using HTTPCodec.DecodeRequest implementation. CallRequest tells Endpoint which method of which service caller is looking for, provides method to decode raw parameter data into pointer to specific type instance and constructor for protocol-level response object.

Decoded CallRequest objects get resolved against service registry by Endpoint.Dispatch method using service and method name. Dispatch looks up requested method, allocates new value of input type and tries to decode payload into receiver. If deserialization succeeds, it constructs CallHandler capturing allocated input and output parameter instances in closure, wrapping it into middleware chain if necessary. Lookup failures and parameter deserialization errors treated the same way as handler returning error.

Then Endpoint schedules successfully resolved calls for background execution using CallScheduler instance and wraps results using CallRequest.Result response constructor to construct complete http response with HTTPCodec.EncodeResults.

Package provides SequentialScheduler implementation to execute multiple method calls in sequential order which is default and ConcurrentScheduler for semaphore-bounded concurrent execution of multiple handlers within request.

Shared state access within handler methods is subject to proper synchronization by user, since multiple instances of multiple method calls could be running concurrently.

Middleware

NewEndpoint, Endpoint.Use, Endpoint.Register and Endpoint.Root functions accept variadic list of functions with

func(*CallContext, CallHandler) CallHandler

signature used as middleware constructors applied to prepared method call context when handler execution is about to start. CallContext object provides access to service and method names, allocated parameter values and CallRequest instance to user middleware. When call handler starts, functions invoked in order of appearance, endpoint middleware invoked first.

CallHandler type represents execution of single method call with specific input and output parameter values defined like this:

type CallHandler func(context.Context) (interface{}, error)

NewEndpoint and Endpoint.Use register endpoint-level middleware applied to every method call of every service, while Endpoint.Register and Endpoint.Root register service-level middleware applied only to methods of one specific service.

ToDo

  • Add tests
  • Improve documentation
  • Find a way to generate handlers with go-swagger

Known limitations

The only implemented HTTPCodec is currently a JSON-RPC 2.0 compatible codec in jsonrpc subpackage. Method specifier resolved into service and method name accordingly by attempting to left split it once with "." separator. If split fails empty string resolved as service name and original method specifier as method name attempting lookup method on Endpoint namespace root service.

It does not implement any rpc namespace introspection (but probably could).

Documentation

Index

Constants

View Source
const DefaultConcurrencyLimit int64 = 3

DefaultConcurrencyLimit - default concurrency limit used by ConcurrentScheduler

Variables

View Source
var (

	// DefaultCallScheduler - default call execution scheduler
	DefaultCallScheduler = &SequentialScheduler{}
)

Functions

This section is empty.

Types

type CallContext

type CallContext struct {
	Request CallRequest   // dispatch source provided by codec, used to construct codec response for this call
	Service string        // target service name
	Method  string        // target method name
	Param   reflect.Value // deserialized parameter
	Result  reflect.Value // allocated result container
}

CallContext - method call dispatch context. Captures information resolved by Endpoint into user middleware applied to handler. Exposes source call request protocol object and handler method information.

type CallHandler

type CallHandler func(context.Context) (interface{}, error)

CallHandler - method call handling function

type CallRequest

type CallRequest interface {
	Target() (string, string)              // decode call request target service and method name
	Payload(interface{}) error             // decode call request parameter payload into specific type pointer
	Result(interface{}, error) interface{} // construct result object specific for this request and protocol
}

CallRequest - HTTPCodec single method call request object

type CallScheduler

type CallScheduler interface {
	Execute(context.Context, []func()) error
}

CallScheduler - used to execute multiple method calls decoded from request untill all methods finish or context gets cancelled

type ConcurrentScheduler

type ConcurrentScheduler struct {
	Limit uint // concurrency limit per request
}

ConcurrentScheduler - concurrently executing call scheduler.

func (*ConcurrentScheduler) Execute

func (cs *ConcurrentScheduler) Execute(ctx context.Context, handlers []func()) error

Execute - executes multiple handlers in background limiting concurrency by semaphore instance.

type Endpoint

type Endpoint struct {
	// contains filtered or unexported fields
}

Endpoint - net/http request handler and RPC service registry. Decodes http requests using HTTPCodec and schedules procedure calls for execution using CallScheduler.

func NewEndpoint

func NewEndpoint(codec HTTPCodec, sched CallScheduler, mw ...func(*CallContext, CallHandler) CallHandler) *Endpoint

NewEndpoint - creates new RPC endpoint, returns nil if codec is nil, registers endpoint-level middleware if provided.

func (*Endpoint) Dispatch

func (ep *Endpoint) Dispatch(cr CallRequest) (CallHandler, error)

Dispatch - resolves service and method handlers and constructs CallHandler closure.

func (*Endpoint) Register

func (ep *Endpoint) Register(name string, inst interface{}, mw ...func(*CallContext, CallHandler) CallHandler) (*ServiceHandler, error)

Register - registers RPC handler instance by service name. All exported instance methods matching following signature will be exposed for public access: func (*ExportedType) ExportedMethod(context.Context, *InType, *OutType) error Registering service with empty name returns result of Endpoint.Root method.

func (*Endpoint) Root

func (ep *Endpoint) Root(name string, inst interface{}, mw ...func(*CallContext, CallHandler) CallHandler) (*ServiceHandler, error)

Root - registers RPC handler instance as namespace root. This service is used for method lookup when dispatched service name is empty.

func (*Endpoint) ServeHTTP

func (ep *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP - decode request body into multiple call requests and execute them sequentially or concurrently.

func (*Endpoint) Service

func (ep *Endpoint) Service(name string) (*ServiceHandler, bool)

Service - looks up service handler by name.

func (*Endpoint) Services

func (ep *Endpoint) Services() map[string]*ServiceHandler

Services - returns copy of service handlers map.

func (*Endpoint) Unregister

func (ep *Endpoint) Unregister(name string) error

Unregister - remove service from endpoint.

func (*Endpoint) Use

func (ep *Endpoint) Use(mw ...func(*CallContext, CallHandler) CallHandler)

Use - replaces endpoint middleware list.

type HTTPCodec

type HTTPCodec interface {
	EncodeError(http.ResponseWriter, error)             // send http response representing single error message
	EncodeResults(http.ResponseWriter, ...interface{})  // send http response representing one or more call results
	DecodeRequest(*http.Request) ([]CallRequest, error) // read and close http request body and decode one or more method call requests to execute, return (nil, nil) if request is valid but no calls was decoded
}

HTTPCodec - request adapter interface implementing protocol validation and data (de-)serialization

type MethodHandler

type MethodHandler struct {
	Meth    reflect.Method // method pointer
	ReqType reflect.Type   // signature parameter type
	ResType reflect.Type   // signature result type
}

MethodHandler - stores reflected method function reference and specific signature request/response types.

type SequentialScheduler

type SequentialScheduler struct {
}

SequentialScheduler - sequential execution call scheduler.

func (*SequentialScheduler) Execute

func (ss *SequentialScheduler) Execute(ctx context.Context, handlers []func()) error

Execute - executes handlers in order of appearance.

type ServiceHandler

type ServiceHandler struct {
	Name     string                    // name used to reference service in registry namespace
	Methods  map[string]*MethodHandler // descriptors for methods with RPC handler compatible signature
	Inst     reflect.Value             // pointer to service instance
	InstType reflect.Type              // type of service instance
	// contains filtered or unexported fields
}

ServiceHandler - stores service instance, type, service-level middleware and method handlers collection.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL