DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,185 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/actor"
|
||||
)
|
||||
|
||||
// ActorKindPlayer 是玩家 actor 在 actor.Manager 中使用的类型标识。
|
||||
const ActorKindPlayer = "player"
|
||||
|
||||
// ActorBackedManager 使用通用 actor 管理器承载玩家事件串行处理。
|
||||
type ActorBackedManager struct {
|
||||
name string
|
||||
handler Handler
|
||||
actors *actor.Manager
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
started bool
|
||||
}
|
||||
|
||||
// NewActorBackedManager 创建基于 actor mailbox 的玩家循环管理器。
|
||||
func NewActorBackedManager(name string, queue int, handler Handler, logger *slog.Logger, options Options) *ActorBackedManager {
|
||||
if queue <= 0 {
|
||||
queue = 64
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = "player-loop"
|
||||
}
|
||||
return &ActorBackedManager{
|
||||
name: name,
|
||||
handler: handler,
|
||||
actors: actor.NewManager(name+"-actors", logger, actor.Options{
|
||||
MailboxSize: queue,
|
||||
HandlerTimeout: options.HandlerTimeout,
|
||||
}),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回 actor 版玩家循环管理器名称。
|
||||
func (m *ActorBackedManager) Name() string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
return m.name
|
||||
}
|
||||
|
||||
// Start 标记 actor 版玩家循环管理器可接收事件。
|
||||
func (m *ActorBackedManager) Start(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if m == nil || m.actors == nil {
|
||||
return ErrStopped
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.started = true
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止 actor 版玩家循环管理器并关闭底层 actor。
|
||||
func (m *ActorBackedManager) Stop(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if m == nil || m.actors == nil {
|
||||
return nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.started = false
|
||||
m.mu.Unlock()
|
||||
return m.actors.Stop(ctx)
|
||||
}
|
||||
|
||||
// Submit 将事件提交到账号对应的玩家 actor。
|
||||
func (m *ActorBackedManager) Submit(ctx context.Context, accountID string, payload any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return ErrMissingAccount
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if m == nil || m.actors == nil || !m.isStarted() {
|
||||
return ErrStopped
|
||||
}
|
||||
act, err := m.actorFor(accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
event := Event{AccountID: accountID, Payload: payload, ReceivedAt: time.Now().UTC()}
|
||||
err = act.Tell(ctx, actor.Message{Route: "player.event", UserID: accountID, Payload: event})
|
||||
if errors.Is(err, actor.ErrMailboxFull) {
|
||||
return ErrQueueFull
|
||||
}
|
||||
if errors.Is(err, actor.ErrActorStopped) || errors.Is(err, actor.ErrActorDraining) {
|
||||
return ErrStopped
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Snapshot 返回所有玩家 actor 的队列快照。
|
||||
func (m *ActorBackedManager) Snapshot() []Snapshot {
|
||||
if m == nil || m.actors == nil {
|
||||
return nil
|
||||
}
|
||||
actorSnapshot := m.actors.Snapshot()
|
||||
out := make([]Snapshot, 0, len(actorSnapshot.Actors))
|
||||
for _, item := range actorSnapshot.Actors {
|
||||
if item.Kind != ActorKindPlayer {
|
||||
continue
|
||||
}
|
||||
out = append(out, Snapshot{AccountID: item.ID, Queued: item.MailboxLen})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ActorSnapshot 返回底层 actor 管理器的完整快照。
|
||||
func (m *ActorBackedManager) ActorSnapshot() actor.Snapshot {
|
||||
if m == nil || m.actors == nil {
|
||||
return actor.Snapshot{}
|
||||
}
|
||||
return m.actors.Snapshot()
|
||||
}
|
||||
|
||||
// DrainAccount 请求指定账号 actor 进入排空状态。
|
||||
func (m *ActorBackedManager) DrainAccount(ctx context.Context, accountID string) error {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return ErrMissingAccount
|
||||
}
|
||||
if m == nil || m.actors == nil {
|
||||
return nil
|
||||
}
|
||||
act, ok := m.actors.Get(ActorKindPlayer, accountID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return act.Drain(ctx)
|
||||
}
|
||||
|
||||
func (m *ActorBackedManager) actorFor(accountID string) (*actor.Actor, error) {
|
||||
if act, ok := m.actors.Get(ActorKindPlayer, accountID); ok {
|
||||
return act, nil
|
||||
}
|
||||
act, err := m.actors.Spawn(ActorKindPlayer, accountID, m.handleActorMessage)
|
||||
if errors.Is(err, actor.ErrActorExists) {
|
||||
if existing, ok := m.actors.Get(ActorKindPlayer, accountID); ok {
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
return act, err
|
||||
}
|
||||
|
||||
func (m *ActorBackedManager) handleActorMessage(ctx context.Context, _ *actor.Actor, msg actor.Message) error {
|
||||
if m == nil || m.handler == nil {
|
||||
return nil
|
||||
}
|
||||
event, ok := msg.Payload.(Event)
|
||||
if !ok {
|
||||
event = Event{AccountID: msg.UserID, Payload: msg.Payload, ReceivedAt: msg.SentAt}
|
||||
}
|
||||
return m.handler(ctx, event)
|
||||
}
|
||||
|
||||
func (m *ActorBackedManager) isStarted() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.started
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package playerloop 将单个玩家的工作串行化到账号维度队列。
|
||||
//
|
||||
// 这样可以保护 Profile 和玩法状态不被并发修改,同时允许不同账号并行处理。Handler
|
||||
// 不能无限阻塞,否则会卡住该玩家自己的队列。
|
||||
package playerloop
|
||||
@@ -0,0 +1,85 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/platform/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetricPlayerLoopQueueLen 记录所有账号循环的待处理事件总数。
|
||||
MetricPlayerLoopQueueLen = "playerloop_queue_len"
|
||||
|
||||
// MetricPlayerLoopLoopCount 记录当前活跃账号循环数量。
|
||||
MetricPlayerLoopLoopCount = "playerloop_loop_count"
|
||||
|
||||
// MetricPlayerLoopSweepsTotal 记录空闲循环扫描累计次数。
|
||||
MetricPlayerLoopSweepsTotal = "playerloop_sweeps_total"
|
||||
|
||||
// MetricPlayerLoopHandlerErrorsTotal 记录玩家事件 handler 错误累计数。
|
||||
MetricPlayerLoopHandlerErrorsTotal = "playerloop_handler_errors_total"
|
||||
)
|
||||
|
||||
// MetricsObserver 是可写入平台指标注册表的玩家循环观测接口。
|
||||
type MetricsObserver interface {
|
||||
ObserveMetrics(reg *metrics.Registry, labels map[string]string)
|
||||
}
|
||||
|
||||
// ObserveMetrics 把队列长度、循环数量和错误计数写入指标注册表。
|
||||
func (m *Manager) ObserveMetrics(reg *metrics.Registry, labels map[string]string) {
|
||||
if m == nil || reg == nil {
|
||||
return
|
||||
}
|
||||
var queued int64
|
||||
m.mu.RLock()
|
||||
loops := int64(len(m.loops))
|
||||
for _, playerLoop := range m.loops {
|
||||
if playerLoop != nil {
|
||||
queued += int64(len(playerLoop.ch))
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
tags := normMetricLabels(labels)
|
||||
if m.name != "" {
|
||||
tags = withMetricLabel(tags, "manager", m.name)
|
||||
}
|
||||
reg.SetGauge(MetricPlayerLoopQueueLen, tags, queued)
|
||||
reg.SetGauge(MetricPlayerLoopLoopCount, tags, loops)
|
||||
reg.SetCounter(MetricPlayerLoopSweepsTotal, tags, metrics.Int64FromUint64(m.sweepCount.Load()))
|
||||
reg.SetCounter(MetricPlayerLoopHandlerErrorsTotal, tags, metrics.Int64FromUint64(m.handlerErrorCount.Load()))
|
||||
}
|
||||
|
||||
func normMetricLabels(labels map[string]string) map[string]string {
|
||||
if len(labels) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(labels))
|
||||
for key, value := range labels {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func withMetricLabel(labels map[string]string, key, value string) map[string]string {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return labels
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
return map[string]string{key: value}
|
||||
}
|
||||
out := make(map[string]string, len(labels)+1)
|
||||
for existingKey, existingValue := range labels {
|
||||
out[existingKey] = existingValue
|
||||
}
|
||||
out[key] = value
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OnlineRecord 记录账号在线状态和最近变更时间。
|
||||
type OnlineRecord struct {
|
||||
AccountID string `json:"account_id"`
|
||||
Online bool `json:"online"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// OnlineRegistry 在内存中维护账号在线状态。
|
||||
type OnlineRegistry struct {
|
||||
mu sync.RWMutex
|
||||
records map[string]OnlineRecord
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewOnlineRegistry 使用系统时钟创建在线状态表。
|
||||
func NewOnlineRegistry() *OnlineRegistry {
|
||||
return NewOnlineRegistryWithClock(time.Now)
|
||||
}
|
||||
|
||||
// NewOnlineRegistryWithClock 使用指定时钟创建在线状态表,便于测试和回放。
|
||||
func NewOnlineRegistryWithClock(now func() time.Time) *OnlineRegistry {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &OnlineRegistry{
|
||||
records: make(map[string]OnlineRecord),
|
||||
now: now,
|
||||
}
|
||||
}
|
||||
|
||||
// MarkOnline 标记账号在线并返回新状态。
|
||||
func (r *OnlineRegistry) MarkOnline(accountID string) OnlineRecord {
|
||||
return r.set(accountID, true)
|
||||
}
|
||||
|
||||
// MarkOffline 标记账号离线并返回新状态。
|
||||
func (r *OnlineRegistry) MarkOffline(accountID string) OnlineRecord {
|
||||
return r.set(accountID, false)
|
||||
}
|
||||
|
||||
// IsOnline 判断账号当前是否在线。
|
||||
func (r *OnlineRegistry) IsOnline(accountID string) bool {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if r == nil || accountID == "" {
|
||||
return false
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.records[accountID].Online
|
||||
}
|
||||
|
||||
// Snapshot 返回在线状态表的当前副本。
|
||||
func (r *OnlineRegistry) Snapshot() []OnlineRecord {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
out := make([]OnlineRecord, 0, len(r.records))
|
||||
for _, record := range r.records {
|
||||
out = append(out, record)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *OnlineRegistry) set(accountID string, online bool) OnlineRecord {
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if r == nil || accountID == "" {
|
||||
return OnlineRecord{}
|
||||
}
|
||||
record := OnlineRecord{AccountID: accountID, Online: online, UpdatedAt: r.now().UTC()}
|
||||
r.mu.Lock()
|
||||
r.records[accountID] = record
|
||||
r.mu.Unlock()
|
||||
return record
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/tracing"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrStopped 表示玩家循环管理器未启动或已经停止。
|
||||
ErrStopped = errors.New("player loop manager is stopped")
|
||||
|
||||
// ErrMissingAccount 表示提交事件时缺少账号 ID。
|
||||
ErrMissingAccount = errors.New("account id is required")
|
||||
|
||||
// ErrQueueFull 表示目标玩家循环队列已经写满。
|
||||
ErrQueueFull = errors.New("player loop queue is full")
|
||||
)
|
||||
|
||||
// Event 是按账号串行处理的玩家事件。
|
||||
type Event struct {
|
||||
AccountID string
|
||||
Payload any
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
// Handler 处理单个玩家事件,框架保证同一账号事件串行进入。
|
||||
type Handler func(context.Context, Event) error
|
||||
|
||||
// Options 描述玩家循环的空闲回收和 handler 超时策略。
|
||||
type Options struct {
|
||||
IdleTTL time.Duration
|
||||
SweepInterval time.Duration
|
||||
HandlerTimeout time.Duration
|
||||
}
|
||||
|
||||
// Snapshot 是单个账号玩家循环的队列快照。
|
||||
type Snapshot struct {
|
||||
AccountID string `json:"account_id"`
|
||||
Queued int `json:"queued"`
|
||||
}
|
||||
|
||||
// Manager 按账号维护独立事件队列,保证同一账号状态修改串行执行。
|
||||
type Manager struct {
|
||||
name string
|
||||
queue int
|
||||
handler Handler
|
||||
logger *slog.Logger
|
||||
|
||||
idleTTL time.Duration
|
||||
sweepInterval time.Duration
|
||||
handlerTimeout time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
started bool
|
||||
loops map[string]*loop
|
||||
active *managerGeneration
|
||||
|
||||
sweepCount atomic.Uint64
|
||||
handlerErrorCount atomic.Uint64
|
||||
}
|
||||
|
||||
// managerGeneration 持有一次 Start 创建的全部运行资源,避免旧 Stop 干扰新一代循环。
|
||||
type managerGeneration struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
loops map[string]*loop
|
||||
wg sync.WaitGroup
|
||||
sweepCancel context.CancelFunc
|
||||
sweepDone chan struct{}
|
||||
}
|
||||
|
||||
type loop struct {
|
||||
accountID string
|
||||
ch chan Event
|
||||
closeOnce sync.Once
|
||||
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
// processing 防止 idle sweep 在 handler 已取出事件、但仍在修改账号状态时关闭 loop。
|
||||
processing bool
|
||||
lastActive time.Time
|
||||
}
|
||||
|
||||
// New 使用默认选项创建玩家循环管理器。
|
||||
func New(name string, queue int, handler Handler, logger *slog.Logger) *Manager {
|
||||
return NewWithOptions(name, queue, handler, logger, Options{})
|
||||
}
|
||||
|
||||
// NewWithOptions 创建带空闲回收和 handler 超时配置的玩家循环管理器。
|
||||
func NewWithOptions(name string, queue int, handler Handler, logger *slog.Logger, options Options) *Manager {
|
||||
if queue <= 0 {
|
||||
queue = 64
|
||||
}
|
||||
if options.SweepInterval <= 0 && options.IdleTTL > 0 {
|
||||
options.SweepInterval = options.IdleTTL / 2
|
||||
if options.SweepInterval <= 0 {
|
||||
options.SweepInterval = time.Second
|
||||
}
|
||||
}
|
||||
return &Manager{
|
||||
name: name,
|
||||
queue: queue,
|
||||
handler: handler,
|
||||
logger: logger,
|
||||
idleTTL: options.IdleTTL,
|
||||
sweepInterval: options.SweepInterval,
|
||||
handlerTimeout: options.HandlerTimeout,
|
||||
loops: make(map[string]*loop),
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回玩家循环管理器名称。
|
||||
func (m *Manager) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
// Start 启动玩家循环管理器和可选空闲回收任务。
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.started {
|
||||
return nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
runCtx, runCancel := context.WithCancel(context.Background())
|
||||
generation := &managerGeneration{
|
||||
ctx: runCtx,
|
||||
cancel: runCancel,
|
||||
loops: make(map[string]*loop),
|
||||
}
|
||||
m.started = true
|
||||
m.active = generation
|
||||
m.loops = generation.loops
|
||||
if m.idleTTL > 0 {
|
||||
sweepCtx, cancel := context.WithCancel(runCtx)
|
||||
generation.sweepCancel = cancel
|
||||
generation.sweepDone = make(chan struct{})
|
||||
go m.sweepIdleLoops(sweepCtx, generation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 关闭所有账号循环并等待已取出的事件处理完成。
|
||||
func (m *Manager) Stop(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var stopErr error
|
||||
recordCtxErr := func() {
|
||||
if stopErr == nil {
|
||||
stopErr = ctx.Err()
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
if !m.started || m.active == nil {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
generation := m.active
|
||||
loops := make([]*loop, 0, len(generation.loops))
|
||||
for _, playerLoop := range generation.loops {
|
||||
loops = append(loops, playerLoop)
|
||||
}
|
||||
m.started = false
|
||||
m.active = nil
|
||||
m.loops = make(map[string]*loop)
|
||||
m.mu.Unlock()
|
||||
|
||||
if generation.sweepCancel != nil {
|
||||
generation.sweepCancel()
|
||||
}
|
||||
if generation.sweepDone != nil {
|
||||
select {
|
||||
case <-generation.sweepDone:
|
||||
case <-ctx.Done():
|
||||
recordCtxErr()
|
||||
}
|
||||
}
|
||||
|
||||
for _, playerLoop := range loops {
|
||||
playerLoop.close()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
generation.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
generation.cancel()
|
||||
return stopErr
|
||||
case <-ctx.Done():
|
||||
generation.cancel()
|
||||
recordCtxErr()
|
||||
return stopErr
|
||||
}
|
||||
}
|
||||
|
||||
// Submit 将事件提交到指定账号的串行队列。
|
||||
func (m *Manager) Submit(ctx context.Context, accountID string, payload any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return ErrMissingAccount
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
if !m.started || m.active == nil {
|
||||
m.mu.Unlock()
|
||||
return ErrStopped
|
||||
}
|
||||
generation := m.active
|
||||
playerLoop := generation.loops[accountID]
|
||||
if playerLoop == nil {
|
||||
playerLoop = &loop{
|
||||
accountID: accountID,
|
||||
ch: make(chan Event, m.queue),
|
||||
lastActive: time.Now().UTC(),
|
||||
}
|
||||
generation.loops[accountID] = playerLoop
|
||||
generation.wg.Add(1)
|
||||
go m.run(generation, playerLoop)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
event := Event{
|
||||
AccountID: accountID,
|
||||
Payload: payload,
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
}
|
||||
return playerLoop.submit(ctx, event)
|
||||
}
|
||||
|
||||
// Snapshot 返回所有账号循环的队列长度快照。
|
||||
func (m *Manager) Snapshot() []Snapshot {
|
||||
m.mu.RLock()
|
||||
out := make([]Snapshot, 0, len(m.loops))
|
||||
for accountID, playerLoop := range m.loops {
|
||||
out = append(out, Snapshot{
|
||||
AccountID: accountID,
|
||||
Queued: len(playerLoop.ch),
|
||||
})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].AccountID < out[j].AccountID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) run(generation *managerGeneration, playerLoop *loop) {
|
||||
defer generation.wg.Done()
|
||||
for event := range playerLoop.ch {
|
||||
m.handle(generation.ctx, playerLoop, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handle(ctx context.Context, playerLoop *loop, event Event) {
|
||||
playerLoop.markProcessing(true)
|
||||
defer playerLoop.markProcessing(false)
|
||||
|
||||
if m.handler == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
handlerCtx := ctx
|
||||
var cancel context.CancelFunc
|
||||
if m.handlerTimeout > 0 {
|
||||
handlerCtx, cancel = context.WithTimeout(ctx, m.handlerTimeout)
|
||||
}
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
m.handlerErrorCount.Add(1)
|
||||
if m.logger != nil {
|
||||
tracing.LogPanic(handlerCtx, m.logger, "playerloop.handler", rec, map[string]string{"account_id": event.AccountID})
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := m.handler(handlerCtx, event); err != nil {
|
||||
m.handlerErrorCount.Add(1)
|
||||
if m.logger != nil && !errors.Is(err, context.Canceled) {
|
||||
m.logger.Error("player loop handler failed", "account_id", event.AccountID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) sweepIdleLoops(ctx context.Context, generation *managerGeneration) {
|
||||
defer close(generation.sweepDone)
|
||||
ticker := time.NewTicker(m.sweepInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.reclaimGenLoops(generation, time.Now().UTC())
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) reclaimIdleLoops(now time.Time) {
|
||||
m.mu.RLock()
|
||||
generation := m.active
|
||||
m.mu.RUnlock()
|
||||
m.reclaimGenLoops(generation, now)
|
||||
}
|
||||
|
||||
func (m *Manager) reclaimGenLoops(generation *managerGeneration, now time.Time) {
|
||||
if m.idleTTL <= 0 {
|
||||
return
|
||||
}
|
||||
m.sweepCount.Add(1)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.started || m.active != generation {
|
||||
return
|
||||
}
|
||||
for accountID, playerLoop := range generation.loops {
|
||||
if !playerLoop.canReclaim(now, m.idleTTL) {
|
||||
continue
|
||||
}
|
||||
delete(generation.loops, accountID)
|
||||
playerLoop.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loop) close() {
|
||||
l.closeOnce.Do(func() {
|
||||
l.mu.Lock()
|
||||
l.closed = true
|
||||
close(l.ch)
|
||||
l.mu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (l *loop) submit(ctx context.Context, event Event) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.closed {
|
||||
return ErrStopped
|
||||
}
|
||||
select {
|
||||
case l.ch <- event:
|
||||
if event.ReceivedAt.After(l.lastActive) {
|
||||
l.lastActive = event.ReceivedAt
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return ErrQueueFull
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loop) markProcessing(processing bool) {
|
||||
l.mu.Lock()
|
||||
l.processing = processing
|
||||
if !processing {
|
||||
l.lastActive = time.Now().UTC()
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *loop) canReclaim(now time.Time, idleTTL time.Duration) bool {
|
||||
l.mu.RLock()
|
||||
processing := l.processing
|
||||
lastActive := l.lastActive
|
||||
l.mu.RUnlock()
|
||||
if processing || len(l.ch) > 0 || lastActive.IsZero() {
|
||||
return false
|
||||
}
|
||||
return now.Sub(lastActive) >= idleTTL
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TickPayloadFunc 为每个账号构造一次 tick 事件负载。
|
||||
type TickPayloadFunc func(accountID string, at time.Time) any
|
||||
|
||||
// TickResult 记录一次 tick 广播覆盖账号数、成功提交数和丢弃数。
|
||||
type TickResult struct {
|
||||
Accounts int `json:"accounts"`
|
||||
Submitted int `json:"submitted"`
|
||||
Dropped int `json:"dropped"`
|
||||
}
|
||||
|
||||
// Tick 向当前所有账号循环提交同一个 tick 负载。
|
||||
func (m *Manager) Tick(ctx context.Context, payload any) (TickResult, error) {
|
||||
return m.TickFunc(ctx, func(string, time.Time) any { return payload })
|
||||
}
|
||||
|
||||
// TickFunc 向当前所有账号循环提交由回调构造的 tick 负载。
|
||||
func (m *Manager) TickFunc(ctx context.Context, build TickPayloadFunc) (TickResult, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return TickResult{}, err
|
||||
}
|
||||
if build == nil {
|
||||
build = func(string, time.Time) any { return nil }
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
|
||||
m.mu.Lock()
|
||||
if !m.started {
|
||||
m.mu.Unlock()
|
||||
return TickResult{}, ErrStopped
|
||||
}
|
||||
loops := make([]*loop, 0, len(m.loops))
|
||||
for _, playerLoop := range m.loops {
|
||||
loops = append(loops, playerLoop)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
result := TickResult{Accounts: len(loops)}
|
||||
for _, playerLoop := range loops {
|
||||
event := Event{
|
||||
AccountID: playerLoop.accountID,
|
||||
Payload: build(playerLoop.accountID, now),
|
||||
ReceivedAt: now,
|
||||
}
|
||||
switch err := playerLoop.submit(ctx, event); {
|
||||
case err == nil:
|
||||
result.Submitted++
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||
return result, ctx.Err()
|
||||
default:
|
||||
result.Dropped++
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user