feat(nodes): 节点生命周期状态机 [tsk_aG4yOS90inF_]
实现 Lifecycle 类型及其全部公开方法,覆盖 6 个状态迁移动作: MarkProbing / MarkUp / MarkDraining / MarkDown / MarkDestroyed / MarkBlockedSuspect。每次合法迁移在同一个 SQL 事务中原子执行 UPDATE nodes + INSERT node_events + BumpVersion;MarkDestroyed 事务提交后异步调用 mtls.CRL.Revoke() 并清除 Redis 负载缓存与 命令队列键。配套集成测试覆盖 5 类场景(合法迁移、非法迁移、 并发写、后置钩子、blocked_suspect 幂等),全部通过。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
//go:build integration
|
||||
|
||||
package nodes_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestMain starts a throw-away MySQL 8 Docker container when PANGOLIN_TEST_DSN
|
||||
// is not already set, runs all tests, and tears the container down on exit.
|
||||
//
|
||||
// If Docker is unavailable the tests are skipped gracefully (requireDSN in
|
||||
// lifecycle_test.go handles the skip).
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("PANGOLIN_TEST_DSN") != "" {
|
||||
// Caller already provided a DSN — run directly.
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
const (
|
||||
containerName = "pangolin-lc-test-mysql"
|
||||
hostPort = "13399"
|
||||
dbName = "pangolin_test"
|
||||
rootPwd = "secret"
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Remove any leftover container from a previous crashed run.
|
||||
_ = exec.CommandContext(ctx, "docker", "rm", "-f", containerName).Run()
|
||||
|
||||
// Start MySQL 8.
|
||||
startCmd := exec.CommandContext(ctx, "docker", "run", "-d",
|
||||
"--name", containerName,
|
||||
"-e", "MYSQL_ROOT_PASSWORD="+rootPwd,
|
||||
"-e", "MYSQL_DATABASE="+dbName,
|
||||
"-p", hostPort+":3306",
|
||||
"mysql:8",
|
||||
)
|
||||
if out, err := startCmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "docker run failed: %v\n%s\n", err, out)
|
||||
fmt.Fprintln(os.Stderr, "skipping lifecycle integration tests (no MySQL)")
|
||||
os.Exit(0) // graceful skip
|
||||
}
|
||||
|
||||
// Tear down on exit regardless of test outcome.
|
||||
defer func() {
|
||||
_ = exec.Command("docker", "stop", containerName).Run()
|
||||
_ = exec.Command("docker", "rm", containerName).Run()
|
||||
}()
|
||||
|
||||
// multiStatements=true is required by golang-migrate's MySQL driver so it
|
||||
// can execute migration files containing multiple SQL statements in one call.
|
||||
// store.Open / buildDSN preserves unrecognised params, so this flag
|
||||
// survives the DSN round-trip through buildDSN.
|
||||
dsn := fmt.Sprintf("root:%s@tcp(127.0.0.1:%s)/%s?multiStatements=true",
|
||||
rootPwd, hostPort, dbName)
|
||||
|
||||
// Wait until MySQL accepts connections (up to 60 s).
|
||||
deadline := time.Now().Add(60 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
ping := exec.CommandContext(ctx, "docker", "exec", containerName,
|
||||
"mysqladmin", "ping", "--silent")
|
||||
if err := ping.Run(); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Verify connectivity.
|
||||
check := exec.CommandContext(ctx, "docker", "exec", containerName,
|
||||
"mysqladmin", "ping", "--silent")
|
||||
if err := check.Run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "MySQL did not become ready within 60 s; skipping tests")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
os.Setenv("PANGOLIN_TEST_DSN", dsn)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
Reference in New Issue
Block a user