197 lines
5.9 KiB
Go
197 lines
5.9 KiB
Go
package mysql
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"hyapp/internal/testutil/mysqlschema"
|
|
"hyapp/pkg/appcode"
|
|
roomservice "hyapp/services/room-service/internal/room/service"
|
|
)
|
|
|
|
func TestSaveSnapshotDoesNotOverwriteSameVersion(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repo := newCommandLogTestRepository(t, ctx)
|
|
|
|
goodPayload := []byte("snapshot-good-v10")
|
|
if err := repo.SaveSnapshot(ctx, roomservice.SnapshotRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: "room-snapshot-version",
|
|
RoomVersion: 10,
|
|
Payload: goodPayload,
|
|
CreatedAtMS: 1000,
|
|
}); err != nil {
|
|
t.Fatalf("save initial snapshot: %v", err)
|
|
}
|
|
if err := repo.SaveSnapshot(ctx, roomservice.SnapshotRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: "room-snapshot-version",
|
|
RoomVersion: 10,
|
|
Payload: []byte("snapshot-bad-same-version"),
|
|
CreatedAtMS: 2000,
|
|
}); err != nil {
|
|
t.Fatalf("save same-version snapshot: %v", err)
|
|
}
|
|
got, exists, err := repo.GetLatestSnapshot(ctx, "room-snapshot-version")
|
|
if err != nil {
|
|
t.Fatalf("get same-version snapshot: %v", err)
|
|
}
|
|
if !exists || got.RoomVersion != 10 || !bytes.Equal(got.Payload, goodPayload) || got.CreatedAtMS != 1000 {
|
|
t.Fatalf("same-version snapshot overwrote recovery source: got version=%d created=%d payload=%q", got.RoomVersion, got.CreatedAtMS, got.Payload)
|
|
}
|
|
|
|
if err := repo.SaveSnapshot(ctx, roomservice.SnapshotRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: "room-snapshot-version",
|
|
RoomVersion: 11,
|
|
Payload: []byte("snapshot-new-v11"),
|
|
CreatedAtMS: 3000,
|
|
}); err != nil {
|
|
t.Fatalf("save newer snapshot: %v", err)
|
|
}
|
|
got, exists, err = repo.GetLatestSnapshot(ctx, "room-snapshot-version")
|
|
if err != nil {
|
|
t.Fatalf("get newer snapshot: %v", err)
|
|
}
|
|
if !exists || got.RoomVersion != 11 || !bytes.Equal(got.Payload, []byte("snapshot-new-v11")) {
|
|
t.Fatalf("newer snapshot should replace old one: got version=%d payload=%q", got.RoomVersion, got.Payload)
|
|
}
|
|
}
|
|
|
|
func TestSaveMutationRejectsDuplicateReplayableRoomVersion(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repo := newCommandLogTestRepository(t, ctx)
|
|
roomID := "room-command-version"
|
|
|
|
if err := repo.SaveMutation(ctx, roomservice.MutationCommit{Command: roomservice.CommandRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: roomID,
|
|
RoomVersion: 20,
|
|
CommandID: "cmd-first",
|
|
CommandType: "join_room",
|
|
Payload: []byte(`{"command_id":"cmd-first"}`),
|
|
Replayable: true,
|
|
OwnerNodeID: "node-a",
|
|
LeaseToken: "lease-a",
|
|
CreatedAtMS: 1000,
|
|
}}); err != nil {
|
|
t.Fatalf("save first replayable command: %v", err)
|
|
}
|
|
if err := repo.SaveMutation(ctx, roomservice.MutationCommit{Command: roomservice.CommandRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: roomID,
|
|
RoomVersion: 20,
|
|
CommandID: "cmd-second",
|
|
CommandType: "leave_room",
|
|
Payload: []byte(`{"command_id":"cmd-second"}`),
|
|
Replayable: true,
|
|
OwnerNodeID: "node-b",
|
|
LeaseToken: "lease-b",
|
|
CreatedAtMS: 2000,
|
|
}}); err == nil {
|
|
t.Fatal("duplicate replayable command version should be rejected")
|
|
}
|
|
if err := repo.SaveMutation(ctx, roomservice.MutationCommit{Command: roomservice.CommandRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: roomID,
|
|
RoomVersion: 20,
|
|
CommandID: "cmd-noop",
|
|
CommandType: "join_room",
|
|
Payload: []byte(`{"command_id":"cmd-noop"}`),
|
|
Replayable: false,
|
|
OwnerNodeID: "node-a",
|
|
LeaseToken: "lease-a",
|
|
CreatedAtMS: 3000,
|
|
}}); err != nil {
|
|
t.Fatalf("same-version non-replayable idempotency record should still be saved: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveMutationSerializesConcurrentReplayableRoomVersion(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repo := newCommandLogTestRepository(t, ctx)
|
|
roomID := "room-command-version-race"
|
|
insertCommandLogTestRoom(t, ctx, repo, roomID)
|
|
|
|
start := make(chan struct{})
|
|
errs := make(chan error, 2)
|
|
var wg sync.WaitGroup
|
|
for _, commandID := range []string{"cmd-race-a", "cmd-race-b"} {
|
|
wg.Add(1)
|
|
go func(commandID string) {
|
|
defer wg.Done()
|
|
<-start
|
|
errs <- repo.SaveMutation(ctx, roomservice.MutationCommit{Command: roomservice.CommandRecord{
|
|
AppCode: appcode.Default,
|
|
RoomID: roomID,
|
|
RoomVersion: 30,
|
|
CommandID: commandID,
|
|
CommandType: "room_heartbeat",
|
|
Payload: []byte(`{"command_id":"` + commandID + `"}`),
|
|
Replayable: true,
|
|
OwnerNodeID: "node-a",
|
|
LeaseToken: "lease-a",
|
|
CreatedAtMS: 4000,
|
|
}})
|
|
}(commandID)
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
successes := 0
|
|
failures := 0
|
|
for err := range errs {
|
|
if err == nil {
|
|
successes++
|
|
continue
|
|
}
|
|
failures++
|
|
}
|
|
if successes != 1 || failures != 1 {
|
|
t.Fatalf("concurrent same-version replayable writes should produce one success and one conflict, got successes=%d failures=%d", successes, failures)
|
|
}
|
|
}
|
|
|
|
func newCommandLogTestRepository(t *testing.T, ctx context.Context) *Repository {
|
|
t.Helper()
|
|
|
|
schema := mysqlschema.New(t, mysqlschema.Config{
|
|
EnvVar: "ROOM_SERVICE_MYSQL_TEST_DSN",
|
|
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_room_service.sql"),
|
|
DatabasePrefix: "hyapp_room_command_test",
|
|
})
|
|
repo, err := Open(ctx, schema.DSN, 4, 4)
|
|
if err != nil {
|
|
t.Fatalf("open repository: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = repo.Close()
|
|
})
|
|
return repo
|
|
}
|
|
|
|
func insertCommandLogTestRoom(t *testing.T, ctx context.Context, repo *Repository, roomID string) {
|
|
t.Helper()
|
|
|
|
if _, err := repo.db.ExecContext(ctx,
|
|
`INSERT INTO rooms (
|
|
app_code, room_id, room_short_id, owner_user_id, seat_count, mode, status, visible_region_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
appcode.Default,
|
|
roomID,
|
|
roomID+"-short",
|
|
1001,
|
|
8,
|
|
"voice",
|
|
"active",
|
|
10,
|
|
1000,
|
|
1000,
|
|
); err != nil {
|
|
t.Fatalf("insert command log test room: %v", err)
|
|
}
|
|
}
|