Files
go-omni/ws/rpc/peer.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()
}
}