Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

915 lines
22 KiB
Go
Raw Permalink 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 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
}