服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
915 lines
22 KiB
Go
915 lines
22 KiB
Go
package eventloop
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"log/slog"
|
||
"runtime/debug"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
var (
|
||
// ErrLoopRequired 表示事件循环实例为空。
|
||
ErrLoopRequired = errors.New("eventloop: loop is required")
|
||
// ErrNoProcessor 表示事件循环缺少处理器。
|
||
ErrNoProcessor = errors.New("eventloop: processor is required")
|
||
// ErrNoSliceStep 表示时间片执行缺少 step 回调。
|
||
ErrNoSliceStep = errors.New("eventloop: slice step is required")
|
||
// ErrInvalidSliceRun 表示时间片执行缺少最大处理数量或截止时间。
|
||
ErrInvalidSliceRun = errors.New("eventloop: slice run requires max items or deadline")
|
||
// ErrNotStarted 表示事件循环尚未启动。
|
||
ErrNotStarted = errors.New("eventloop: loop is not started")
|
||
// ErrClosed 表示事件循环已经关闭。
|
||
ErrClosed = errors.New("eventloop: loop is closed")
|
||
// ErrDraining 表示事件循环正在排空队列。
|
||
ErrDraining = errors.New("eventloop: loop is draining")
|
||
// ErrQueueFull 表示目标优先级队列已满。
|
||
ErrQueueFull = errors.New("eventloop: queue is full")
|
||
// ErrUnknownPriority 表示事件优先级不在框架支持范围内。
|
||
ErrUnknownPriority = errors.New("eventloop: unknown priority")
|
||
)
|
||
|
||
const (
|
||
// DefaultFrameInterval 是帧驱动事件循环的默认帧间隔。
|
||
DefaultFrameInterval = 50 * time.Millisecond
|
||
// DefaultFrameBudget 是单帧可用于处理事件的默认预算。
|
||
DefaultFrameBudget = 25 * time.Millisecond
|
||
// DefaultMaxLowTime 是单帧处理低优先级事件的默认时间上限。
|
||
DefaultMaxLowTime = 2 * time.Millisecond
|
||
// DefaultSlowThreshold 是慢事件告警的默认阈值。
|
||
DefaultSlowThreshold = time.Millisecond
|
||
)
|
||
|
||
// Priority 描述事件队列优先级。
|
||
type Priority int
|
||
|
||
const (
|
||
// PriorityMedium 是默认优先级,适合普通业务事件。
|
||
PriorityMedium Priority = iota
|
||
// PriorityHigh 适合登录、支付回调等需要优先处理的事件。
|
||
PriorityHigh
|
||
// PriorityLow 适合可延后处理的后台事件。
|
||
PriorityLow
|
||
)
|
||
|
||
// String 返回优先级的稳定标签。
|
||
func (p Priority) String() string {
|
||
switch p {
|
||
case PriorityHigh:
|
||
return "high"
|
||
case PriorityMedium:
|
||
return "medium"
|
||
case PriorityLow:
|
||
return "low"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// Result 表示事件处理器返回的数据或错误。
|
||
type Result struct {
|
||
Data any
|
||
Err error
|
||
}
|
||
|
||
// Event 是提交到事件循环中的最小调度单元。
|
||
type Event struct {
|
||
Type string
|
||
Data any
|
||
Metadata map[string]string
|
||
Priority Priority
|
||
Callback chan Result
|
||
Context context.Context
|
||
SentAt time.Time
|
||
}
|
||
|
||
// Processor 是事件循环消费事件时调用的处理器接口。
|
||
type Processor interface {
|
||
Process(context.Context, Event) Result
|
||
}
|
||
|
||
// ProcessorFunc 允许普通函数直接作为事件处理器使用。
|
||
type ProcessorFunc func(context.Context, Event) Result
|
||
|
||
// Process 调用底层函数处理事件。
|
||
func (f ProcessorFunc) Process(ctx context.Context, event Event) Result {
|
||
return f(ctx, event)
|
||
}
|
||
|
||
// Options 配置事件循环的队列容量、帧预算、回调和日志行为。
|
||
type Options struct {
|
||
QueueSize int
|
||
// HighQueueSize / LowQueueSize 允许为高/低优先级单独指定队列容量,0 表示沿用
|
||
// QueueSize(也就是保持原来的行为)。
|
||
//
|
||
// 加这两个字段的原因:每条循环都会按 QueueSize 预分配三条优先级通道,而
|
||
// dnfbridge 的全部事件提交点(game_session_events.go 的 postGameSessionEvent /
|
||
// callGameSession / postGameSessionCharacterEvent)都只用 PriorityMedium ——
|
||
// 高/低两条 512 深的通道从创建到销毁一次都不会被写入。按 amd64 上
|
||
// Event 约 96 字节计算,那是 2 × 512 × 96 ≈ 96 KiB/会话 的纯浪费,
|
||
// 1000 会话即约 96 MB。
|
||
//
|
||
// 缩小后仍然保留容量(不是 0 容量的无缓冲通道),所以即便将来真的有代码
|
||
// 往这两个优先级投递,也只是更早触发 SubmitBlocking 的 1ms 重试退避,
|
||
// 不会死锁或丢事件;那时把对应值调回 QueueSize 即可。
|
||
HighQueueSize int
|
||
LowQueueSize int
|
||
CallbackQueueSize int
|
||
CallbackTimeout time.Duration
|
||
FrameDriven bool
|
||
FrameInterval time.Duration
|
||
FrameBudget time.Duration
|
||
MaxLowTime time.Duration
|
||
HighBurst int
|
||
MediumBurst int
|
||
SlowThreshold time.Duration
|
||
Logger *slog.Logger
|
||
}
|
||
|
||
func (o Options) normalized() Options {
|
||
if o.QueueSize <= 0 {
|
||
o.QueueSize = 1024
|
||
}
|
||
if o.HighQueueSize <= 0 {
|
||
o.HighQueueSize = o.QueueSize
|
||
}
|
||
if o.LowQueueSize <= 0 {
|
||
o.LowQueueSize = o.QueueSize
|
||
}
|
||
if o.CallbackQueueSize <= 0 {
|
||
o.CallbackQueueSize = 1024
|
||
}
|
||
if o.CallbackTimeout <= 0 {
|
||
o.CallbackTimeout = 500 * time.Millisecond
|
||
}
|
||
if o.FrameInterval <= 0 {
|
||
o.FrameInterval = DefaultFrameInterval
|
||
}
|
||
if o.FrameBudget <= 0 {
|
||
o.FrameBudget = o.FrameInterval / 2
|
||
if o.FrameBudget <= 0 {
|
||
o.FrameBudget = DefaultFrameBudget
|
||
}
|
||
}
|
||
if o.MaxLowTime <= 0 {
|
||
o.MaxLowTime = DefaultMaxLowTime
|
||
if o.MaxLowTime > o.FrameBudget {
|
||
o.MaxLowTime = o.FrameBudget / 4
|
||
if o.MaxLowTime <= 0 {
|
||
o.MaxLowTime = o.FrameBudget
|
||
}
|
||
}
|
||
}
|
||
if o.HighBurst <= 0 {
|
||
o.HighBurst = 64
|
||
}
|
||
if o.MediumBurst <= 0 {
|
||
o.MediumBurst = 32
|
||
}
|
||
if o.SlowThreshold <= 0 {
|
||
o.SlowThreshold = DefaultSlowThreshold
|
||
}
|
||
return o
|
||
}
|
||
|
||
// QueueLengths 描述各优先级队列和回调队列的当前长度。
|
||
type QueueLengths struct {
|
||
High int
|
||
Medium int
|
||
Low int
|
||
Callback int
|
||
}
|
||
|
||
// MetricsSnapshot 是事件循环内部计数器的瞬时快照。
|
||
type MetricsSnapshot struct {
|
||
Submitted map[Priority]uint64
|
||
Dropped map[Priority]uint64
|
||
Processed map[Priority]uint64
|
||
Failed map[Priority]uint64
|
||
Panicked map[Priority]uint64
|
||
Slow map[Priority]uint64
|
||
QueueWaitNanos map[Priority]uint64
|
||
ProcessingNanos map[Priority]uint64
|
||
CallbackDropped uint64
|
||
}
|
||
|
||
type metricSet struct {
|
||
submitted [3]atomic.Uint64
|
||
dropped [3]atomic.Uint64
|
||
processed [3]atomic.Uint64
|
||
failed [3]atomic.Uint64
|
||
panicked [3]atomic.Uint64
|
||
slow [3]atomic.Uint64
|
||
queueWaitNanos [3]atomic.Uint64
|
||
processingNanos [3]atomic.Uint64
|
||
callbackDropped atomic.Uint64
|
||
}
|
||
|
||
type callbackItem struct {
|
||
ctx context.Context
|
||
callback chan Result
|
||
result Result
|
||
}
|
||
|
||
type Loop struct {
|
||
name string
|
||
processor Processor
|
||
options Options
|
||
logger *slog.Logger
|
||
|
||
high chan Event
|
||
medium chan Event
|
||
low chan Event
|
||
callbacks chan callbackItem
|
||
wake chan struct{}
|
||
|
||
submitMu sync.Mutex
|
||
mu sync.Mutex
|
||
ctx context.Context
|
||
cancel context.CancelFunc
|
||
done chan struct{}
|
||
started bool
|
||
closed bool
|
||
wg sync.WaitGroup
|
||
running atomic.Bool
|
||
accepting atomic.Bool
|
||
draining atomic.Bool
|
||
metrics metricSet
|
||
// slowEvents / worstSlowNanos / worstSlowType back SlowEventStats, the
|
||
// server-side answer to "did this session freeze" (see that method).
|
||
slowEvents atomic.Int64
|
||
worstSlowNanos atomic.Int64
|
||
worstSlowType atomic.Value
|
||
}
|
||
|
||
// New 创建一个事件循环实例,调用 Start 后才会开始消费事件。
|
||
func New(name string, processor Processor, options Options) *Loop {
|
||
options = options.normalized()
|
||
if name == "" {
|
||
name = "eventloop"
|
||
}
|
||
return &Loop{
|
||
name: name,
|
||
processor: processor,
|
||
options: options,
|
||
logger: options.Logger,
|
||
high: make(chan Event, options.HighQueueSize),
|
||
medium: make(chan Event, options.QueueSize),
|
||
low: make(chan Event, options.LowQueueSize),
|
||
callbacks: make(chan callbackItem, options.CallbackQueueSize),
|
||
wake: make(chan struct{}, 1),
|
||
done: make(chan struct{}),
|
||
}
|
||
}
|
||
|
||
// Start 启动事件循环和回调派发协程。
|
||
func (l *Loop) Start(ctx context.Context) error {
|
||
if l == nil {
|
||
return ErrLoopRequired
|
||
}
|
||
if l.processor == nil {
|
||
return ErrNoProcessor
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
l.mu.Lock()
|
||
defer l.mu.Unlock()
|
||
if l.closed {
|
||
return ErrClosed
|
||
}
|
||
if l.started {
|
||
return nil
|
||
}
|
||
l.ctx, l.cancel = context.WithCancel(ctx)
|
||
l.done = make(chan struct{})
|
||
l.wake = make(chan struct{}, 1)
|
||
l.started = true
|
||
l.running.Store(true)
|
||
l.accepting.Store(true)
|
||
l.draining.Store(false)
|
||
|
||
l.wg.Add(2)
|
||
if l.options.FrameDriven {
|
||
// 帧驱动模式按固定 tick 频率消费事件。
|
||
go l.runFrameDriven()
|
||
} else {
|
||
// 优先级驱动模式用于控制面和异步任务。
|
||
go l.runPriorityDriven()
|
||
}
|
||
go l.dispatchCallbacks()
|
||
go func() {
|
||
l.wg.Wait()
|
||
l.running.Store(false)
|
||
l.accepting.Store(false)
|
||
l.mu.Lock()
|
||
l.closed = true
|
||
l.mu.Unlock()
|
||
close(l.done)
|
||
}()
|
||
return nil
|
||
}
|
||
|
||
// Stop 立即停止事件循环,并拒绝仍在队列中的事件。
|
||
func (l *Loop) Stop(ctx context.Context) error {
|
||
if l == nil {
|
||
return ErrLoopRequired
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
l.mu.Lock()
|
||
if !l.started {
|
||
l.mu.Unlock()
|
||
return ErrNotStarted
|
||
}
|
||
done := l.done
|
||
l.mu.Unlock()
|
||
l.submitMu.Lock()
|
||
l.mu.Lock()
|
||
cancel := l.cancel
|
||
l.closed = true
|
||
l.mu.Unlock()
|
||
// Stop 会立即停止;如果队列事件必须执行完,应使用 Drain。
|
||
l.accepting.Store(false)
|
||
l.draining.Store(false)
|
||
l.rejectQueued(ErrClosed)
|
||
if cancel != nil {
|
||
cancel()
|
||
}
|
||
l.submitMu.Unlock()
|
||
select {
|
||
case <-done:
|
||
l.drainCallbacks()
|
||
return nil
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
}
|
||
}
|
||
|
||
// Drain 停止接收新事件,并等待已排队事件处理完成。
|
||
func (l *Loop) Drain(ctx context.Context) error {
|
||
if l == nil {
|
||
return ErrLoopRequired
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
l.mu.Lock()
|
||
if !l.started {
|
||
l.mu.Unlock()
|
||
return ErrNotStarted
|
||
}
|
||
done := l.done
|
||
l.mu.Unlock()
|
||
l.submitMu.Lock()
|
||
l.accepting.Store(false)
|
||
l.draining.Store(true)
|
||
l.submitMu.Unlock()
|
||
l.wakeLoop()
|
||
|
||
// Drain 停止接收新事件,并处理完已排队事件。
|
||
select {
|
||
case <-done:
|
||
return nil
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
}
|
||
}
|
||
|
||
// Submit 快速提交事件;队列满时立即返回 ErrQueueFull。
|
||
func (l *Loop) Submit(event Event) error {
|
||
if l == nil {
|
||
return ErrLoopRequired
|
||
}
|
||
l.submitMu.Lock()
|
||
defer l.submitMu.Unlock()
|
||
if err := l.prepareEvent(&event); err != nil {
|
||
return err
|
||
}
|
||
queue := l.queueFor(event.Priority)
|
||
select {
|
||
case queue <- event:
|
||
l.metrics.submitted[event.Priority].Add(1)
|
||
return nil
|
||
default:
|
||
// Submit 是快速失败入口;调用方自行选择丢弃、降级或改用 SubmitBlocking。
|
||
l.metrics.dropped[event.Priority].Add(1)
|
||
return ErrQueueFull
|
||
}
|
||
}
|
||
|
||
// SubmitBlocking 等待队列容量,直到提交成功、ctx 取消或事件循环关闭。
|
||
func (l *Loop) SubmitBlocking(ctx context.Context, event Event) error {
|
||
if l == nil {
|
||
return ErrLoopRequired
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
retryTimer := time.NewTimer(time.Millisecond)
|
||
if !retryTimer.Stop() {
|
||
<-retryTimer.C
|
||
}
|
||
defer retryTimer.Stop()
|
||
// SubmitBlocking 会等待队列容量、ctx 取消或 loop 关闭。
|
||
for {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
l.submitMu.Lock()
|
||
if err := l.prepareEvent(&event); err != nil {
|
||
l.submitMu.Unlock()
|
||
return err
|
||
}
|
||
queue := l.queueFor(event.Priority)
|
||
select {
|
||
case queue <- event:
|
||
l.metrics.submitted[event.Priority].Add(1)
|
||
l.submitMu.Unlock()
|
||
return nil
|
||
default:
|
||
l.submitMu.Unlock()
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
if validPriority(event.Priority) {
|
||
l.metrics.dropped[event.Priority].Add(1)
|
||
}
|
||
return ctx.Err()
|
||
case <-l.contextDone():
|
||
if validPriority(event.Priority) {
|
||
l.metrics.dropped[event.Priority].Add(1)
|
||
}
|
||
return ErrClosed
|
||
case <-resetTimer(retryTimer, time.Millisecond):
|
||
}
|
||
}
|
||
}
|
||
|
||
// QueueLengths 返回当前队列长度。
|
||
func (l *Loop) QueueLengths() QueueLengths {
|
||
if l == nil {
|
||
return QueueLengths{}
|
||
}
|
||
return QueueLengths{
|
||
High: len(l.high),
|
||
Medium: len(l.medium),
|
||
Low: len(l.low),
|
||
Callback: len(l.callbacks),
|
||
}
|
||
}
|
||
|
||
// MetricsSnapshot 返回事件循环累计指标快照。
|
||
func (l *Loop) MetricsSnapshot() MetricsSnapshot {
|
||
if l == nil {
|
||
return MetricsSnapshot{}
|
||
}
|
||
return MetricsSnapshot{
|
||
Submitted: l.loadPriorityCounters(&l.metrics.submitted),
|
||
Dropped: l.loadPriorityCounters(&l.metrics.dropped),
|
||
Processed: l.loadPriorityCounters(&l.metrics.processed),
|
||
Failed: l.loadPriorityCounters(&l.metrics.failed),
|
||
Panicked: l.loadPriorityCounters(&l.metrics.panicked),
|
||
Slow: l.loadPriorityCounters(&l.metrics.slow),
|
||
QueueWaitNanos: l.loadPriorityCounters(&l.metrics.queueWaitNanos),
|
||
ProcessingNanos: l.loadPriorityCounters(&l.metrics.processingNanos),
|
||
CallbackDropped: l.metrics.callbackDropped.Load(),
|
||
}
|
||
}
|
||
|
||
func (l *Loop) prepareEvent(event *Event) error {
|
||
if l.isClosed() {
|
||
return ErrClosed
|
||
}
|
||
if !l.running.Load() {
|
||
return ErrNotStarted
|
||
}
|
||
if l.draining.Load() {
|
||
return ErrDraining
|
||
}
|
||
if !l.accepting.Load() {
|
||
return ErrClosed
|
||
}
|
||
if !validPriority(event.Priority) {
|
||
return ErrUnknownPriority
|
||
}
|
||
if event.SentAt.IsZero() {
|
||
event.SentAt = time.Now()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (l *Loop) queueFor(priority Priority) chan Event {
|
||
switch priority {
|
||
case PriorityHigh:
|
||
return l.high
|
||
case PriorityMedium:
|
||
return l.medium
|
||
default:
|
||
return l.low
|
||
}
|
||
}
|
||
|
||
func (l *Loop) contextDone() <-chan struct{} {
|
||
l.mu.Lock()
|
||
ctx := l.ctx
|
||
l.mu.Unlock()
|
||
if ctx == nil {
|
||
closed := make(chan struct{})
|
||
close(closed)
|
||
return closed
|
||
}
|
||
return ctx.Done()
|
||
}
|
||
|
||
func (l *Loop) runPriorityDriven() {
|
||
defer l.wg.Done()
|
||
for {
|
||
if l.shouldStopAfterDrain() {
|
||
return
|
||
}
|
||
highProcessed := l.processBurst(l.high, l.options.HighBurst)
|
||
mediumProcessed := l.processBurst(l.medium, l.options.MediumBurst)
|
||
if event, ok := l.tryReceive(l.low); ok {
|
||
l.processEvent(event)
|
||
continue
|
||
}
|
||
if highProcessed || mediumProcessed {
|
||
continue
|
||
}
|
||
select {
|
||
case <-l.ctx.Done():
|
||
return
|
||
case <-l.wake:
|
||
continue
|
||
case event := <-l.high:
|
||
l.processEvent(event)
|
||
case event := <-l.medium:
|
||
_ = l.processBurst(l.high, l.options.HighBurst)
|
||
l.processEvent(event)
|
||
case event := <-l.low:
|
||
_ = l.processBurst(l.high, l.options.HighBurst)
|
||
_ = l.processBurst(l.medium, l.options.MediumBurst)
|
||
l.processEvent(event)
|
||
}
|
||
}
|
||
}
|
||
func (l *Loop) runFrameDriven() {
|
||
defer l.wg.Done()
|
||
ticker := time.NewTicker(l.options.FrameInterval)
|
||
defer ticker.Stop()
|
||
for {
|
||
if l.shouldStopAfterDrain() {
|
||
return
|
||
}
|
||
select {
|
||
case <-l.ctx.Done():
|
||
return
|
||
case <-l.wake:
|
||
continue
|
||
case <-ticker.C:
|
||
l.processFrame()
|
||
}
|
||
}
|
||
}
|
||
|
||
func (l *Loop) processFrame() {
|
||
frameStart := time.Now()
|
||
frameDeadline := frameStart.Add(l.options.FrameBudget)
|
||
lowDeadline := frameStart.Add(l.options.MaxLowTime)
|
||
// 每一帧只在配置的预算内消费事件。
|
||
for time.Now().Before(frameDeadline) {
|
||
if event, ok := l.tryReceive(l.high); ok {
|
||
l.processEvent(event)
|
||
continue
|
||
}
|
||
if event, ok := l.tryReceive(l.medium); ok {
|
||
l.processEvent(event)
|
||
continue
|
||
}
|
||
if time.Now().Before(lowDeadline) {
|
||
if event, ok := l.tryReceive(l.low); ok {
|
||
l.processEvent(event)
|
||
continue
|
||
}
|
||
}
|
||
return
|
||
}
|
||
}
|
||
|
||
func (l *Loop) processBurst(queue chan Event, limit int) bool {
|
||
processed := false
|
||
for i := 0; i < limit; i++ {
|
||
event, ok := l.tryReceive(queue)
|
||
if !ok {
|
||
return processed
|
||
}
|
||
processed = true
|
||
l.processEvent(event)
|
||
}
|
||
return processed
|
||
}
|
||
|
||
func (l *Loop) tryReceive(queue chan Event) (Event, bool) {
|
||
select {
|
||
case event := <-queue:
|
||
return event, true
|
||
default:
|
||
return Event{}, false
|
||
}
|
||
}
|
||
|
||
func (l *Loop) processEvent(event Event) {
|
||
ctx := event.Context
|
||
if ctx == nil {
|
||
ctx = l.ctx
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
l.recordFailure(event.Priority)
|
||
l.deliverCallback(event, Result{Err: err})
|
||
return
|
||
}
|
||
|
||
start := time.Now()
|
||
if !event.SentAt.IsZero() {
|
||
if wait := start.Sub(event.SentAt); wait > 0 {
|
||
l.metrics.queueWaitNanos[event.Priority].Add(uint64(wait))
|
||
}
|
||
}
|
||
result := Result{}
|
||
panicked := false
|
||
func() {
|
||
defer func() {
|
||
if recovered := recover(); recovered != nil {
|
||
panicked = true
|
||
result.Err = fmt.Errorf("eventloop: processor panic: %v", recovered)
|
||
l.logWarn("event processor panic recovered", slog.Any("panic", recovered), slog.String("stack", string(debug.Stack())))
|
||
}
|
||
}()
|
||
result = l.processor.Process(ctx, event)
|
||
}()
|
||
duration := time.Since(start)
|
||
l.recordProcessed(event.Type, event.Priority, duration, result.Err, panicked)
|
||
l.deliverCallback(event, result)
|
||
}
|
||
|
||
func (l *Loop) deliverCallback(event Event, result Result) {
|
||
if event.Callback == nil {
|
||
return
|
||
}
|
||
if l.isClosed() {
|
||
l.deliverCallbackNow(event, result)
|
||
return
|
||
}
|
||
ctx := event.Context
|
||
if ctx == nil {
|
||
ctx = l.ctx
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
item := callbackItem{ctx: ctx, callback: event.Callback, result: result}
|
||
select {
|
||
case l.callbacks <- item:
|
||
default:
|
||
l.metrics.callbackDropped.Add(1)
|
||
l.logWarn("event callback queue is full; callback dropped", slog.String("event", event.Type))
|
||
}
|
||
}
|
||
|
||
func (l *Loop) dispatchCallbacks() {
|
||
defer l.wg.Done()
|
||
for {
|
||
select {
|
||
case <-l.ctx.Done():
|
||
l.drainCallbacks()
|
||
return
|
||
case item := <-l.callbacks:
|
||
l.sendCallback(item)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (l *Loop) drainCallbacks() {
|
||
for {
|
||
select {
|
||
case item := <-l.callbacks:
|
||
l.deliverItemNow(item)
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
func (l *Loop) deliverItemNow(item callbackItem) {
|
||
select {
|
||
case item.callback <- item.result:
|
||
default:
|
||
l.metrics.callbackDropped.Add(1)
|
||
}
|
||
}
|
||
|
||
func (l *Loop) sendCallback(item callbackItem) {
|
||
ctx := item.ctx
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
if l.options.CallbackTimeout > 0 {
|
||
var cancel context.CancelFunc
|
||
ctx, cancel = context.WithTimeout(ctx, l.options.CallbackTimeout)
|
||
defer cancel()
|
||
}
|
||
select {
|
||
case item.callback <- item.result:
|
||
return
|
||
default:
|
||
}
|
||
select {
|
||
case item.callback <- item.result:
|
||
case <-ctx.Done():
|
||
l.metrics.callbackDropped.Add(1)
|
||
}
|
||
}
|
||
|
||
func (l *Loop) shouldStopAfterDrain() bool {
|
||
if !l.draining.Load() {
|
||
return false
|
||
}
|
||
if len(l.high) > 0 || len(l.medium) > 0 || len(l.low) > 0 {
|
||
return false
|
||
}
|
||
if len(l.callbacks) > 0 {
|
||
time.Sleep(time.Millisecond)
|
||
return false
|
||
}
|
||
// 队列清空后再 cancel,让 run loop 自然退出。
|
||
if l.cancel != nil {
|
||
l.cancel()
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (l *Loop) wakeLoop() {
|
||
if l == nil || l.wake == nil {
|
||
return
|
||
}
|
||
select {
|
||
case l.wake <- struct{}{}:
|
||
default:
|
||
}
|
||
}
|
||
|
||
func (l *Loop) rejectQueued(err error) {
|
||
reject := func(queue chan Event) {
|
||
for {
|
||
select {
|
||
case event := <-queue:
|
||
l.deliverCallbackNow(event, Result{Err: err})
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
reject(l.high)
|
||
reject(l.medium)
|
||
reject(l.low)
|
||
}
|
||
|
||
func (l *Loop) deliverCallbackNow(event Event, result Result) {
|
||
if event.Callback == nil {
|
||
return
|
||
}
|
||
select {
|
||
case event.Callback <- result:
|
||
default:
|
||
l.metrics.callbackDropped.Add(1)
|
||
}
|
||
}
|
||
|
||
// recordProcessed 记录一次事件处理的耗时。
|
||
//
|
||
// eventType 只用于慢事件告警:2026-09-24 23:24 那次 8 人集体掉线里,日志只有
|
||
// "哪个 loop 卡了多久",没有"当时在跑什么",导致定位只能靠推测。把事件类型带上,
|
||
// 下一次复现就能直接读出是哪条路径卡住。
|
||
func (l *Loop) recordProcessed(eventType string, priority Priority, duration time.Duration, err error, panicked bool) {
|
||
if !validPriority(priority) {
|
||
return
|
||
}
|
||
l.metrics.processed[priority].Add(1)
|
||
if duration > 0 {
|
||
l.metrics.processingNanos[priority].Add(uint64(duration)) //nolint:gosec // G115:duration 已确认非负,纳秒值按 uint64 累加。
|
||
}
|
||
if err != nil {
|
||
l.metrics.failed[priority].Add(1)
|
||
}
|
||
if panicked {
|
||
l.metrics.panicked[priority].Add(1)
|
||
}
|
||
if l.options.SlowThreshold > 0 && duration >= l.options.SlowThreshold {
|
||
l.metrics.slow[priority].Add(1)
|
||
l.noteSlowEvent(eventType, duration)
|
||
l.logWarn("event processing exceeded slow threshold",
|
||
slog.String("event_type", eventType),
|
||
slog.String("priority", priority.String()),
|
||
slog.Duration("duration", duration))
|
||
}
|
||
}
|
||
|
||
// noteSlowEvent keeps the running worst case and count of slow events for
|
||
// SlowEventStats. Cheap enough to sit on the warning path: two atomic stores.
|
||
func (l *Loop) noteSlowEvent(eventType string, duration time.Duration) {
|
||
l.slowEvents.Add(1)
|
||
nanos := duration.Nanoseconds()
|
||
for {
|
||
worst := l.worstSlowNanos.Load()
|
||
if nanos <= worst {
|
||
break
|
||
}
|
||
if l.worstSlowNanos.CompareAndSwap(worst, nanos) {
|
||
l.worstSlowType.Store(eventType)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// SlowEventStats reports how many events exceeded this loop's slow threshold, the
|
||
// worst one, and the event type behind that worst one.
|
||
//
|
||
// It exists for the server-side raid cost summary: the operator cannot collect
|
||
// client logs from a raid of more than eight players, so "did anyone freeze" has
|
||
// to be answerable from the server alone — and this is the server's own
|
||
// measurement of that (2026-09-29: the eight-member phase-activation stall showed
|
||
// up here as ~1.0s on all eight loops).
|
||
func (l *Loop) SlowEventStats() (count int64, worst time.Duration, worstType string) {
|
||
if l == nil {
|
||
return 0, 0, ""
|
||
}
|
||
value, _ := l.worstSlowType.Load().(string)
|
||
return l.slowEvents.Load(), time.Duration(l.worstSlowNanos.Load()), value
|
||
}
|
||
|
||
func (l *Loop) recordFailure(priority Priority) {
|
||
if !validPriority(priority) {
|
||
return
|
||
}
|
||
l.metrics.processed[priority].Add(1)
|
||
l.metrics.failed[priority].Add(1)
|
||
}
|
||
|
||
func (l *Loop) loadPriorityCounters(counters *[3]atomic.Uint64) map[Priority]uint64 {
|
||
return map[Priority]uint64{
|
||
PriorityHigh: counters[PriorityHigh].Load(),
|
||
PriorityMedium: counters[PriorityMedium].Load(),
|
||
PriorityLow: counters[PriorityLow].Load(),
|
||
}
|
||
}
|
||
|
||
func (l *Loop) logWarn(message string, attrs ...slog.Attr) {
|
||
if l.logger == nil {
|
||
return
|
||
}
|
||
args := make([]any, 0, len(attrs)*2+2)
|
||
args = append(args, slog.String("loop", l.name))
|
||
for _, attr := range attrs {
|
||
args = append(args, attr)
|
||
}
|
||
l.logger.Warn(message, args...)
|
||
}
|
||
|
||
func (l *Loop) isClosed() bool {
|
||
l.mu.Lock()
|
||
defer l.mu.Unlock()
|
||
return l.closed
|
||
}
|
||
|
||
func validPriority(priority Priority) bool {
|
||
return priority == PriorityHigh || priority == PriorityMedium || priority == PriorityLow
|
||
}
|
||
|
||
func resetTimer(timer *time.Timer, duration time.Duration) <-chan time.Time {
|
||
if !timer.Stop() {
|
||
select {
|
||
case <-timer.C:
|
||
default:
|
||
}
|
||
}
|
||
timer.Reset(duration)
|
||
return timer.C
|
||
}
|