2026-04-25 01:18:30 +08:00

220 lines
6.4 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 memory 提供 room-service Repository 的测试用内存实现。
package memory
import (
"context"
"sort"
"sync"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/outbox"
roomservice "hyapp/services/room-service/internal/room/service"
)
// Repository 是 room-service 持久化接口的内存实现。
// 它主要服务于首版开发和单测,不试图模拟 MySQL 事务细节。
type Repository struct {
// mu 保护所有 map 和序列,测试中可能同时模拟多个 room-service 节点。
mu sync.Mutex
// rooms 保存房间基础元数据。
rooms map[string]roomservice.RoomMeta
// commandSeen 保存 room_id -> command_id 幂等集合。
commandSeen map[string]map[string]bool
// commandLogs 保存可回放命令日志。
commandLogs map[string][]roomservice.CommandRecord
// snapshots 保存每个房间最新快照。
snapshots map[string]roomservice.SnapshotRecord
// outboxByID 保存 event_id -> outbox record。
outboxByID map[string]outbox.Record
// outboxSequence 保留插入顺序,模拟 MySQL created_at 顺序扫描。
outboxSequence []string
}
// NewRepository 创建空的内存仓储。
func NewRepository() *Repository {
return &Repository{
rooms: make(map[string]roomservice.RoomMeta),
commandSeen: make(map[string]map[string]bool),
commandLogs: make(map[string][]roomservice.CommandRecord),
snapshots: make(map[string]roomservice.SnapshotRecord),
outboxByID: make(map[string]outbox.Record),
}
}
// SaveRoomMeta 保存房间元数据。
func (r *Repository) SaveRoomMeta(_ context.Context, meta roomservice.RoomMeta) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.rooms[meta.RoomID]; exists {
// 内存实现显式返回冲突,帮助测试发现重复创建。
return xerr.New(xerr.Conflict, "room meta already exists")
}
r.rooms[meta.RoomID] = meta
return nil
}
// GetRoomMeta 查询房间元数据。
func (r *Repository) GetRoomMeta(_ context.Context, roomID string) (roomservice.RoomMeta, bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
meta, exists := r.rooms[roomID]
return meta, exists, nil
}
// HasCommand 查询命令是否已被执行过。
func (r *Repository) HasCommand(_ context.Context, roomID string, commandID string) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
seenByRoom := r.commandSeen[roomID]
// nil map 表示该房间还没有任何成功命令。
return seenByRoom != nil && seenByRoom[commandID], nil
}
// SaveCommand 追加一条成功命令日志。
func (r *Repository) SaveCommand(_ context.Context, record roomservice.CommandRecord) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.commandSeen[record.RoomID] == nil {
// 首条命令时懒创建房间级幂等集合。
r.commandSeen[record.RoomID] = make(map[string]bool)
}
if r.commandSeen[record.RoomID][record.CommandID] {
// 测试实现返回冲突;生产 MySQL 使用 ON DUPLICATE KEY 保持幂等。
return xerr.New(xerr.Conflict, "duplicate command")
}
r.commandSeen[record.RoomID][record.CommandID] = true
r.commandLogs[record.RoomID] = append(r.commandLogs[record.RoomID], record)
sort.Slice(r.commandLogs[record.RoomID], func(i int, j int) bool {
// 恢复依赖 room_version 顺序,内存实现写入后立即排序。
return r.commandLogs[record.RoomID][i].RoomVersion < r.commandLogs[record.RoomID][j].RoomVersion
})
return nil
}
// ListCommandsAfter 返回快照版本之后的全部命令。
func (r *Repository) ListCommandsAfter(_ context.Context, roomID string, roomVersion int64) ([]roomservice.CommandRecord, error) {
r.mu.Lock()
defer r.mu.Unlock()
records := make([]roomservice.CommandRecord, 0)
for _, record := range r.commandLogs[roomID] {
if record.RoomVersion > roomVersion {
// 返回快照版本之后的全部命令,调用方负责过滤 Replayable。
records = append(records, record)
}
}
return records, nil
}
// SaveSnapshot 保存当前最新快照。
func (r *Repository) SaveSnapshot(_ context.Context, snapshot roomservice.SnapshotRecord) error {
r.mu.Lock()
defer r.mu.Unlock()
current, exists := r.snapshots[snapshot.RoomID]
if exists && current.RoomVersion > snapshot.RoomVersion {
// 禁止旧快照覆盖新快照,保持和 MySQL IF 逻辑一致。
return nil
}
r.snapshots[snapshot.RoomID] = snapshot
return nil
}
// GetLatestSnapshot 返回房间最新快照。
func (r *Repository) GetLatestSnapshot(_ context.Context, roomID string) (roomservice.SnapshotRecord, bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
snapshot, exists := r.snapshots[roomID]
return snapshot, exists, nil
}
// SaveOutbox 追加待投递 outbox 事件。
func (r *Repository) SaveOutbox(_ context.Context, records []outbox.Record) error {
r.mu.Lock()
defer r.mu.Unlock()
for _, record := range records {
if _, exists := r.outboxByID[record.EventID]; exists {
// outbox event_id 幂等,重复保存直接跳过。
continue
}
r.outboxByID[record.EventID] = record
r.outboxSequence = append(r.outboxSequence, record.EventID)
}
return nil
}
// ListPendingOutbox 返回待投递事件。
func (r *Repository) ListPendingOutbox(_ context.Context, limit int) ([]outbox.Record, error) {
r.mu.Lock()
defer r.mu.Unlock()
result := make([]outbox.Record, 0, limit)
for _, eventID := range r.outboxSequence {
record := r.outboxByID[eventID]
if record.Status != outbox.StatusPending {
// 只返回待投递事件published 事件保留在 map 中便于测试检查状态。
continue
}
result = append(result, record)
if limit > 0 && len(result) >= limit {
// limit<=0 表示不限制;正数 limit 达到后停止扫描。
break
}
}
return result, nil
}
// MarkOutboxPublished 标记事件投递成功。
func (r *Repository) MarkOutboxPublished(_ context.Context, eventID string) error {
r.mu.Lock()
defer r.mu.Unlock()
record := r.outboxByID[eventID]
// 内存实现不校验 eventID 是否存在,保持测试路径简单。
record.Status = outbox.StatusPublished
record.LastError = ""
r.outboxByID[eventID] = record
return nil
}
// MarkOutboxFailed 标记事件投递失败。
func (r *Repository) MarkOutboxFailed(_ context.Context, eventID string, lastErr string) error {
r.mu.Lock()
defer r.mu.Unlock()
record := r.outboxByID[eventID]
// 失败保持 pending并累计重试次数模拟 MySQL 更新语义。
record.Status = outbox.StatusPending
record.LastError = lastErr
record.RetryCount++
r.outboxByID[eventID] = record
return nil
}