feat: transaction implement
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user