2026-05-12 21:51:39 +08:00

213 lines
7.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 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
}
// NodeRegistration 记录一个 room-service 实例可被其他实例直连的内网地址。
type NodeRegistration struct {
// NodeID 必须和 Redis lease 中的 owner node_id 完全一致。
NodeID string
// GRPCAddr 是实例私网 gRPC 地址,必须指向单个实例,不能写 room-service 负载均衡地址。
GRPCAddr string
// UpdatedAt 是最近一次心跳写入时间,便于排查节点注册是否卡住。
UpdatedAt time.Time
// ExpiresAt 是节点注册过期时间;过期节点不能作为 owner 转发目标。
ExpiresAt time.Time
}
// ValidAt 判断节点注册在指定时间是否仍可作为转发目标。
func (n NodeRegistration) ValidAt(now time.Time) bool {
// 没有地址或过期时间的注册不能被使用,避免把请求转发到空地址或旧实例。
return n.NodeID != "" && n.GRPCAddr != "" && !n.ExpiresAt.IsZero() && now.Before(n.ExpiresAt)
}
// 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)
// ReleaseOwner 在节点安全下线后释放自己仍持有的 owner lease让新节点无需等待 TTL 才能恢复。
ReleaseOwner(ctx context.Context, roomID string, nodeID string) (bool, error)
// ForceExpire 只用于测试模拟租约过期。
ForceExpire(roomID string)
}
// NodeRegistry 维护 room-service node_id 到实例私网 gRPC 地址的短租约。
type NodeRegistry interface {
// RegisterNode 写入或续约当前节点地址ttl 过期后其他节点不能继续转发到该实例。
RegisterNode(ctx context.Context, node NodeRegistration, ttl time.Duration) error
// LookupNode 查询一个 owner node_id 的实例地址。
LookupNode(ctx context.Context, nodeID string) (NodeRegistration, bool, error)
// UnregisterNode 在实例安全退出后删除注册,避免转发到已停止进程。
UnregisterNode(ctx context.Context, nodeID string) error
}
// MemoryDirectory 是首版测试与本地运行使用的内存目录实现。
type MemoryDirectory struct {
// mu 保护 leases测试里可能用多个服务实例模拟接管。
mu sync.Mutex
// leases 保存 room_id -> lease。
leases map[string]Lease
// nodes 保存 node_id -> 注册地址,供 transport 层 owner 转发测试复用。
nodes map[string]NodeRegistration
}
// NewMemoryDirectory 初始化空的内存目录。
func NewMemoryDirectory() *MemoryDirectory {
return &MemoryDirectory{
leases: make(map[string]Lease),
nodes: make(map[string]NodeRegistration),
}
}
// Lookup 查询当前房间归属。
func (d *MemoryDirectory) Lookup(_ context.Context, roomID string) (Lease, bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
// 内存实现不主动清理过期 leaseEnsureOwner 会在写路径判断有效性。
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
}
if ok && current.ValidAt(now) && current.NodeID == nodeID {
// 同一节点续租只能延长过期时间,不能轮换 token。
// Room Cell 命令在进入 mailbox 前拿到 lease提交前会用 token 做 fencing
// 如果后台 sweep 续租时换 token就会把同节点正在执行的命令误判为旧 owner。
current.ExpiresAt = now.Add(ttl)
current.RoomID = roomID
d.leases[roomID] = current
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
}
// ReleaseOwner 删除当前节点持有的内存 lease其他节点持有时保持不变。
func (d *MemoryDirectory) ReleaseOwner(_ context.Context, roomID string, nodeID string) (bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
current, ok := d.leases[roomID]
if !ok || current.NodeID != nodeID {
return false, nil
}
delete(d.leases, roomID)
return true, 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
}
// RegisterNode 写入测试用节点地址。
func (d *MemoryDirectory) RegisterNode(_ context.Context, node NodeRegistration, ttl time.Duration) error {
d.mu.Lock()
defer d.mu.Unlock()
now := time.Now().UTC()
if ttl <= 0 {
ttl = 30 * time.Second
}
node.UpdatedAt = now
node.ExpiresAt = now.Add(ttl)
d.nodes[node.NodeID] = node
return nil
}
// LookupNode 查询测试用节点地址,过期注册按不存在处理。
func (d *MemoryDirectory) LookupNode(_ context.Context, nodeID string) (NodeRegistration, bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
node, ok := d.nodes[nodeID]
if !ok || !node.ValidAt(time.Now().UTC()) {
return NodeRegistration{}, false, nil
}
return node, true, nil
}
// UnregisterNode 删除测试用节点地址。
func (d *MemoryDirectory) UnregisterNode(_ context.Context, nodeID string) error {
d.mu.Lock()
defer d.mu.Unlock()
delete(d.nodes, nodeID)
return nil
}