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"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// 锁辅助进程通过环境变量传递模式与锁路径,用于在独立进程中验证 flock 的互斥语义。
|
2026-08-15 02:30:36 +08:00
|
|
|
const (
|
2026-08-17 10:10:14 +08:00
|
|
|
// lockHelperModeEnvironment 传递锁辅助进程运行模式的环境变量名。
|
2026-08-15 02:30:36 +08:00
|
|
|
lockHelperModeEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_MODE"
|
2026-08-17 10:10:14 +08:00
|
|
|
// lockHelperPathEnvironment 传递锁文件路径的环境变量名。
|
2026-08-15 02:30:36 +08:00
|
|
|
lockHelperPathEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_PATH"
|
2026-08-17 10:10:14 +08:00
|
|
|
// lockHelperExpectBlocked 表示期望锁辅助进程因锁被占用而加锁失败。
|
|
|
|
|
lockHelperExpectBlocked = "EXPECT_BLOCKED"
|
|
|
|
|
// lockHelperExpectAcquired 表示期望锁辅助进程在锁释放后成功加锁。
|
|
|
|
|
lockHelperExpectAcquired = "EXPECT_ACQUIRED"
|
2026-08-15 02:30:36 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// runLockHelper 启动一个独立的锁辅助测试进程,通过环境变量传入模式与锁路径,
|
|
|
|
|
// 并校验该进程按预期成功退出。
|
2026-08-15 02:30:36 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
// assertStateEvents 校验事务事件流与给定的状态迁移序列一致:事件数量应为迁移数加一,
|
|
|
|
|
// 首条为事务创建事件,其后每条状态变更事件的前后状态与序列号递增关系均需匹配,最终状态为 Committed。
|
2026-08-15 02:30:36 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|