Files
orbit/sdk/go/event_consumer.go

369 lines
11 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"
"math/rand/v2"
"sync/atomic"
"time"
)
var ErrEventConsumerAlreadyRun = errors.New("event consumer may only Run once")
var ErrEventShutdownTimeout = errors.New("event consumer shutdown grace period expired")
// EventConsumerOptions 定义自动消费器。Handle必须以事件ID做持久化去重,
// 在去重记录和业务效果同一事务提交后才返回nil;提交结果未知时返回error。
// error或panic不ACK,平台在租约到期后按原预算补发;SDK不在内存中立即重跑。
type EventConsumerOptions struct {
SubscriptionID string
Capacity uint32 // Capacity 最大本地在途事件/处理器,默认4,范围1至32。
Handle func(context.Context, ResultEvent) error // Handle不接收租约秘密;必须响应context取消。
ShutdownTimeout time.Duration // ShutdownTimeout停止时取消处理器,等待返回及ACK,默认30秒,最多5分钟。
ReconnectMinDelay time.Duration // ReconnectMinDelay默认200毫秒,至少10毫秒。
ReconnectMaxDelay time.Duration // ReconnectMaxDelay默认5秒,最多30秒;指数退避并加入抖动。
}
// EventConsumerEvent 只用于本地观察,缓冲满可丢弃;不是可靠结果或审计。
type EventConsumerEvent struct {
Type string // Type为connected、reconnecting、handler_started、handler_failed、acknowledged、proof_lost、draining。
EventID string
Code string // Code不含业务异常、数据或秘密。
}
type EventConsumerStats struct {
Running int64
Started uint64
HandlerFailures uint64
Acknowledged uint64
Reconnects uint64
DroppedEvents uint64
}
type EventConsumerRunError struct {
Cause error
Unconfirmed int // Unconfirmed本进程已知且未确认的事件数,不是订阅全局积压。
Running int64 // Running仍未退出的处理器;Go不能强杀忽略context的函数。
}
func (e *EventConsumerRunError) Error() string {
return fmt.Sprintf("event consumer stopped with %d unconfirmed events and %d running handlers: %v", e.Unconfirmed, e.Running, e.Cause)
}
func (e *EventConsumerRunError) Unwrap() error { return e.Cause }
// EventConsumer 自动维护心跳、重连、处理器并发和ACK核对。只能Run一次。
// 服务端交付租约不是业务锁;跨进程重复及并行消费由业务Inbox唯一键和事务处理。
type EventConsumer struct {
client *Client
options EventConsumerOptions
used atomic.Bool
events chan EventConsumerEvent
running atomic.Int64
started, failures, acked, reconnects, dropped atomic.Uint64
}
func NewEventConsumer(client *Client, in EventConsumerOptions) (*EventConsumer, error) {
if client == nil || client.token == "" || !validID(in.SubscriptionID) || in.Handle == nil {
return nil, errors.New("authenticated client, subscription and durable event handler are required")
}
if in.Capacity == 0 {
in.Capacity = 4
}
if in.ShutdownTimeout == 0 {
in.ShutdownTimeout = 30 * time.Second
}
if in.ReconnectMinDelay == 0 {
in.ReconnectMinDelay = 200 * time.Millisecond
}
if in.ReconnectMaxDelay == 0 {
in.ReconnectMaxDelay = 5 * time.Second
}
if in.Capacity > 32 || in.ShutdownTimeout < time.Millisecond || in.ShutdownTimeout > 5*time.Minute || in.ReconnectMinDelay < 10*time.Millisecond || in.ReconnectMaxDelay < in.ReconnectMinDelay || in.ReconnectMaxDelay > 30*time.Second {
return nil, errors.New("invalid event consumer capacity or durations")
}
return &EventConsumer{client: client, options: in, events: make(chan EventConsumerEvent, 128)}, nil
}
func (c *EventConsumer) Events() <-chan EventConsumerEvent { return c.events }
func (c *EventConsumer) Stats() EventConsumerStats {
return EventConsumerStats{Running: c.running.Load(), Started: c.started.Load(), HandlerFailures: c.failures.Load(), Acknowledged: c.acked.Load(), Reconnects: c.reconnects.Load(), DroppedEvents: c.dropped.Load()}
}
func (c *EventConsumer) emit(typ, id, code string) {
select {
case c.events <- EventConsumerEvent{Type: typ, EventID: id, Code: code}:
default:
c.dropped.Add(1)
}
}
func (c *EventConsumer) String() string {
return fmt.Sprintf("EventConsumer{SubscriptionID:%s Capacity:%d secrets:redacted}", c.options.SubscriptionID, c.options.Capacity)
}
func (c *EventConsumer) GoString() string { return c.String() }
type consumerJob struct {
proof EventProof
complete bool
}
type consumerResult struct {
id string
failed bool
}
// Run在ctx取消后停止启动新处理器,取消在途处理器,在收尾时限内确认已提交结果。
// 收到认证/协议等永久错误时也取消并等待在途处理器;未确认结果仍保留在服务端。
// 重连先核对已完成处理器的原证明;证明被替代时等待新投递,由业务去重命中后确认。
func (c *EventConsumer) Run(parent context.Context) error {
if !c.used.CompareAndSwap(false, true) {
return ErrEventConsumerAlreadyRun
}
defer close(c.events)
if parent.Err() != nil {
return nil
}
ctx, cancel := context.WithCancel(context.WithoutCancel(parent))
defer cancel()
handlerCtx, stopHandlers := context.WithCancel(ctx)
defer stopHandlers()
abort := make(chan struct{})
watchDone := make(chan struct{})
go func() {
defer close(watchDone)
select {
case <-ctx.Done():
return
case <-parent.Done():
case <-abort:
}
stopHandlers()
timer := time.NewTimer(c.options.ShutdownTimeout)
defer timer.Stop()
select {
case <-ctx.Done():
case <-timer.C:
cancel()
}
}()
defer func() { cancel(); <-watchDone }()
var conn *EventConnection
var heartbeatDone <-chan struct{}
closeConnection := func() {
if conn != nil {
_ = conn.Close()
<-heartbeatDone
conn = nil
}
}
defer closeConnection()
jobs := make(map[string]*consumerJob)
results := make(chan consumerResult, c.options.Capacity)
draining := false
var cause error
beginDrain := func(err error) {
if !draining {
draining = true
stopHandlers()
close(abort)
c.emit("draining", "", "")
}
if cause == nil {
cause = err
}
}
finish := func(err error) error {
if err == nil {
return nil
}
return &EventConsumerRunError{Cause: err, Unconfirmed: len(jobs), Running: c.running.Load()}
}
backoff := c.options.ReconnectMinDelay
var dialAt time.Time
everConnected := false
disconnect := func(err error) {
closeConnection()
if !eventRetryable(err) {
beginDrain(err)
return
}
floor := backoff / 2
dialAt = time.Now().Add(floor + time.Duration(rand.Int64N(int64(backoff-floor)+1)))
backoff = min(backoff*2, c.options.ReconnectMaxDelay)
c.emit("reconnecting", "", eventErrorCode(err))
}
for {
if parent.Err() != nil {
beginDrain(nil)
}
if ctx.Err() != nil {
return finish(ErrEventShutdownTimeout)
}
if draining && (len(jobs) == 0 || cause != nil && c.running.Load() == 0) {
return finish(cause)
}
if conn == nil && cause == nil && !time.Now().Before(dialAt) {
if everConnected {
c.reconnects.Add(1)
}
var err error
conn, err = c.client.DialEvents(ctx, EventRegistration{SubscriptionID: c.options.SubscriptionID, Capacity: c.options.Capacity})
if err != nil {
disconnect(err)
continue
}
everConnected = true
heartbeatDone = startEventHeartbeat(ctx, conn)
c.emit("connected", "", "")
continue
}
if conn != nil {
if err := conn.Err(); err != nil {
disconnect(err)
continue
}
// 一个ACK在途;读取与自动心跳独立,不需要接收方不断调用Next维持连接。
acknowledged := false
for id, j := range jobs {
if !j.complete {
continue
}
messageID, err := NewEventMessageID()
if err == nil {
_, err = conn.Ack(ctx, messageID, j.proof)
}
if err == nil {
delete(jobs, id)
c.acked.Add(1)
c.emit("acknowledged", id, "")
backoff = c.options.ReconnectMinDelay
} else {
var remote *EventError
if errors.As(err, &remote) && remote.Code == "DELIVERY_LEASE_LOST" {
delete(jobs, id)
c.emit("proof_lost", id, remote.Code)
} else {
disconnect(err)
}
}
acknowledged = true
break
}
if acknowledged {
continue
}
}
var deliveries <-chan EventGrant
var disconnected <-chan struct{}
if conn != nil {
disconnected = conn.Done()
if !draining && len(jobs) < int(c.options.Capacity) {
deliveries = conn.deliveries
}
}
var parentDone <-chan struct{}
if !draining {
parentDone = parent.Done()
}
// 定时器只负责重连;活跃连接没有每轮轮询。
var wake <-chan time.Time
var timer *time.Timer
if conn == nil && cause == nil {
timer = time.NewTimer(max(time.Millisecond, time.Until(dialAt)))
wake = timer.C
}
select {
case <-ctx.Done():
case <-parentDone:
beginDrain(nil)
case <-wake:
case <-disconnected:
disconnect(conn.Err())
case result := <-results:
j := jobs[result.id]
if result.failed {
delete(jobs, result.id)
c.failures.Add(1)
c.emit("handler_failed", result.id, "HANDLER_FAILED")
} else {
j.complete = true
}
case grant := <-deliveries:
if parent.Err() != nil || conn.Err() != nil {
break
}
id := grant.Event.ID
if j := jobs[id]; j != nil {
if j.proof.DeliveryID != grant.Delivery.ID {
disconnect(ErrEventProtocol)
break
}
if grant.Delivery.LeaseEpoch > j.proof.Epoch {
j.proof = grant.Proof()
}
break
}
jobs[id] = &consumerJob{proof: grant.Proof()}
c.running.Add(1)
c.started.Add(1)
c.emit("handler_started", id, "")
go func(event ResultEvent) {
failed := true
defer func() {
_ = recover()
c.running.Add(-1)
select {
case results <- consumerResult{id: event.ID, failed: failed}:
case <-ctx.Done():
}
}()
failed = c.options.Handle(handlerCtx, event) != nil
}(grant.Event)
}
if timer != nil {
timer.Stop()
}
}
}
func startEventHeartbeat(ctx context.Context, conn *EventConnection) <-chan struct{} {
done := make(chan struct{})
go func() {
defer close(done)
ticker := time.NewTicker(time.Duration(conn.Welcome().HeartbeatIntervalMS) * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-conn.Done():
return
case <-ticker.C:
id, err := NewEventMessageID()
if err == nil {
_, err = conn.Ping(ctx, id)
}
if err != nil {
conn.fail(err)
return
}
}
}
}()
return done
}
func eventRetryable(err error) bool {
if errors.Is(err, ErrEventDisconnected) || errors.Is(err, ErrEventBackpressure) || errors.Is(err, context.DeadlineExceeded) {
return true
}
var remote *EventError
if errors.As(err, &remote) {
return remote.Code == "UNAVAILABLE"
}
var httpErr *HTTPError
if errors.As(err, &httpErr) {
return httpErr.StatusCode == 408 || httpErr.StatusCode == 429 || httpErr.StatusCode >= 500 && httpErr.StatusCode <= 599
}
return false
}
func eventErrorCode(err error) string {
var remote *EventError
if errors.As(err, &remote) {
return remote.Code
}
return "TRANSPORT_INTERRUPTED"
}