Files
90CNServer/server/internal/platform/db/async_store.go
T
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

404 lines
11 KiB
Go

package db
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
// AsyncStoreOptions 配置异步写后队列、重试和死信存储。
type AsyncStoreOptions[T any, F comparable] struct {
FlushInterval time.Duration
MaxPending int
RetryBackoff time.Duration
MaxRetries int
DeadLetterLimit int
DeadLetterStore DeadLetterStore[T, F]
RecordKey KeyFunc[T]
NormalizeKey func(string) string
Clone CloneFunc[T]
AllFields func() []F
NormalizeFields func([]F) []F
SaveFields SaveFieldsFunc[T, F]
SaveBatch SaveFieldBatchFunc[T, F]
AutoExpire bool
AutoExpireTTL time.Duration
Expire ExpireFunc[T]
ClosedError error
}
// AsyncStoreStats 描述异步存储当前运行状态。
type AsyncStoreStats struct {
Pending int `json:"pending"`
PendingDue int `json:"pending_due"`
DeadLetters int `json:"dead_letters"`
Closed bool `json:"closed"`
FlushInterval string `json:"flush_interval"`
RetryBackoff string `json:"retry_backoff"`
MaxPending int `json:"max_pending"`
MaxRetries int `json:"max_retries"`
AutoExpireTTL string `json:"auto_expire_ttl,omitempty"`
DeadLetterLimit int `json:"dead_letter_limit"`
DeadLetterStore string `json:"dead_letter_store,omitempty"`
DeadLetterStoreError string `json:"dead_letter_store_error,omitempty"`
}
const defAsyncFlush = 5 * time.Second
// AsyncStore 用内存队列包装同步 Store,实现异步刷盘和死信兜底。
type AsyncStore[T any, F comparable] struct {
base Store[T]
flushInterval time.Duration
maxPending int
allFields func() []F
closedError error
mu sync.Mutex
closed bool
queue *asyncPendingQueue[T, F]
retry asyncRetryPolicy[T, F]
deadLetters *asyncDeadLetterSink[T, F]
flusher *asyncFlusher[T, F]
done chan struct{}
flushNow chan struct{}
stopped chan struct{}
stopOnce sync.Once
}
// NewAsyncStore 用写后队列包装同步 Store。
// Load 会先读 pending 和 dead-letter,再读 base store;这样即使后台刷盘还没落到持久层,
// 调用方也能看到自己已被接受的最新 mutation。
func NewAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) (*AsyncStore[T, F], error) {
if base == nil {
return nil, fmt.Errorf("db.AsyncStore requires a base store")
}
if options.AutoExpire && options.AutoExpireTTL <= 0 {
return nil, fmt.Errorf("db.AsyncStore auto expire ttl must be positive")
}
return newAsyncStore(base, options), nil
}
// MustNewAsyncStore 创建异步存储,失败时 panic,适合启动期固定配置。
func MustNewAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) *AsyncStore[T, F] {
store, err := NewAsyncStore(base, options)
if err != nil {
panic(err)
}
return store
}
func newAsyncStore[T any, F comparable](base Store[T], options AsyncStoreOptions[T, F]) *AsyncStore[T, F] {
if options.RetryBackoff < 0 {
options.RetryBackoff = time.Second
}
if options.DeadLetterLimit <= 0 {
options.DeadLetterLimit = 128
}
if options.Clone == nil {
options.Clone = IdentityClone[T]
}
if options.SaveFields == nil {
options.SaveFields = func(ctx context.Context, store Store[T], record T, fields ...F) error {
return SaveFields(ctx, store, record, options.NormalizeFields, fields...)
}
}
if options.SaveBatch == nil {
if _, ok := base.(BatchFieldStore[T, F]); ok {
options.SaveBatch = func(ctx context.Context, store Store[T], saves []FieldSave[T, F]) error {
return SaveFieldBatch(ctx, store, options.NormalizeFields, saves)
}
}
}
if options.AutoExpire && options.Expire == nil {
options.Expire = func(ctx context.Context, store Store[T], key string, ttl time.Duration) error {
return Expire(ctx, store, key, ttl)
}
}
if options.ClosedError == nil {
options.ClosedError = ErrAsyncStoreClosed
}
queue := newAsyncPendingQueue(options.RecordKey, options.NormalizeKey, options.Clone, options.NormalizeFields)
retry := asyncRetryPolicy[T, F]{backoff: options.RetryBackoff, maxRetries: options.MaxRetries}
deadLetters := newAsyncDeadSink(asyncDeadSinkOpts[T, F]{
Limit: options.DeadLetterLimit,
Store: options.DeadLetterStore,
RecordKey: options.RecordKey,
NormalizeKey: options.NormalizeKey,
Clone: options.Clone,
NormalizeFields: options.NormalizeFields,
})
flusher := newAsyncFlusher(asyncFlusherOptions[T, F]{
Base: base,
Queue: queue,
Retry: retry,
DeadLetters: deadLetters,
Clone: options.Clone,
NormalizeFields: options.NormalizeFields,
SaveFields: options.SaveFields,
SaveBatch: options.SaveBatch,
AutoExpireTTL: options.AutoExpireTTL,
Expire: options.Expire,
})
store := &AsyncStore[T, F]{
base: base,
flushInterval: options.FlushInterval,
maxPending: options.MaxPending,
allFields: options.AllFields,
closedError: options.ClosedError,
queue: queue,
retry: retry,
deadLetters: deadLetters,
flusher: flusher,
done: make(chan struct{}),
flushNow: make(chan struct{}, 1),
stopped: make(chan struct{}),
}
store.deadLetters.Load()
go store.run()
return store
}
// Load 优先读取 pending 和 dead-letter,再读取底层存储。
func (s *AsyncStore[T, F]) Load(ctx context.Context, accountID string) (T, bool, error) {
ctx = contextOrBackground(ctx)
accountID = s.queue.NormalizeRecordKey(accountID)
if pending, ok := s.queue.Get(accountID); ok {
return s.queue.Clone(pending.profile), true, nil
}
if dead, ok := s.deadLetters.Get(accountID); ok {
return s.queue.Clone(dead.Profile), true, nil
}
return s.base.Load(ctx, accountID)
}
// Check 检查异步存储、底层存储和死信存储状态。
func (s *AsyncStore[T, F]) Check(ctx context.Context) error {
ctx = contextOrBackground(ctx)
s.mu.Lock()
closed := s.closed
base := s.base
closedError := s.closedError
s.mu.Unlock()
if closed {
return closedError
}
if deadLetterStoreError := s.deadLetters.LastError(); deadLetterStoreError != "" {
return fmt.Errorf("dead letter store: %s", deadLetterStoreError)
}
if err := Check(ctx, base); err != nil {
return err
}
return s.deadLetters.CheckStore(ctx)
}
// Save 保存完整记录字段。
func (s *AsyncStore[T, F]) Save(ctx context.Context, profile T) error {
fields := s.allFieldsForSave()
if len(fields) == 0 {
return ErrAllFieldsRequired
}
return s.SaveFields(ctx, profile, fields...)
}
// SaveFields 把局部字段写入 pending 队列。
func (s *AsyncStore[T, F]) SaveFields(ctx context.Context, profile T, fields ...F) error {
ctx = contextOrBackground(ctx)
if err := ctx.Err(); err != nil {
return err
}
accountID, err := s.queue.RecordKey(profile)
if err != nil {
return err
}
fields = s.queue.NormalizeFields(fields)
if len(fields) == 0 {
return nil
}
save := pendingSave[T, F]{
profile: s.queue.Clone(profile),
fields: fields,
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return s.closedError
}
pending := s.queue.Add(accountID, save)
s.mu.Unlock()
if s.maxPending > 0 && pending >= s.maxPending {
s.requestFlush()
}
return nil
}
// Flush 强制刷出当前可写 pending 队列。
func (s *AsyncStore[T, F]) Flush(ctx context.Context) error {
ctx = contextOrBackground(ctx)
return s.flusher.Flush(ctx, true)
}
// Stats 返回异步队列、死信和重试状态。
func (s *AsyncStore[T, F]) Stats() AsyncStoreStats {
now := time.Now().UTC()
pending, pendingDue := s.queue.Stats(now)
s.mu.Lock()
closed := s.closed
s.mu.Unlock()
return AsyncStoreStats{
Pending: pending,
PendingDue: pendingDue,
DeadLetters: s.deadLetters.Count(),
Closed: closed,
FlushInterval: s.flushInterval.String(),
RetryBackoff: s.retry.backoff.String(),
MaxPending: s.maxPending,
MaxRetries: s.retry.maxRetries,
AutoExpireTTL: asyncDurationString(s.flusher.AutoExpireTTL()),
DeadLetterLimit: s.deadLetters.Limit(),
DeadLetterStore: s.deadLetters.StoreName(),
DeadLetterStoreError: s.deadLetters.LastError(),
}
}
// DeadLetters 返回当前死信快照。
func (s *AsyncStore[T, F]) DeadLetters() []AsyncStoreDeadLetter[T, F] {
return s.deadLetters.List()
}
// RequeueDeadLetter 把指定死信重新放回 pending 队列。
func (s *AsyncStore[T, F]) RequeueDeadLetter(accountID string) bool {
accountID = s.queue.NormalizeRecordKey(accountID)
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return false
}
s.mu.Unlock()
dead, ok := s.deadLetters.Take(accountID)
if !ok {
return false
}
fields := s.queue.NormalizeFields(dead.Fields)
if len(fields) == 0 {
fields = s.allFieldsForSave()
}
save := pendingSave[T, F]{
profile: s.queue.Clone(dead.Profile),
fields: fields,
attempts: dead.Attempts,
lastError: dead.Error,
firstFailedAt: dead.FirstFailedAt,
lastFailedAt: dead.LastFailedAt,
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
if err := s.deadLetters.Add(save); err != nil {
s.deadLetters.restoreMemory(dead)
}
return false
}
s.queue.Requeue(accountID, save)
return true
}
// Close 停止后台刷盘并做最后一次 Flush。
func (s *AsyncStore[T, F]) Close(ctx context.Context) error {
ctx, cancel := asyncCloseCtx(ctx)
defer cancel()
s.mu.Lock()
s.closed = true
s.mu.Unlock()
s.stopOnce.Do(func() {
close(s.done)
})
select {
case <-s.stopped:
case <-ctx.Done():
return ctx.Err()
}
return errors.Join(s.Flush(ctx), CloseOrFlush(ctx, s.base))
}
func asyncCloseCtx(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
return context.WithTimeout(ctx, defAsyncFlush)
}
func (s *AsyncStore[T, F]) run() {
defer close(s.stopped)
if s.flushInterval <= 0 {
for {
select {
case <-s.flushNow:
s.flushBackground()
case <-s.done:
return
}
}
}
ticker := time.NewTicker(s.flushInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.flushBackground()
case <-s.flushNow:
s.flushBackground()
case <-s.done:
return
}
}
}
func (s *AsyncStore[T, F]) requestFlush() {
select {
case s.flushNow <- struct{}{}:
default:
return
}
}
func (s *AsyncStore[T, F]) flushBackground() {
ctx, cancel := context.WithTimeout(context.Background(), defAsyncFlush)
_ = s.flusher.Flush(ctx, false)
cancel()
}
func (s *AsyncStore[T, F]) allFieldsForSave() []F {
if s.allFields == nil {
return nil
}
return s.queue.NormalizeFields(s.allFields())
}
func asyncDurationString(duration time.Duration) string {
if duration <= 0 {
return ""
}
return duration.String()
}