220 lines
6.4 KiB
Go
220 lines
6.4 KiB
Go
// 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
|
||
}
|