Files
orbit/sdk/go/event_connection.go

286 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package orbit
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/coder/websocket"
)
const eventSubprotocol = "orbit.events.v1"
const eventMaxPending = 32
// EventConnection 管理单次连接、请求关联和有界收发。可并发调用Ack/Ping。
// 本类型不自动心跳/重连/调用处理器;高层消费请使用EventConsumer。
type EventConnection struct {
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
welcome EventWelcome
deliveries chan EventGrant
done chan struct{}
mu sync.Mutex
err error
pending map[string]chan eventFrame
}
// DialEvents 的ctx仅控制握手;成功后必须Close,取消握手ctx不会关闭已建立的连接。
func (c *Client) DialEvents(ctx context.Context, in EventRegistration) (*EventConnection, error) {
if c.token == "" || !validID(in.SubscriptionID) || in.Capacity < 1 || in.Capacity > 32 {
return nil, errors.New("authenticated client, valid subscription and capacity 1..32 are required")
}
bounded, stop := context.WithTimeout(ctx, 8*time.Second)
defer stop()
headers := http.Header{}
headers.Set("Authorization", "Bearer "+c.token)
conn, response, err := websocket.Dial(bounded, c.baseURL+"/v1/events/connect", &websocket.DialOptions{HTTPClient: c.http, HTTPHeader: headers, Subprotocols: []string{eventSubprotocol}, CompressionMode: websocket.CompressionDisabled})
if err != nil {
if response != nil && response.StatusCode != http.StatusSwitchingProtocols {
return nil, &HTTPError{StatusCode: response.StatusCode}
}
return nil, eventIOError(err)
}
ok := false
defer func() {
if !ok {
_ = conn.CloseNow()
}
}()
if conn.Subprotocol() != eventSubprotocol {
return nil, ErrEventProtocol
}
conn.SetReadLimit(512 << 10)
id, err := NewEventMessageID()
if err != nil {
return nil, err
}
body, _ := marshalWire(in)
raw, _ := marshalWire(eventFrame{Version: "1", Type: "events.hello", MessageID: id, Body: body})
if err = conn.Write(bounded, websocket.MessageText, raw); err != nil {
return nil, eventIOError(err)
}
kind, raw, err := conn.Read(bounded)
if err != nil {
return nil, eventIOError(err)
}
f, err := parseEventFrame(kind, raw)
if err != nil || f.ReplyTo != id {
return nil, ErrEventProtocol
}
if f.Type == "error" {
return nil, f.Error
}
var welcome EventWelcome
if f.Type != "events.welcome" || decodeEventValue(f.Body, &welcome) != nil || welcome.SessionID != f.SessionID || welcome.SubscriptionID != in.SubscriptionID || welcome.Capacity != in.Capacity || welcome.ServerTime.IsZero() || welcome.LeaseDurationMS != 30000 || welcome.HeartbeatIntervalMS != 15000 {
return nil, ErrEventProtocol
}
lifetime, cancel := context.WithCancel(context.Background())
e := &EventConnection{conn: conn, ctx: lifetime, cancel: cancel, welcome: welcome, deliveries: make(chan EventGrant, in.Capacity), done: make(chan struct{}), pending: make(map[string]chan eventFrame)}
ok = true
go e.readLoop()
return e, nil
}
func (e *EventConnection) Welcome() EventWelcome { return e.welcome }
func (e *EventConnection) Done() <-chan struct{} { return e.done }
func (e *EventConnection) Err() error { e.mu.Lock(); defer e.mu.Unlock(); return e.err }
func (e *EventConnection) Close() error { e.fail(ErrEventClosed); <-e.done; return nil }
func (e *EventConnection) fail(err error) {
e.mu.Lock()
if e.err != nil {
e.mu.Unlock()
return
}
e.err = err
e.cancel()
e.mu.Unlock()
_ = e.conn.CloseNow()
}
func (e *EventConnection) readLoop() {
defer close(e.done)
for {
ctx, stop := context.WithTimeout(e.ctx, 60*time.Second)
kind, raw, err := e.conn.Read(ctx)
stop()
if err != nil {
e.fail(eventIOError(err))
return
}
f, err := parseEventFrame(kind, raw)
if err != nil || f.SessionID != e.welcome.SessionID {
e.fail(ErrEventProtocol)
return
}
if f.Type == "event.deliver" {
var grant EventGrant
if decodeEventValue(f.Body, &grant) != nil || !validEventGrant(grant, e.welcome.SubscriptionID) {
e.fail(ErrEventProtocol)
return
}
select {
case e.deliveries <- grant:
default:
e.fail(ErrEventBackpressure)
return
}
continue
}
if f.Type != "ack" && f.Type != "error" {
e.fail(ErrEventProtocol)
return
}
if f.Type == "error" && f.ReplyTo == "" {
e.fail(f.Error)
return
}
e.mu.Lock()
waiter := e.pending[f.ReplyTo]
e.mu.Unlock()
if waiter == nil {
e.fail(ErrEventProtocol)
return
}
select {
case waiter <- f:
default:
e.fail(ErrEventProtocol)
return
}
if f.Error != nil && (f.Error.Code == "UNAUTHENTICATED" || f.Error.Code == "PERMISSION_DENIED" || f.Error.Code == "NOT_FOUND" || f.Error.Code == "UNAVAILABLE") {
e.fail(f.Error)
return
}
}
}
// Next 等待下一条交付。ctx到期只中断本次等待;未确认交付按服务端预算补发。
func (e *EventConnection) Next(ctx context.Context) (EventGrant, error) {
if err := ctx.Err(); err != nil {
return EventGrant{}, err
}
select {
case <-ctx.Done():
return EventGrant{}, ctx.Err()
case <-e.ctx.Done():
return EventGrant{}, e.Err()
case g := <-e.deliveries:
if err := e.Err(); err != nil {
return EventGrant{}, err
}
return g, nil
}
}
func (e *EventConnection) call(ctx context.Context, kind, id string, in, out any) error {
if !validID(id) {
return errors.New("invalid event message id")
}
if err := ctx.Err(); err != nil {
return err
}
bounded, stop := context.WithTimeout(ctx, 10*time.Second)
defer stop()
waiter := make(chan eventFrame, 1)
e.mu.Lock()
if e.err != nil {
err := e.err
e.mu.Unlock()
return err
}
if len(e.pending) >= eventMaxPending {
e.mu.Unlock()
return ErrEventBusy
}
if e.pending[id] != nil {
e.mu.Unlock()
return errors.New("event message id is already in flight")
}
e.pending[id] = waiter
e.mu.Unlock()
defer func() { e.mu.Lock(); delete(e.pending, id); e.mu.Unlock() }()
body, err := marshalWire(in)
if err != nil {
return errors.New("invalid event request")
}
raw, err := marshalWire(eventFrame{Version: "1", Type: kind, MessageID: id, SessionID: e.welcome.SessionID, Body: body})
if err != nil || len(raw) > 512<<10 {
return errors.New("event request exceeds frame limit")
}
if err = e.conn.Write(bounded, websocket.MessageText, raw); err != nil {
err = eventIOError(err)
e.fail(err)
return err
}
var f eventFrame
select {
case f = <-waiter:
case <-e.ctx.Done():
select {
case f = <-waiter:
default:
return e.Err()
}
case <-bounded.Done():
e.fail(bounded.Err())
return bounded.Err()
}
if f.Error != nil {
return f.Error
}
if decodeEventValue(f.Body, out) != nil {
e.fail(ErrEventProtocol)
return ErrEventProtocol
}
return nil
}
// Ack 仅在业务事务已提交或持久化去重命中后调用。重连可用原Proof重报,MessageID可新建。
// 超时/断线不证明ACK未提交;DELIVERY_LEASE_LOST表示证明已失效,不应撤销已经提交的业务。
func (e *EventConnection) Ack(ctx context.Context, id string, p EventProof) (EventAckResult, error) {
var out EventAckResult
if !validEventProof(p) || p.SubscriptionID != e.welcome.SubscriptionID {
return out, errors.New("invalid event proof or subscription")
}
in := struct {
DeliveryID string `json:"delivery_id"`
Epoch uint64 `json:"epoch,string"`
LeaseSecret string `json:"lease_secret"`
}{p.DeliveryID, p.Epoch, p.LeaseSecret}
err := e.call(ctx, "event.ack", id, in, &out)
if err == nil && (out.DeliveryID != p.DeliveryID || out.EventID != p.EventID || out.Epoch != p.Epoch || out.AcknowledgedAt.IsZero()) {
err = ErrEventProtocol
e.fail(err)
}
return out, err
}
// Ping 重新核验凭据及订阅,并维持连接;不会续期交付租约。
func (e *EventConnection) Ping(ctx context.Context, id string) (EventPong, error) {
var out EventPong
err := e.call(ctx, "events.ping", id, struct{}{}, &out)
if err == nil && out.ServerTime.IsZero() {
err = ErrEventProtocol
e.fail(err)
}
return out, err
}
func eventIOError(err error) error {
if errors.Is(err, context.Canceled) {
return context.Canceled
}
if errors.Is(err, context.DeadlineExceeded) {
return context.DeadlineExceeded
}
switch websocket.CloseStatus(err) {
case websocket.StatusProtocolError, websocket.StatusUnsupportedData, websocket.StatusInvalidFramePayloadData, websocket.StatusPolicyViolation, websocket.StatusMessageTooBig:
return ErrEventProtocol
}
return ErrEventDisconnected
}
func (e *EventConnection) String() string {
return fmt.Sprintf("EventConnection{SubscriptionID:%s SessionID:%s secrets:redacted}", e.welcome.SubscriptionID, e.welcome.SessionID)
}
func (e *EventConnection) GoString() string { return e.String() }