2026-05-12 20:07:49 +08:00

174 lines
5.3 KiB
Go
Raw 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 service
import (
"context"
"slices"
"time"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/idgen"
"hyapp/services/room-service/internal/room/cell"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/state"
"hyapp/services/room-service/internal/router"
)
type loadedRoomRef struct {
AppCode string
RoomID string
}
func (s *Service) installCell(ctx context.Context, roomID string, roomCell *cell.RoomCell) {
s.mu.Lock()
defer s.mu.Unlock()
// 注册表只保存当前进程内已装载 Cell不代表 Redis lease 的全局 owner。
s.cells[runtimeRoomKey(appcode.FromContext(ctx), roomID)] = roomCell
}
func (s *Service) loadCell(ctx context.Context, roomID string) *cell.RoomCell {
s.mu.Lock()
defer s.mu.Unlock()
// 返回指针后由 RoomCell 自己的 mailbox 保证状态串行化。
return s.cells[runtimeRoomKey(appcode.FromContext(ctx), roomID)]
}
func (s *Service) loadedRoomRefs() []loadedRoomRef {
s.mu.Lock()
defer s.mu.Unlock()
// 复制运行态房间引用后释放锁,避免后台清理期间阻塞其他房间装载。
roomRefs := make([]loadedRoomRef, 0, len(s.cells))
for key := range s.cells {
roomRefs = append(roomRefs, runtimeRoomRef(key))
}
return roomRefs
}
func runtimeRoomKey(appCode string, roomID string) string {
appCode = appcode.Normalize(appCode)
return appCode + "\x00" + roomID
}
func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
return runtimeRoomKey(appcode.FromContext(ctx), roomID)
}
func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
lease, err := s.directory.EnsureOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, now, s.leaseTTL)
if err != nil {
return router.Lease{}, false, err
}
if lease.NodeID != s.nodeID || !lease.ValidAt(now) {
// 后台 worker 只处理自己仍然拥有或可安全续租的 Cell。
// 如果其他节点已经接管并持有有效 lease当前节点必须跳过避免旧内存态覆盖新 owner。
return router.Lease{}, false, nil
}
return lease, true, nil
}
func (s *Service) verifyLoadedRoomLease(ctx context.Context, roomRef loadedRoomRef, lease router.Lease, now time.Time) (bool, error) {
return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
}
func runtimeRoomRef(key string) loadedRoomRef {
for index := 0; index < len(key); index++ {
if key[index] == 0 {
return loadedRoomRef{AppCode: appcode.Normalize(key[:index]), RoomID: key[index+1:]}
}
}
return loadedRoomRef{AppCode: "", RoomID: key}
}
func commandResult(applied bool, roomVersion int64, now time.Time) *roomv1.CommandResult {
// 所有命令响应统一带 applied、room_version 和服务端时间,便于客户端处理幂等结果。
return &roomv1.CommandResult{
Applied: applied,
RoomVersion: roomVersion,
ServerTimeMs: now.UnixMilli(),
}
}
func baseFromMeta(meta *roomv1.RequestMeta) command.Base {
if meta == nil {
// 上层 mutateRoom 会校验关键字段nil meta 在这里先收敛为空 Base。
return command.Base{}
}
// RequestMeta 是跨服务 protobuf 契约,领域层统一转换成 command.Base。
return command.Base{
AppCode: appcode.Normalize(meta.GetAppCode()),
RequestID: meta.GetRequestId(),
CommandID: meta.GetCommandId(),
ActorID: meta.GetActorUserId(),
Room: meta.GetRoomId(),
GatewayNodeID: meta.GetGatewayNodeId(),
SessionID: meta.GetSessionId(),
SentAtMS: meta.GetSentAtMs(),
}
}
func contextFromMeta(ctx context.Context, meta *roomv1.RequestMeta) context.Context {
if meta == nil {
return appcode.WithContext(ctx, "")
}
return appcode.WithContext(ctx, meta.GetAppCode())
}
func findProtoUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
if snapshot == nil {
return nil
}
// protobuf 快照用户列表规模受房间在线人数限制,线性扫描足够简单。
for _, user := range snapshot.GetOnlineUsers() {
if user.GetUserId() == userID {
return user
}
}
return nil
}
func containsID(values []int64, target int64) bool {
// 快照里的集合以有序数组表达,守卫查询用线性查找即可。
return slices.Contains(values, target)
}
func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
// SendGift 响应需要 protobuf 榜单项,不能直接暴露内部 state.RankItem。
result := make([]*roomv1.RankItem, 0, len(items))
for _, item := range items {
result = append(result, &roomv1.RankItem{
UserId: item.UserID,
Score: item.Score,
GiftValue: item.GiftValue,
UpdatedAtMs: item.UpdatedAtMS,
})
}
return result
}
// NewRequestMeta 是测试和 stub 组装命令元信息时的便捷函数。
func NewRequestMeta(roomID string, actorUserID int64) *roomv1.RequestMeta {
// 该函数只用于测试和 stub真实入口由 gateway 生成 request_id并透传客户端动作级 command_id。
return &roomv1.RequestMeta{
AppCode: appcode.Default,
RequestId: idgen.New("req"),
CommandId: idgen.New("cmd"),
ActorUserId: actorUserID,
RoomId: roomID,
GatewayNodeId: "gateway-local",
SessionId: idgen.New("sess"),
SentAtMs: time.Now().UnixMilli(),
}
}