211 lines
6.9 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 是运行态 Cell 所属租户,后台 worker 必须用它恢复 ctx。
AppCode string
// RoomID 是已装载 Room Cell 的业务房间 ID。
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)
// 使用 NUL 分隔 app_code 和 room_id避免普通字符串拼接出现歧义。
return appCode + "\x00" + roomID
}
// RuntimeRoomKey 返回 Redis owner lease 使用的运行时路由 key。
// transport 层 owner 转发必须和领域层 EnsureOwner 使用同一个 key否则会出现同一房间多 owner。
func RuntimeRoomKey(appCode string, roomID string) string {
return runtimeRoomKey(appCode, roomID)
}
func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
// 命令入口统一从 ctx 读取 app_code避免 service 方法忘记把租户键传给 Redis lease。
return runtimeRoomKey(appcode.FromContext(ctx), roomID)
}
func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
// 后台 worker 通过 EnsureOwner 续租当前 Cell不能在没有 lease 的情况下读取旧内存态。
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) {
// 使用同一个 lease token 做 fencing 校验,防止 worker 批处理期间 owner 已经切走。
return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
}
func runtimeRoomRef(key string) loadedRoomRef {
// runtime key 是内部格式;解析失败时保留 room_id便于旧测试数据仍能被安全跳过或排查。
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 firstNonZeroInt64(values ...int64) int64 {
for _, value := range values {
if value != 0 {
return value
}
}
return 0
}
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 snapshotUserBanned(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
if snapshot == nil || userID <= 0 {
return false
}
for _, ban := range snapshot.GetBanStates() {
if ban.GetUserId() != userID {
continue
}
// ban_states 是带时间边界的权威状态;过期后即使 ban_user_ids 还没刷新也不能继续拦截。
return ban.GetExpiresAtMs() == 0 || ban.GetExpiresAtMs() > nowMS
}
// 旧快照没有 ban_states 时只能按永久 ban 解释 ban_user_ids避免恢复路径放宽权限。
return containsID(snapshot.GetBanUserIds(), userID)
}
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(),
}
}