425 lines
10 KiB
Go
425 lines
10 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/go-omni/ws"
|
|
)
|
|
|
|
type Handler func(context.Context, json.RawMessage) (any, error)
|
|
|
|
type transport interface {
|
|
Context() context.Context
|
|
Send(context.Context, ws.Message) error
|
|
TrySend(ws.Message) error
|
|
Close(int, string) error
|
|
Abort()
|
|
}
|
|
|
|
// Peer belongs to exactly one physical session. Construct a new Peer on every
|
|
// reconnect. Register routes before passing Handlers to a client/server.
|
|
type Peer struct {
|
|
mu sync.Mutex
|
|
cfg Config
|
|
conn transport
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
routes map[string]Handler
|
|
pending map[string]chan envelope
|
|
incoming map[string]context.CancelFunc
|
|
seq uint64
|
|
active int
|
|
closed bool
|
|
done chan struct{}
|
|
ready chan struct{}
|
|
doneOnce sync.Once
|
|
}
|
|
|
|
func New(cfg Config) (*Peer, error) {
|
|
cfg, err := normalize(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &Peer{cfg: cfg, ctx: ctx, cancel: cancel, routes: make(map[string]Handler), pending: make(map[string]chan envelope), incoming: make(map[string]context.CancelFunc), done: make(chan struct{}), ready: make(chan struct{})}, nil
|
|
}
|
|
|
|
func (p *Peer) Handle(method string, handler Handler) error {
|
|
if !validName(method, 128) || handler == nil {
|
|
return errors.New("rpc: valid method and handler required")
|
|
}
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.closed {
|
|
return ErrClosed
|
|
}
|
|
if p.conn != nil {
|
|
return errors.New("rpc: register methods before binding")
|
|
}
|
|
if _, ok := p.routes[method]; ok {
|
|
return errors.New("rpc: duplicate method")
|
|
}
|
|
p.routes[method] = handler
|
|
return nil
|
|
}
|
|
|
|
// Handlers integrates protocol work into the Session completion barrier.
|
|
// OnClose waits for all accepted handlers, including handlers ignoring cancel.
|
|
// Owners must use a bounded Session.Wait/Shutdown and retain dependencies if it
|
|
// times out. Do not call Wait or Shutdown from a registered handler itself.
|
|
func (p *Peer) Handlers() ws.Handlers {
|
|
return ws.Handlers{
|
|
OnOpen: func(_ context.Context, s *ws.Session) error {
|
|
if s.Subprotocol() != Subprotocol {
|
|
return ErrProtocol
|
|
}
|
|
return p.bind(s)
|
|
},
|
|
OnMessage: func(_ context.Context, _ *ws.Session, m ws.Message) error { return p.receive(m) },
|
|
OnClose: func(_ *ws.Session, _ error) { p.Stop(); _ = p.Wait(context.Background()) },
|
|
}
|
|
}
|
|
|
|
func (p *Peer) bind(c transport) error {
|
|
p.mu.Lock()
|
|
if p.closed {
|
|
p.mu.Unlock()
|
|
return ErrClosed
|
|
}
|
|
if p.conn != nil {
|
|
p.mu.Unlock()
|
|
return errors.New("rpc: peer already bound")
|
|
}
|
|
p.cancel()
|
|
p.ctx, p.cancel = context.WithCancel(c.Context())
|
|
p.conn = c
|
|
close(p.ready)
|
|
ctx := p.ctx
|
|
p.mu.Unlock()
|
|
context.AfterFunc(ctx, p.Stop)
|
|
return nil
|
|
}
|
|
|
|
// Stop cancels pending calls and incoming handlers without waiting for them.
|
|
// It does not own the underlying Session; Shutdown additionally closes it.
|
|
func (p *Peer) Stop() {
|
|
p.mu.Lock()
|
|
if !p.closed {
|
|
p.closed = true
|
|
p.cancel()
|
|
for _, cancel := range p.incoming {
|
|
cancel()
|
|
}
|
|
}
|
|
if p.active == 0 {
|
|
p.doneOnce.Do(func() { close(p.done) })
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
func (p *Peer) Done() <-chan struct{} { return p.done }
|
|
|
|
// WaitReady waits for OnOpen to bind the transport. Call and Restore should run
|
|
// outside OnOpen: replies are delivered only after that callback returns.
|
|
func (p *Peer) WaitReady(ctx context.Context) error {
|
|
select {
|
|
case <-p.ready:
|
|
p.mu.Lock()
|
|
closed := p.closed
|
|
p.mu.Unlock()
|
|
if closed {
|
|
return ErrClosed
|
|
}
|
|
return nil
|
|
case <-p.done:
|
|
return ErrClosed
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
func (p *Peer) Wait(ctx context.Context) error {
|
|
select {
|
|
case <-p.done:
|
|
return nil
|
|
default:
|
|
}
|
|
select {
|
|
case <-p.done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
func (p *Peer) Shutdown(ctx context.Context) error {
|
|
p.Stop()
|
|
p.mu.Lock()
|
|
c := p.conn
|
|
p.mu.Unlock()
|
|
if c != nil {
|
|
_ = c.Close(1000, "")
|
|
}
|
|
return p.Wait(ctx)
|
|
}
|
|
|
|
// Call waits for the peer's reply, bounded by ctx and Config.CallTimeout.
|
|
// Cancellation is best effort on the wire; a timeout never proves rollback.
|
|
func (p *Peer) Call(ctx context.Context, method string, input, out any) error {
|
|
if !validName(method, 128) {
|
|
return errors.New("rpc: invalid method")
|
|
}
|
|
params, err := json.Marshal(input)
|
|
if err != nil {
|
|
return fmt.Errorf("rpc: encode input: %w", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, p.cfg.CallTimeout)
|
|
defer cancel()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
p.mu.Lock()
|
|
if p.closed {
|
|
p.mu.Unlock()
|
|
return ErrClosed
|
|
}
|
|
if p.conn == nil {
|
|
p.mu.Unlock()
|
|
return ErrNotConnected
|
|
}
|
|
if len(p.pending) >= p.cfg.MaxPending {
|
|
p.mu.Unlock()
|
|
return ErrBusy
|
|
}
|
|
p.seq++
|
|
id := strconv.FormatUint(p.seq, 36)
|
|
ch := make(chan envelope, 1)
|
|
p.pending[id] = ch
|
|
c, work := p.conn, p.ctx
|
|
p.mu.Unlock()
|
|
defer func() { p.mu.Lock(); delete(p.pending, id); p.mu.Unlock() }()
|
|
deadline, _ := ctx.Deadline()
|
|
budget := max(int64(1), time.Until(deadline).Milliseconds())
|
|
if err = p.send(ctx, c, envelope{Version: 1, Type: "call", ID: id, Method: method, Params: params, TimeoutMS: budget}); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case reply := <-ch:
|
|
if reply.Error != nil {
|
|
return reply.Error
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(reply.Result, out)
|
|
case <-ctx.Done():
|
|
// Never block a caller waiting for room to send cancellation. The remote
|
|
// handler is independently bounded by the transmitted deadline.
|
|
_ = p.try(c, envelope{Version: 1, Type: "cancel", ID: id})
|
|
return ctx.Err()
|
|
case <-work.Done():
|
|
return ErrClosed
|
|
}
|
|
}
|
|
|
|
// Notify confirms a local write, not remote processing. Notifications are
|
|
// handled with the same concurrency and timeout limits as incoming calls.
|
|
func (p *Peer) Notify(ctx context.Context, method string, input any) error {
|
|
if !validName(method, 128) {
|
|
return errors.New("rpc: invalid method")
|
|
}
|
|
params, err := json.Marshal(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.mu.Lock()
|
|
c, closed := p.conn, p.closed
|
|
p.mu.Unlock()
|
|
if closed {
|
|
return ErrClosed
|
|
}
|
|
if c == nil {
|
|
return ErrNotConnected
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, p.cfg.WriteTimeout)
|
|
defer cancel()
|
|
return p.send(ctx, c, envelope{Version: 1, Type: "event", Method: method, Params: params})
|
|
}
|
|
|
|
func (p *Peer) encode(e envelope) (ws.Message, error) {
|
|
data, err := json.Marshal(e)
|
|
if err != nil {
|
|
return ws.Message{}, err
|
|
}
|
|
if len(data) > p.cfg.MaxMessageBytes {
|
|
return ws.Message{}, ws.ErrMessageTooLarge
|
|
}
|
|
return ws.Message{Type: ws.Text, Data: data}, nil
|
|
}
|
|
func (p *Peer) send(ctx context.Context, c transport, e envelope) error {
|
|
m, err := p.encode(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Send(ctx, m)
|
|
}
|
|
func (p *Peer) try(c transport, e envelope) error {
|
|
m, err := p.encode(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.TrySend(m)
|
|
}
|
|
|
|
func (p *Peer) receive(m ws.Message) error {
|
|
if m.Type != ws.Text {
|
|
return ErrProtocol
|
|
}
|
|
e, err := decode(m.Data, p.cfg.MaxMessageBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.mu.Lock()
|
|
if p.closed {
|
|
p.mu.Unlock()
|
|
return ErrClosed
|
|
}
|
|
if p.conn == nil {
|
|
p.mu.Unlock()
|
|
return ErrNotConnected
|
|
}
|
|
switch e.Type {
|
|
case "reply":
|
|
if ch := p.pending[e.ID]; ch != nil {
|
|
select {
|
|
case ch <- e:
|
|
default:
|
|
}
|
|
}
|
|
p.mu.Unlock()
|
|
return nil
|
|
case "cancel":
|
|
if cancel := p.incoming[e.ID]; cancel != nil {
|
|
cancel()
|
|
}
|
|
p.mu.Unlock()
|
|
return nil
|
|
}
|
|
c := p.conn
|
|
if e.Type == "call" {
|
|
if _, exists := p.incoming[e.ID]; exists {
|
|
p.mu.Unlock()
|
|
return ErrProtocol
|
|
}
|
|
}
|
|
handler := p.routes[e.Method]
|
|
if handler == nil {
|
|
p.mu.Unlock()
|
|
if e.Type == "event" {
|
|
return ErrProtocol
|
|
}
|
|
return p.try(c, envelope{Version: 1, Type: "reply", ID: e.ID, Error: &Error{"METHOD_NOT_FOUND", "method not found"}})
|
|
}
|
|
if p.active >= p.cfg.MaxConcurrent {
|
|
p.mu.Unlock()
|
|
if e.Type == "event" {
|
|
return ErrBusy
|
|
}
|
|
return p.try(c, envelope{Version: 1, Type: "reply", ID: e.ID, Error: &Error{"BUSY", "peer capacity reached"}})
|
|
}
|
|
timeout := p.cfg.CallTimeout
|
|
if e.TimeoutMS > 0 {
|
|
timeout = min(timeout, time.Duration(e.TimeoutMS)*time.Millisecond)
|
|
}
|
|
ctx, cancel := context.WithTimeout(p.ctx, timeout)
|
|
if e.Type == "call" {
|
|
p.incoming[e.ID] = cancel
|
|
}
|
|
p.active++
|
|
p.mu.Unlock()
|
|
go p.execute(ctx, cancel, c, e, handler)
|
|
return nil
|
|
}
|
|
|
|
func invoke(ctx context.Context, h Handler, params json.RawMessage) (value any, err error) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
value = nil
|
|
err = errors.New("rpc: handler panic")
|
|
}
|
|
}()
|
|
return h(ctx, params)
|
|
}
|
|
func publicError(err error) (result *Error) {
|
|
result = &Error{"INTERNAL", "request failed"}
|
|
defer func() {
|
|
if recover() != nil {
|
|
result = &Error{"INTERNAL", "request failed"}
|
|
}
|
|
}()
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return &Error{"DEADLINE_EXCEEDED", "request deadline exceeded"}
|
|
}
|
|
if errors.Is(err, context.Canceled) {
|
|
return &Error{"CANCELED", "request canceled"}
|
|
}
|
|
var exposed *Error
|
|
if errors.As(err, &exposed) && validError(exposed) {
|
|
return &Error{exposed.Code, exposed.Message}
|
|
}
|
|
return &Error{"INTERNAL", "request failed"}
|
|
}
|
|
|
|
func marshalResult(value any) (raw json.RawMessage, err error) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
raw = nil
|
|
err = errors.New("rpc: result encoding failed")
|
|
}
|
|
}()
|
|
return json.Marshal(value)
|
|
}
|
|
func (p *Peer) execute(ctx context.Context, cancel context.CancelFunc, c transport, e envelope, h Handler) {
|
|
defer func() {
|
|
cancel()
|
|
p.mu.Lock()
|
|
delete(p.incoming, e.ID)
|
|
p.active--
|
|
if p.closed && p.active == 0 {
|
|
p.doneOnce.Do(func() { close(p.done) })
|
|
}
|
|
p.mu.Unlock()
|
|
}()
|
|
value, err := invoke(ctx, h, e.Params)
|
|
if e.Type == "event" {
|
|
if err != nil && p.cfg.OnEventError != nil {
|
|
func() { defer func() { _ = recover() }(); p.cfg.OnEventError(ctx, err) }()
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
err = ctx.Err()
|
|
}
|
|
reply := envelope{Version: 1, Type: "reply", ID: e.ID}
|
|
if err == nil {
|
|
reply.Result, err = marshalResult(value)
|
|
}
|
|
if err != nil {
|
|
reply.Result = nil
|
|
reply.Error = publicError(err)
|
|
}
|
|
writeCtx, writeCancel := context.WithTimeout(p.ctx, p.cfg.WriteTimeout)
|
|
defer writeCancel()
|
|
if err = p.send(writeCtx, c, reply); errors.Is(err, ws.ErrMessageTooLarge) {
|
|
reply.Result = nil
|
|
reply.Error = &Error{"RESULT_TOO_LARGE", "result exceeds message limit"}
|
|
err = p.send(writeCtx, c, reply)
|
|
}
|
|
if err != nil {
|
|
c.Abort()
|
|
}
|
|
}
|