174 lines
5.3 KiB
Go
174 lines
5.3 KiB
Go
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(),
|
||
}
|
||
}
|