663 lines
16 KiB
Go
663 lines
16 KiB
Go
package proto_socket
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"git.toki-labs.com/toki/proto-socket/go/packets"
|
|
)
|
|
|
|
var ErrNotConnected = errors.New("not connected")
|
|
|
|
const MaxNonce int32 = 1<<31 - 1
|
|
|
|
type ParserMap map[string]func([]byte) (proto.Message, error)
|
|
|
|
type Transport interface {
|
|
WritePacket(base *packets.PacketBase) error
|
|
Close() error
|
|
}
|
|
|
|
type pendingRequest struct {
|
|
expectedTypeName string
|
|
ch chan proto.Message
|
|
errCh chan error
|
|
}
|
|
|
|
type queuedPacket struct {
|
|
base *packets.PacketBase
|
|
done chan error
|
|
}
|
|
|
|
type inboundItem struct {
|
|
typeName string
|
|
data []byte
|
|
incomingNonce int32
|
|
responseNonce int32
|
|
}
|
|
|
|
type Communicator struct {
|
|
mu sync.RWMutex
|
|
nonce atomic.Int32
|
|
frameSeq atomic.Int64
|
|
isAlive atomic.Bool
|
|
drainOnClose atomic.Bool
|
|
gateway InboundGateway
|
|
parserMap ParserMap
|
|
handlers map[string][]func(proto.Message)
|
|
reqHandlers map[string]func(proto.Message, int32)
|
|
pendingRequests map[int32]*pendingRequest
|
|
writeQueue chan queuedPacket
|
|
inboundQueue chan inboundItem
|
|
closed chan struct{}
|
|
receiveDone chan struct{}
|
|
closeOnce sync.Once
|
|
transport Transport
|
|
writeErrHandler func(error)
|
|
frameErrHandler func(error)
|
|
canonicalNameMap map[string]string
|
|
}
|
|
|
|
func TypeNameOf(m proto.Message) string {
|
|
return string(proto.MessageName(m))
|
|
}
|
|
|
|
func NewCommunicator(transport Transport, parserMap ParserMap) *Communicator {
|
|
c := &Communicator{}
|
|
c.Initialize(transport, parserMap)
|
|
return c
|
|
}
|
|
|
|
func getShortTypeName(typeName string) string {
|
|
idx := strings.LastIndex(typeName, ".")
|
|
if idx < 0 || idx == len(typeName)-1 {
|
|
return typeName
|
|
}
|
|
return typeName[idx+1:]
|
|
}
|
|
|
|
func (c *Communicator) canonicalizeLocked(typeName string) string {
|
|
if c.canonicalNameMap == nil {
|
|
return typeName
|
|
}
|
|
if name, ok := c.canonicalNameMap[typeName]; ok {
|
|
return name
|
|
}
|
|
short := getShortTypeName(typeName)
|
|
if name, ok := c.canonicalNameMap[short]; ok {
|
|
return name
|
|
}
|
|
return typeName
|
|
}
|
|
|
|
func (c *Communicator) canonicalize(typeName string) string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.canonicalizeLocked(typeName)
|
|
}
|
|
|
|
func (c *Communicator) Initialize(transport Transport, parserMap ParserMap) {
|
|
c.transport = transport
|
|
c.handlers = make(map[string][]func(proto.Message))
|
|
c.reqHandlers = make(map[string]func(proto.Message, int32))
|
|
c.pendingRequests = make(map[int32]*pendingRequest)
|
|
c.writeQueue = make(chan queuedPacket, 64)
|
|
c.inboundQueue = make(chan inboundItem, 64)
|
|
c.closed = make(chan struct{})
|
|
c.receiveDone = make(chan struct{})
|
|
c.isAlive.Store(true)
|
|
|
|
c.mu.Lock()
|
|
c.canonicalNameMap = make(map[string]string)
|
|
shortToFull := make(map[string]string)
|
|
|
|
hbName := TypeNameOf(&packets.HeartBeat{})
|
|
|
|
tempParsers := make(ParserMap)
|
|
for k, v := range parserMap {
|
|
tempParsers[k] = v
|
|
}
|
|
tempParsers[hbName] = func(b []byte) (proto.Message, error) {
|
|
m := &packets.HeartBeat{}
|
|
return m, proto.Unmarshal(b, m)
|
|
}
|
|
|
|
for fullName := range tempParsers {
|
|
shortName := getShortTypeName(fullName)
|
|
if existing, ok := shortToFull[shortName]; ok && existing != fullName {
|
|
c.mu.Unlock()
|
|
panic(fmt.Sprintf("Duplicate alias mapping: %s maps to both %s and %s", shortName, existing, fullName))
|
|
}
|
|
shortToFull[shortName] = fullName
|
|
}
|
|
|
|
for fullName := range tempParsers {
|
|
c.canonicalNameMap[fullName] = fullName
|
|
shortName := getShortTypeName(fullName)
|
|
c.canonicalNameMap[shortName] = fullName
|
|
}
|
|
|
|
c.parserMap = make(ParserMap)
|
|
for k, v := range tempParsers {
|
|
c.parserMap[c.canonicalizeLocked(k)] = v
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
go c.writeLoop()
|
|
go c.receiveLoop()
|
|
}
|
|
|
|
func (c *Communicator) IsAlive() bool {
|
|
return c.isAlive.Load()
|
|
}
|
|
|
|
func (c *Communicator) SetWriteErrorHandler(fn func(error)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.writeErrHandler = fn
|
|
}
|
|
|
|
// SetFrameErrorHandler registers a handler invoked when an inbound gateway fails
|
|
// to decode a raw frame. Transports use it to apply their parse-error disconnect
|
|
// semantics for the gateway path, mirroring SetWriteErrorHandler for the write
|
|
// path. Without a gateway, OnReceivedFrame surfaces decode errors via its return
|
|
// value instead and this handler is unused.
|
|
func (c *Communicator) SetFrameErrorHandler(fn func(error)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.frameErrHandler = fn
|
|
}
|
|
|
|
func (c *Communicator) onFrameError(err error) {
|
|
c.mu.RLock()
|
|
handler := c.frameErrHandler
|
|
c.mu.RUnlock()
|
|
if handler != nil {
|
|
handler(err)
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) nextNonce() (int32, error) {
|
|
for i := 0; i < int(MaxNonce); i++ {
|
|
var next int32
|
|
for {
|
|
current := c.nonce.Load()
|
|
if current >= MaxNonce {
|
|
next = 1
|
|
} else {
|
|
next = current + 1
|
|
}
|
|
if c.nonce.CompareAndSwap(current, next) {
|
|
break
|
|
}
|
|
}
|
|
c.mu.RLock()
|
|
_, taken := c.pendingRequests[next]
|
|
c.mu.RUnlock()
|
|
if !taken {
|
|
return next, nil
|
|
}
|
|
}
|
|
return 0, errors.New("no available nonce: all positive int32 nonces are pending")
|
|
}
|
|
|
|
func (c *Communicator) shutdown() {
|
|
c.isAlive.Store(false)
|
|
c.closeOnce.Do(func() {
|
|
close(c.closed)
|
|
})
|
|
}
|
|
|
|
func (c *Communicator) ForceShutdown() {
|
|
c.drainOnClose.Store(false)
|
|
c.shutdown()
|
|
c.closeGateway()
|
|
if c.receiveDone != nil {
|
|
<-c.receiveDone
|
|
}
|
|
c.cancelPendingRequests()
|
|
}
|
|
|
|
func (c *Communicator) Close() error {
|
|
c.drainOnClose.Store(true)
|
|
c.shutdown()
|
|
c.closeGateway()
|
|
if c.receiveDone != nil {
|
|
<-c.receiveDone
|
|
}
|
|
c.cancelPendingRequests()
|
|
return nil
|
|
}
|
|
|
|
func (c *Communicator) cancelPendingRequests() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for k, pending := range c.pendingRequests {
|
|
pending.errCh <- ErrNotConnected
|
|
delete(c.pendingRequests, k)
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) writeLoop() {
|
|
for {
|
|
select {
|
|
case item := <-c.writeQueue:
|
|
err := c.transport.WritePacket(item.base)
|
|
item.done <- err
|
|
if err != nil {
|
|
c.mu.RLock()
|
|
handler := c.writeErrHandler
|
|
c.mu.RUnlock()
|
|
if handler != nil {
|
|
handler(err)
|
|
}
|
|
}
|
|
case <-c.closed:
|
|
for {
|
|
select {
|
|
case item := <-c.writeQueue:
|
|
err := c.transport.WritePacket(item.base)
|
|
item.done <- err
|
|
case <-c.receiveDone:
|
|
for {
|
|
select {
|
|
case item := <-c.writeQueue:
|
|
err := c.transport.WritePacket(item.base)
|
|
item.done <- err
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) QueuePacket(base *packets.PacketBase) error {
|
|
select {
|
|
case <-c.receiveDone:
|
|
return ErrNotConnected
|
|
default:
|
|
}
|
|
done := make(chan error, 1)
|
|
select {
|
|
case c.writeQueue <- queuedPacket{base: base, done: done}:
|
|
case <-c.receiveDone:
|
|
return ErrNotConnected
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
return err
|
|
case <-c.receiveDone:
|
|
return ErrNotConnected
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) Send(m proto.Message) error {
|
|
if !c.IsAlive() {
|
|
return ErrNotConnected
|
|
}
|
|
data, err := proto.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err := c.nextNonce()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.QueuePacket(&packets.PacketBase{
|
|
TypeName: TypeNameOf(m),
|
|
Nonce: n,
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
func (c *Communicator) SendRequest(req proto.Message, resType proto.Message, timeout time.Duration) (proto.Message, error) {
|
|
if !c.IsAlive() {
|
|
return nil, ErrNotConnected
|
|
}
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
|
|
requestNonce, err := c.nextNonce()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pending := &pendingRequest{
|
|
expectedTypeName: TypeNameOf(resType),
|
|
ch: make(chan proto.Message, 1),
|
|
errCh: make(chan error, 1),
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.pendingRequests[requestNonce] = pending
|
|
c.mu.Unlock()
|
|
|
|
data, err := proto.Marshal(req)
|
|
if err != nil {
|
|
c.removePending(requestNonce)
|
|
return nil, err
|
|
}
|
|
err = c.QueuePacket(&packets.PacketBase{
|
|
TypeName: TypeNameOf(req),
|
|
Nonce: requestNonce,
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
c.removePending(requestNonce)
|
|
return nil, err
|
|
}
|
|
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case res := <-pending.ch:
|
|
return res, nil
|
|
case err := <-pending.errCh:
|
|
return nil, err
|
|
case <-timer.C:
|
|
c.removePending(requestNonce)
|
|
return nil, fmt.Errorf("request timeout for nonce %d", requestNonce)
|
|
case <-c.closed:
|
|
c.removePending(requestNonce)
|
|
return nil, ErrNotConnected
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) AddListener(typeName string, fn func(proto.Message)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
canonical := c.canonicalizeLocked(typeName)
|
|
if _, ok := c.reqHandlers[canonical]; ok {
|
|
panic(fmt.Sprintf("type %s is already registered with AddRequestListener", typeName))
|
|
}
|
|
c.handlers[canonical] = append(c.handlers[canonical], fn)
|
|
}
|
|
|
|
func (c *Communicator) RemoveListeners(typeName string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
canonical := c.canonicalizeLocked(typeName)
|
|
delete(c.handlers, canonical)
|
|
}
|
|
|
|
func (c *Communicator) AddRequestListener(typeName string, fn func(proto.Message, int32)) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
canonical := c.canonicalizeLocked(typeName)
|
|
if len(c.handlers[canonical]) > 0 {
|
|
panic(fmt.Sprintf("type %s is already registered with AddListener", typeName))
|
|
}
|
|
if _, ok := c.reqHandlers[canonical]; ok {
|
|
panic(fmt.Sprintf("type %s is already registered with AddRequestListener", typeName))
|
|
}
|
|
c.reqHandlers[canonical] = fn
|
|
}
|
|
|
|
func (c *Communicator) OnReceivedData(typeName string, data []byte, incomingNonce, responseNonce int32) {
|
|
if responseNonce > 0 {
|
|
c.handleResponse(typeName, data, responseNonce)
|
|
return
|
|
}
|
|
c.EnqueueInbound(typeName, data, incomingNonce, responseNonce)
|
|
}
|
|
|
|
// EnableInboundGateway attaches a goroutine worker-pool gateway in front of the
|
|
// receive coordinator. Raw frames submitted via OnReceivedFrame are decoded by
|
|
// the pool and reordered by an internal seq before reaching OnReceivedData, so
|
|
// the coordinator keeps FIFO dispatch and sole ownership of stateful handling.
|
|
//
|
|
// The gateway performs pure PacketBase envelope decode only; it never touches
|
|
// the pending-request map, listeners, or the write queue. Opt-in: without it
|
|
// OnReceivedFrame decodes inline on the calling goroutine.
|
|
func (c *Communicator) EnableInboundGateway(workers, queueSize int) {
|
|
gateway := NewWorkerGateway(workers, queueSize,
|
|
func(f DecodedFrame) {
|
|
c.OnReceivedData(f.TypeName, f.Data, f.IncomingNonce, f.ResponseNonce)
|
|
},
|
|
func(err error) {
|
|
// Surface gateway decode errors to the transport's frame error
|
|
// handler so the parse-error disconnect semantics match the inline
|
|
// path. The gateway never tears down the transport itself.
|
|
c.onFrameError(err)
|
|
},
|
|
)
|
|
c.AttachInboundGateway(gateway)
|
|
}
|
|
|
|
// AttachInboundGateway installs a custom InboundGateway in front of the receive
|
|
// coordinator. The gateway's sink must forward decoded frames to OnReceivedData
|
|
// to preserve coordinator ownership. Use EnableInboundGateway for the default
|
|
// worker-pool gateway.
|
|
func (c *Communicator) AttachInboundGateway(gateway InboundGateway) {
|
|
c.mu.Lock()
|
|
c.gateway = gateway
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// OnReceivedFrame ingests a raw PacketBase frame from a transport read loop.
|
|
//
|
|
// When a gateway is attached the frame is submitted with an internal seq for
|
|
// off-coordinator decode + reorder and nil is returned; a frame that fails to
|
|
// decode is reported asynchronously, in seq order, to the frame error handler
|
|
// registered via SetFrameErrorHandler so the transport can apply the same
|
|
// parse-error disconnect semantics as the inline path. The gateway never tears
|
|
// down the transport itself.
|
|
//
|
|
// Without a gateway the frame is decoded inline on the calling goroutine and
|
|
// forwarded to OnReceivedData; a decode error is returned so the transport can
|
|
// apply its existing parse-error disconnect semantics.
|
|
func (c *Communicator) OnReceivedFrame(raw []byte) error {
|
|
if !c.IsAlive() {
|
|
return nil
|
|
}
|
|
c.mu.RLock()
|
|
gateway := c.gateway
|
|
c.mu.RUnlock()
|
|
if gateway != nil {
|
|
gateway.Submit(InboundFrame{Seq: c.frameSeq.Add(1), Bytes: raw})
|
|
return nil
|
|
}
|
|
base, err := decodePacketBase(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.OnReceivedData(base.GetTypeName(), base.GetData(), base.GetNonce(), base.GetResponseNonce())
|
|
return nil
|
|
}
|
|
|
|
func (c *Communicator) closeGateway() {
|
|
c.mu.Lock()
|
|
gateway := c.gateway
|
|
c.gateway = nil
|
|
c.mu.Unlock()
|
|
if gateway != nil {
|
|
gateway.Close()
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) EnqueueInbound(typeName string, data []byte, incomingNonce, responseNonce int32) {
|
|
if !c.IsAlive() {
|
|
return
|
|
}
|
|
item := inboundItem{
|
|
typeName: typeName,
|
|
data: data,
|
|
incomingNonce: incomingNonce,
|
|
responseNonce: responseNonce,
|
|
}
|
|
select {
|
|
case c.inboundQueue <- item:
|
|
case <-c.closed:
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) receiveLoop() {
|
|
defer close(c.receiveDone)
|
|
for {
|
|
select {
|
|
case item := <-c.inboundQueue:
|
|
c.dispatchInbound(item)
|
|
case <-c.closed:
|
|
if c.drainOnClose.Load() {
|
|
// drain remaining in queue
|
|
for {
|
|
select {
|
|
case item := <-c.inboundQueue:
|
|
c.dispatchInbound(item)
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) dispatchInbound(item inboundItem) {
|
|
c.mu.RLock()
|
|
canonical := c.canonicalizeLocked(item.typeName)
|
|
reqHandler := c.reqHandlers[canonical]
|
|
listeners := append([]func(proto.Message){}, c.handlers[canonical]...)
|
|
c.mu.RUnlock()
|
|
|
|
if reqHandler != nil {
|
|
msg, err := c.parse(item.typeName, item.data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
reqHandler(msg, item.incomingNonce)
|
|
return
|
|
}
|
|
|
|
if len(listeners) == 0 {
|
|
return
|
|
}
|
|
msg, err := c.parse(item.typeName, item.data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, listener := range listeners {
|
|
listener(msg)
|
|
}
|
|
}
|
|
|
|
func (c *Communicator) handleResponse(typeName string, data []byte, responseNonce int32) {
|
|
pending := c.removePending(responseNonce)
|
|
if pending == nil {
|
|
return
|
|
}
|
|
canonicalExpected := c.canonicalize(pending.expectedTypeName)
|
|
canonicalReceived := c.canonicalize(typeName)
|
|
if canonicalReceived != canonicalExpected {
|
|
pending.errCh <- fmt.Errorf("response type mismatch for nonce %d: expected %s, got %s", responseNonce, canonicalExpected, typeName)
|
|
return
|
|
}
|
|
msg, err := c.parse(typeName, data)
|
|
if err != nil {
|
|
pending.errCh <- err
|
|
return
|
|
}
|
|
pending.ch <- msg
|
|
}
|
|
|
|
func (c *Communicator) parse(typeName string, data []byte) (proto.Message, error) {
|
|
c.mu.RLock()
|
|
canonical := c.canonicalizeLocked(typeName)
|
|
parser := c.parserMap[canonical]
|
|
c.mu.RUnlock()
|
|
if parser == nil {
|
|
return nil, fmt.Errorf("protobuf parser is not registered for type %s", typeName)
|
|
}
|
|
return parser(data)
|
|
}
|
|
|
|
func (c *Communicator) removePending(nonce int32) *pendingRequest {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
pending := c.pendingRequests[nonce]
|
|
delete(c.pendingRequests, nonce)
|
|
return pending
|
|
}
|
|
|
|
func AddListenerTyped[T proto.Message](c *Communicator, fn func(T)) {
|
|
typeName := TypeNameOf(newMessageOf[T]())
|
|
c.AddListener(typeName, func(m proto.Message) {
|
|
typed, ok := m.(T)
|
|
if !ok {
|
|
panic(fmt.Sprintf("received %T for listener %s", m, typeName))
|
|
}
|
|
fn(typed)
|
|
})
|
|
}
|
|
|
|
func AddRequestListenerTyped[Req proto.Message, Res proto.Message](c *Communicator, fn func(Req) (Res, error)) {
|
|
reqTypeName := TypeNameOf(newMessageOf[Req]())
|
|
c.AddRequestListener(reqTypeName, func(m proto.Message, requestNonce int32) {
|
|
req, ok := m.(Req)
|
|
if !ok {
|
|
return
|
|
}
|
|
res, err := fn(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
select {
|
|
case <-c.receiveDone:
|
|
return
|
|
default:
|
|
}
|
|
data, err := proto.Marshal(res)
|
|
if err != nil {
|
|
return
|
|
}
|
|
n, err := c.nextNonce()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = c.QueuePacket(&packets.PacketBase{
|
|
TypeName: TypeNameOf(res),
|
|
Nonce: n,
|
|
ResponseNonce: requestNonce,
|
|
Data: data,
|
|
})
|
|
})
|
|
}
|
|
|
|
func SendRequestTyped[Req proto.Message, Res proto.Message](c *Communicator, req Req, timeout time.Duration) (Res, error) {
|
|
resType := newMessageOf[Res]()
|
|
msg, err := c.SendRequest(req, resType, timeout)
|
|
if err != nil {
|
|
var zero Res
|
|
return zero, err
|
|
}
|
|
res, ok := msg.(Res)
|
|
if !ok {
|
|
var zero Res
|
|
return zero, fmt.Errorf("received %T, expected %T", msg, resType)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func newMessageOf[T proto.Message]() T {
|
|
var zero T
|
|
t := reflect.TypeOf(zero)
|
|
if t == nil || t.Kind() != reflect.Ptr {
|
|
panic("protobuf type parameter must be a pointer message type")
|
|
}
|
|
return reflect.New(t.Elem()).Interface().(T)
|
|
}
|