// Package router 维护 room-service 的房间 owner 路由目录和 lease 语义。 package router import ( "context" "sync" "time" "hyapp/pkg/idgen" ) // Lease 表达房间当前执行权归属。 type Lease struct { // RoomID 是租约所属房间。 RoomID string // NodeID 是当前拥有房间执行权的 room-service 实例。 NodeID string // LeaseToken 是租约实例标识,便于未来做续约和 fencing。 LeaseToken string // ExpiresAt 是租约过期时间,过期后其他节点可以接管并恢复房间。 ExpiresAt time.Time } // ValidAt 判断租约在指定时刻是否仍有效。 func (l Lease) ValidAt(now time.Time) bool { // 零值过期时间永远无效,避免空 lease 被误认为可继续持有。 return !l.ExpiresAt.IsZero() && now.Before(l.ExpiresAt) } // Directory 抽象 room_id -> node_id 的房间归属目录。 type Directory interface { // Lookup 查询当前记录的房间 owner,不会主动接管。 Lookup(ctx context.Context, roomID string) (Lease, bool, error) // EnsureOwner 在无 owner、owner 过期或 owner 是当前节点时授予当前节点执行权。 EnsureOwner(ctx context.Context, roomID string, nodeID string, now time.Time, ttl time.Duration) (Lease, error) // VerifyOwner 校验当前节点和 lease token 仍然持有执行权,不续租也不接管。 VerifyOwner(ctx context.Context, roomID string, nodeID string, leaseToken string, now time.Time) (bool, error) // ForceExpire 只用于测试模拟租约过期。 ForceExpire(roomID string) } // MemoryDirectory 是首版测试与本地运行使用的内存目录实现。 type MemoryDirectory struct { // mu 保护 leases,测试里可能用多个服务实例模拟接管。 mu sync.Mutex // leases 保存 room_id -> lease。 leases map[string]Lease } // NewMemoryDirectory 初始化空的内存目录。 func NewMemoryDirectory() *MemoryDirectory { return &MemoryDirectory{ leases: make(map[string]Lease), } } // Lookup 查询当前房间归属。 func (d *MemoryDirectory) Lookup(_ context.Context, roomID string) (Lease, bool, error) { d.mu.Lock() defer d.mu.Unlock() // 内存实现不主动清理过期 lease,EnsureOwner 会在写路径判断有效性。 lease, ok := d.leases[roomID] return lease, ok, nil } // EnsureOwner 在租约不存在、已过期或已归当前节点时,授予本节点执行权。 func (d *MemoryDirectory) EnsureOwner(_ context.Context, roomID string, nodeID string, now time.Time, ttl time.Duration) (Lease, error) { d.mu.Lock() defer d.mu.Unlock() current, ok := d.leases[roomID] if ok && current.ValidAt(now) && current.NodeID != nodeID { // 其他节点持有有效 lease 时返回现有 owner,调用方应拒绝执行房间命令。 return current, nil } // 没有 lease、lease 过期或当前节点续租时生成新 token 和过期时间。 next := Lease{ RoomID: roomID, NodeID: nodeID, LeaseToken: idgen.New("lease"), ExpiresAt: now.Add(ttl), } d.leases[roomID] = next return next, nil } // VerifyOwner 校验调用方手里的 lease token 仍是当前有效 owner。 func (d *MemoryDirectory) VerifyOwner(_ context.Context, roomID string, nodeID string, leaseToken string, now time.Time) (bool, error) { d.mu.Lock() defer d.mu.Unlock() current, ok := d.leases[roomID] if !ok { return false, nil } return current.NodeID == nodeID && current.LeaseToken == leaseToken && current.ValidAt(now), nil } // ForceExpire 只用于测试时模拟租约自然过期。 func (d *MemoryDirectory) ForceExpire(roomID string) { d.mu.Lock() defer d.mu.Unlock() lease, ok := d.leases[roomID] if !ok { // 没有租约时无需处理,测试可安全重复调用。 return } lease.ExpiresAt = time.Unix(0, 0) d.leases[roomID] = lease }