服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
289 lines
6.5 KiB
Go
289 lines
6.5 KiB
Go
package hotreload
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Registry 保存 route 到 handler 版本的映射,并负责原子切换当前版本。
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
routes map[string]*routeSlot
|
|
now func() time.Time
|
|
}
|
|
|
|
type routeSlot struct {
|
|
mu sync.RWMutex
|
|
activateMu sync.Mutex
|
|
route string
|
|
modules map[string]Module
|
|
active string
|
|
swaps int64
|
|
updatedAt time.Time
|
|
}
|
|
|
|
// NewRegistry 创建热重载版本注册表。
|
|
func NewRegistry(options Options) *Registry {
|
|
registry := &Registry{now: options.Now}
|
|
_ = registry.ensureReady()
|
|
return registry
|
|
}
|
|
|
|
// Register 注册一个 route 的可切换实现版本。
|
|
func (r *Registry) Register(module Module) error {
|
|
module, err := normalizeModule(module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := r.ensureReady(); err != nil {
|
|
return err
|
|
}
|
|
r.mu.Lock()
|
|
slot := r.routes[module.Route]
|
|
if slot == nil {
|
|
slot = &routeSlot{
|
|
route: module.Route,
|
|
modules: make(map[string]Module),
|
|
}
|
|
r.routes[module.Route] = slot
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
slot.mu.Lock()
|
|
if _, ok := slot.modules[module.Version]; ok {
|
|
slot.mu.Unlock()
|
|
return ErrVersionExists
|
|
}
|
|
slot.modules[module.Version] = module
|
|
if slot.active == "" {
|
|
slot.active = module.Version
|
|
slot.updatedAt = r.now()
|
|
}
|
|
slot.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Activate 将指定 route 原子切换到目标版本。
|
|
func (r *Registry) Activate(ctx context.Context, route, version string) (Result, error) {
|
|
route, version, err := normRouteVersion(route, version)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if err := r.ensureReady(); err != nil {
|
|
return Result{}, err
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Result{}, err
|
|
}
|
|
slot := r.routeSlot(route)
|
|
if slot == nil {
|
|
return Result{}, ErrRouteNotFound
|
|
}
|
|
|
|
var previous Module
|
|
var hasPrevious bool
|
|
unlock, err := slot.lockActivate(ctx)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
defer unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return Result{}, err
|
|
}
|
|
|
|
slot.mu.RLock()
|
|
module, ok := slot.modules[version]
|
|
if !ok {
|
|
slot.mu.RUnlock()
|
|
return Result{}, ErrVersionNotFound
|
|
}
|
|
if slot.active == version {
|
|
result := Result{Route: route, PreviousVersion: version, ActiveVersion: version, ActivatedAt: slot.updatedAt}
|
|
slot.mu.RUnlock()
|
|
return result, nil
|
|
}
|
|
if active := slot.active; active != "" {
|
|
previous, hasPrevious = slot.modules[active]
|
|
}
|
|
slot.mu.RUnlock()
|
|
|
|
if module.Preflight != nil {
|
|
if err := module.Preflight(ctx, module); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Result{}, err
|
|
}
|
|
if module.Activate != nil {
|
|
if err := module.Activate(ctx, module); err != nil {
|
|
return Result{}, err
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Result{}, err
|
|
}
|
|
now := r.now()
|
|
|
|
slot.mu.Lock()
|
|
result := Result{
|
|
Route: route,
|
|
PreviousVersion: slot.active,
|
|
ActiveVersion: version,
|
|
ActivatedAt: now,
|
|
}
|
|
slot.active = version
|
|
slot.updatedAt = now
|
|
slot.swaps++
|
|
slot.mu.Unlock()
|
|
|
|
if hasPrevious && previous.Version != version && previous.Deactivate != nil {
|
|
if err := previous.Deactivate(ctx, previous); err != nil {
|
|
result.DeactivateError = err.Error()
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Rollback 通过重新激活旧版本完成 route 回滚。
|
|
func (r *Registry) Rollback(ctx context.Context, route, version string) (Result, error) {
|
|
return r.Activate(ctx, route, version)
|
|
}
|
|
|
|
// Dispatch 将请求分发到 route 当前激活版本的 handler。
|
|
func (r *Registry) Dispatch(ctx context.Context, route string, payload any, metadata map[string]any) (Response, error) {
|
|
route = normalizeName(route)
|
|
if route == "" {
|
|
return Response{}, ErrRouteRequired
|
|
}
|
|
if err := r.ensureReady(); err != nil {
|
|
return Response{}, err
|
|
}
|
|
slot := r.routeSlot(route)
|
|
if slot == nil {
|
|
return Response{}, ErrRouteNotFound
|
|
}
|
|
slot.mu.RLock()
|
|
module, ok := slot.modules[slot.active]
|
|
slot.mu.RUnlock()
|
|
if !ok || module.Handler == nil {
|
|
return Response{}, ErrHandlerUnavailable
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Response{}, err
|
|
}
|
|
return module.Handler(ctx, Request{
|
|
Route: route,
|
|
Version: module.Version,
|
|
Payload: payload,
|
|
Metadata: copyMetadata(metadata),
|
|
})
|
|
}
|
|
|
|
// ActiveVersion 查询 route 当前激活版本。
|
|
func (r *Registry) ActiveVersion(route string) (string, bool) {
|
|
route = normalizeName(route)
|
|
if route == "" {
|
|
return "", false
|
|
}
|
|
if err := r.ensureReady(); err != nil {
|
|
return "", false
|
|
}
|
|
slot := r.routeSlot(route)
|
|
if slot == nil {
|
|
return "", false
|
|
}
|
|
slot.mu.RLock()
|
|
version := slot.active
|
|
slot.mu.RUnlock()
|
|
return version, version != ""
|
|
}
|
|
|
|
// Snapshot 返回所有 route 的稳定排序快照。
|
|
func (r *Registry) Snapshot() Snapshot {
|
|
if err := r.ensureReady(); err != nil {
|
|
return Snapshot{}
|
|
}
|
|
r.mu.RLock()
|
|
slots := make([]*routeSlot, 0, len(r.routes))
|
|
for _, slot := range r.routes {
|
|
slots = append(slots, slot)
|
|
}
|
|
r.mu.RUnlock()
|
|
sort.Slice(slots, func(i, j int) bool { return slots[i].route < slots[j].route })
|
|
|
|
snapshot := Snapshot{Routes: make([]RouteSnapshot, 0, len(slots))}
|
|
for _, slot := range slots {
|
|
slot.mu.RLock()
|
|
versions := make([]string, 0, len(slot.modules))
|
|
for version := range slot.modules {
|
|
versions = append(versions, version)
|
|
}
|
|
sort.Strings(versions)
|
|
snapshot.Routes = append(snapshot.Routes, RouteSnapshot{
|
|
Route: slot.route,
|
|
ActiveVersion: slot.active,
|
|
Versions: versions,
|
|
Swaps: slot.swaps,
|
|
UpdatedAt: slot.updatedAt,
|
|
})
|
|
slot.mu.RUnlock()
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func (r *Registry) ensureReady() error {
|
|
if r == nil {
|
|
return ErrRouteNotFound
|
|
}
|
|
r.mu.Lock()
|
|
if r.routes == nil {
|
|
r.routes = make(map[string]*routeSlot)
|
|
}
|
|
if r.now == nil {
|
|
r.now = func() time.Time { return time.Now().UTC() }
|
|
}
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) routeSlot(route string) *routeSlot {
|
|
r.mu.RLock()
|
|
slot := r.routes[route]
|
|
r.mu.RUnlock()
|
|
return slot
|
|
}
|
|
|
|
func (s *routeSlot) lockActivate(ctx context.Context) (func(), error) {
|
|
if s.activateMu.TryLock() {
|
|
return s.activateMu.Unlock, nil
|
|
}
|
|
timer := time.NewTimer(time.Millisecond)
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
if s.activateMu.TryLock() {
|
|
return s.activateMu.Unlock, nil
|
|
}
|
|
timer.Reset(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// IsNotFound 判断错误是否属于热重载 route 或版本不存在。
|
|
func IsNotFound(err error) bool {
|
|
return errors.Is(err, ErrRouteNotFound) || errors.Is(err, ErrVersionNotFound)
|
|
}
|