Files
yms-daemon/integration/transaction_kernel_linux_test.go
T

183 lines
5.8 KiB
Go
Raw Normal View History

2026-08-15 02:30:36 +08:00
//go:build linux
package integration_test
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"testing"
"yms-daemon/internal/processlock"
"yms-daemon/internal/transaction"
)
const (
lockHelperModeEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_MODE"
lockHelperPathEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_PATH"
lockHelperExpectBlocked = "EXPECT_BLOCKED"
lockHelperExpectAcquired = "EXPECT_ACQUIRED"
)
// TestTransactionKernelIntegration 使用真实子进程、flock 和 SQLite 文件验证事务底座。
// 测试期间父进程始终持有 serve 锁,完整事务提交并重开数据库后才释放锁。
func TestTransactionKernelIntegration(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
lockPath := filepath.Join(root, "serve.lock")
databasePath := filepath.Join(root, "transaction.db")
serveLock, err := processlock.Acquire(lockPath)
if err != nil {
t.Fatalf("acquire serve lock: %v", err)
}
t.Cleanup(func() {
if err := serveLock.Close(); err != nil {
t.Errorf("release serve lock during cleanup: %v", err)
}
})
// 同进程重复加锁不足以证明客户服务器上的进程互斥,因此必须启动独立进程核对。
runLockHelper(t, lockHelperExpectBlocked, lockPath)
store, err := transaction.OpenStore(ctx, databasePath)
if err != nil {
t.Fatalf("open transaction store: %v", err)
}
record, created, err := store.CreateTransaction(ctx, transaction.CreateRequest{
ID: "integration-transaction",
IdempotencyKey: "integration-request",
Source: "integration-test",
Service: "all",
})
if err != nil || !created {
t.Fatalf("create transaction: record=%+v created=%v err=%v", record, created, err)
}
transitions := []transaction.State{
transaction.StateValidating,
transaction.StatePrepared,
transaction.StateStarting,
transaction.StateSwitching,
transaction.StateVerifying,
transaction.StateDraining,
transaction.StateCommitted,
}
for index, next := range transitions {
record, err = store.Transition(ctx, record.ID, next, "integration transition")
if err != nil {
t.Fatalf("transition to %s: %v", next, err)
}
expectedVersion := int64(index + 2)
if record.State != next || record.Version != expectedVersion {
t.Fatalf("unexpected transition result: state=%s version=%d, want state=%s version=%d",
record.State, record.Version, next, expectedVersion)
}
}
if err := store.Close(); err != nil {
t.Fatalf("close transaction store: %v", err)
}
// 重新打开同一文件,证明终态和事件不是内存中的临时结果。
reopened, err := transaction.OpenStore(ctx, databasePath)
if err != nil {
t.Fatalf("reopen transaction store: %v", err)
}
t.Cleanup(func() {
if err := reopened.Close(); err != nil {
t.Errorf("close reopened transaction store: %v", err)
}
})
persisted, err := reopened.Transaction(ctx, record.ID)
if err != nil {
t.Fatalf("read committed transaction: %v", err)
}
if persisted.State != transaction.StateCommitted || persisted.Version != 8 {
t.Fatalf("unexpected committed transaction: %+v", persisted)
}
events, err := reopened.EventsAfter(ctx, record.ID, 0, 100)
if err != nil {
t.Fatalf("read transaction events: %v", err)
}
assertStateEvents(t, events, transitions)
if err := serveLock.Close(); err != nil {
t.Fatalf("release serve lock: %v", err)
}
runLockHelper(t, lockHelperExpectAcquired, lockPath)
}
// TestTransactionKernelLockHelper 只由 TestTransactionKernelIntegration 启动。
// 独立测试进程让 flock 的互斥语义得到端到端验证。
func TestTransactionKernelLockHelper(t *testing.T) {
mode, enabled := os.LookupEnv(lockHelperModeEnvironment)
if !enabled {
t.Skip("lock helper process is not enabled")
}
lockPath, present := os.LookupEnv(lockHelperPathEnvironment)
if !present || lockPath == "" {
t.Fatal("lock helper path is missing")
}
lock, err := processlock.Acquire(lockPath)
switch mode {
case lockHelperExpectBlocked:
if lock != nil {
_ = lock.Close()
}
if !errors.Is(err, processlock.ErrAlreadyLocked) {
t.Fatalf("expected kernel lock contention, lock=%v err=%v", lock, err)
}
case lockHelperExpectAcquired:
if err != nil || lock == nil {
t.Fatalf("expected released kernel lock to be acquired, lock=%v err=%v", lock, err)
}
if err := lock.Close(); err != nil {
t.Fatalf("release helper lock: %v", err)
}
default:
t.Fatalf("unknown lock helper mode: %q", mode)
}
}
func runLockHelper(t *testing.T, mode, lockPath string) {
t.Helper()
command := exec.Command(os.Args[0], "-test.run=^TestTransactionKernelLockHelper$", "-test.v")
command.Env = []string{
lockHelperModeEnvironment + "=" + mode,
lockHelperPathEnvironment + "=" + lockPath,
}
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("run lock helper in mode %s: %v\n%s", mode, err, output)
}
t.Logf("lock helper mode %s passed", mode)
}
func assertStateEvents(t *testing.T, events []transaction.Event, transitions []transaction.State) {
t.Helper()
if len(events) != len(transitions)+1 {
t.Fatalf("unexpected event count: got %d, want %d", len(events), len(transitions)+1)
}
if events[0].Kind != "TRANSACTION_CREATED" || events[0].ToState != transaction.StateCreated {
t.Fatalf("unexpected transaction creation event: %+v", events[0])
}
previous := transaction.StateCreated
for index, next := range transitions {
event := events[index+1]
if event.Kind != "TRANSACTION_STATE_CHANGED" || event.FromState != previous || event.ToState != next {
t.Fatalf("unexpected state event at index %d: %+v", index+1, event)
}
if event.Sequence <= events[index].Sequence {
t.Fatalf("event sequence is not increasing at index %d: previous=%d current=%d",
index+1, events[index].Sequence, event.Sequence)
}
previous = next
}
if previous != transaction.StateCommitted {
t.Fatalf("state flow ended at %s", previous)
}
}