feat: power loss
This commit is contained in:
parent
8ce3e5139f
commit
56cc4309d3
33
README.md
33
README.md
@ -58,3 +58,36 @@ go run cmd/client/main.go -server localhost:8080 -test all
|
|||||||
6. 长期稳定性测试
|
6. 长期稳定性测试
|
||||||
|
|
||||||
详细测试指标和结果分析请参阅测试报告。
|
详细测试指标和结果分析请参阅测试报告。
|
||||||
|
|
||||||
|
## 新增功能: 实时进度和数据完整性检测
|
||||||
|
|
||||||
|
### 实时进度追踪
|
||||||
|
|
||||||
|
客户端现在支持从服务器实时获取测试进度,使用流式传输技术(Server-Sent Events)实现。这使得客户端可以实时查看测试的进度和状态,而不需要频繁轮询。
|
||||||
|
|
||||||
|
### 断电数据完整性检测
|
||||||
|
|
||||||
|
系统现在支持在服务器意外断电后进行数据完整性检测,能够详细报告:
|
||||||
|
|
||||||
|
1. 有多少数据块丢失
|
||||||
|
2. 有多少数据块已损坏
|
||||||
|
3. 总共丢失了多少MB的数据
|
||||||
|
4. 提供数据恢复结果
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
#### 运行常规测试(实时进度)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/client -test power_loss -server localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 运行断电后的恢复测试
|
||||||
|
|
||||||
|
在服务器断电并重启后,执行以下命令检查数据完整性:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/client -recovery -server localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
这将执行数据完整性检测,并提供详细的数据丢失报告。
|
@ -2,15 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"plp-test/internal/config"
|
"plp-test/internal/config"
|
||||||
"plp-test/internal/model"
|
"plp-test/internal/model"
|
||||||
"plp-test/internal/utils"
|
|
||||||
|
"bufio"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -25,6 +30,7 @@ var (
|
|||||||
dataSizeMB int
|
dataSizeMB int
|
||||||
blockSize int
|
blockSize int
|
||||||
concurrent bool
|
concurrent bool
|
||||||
|
recovery bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -36,6 +42,7 @@ func init() {
|
|||||||
flag.IntVar(&dataSizeMB, "data-size", 0, "测试数据大小(MB)")
|
flag.IntVar(&dataSizeMB, "data-size", 0, "测试数据大小(MB)")
|
||||||
flag.IntVar(&blockSize, "block-size", 0, "数据块大小(KB)")
|
flag.IntVar(&blockSize, "block-size", 0, "数据块大小(KB)")
|
||||||
flag.BoolVar(&concurrent, "concurrent", false, "是否并发执行所有测试")
|
flag.BoolVar(&concurrent, "concurrent", false, "是否并发执行所有测试")
|
||||||
|
flag.BoolVar(&recovery, "recovery", false, "是否请求恢复测试")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client 客户端
|
// Client 客户端
|
||||||
@ -122,6 +129,102 @@ func (c *Client) RunTest(testType string, dataSizeMB, blockSize int) error {
|
|||||||
func (c *Client) MonitorTestStatus(testID string) error {
|
func (c *Client) MonitorTestStatus(testID string) error {
|
||||||
c.logger.Infof("监控测试状态: %s", testID)
|
c.logger.Infof("监控测试状态: %s", testID)
|
||||||
|
|
||||||
|
// 使用流式API监控进度
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
// 启动流式监控
|
||||||
|
go func() {
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s/stream?test_id=%s&client_id=%s", c.serverAddr, testID, c.clientID)
|
||||||
|
// 设置超时时间 30分钟
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("创建请求失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("发送流请求失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
errCh <- fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建事件扫描器
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
|
||||||
|
// 处理Server-Sent Events
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// 跳过空行
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数据行
|
||||||
|
if strings.HasPrefix(line, "data: ") {
|
||||||
|
data := line[6:] // 去掉 "data: " 前缀
|
||||||
|
|
||||||
|
// 解析更新数据
|
||||||
|
var update model.StreamUpdate
|
||||||
|
if err := json.Unmarshal([]byte(data), &update); err != nil {
|
||||||
|
c.logger.Warnf("解析更新数据失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理不同类型的更新
|
||||||
|
switch update.Type {
|
||||||
|
case "status":
|
||||||
|
c.logger.Infof("测试状态: 进度: %.2f%%, 阶段: %s",
|
||||||
|
update.Progress, update.CurrentPhase)
|
||||||
|
case "error":
|
||||||
|
c.logger.Errorf("测试错误: %s", update.Message)
|
||||||
|
case "completion":
|
||||||
|
c.logger.Infof("测试完成: %s", update.Message)
|
||||||
|
return
|
||||||
|
case "integrity":
|
||||||
|
if info, ok := update.Data.(map[string]interface{}); ok {
|
||||||
|
c.logger.Infof("数据完整性: 可用块: %.0f, 损坏块: %.0f, 丢失块: %.0f, 数据丢失: %.2f MB",
|
||||||
|
info["available_blocks"], info["corrupted_blocks"], info["missing_blocks"], info["data_loss_mb"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
// 如果是客户端主动断开连接,不报错
|
||||||
|
if !strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
|
errCh <- fmt.Errorf("读取流数据失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待流结束或出错
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
// 如果流模式失败,回退到轮询模式
|
||||||
|
c.logger.Warnf("流式监控失败: %v, 回退到轮询模式", err)
|
||||||
|
return c.pollTestStatus(testID)
|
||||||
|
case <-doneCh:
|
||||||
|
c.logger.Info("流式监控完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollTestStatus 使用轮询方式监控测试状态
|
||||||
|
func (c *Client) pollTestStatus(testID string) error {
|
||||||
|
c.logger.Infof("使用轮询监控测试状态: %s", testID)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// 获取测试状态
|
// 获取测试状态
|
||||||
url := fmt.Sprintf("http://%s/status?test_id=%s", c.serverAddr, testID)
|
url := fmt.Sprintf("http://%s/status?test_id=%s", c.serverAddr, testID)
|
||||||
@ -231,6 +334,82 @@ func (c *Client) RunAllTests(concurrent bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckDataIntegrity 检查断电后的数据完整性
|
||||||
|
func (c *Client) CheckDataIntegrity(testID string) error {
|
||||||
|
c.logger.Info("检查数据完整性")
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s/integrity?test_id=%s", c.serverAddr, testID)
|
||||||
|
resp, err := c.httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取数据完整性信息失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var integrity model.IntegrityInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&integrity); err != nil {
|
||||||
|
return fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Infof("数据完整性检查结果:")
|
||||||
|
c.logger.Infof(" 测试ID: %s", integrity.TestID)
|
||||||
|
c.logger.Infof(" 检查时间: %s", integrity.CheckTime.Format("2006-01-02 15:04:05"))
|
||||||
|
c.logger.Infof(" 总块数: %d", integrity.TotalBlocks)
|
||||||
|
c.logger.Infof(" 可用块数: %d (%.2f%%)", integrity.AvailableBlocks, float64(integrity.AvailableBlocks)/float64(integrity.TotalBlocks)*100)
|
||||||
|
c.logger.Infof(" 损坏块数: %d (%.2f%%)", integrity.CorruptedBlocks, float64(integrity.CorruptedBlocks)/float64(integrity.TotalBlocks)*100)
|
||||||
|
c.logger.Infof(" 丢失块数: %d (%.2f%%)", integrity.MissingBlocks, float64(integrity.MissingBlocks)/float64(integrity.TotalBlocks)*100)
|
||||||
|
c.logger.Infof(" 数据丢失: %.2f MB", integrity.DataLossMB)
|
||||||
|
c.logger.Infof(" 恢复成功: %v", integrity.RecoverySuccess)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestRecoveryTest 请求执行恢复测试
|
||||||
|
func (c *Client) RequestRecoveryTest(testType string) error {
|
||||||
|
c.logger.Infof("请求恢复测试: %s", testType)
|
||||||
|
|
||||||
|
// 准备请求数据
|
||||||
|
req := struct {
|
||||||
|
TestType string `json:"test_type"`
|
||||||
|
TestDir string `json:"test_dir,omitempty"`
|
||||||
|
}{
|
||||||
|
TestType: testType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化请求数据
|
||||||
|
reqData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化请求数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
url := fmt.Sprintf("http://%s/recovery", c.serverAddr)
|
||||||
|
resp, err := c.httpClient.Post(url, "application/json", bytes.NewBuffer(reqData))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("发送请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
|
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var testResp model.TestResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&testResp); err != nil {
|
||||||
|
return fmt.Errorf("解析响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Infof("恢复测试请求已接受,RequestID: %s", testResp.RequestID)
|
||||||
|
|
||||||
|
// 监控测试状态
|
||||||
|
return c.MonitorTestStatus(testResp.RequestID)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -249,7 +428,7 @@ func main() {
|
|||||||
level = logrus.InfoLevel
|
level = logrus.InfoLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化日志
|
// 配置日志
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
logger.SetLevel(level)
|
logger.SetLevel(level)
|
||||||
logger.SetFormatter(&logrus.TextFormatter{
|
logger.SetFormatter(&logrus.TextFormatter{
|
||||||
@ -257,20 +436,22 @@ func main() {
|
|||||||
TimestampFormat: "2006-01-02 15:04:05",
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加日志文件输出
|
||||||
|
logFile, err := os.OpenFile("client.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err == nil {
|
||||||
|
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
||||||
|
logger.SetOutput(multiWriter)
|
||||||
|
} else {
|
||||||
|
logger.Warnf("无法打开日志文件: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
logger.Infof("加载配置文件: %s", configFile)
|
|
||||||
cfg, err := config.Load(configFile)
|
cfg, err := config.Load(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("加载配置失败: %v", err)
|
logger.Fatalf("加载配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化日志文件
|
// 如果指定了timeout,覆盖配置
|
||||||
if cfg.Client.LogFile != "" {
|
|
||||||
utils.InitLogger(cfg.Client.LogFile, level)
|
|
||||||
logger = utils.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置超时时间
|
|
||||||
if timeout > 0 {
|
if timeout > 0 {
|
||||||
cfg.Client.TimeoutSec = timeout
|
cfg.Client.TimeoutSec = timeout
|
||||||
}
|
}
|
||||||
@ -283,16 +464,33 @@ func main() {
|
|||||||
logger.Fatalf("服务器健康检查失败: %v", err)
|
logger.Fatalf("服务器健康检查失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行测试
|
// 处理恢复测试
|
||||||
|
if recovery {
|
||||||
|
if testType == "" {
|
||||||
|
testType = "power_loss"
|
||||||
|
}
|
||||||
|
logger.Infof("开始执行恢复测试: %s", testType)
|
||||||
|
if err := client.RequestRecoveryTest(testType); err != nil {
|
||||||
|
logger.Fatalf("恢复测试失败: %v", err)
|
||||||
|
}
|
||||||
|
logger.Info("恢复测试完成")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行指定的测试
|
||||||
|
if testType != "" {
|
||||||
if testType == "all" {
|
if testType == "all" {
|
||||||
if err := client.RunAllTests(concurrent); err != nil {
|
if err := client.RunAllTests(concurrent); err != nil {
|
||||||
logger.Fatalf("运行所有测试失败: %v", err)
|
logger.Fatalf("执行所有测试失败: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := client.RunTest(testType, dataSizeMB, blockSize); err != nil {
|
if err := client.RunTest(testType, dataSizeMB, blockSize); err != nil {
|
||||||
logger.Fatalf("运行测试 %s 失败: %v", testType, err)
|
logger.Fatalf("执行测试 %s 失败: %v", testType, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.Fatal("未指定测试类型,请使用 -test 参数")
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("所有测试已完成")
|
logger.Info("所有测试完成")
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,10 @@ type TestRunner struct {
|
|||||||
testsMu sync.RWMutex
|
testsMu sync.RWMutex
|
||||||
testResult map[string]*model.TestResult
|
testResult map[string]*model.TestResult
|
||||||
resultMu sync.RWMutex
|
resultMu sync.RWMutex
|
||||||
|
streams map[string]map[string]http.ResponseWriter
|
||||||
|
streamsMu sync.RWMutex
|
||||||
|
integrityInfo map[string]*model.IntegrityInfo
|
||||||
|
integrityMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestRunner 创建测试运行器
|
// NewTestRunner 创建测试运行器
|
||||||
@ -49,6 +53,8 @@ func NewTestRunner(cfg *config.Config, logger *logrus.Logger) *TestRunner {
|
|||||||
factory: testcase.NewTestCaseFactory(cfg, logger),
|
factory: testcase.NewTestCaseFactory(cfg, logger),
|
||||||
tests: make(map[string]testcase.TestCase),
|
tests: make(map[string]testcase.TestCase),
|
||||||
testResult: make(map[string]*model.TestResult),
|
testResult: make(map[string]*model.TestResult),
|
||||||
|
streams: make(map[string]map[string]http.ResponseWriter),
|
||||||
|
integrityInfo: make(map[string]*model.IntegrityInfo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,18 +81,42 @@ func (r *TestRunner) RunTest(testType string) (*model.TestResult, error) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// 发送测试开始状态更新
|
||||||
|
r.sendStatusUpdate(test)
|
||||||
|
|
||||||
// 设置测试环境
|
// 设置测试环境
|
||||||
r.logger.Info("设置测试环境")
|
r.logger.Info("设置测试环境")
|
||||||
if err := test.Setup(ctx); err != nil {
|
if err := test.Setup(ctx); err != nil {
|
||||||
r.logger.Errorf("设置测试环境失败: %v", err)
|
r.logger.Errorf("设置测试环境失败: %v", err)
|
||||||
|
r.sendErrorUpdate(testID, fmt.Sprintf("设置测试环境失败: %v", err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动状态监控协程
|
||||||
|
statusDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(statusDone)
|
||||||
|
ticker := time.NewTicker(200 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// 发送状态更新
|
||||||
|
r.sendStatusUpdate(test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 运行测试
|
// 运行测试
|
||||||
r.logger.Info("运行测试")
|
r.logger.Info("运行测试")
|
||||||
result, err := test.Run(ctx)
|
result, err := test.Run(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Errorf("测试运行失败: %v", err)
|
r.logger.Errorf("测试运行失败: %v", err)
|
||||||
|
r.sendErrorUpdate(testID, fmt.Sprintf("测试运行失败: %v", err))
|
||||||
|
|
||||||
// 尝试清理
|
// 尝试清理
|
||||||
cleanupErr := test.Cleanup(ctx)
|
cleanupErr := test.Cleanup(ctx)
|
||||||
if cleanupErr != nil {
|
if cleanupErr != nil {
|
||||||
@ -99,9 +129,14 @@ func (r *TestRunner) RunTest(testType string) (*model.TestResult, error) {
|
|||||||
r.logger.Info("清理测试环境")
|
r.logger.Info("清理测试环境")
|
||||||
if err := test.Cleanup(ctx); err != nil {
|
if err := test.Cleanup(ctx); err != nil {
|
||||||
r.logger.Errorf("测试清理失败: %v", err)
|
r.logger.Errorf("测试清理失败: %v", err)
|
||||||
|
r.sendErrorUpdate(testID, fmt.Sprintf("测试清理失败: %v", err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止状态监控
|
||||||
|
cancel()
|
||||||
|
<-statusDone
|
||||||
|
|
||||||
// 存储测试结果
|
// 存储测试结果
|
||||||
r.resultMu.Lock()
|
r.resultMu.Lock()
|
||||||
r.testResult[testID] = result
|
r.testResult[testID] = result
|
||||||
@ -112,10 +147,52 @@ func (r *TestRunner) RunTest(testType string) (*model.TestResult, error) {
|
|||||||
delete(r.tests, testID)
|
delete(r.tests, testID)
|
||||||
r.testsMu.Unlock()
|
r.testsMu.Unlock()
|
||||||
|
|
||||||
|
// 发送完成通知
|
||||||
|
r.sendCompletionUpdate(testID, result)
|
||||||
|
|
||||||
r.logger.Infof("测试 %s 完成", testType)
|
r.logger.Infof("测试 %s 完成", testType)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendStatusUpdate 发送状态更新
|
||||||
|
func (r *TestRunner) sendStatusUpdate(test testcase.TestCase) {
|
||||||
|
status := test.Status()
|
||||||
|
update := model.StreamUpdate{
|
||||||
|
Type: "status",
|
||||||
|
TestID: status.TestID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Progress: status.Progress,
|
||||||
|
CurrentPhase: status.CurrentPhase,
|
||||||
|
Message: status.Message,
|
||||||
|
Data: status,
|
||||||
|
}
|
||||||
|
r.SendStreamUpdate(status.TestID, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendErrorUpdate 发送错误更新
|
||||||
|
func (r *TestRunner) sendErrorUpdate(testID, message string) {
|
||||||
|
update := model.StreamUpdate{
|
||||||
|
Type: "error",
|
||||||
|
TestID: testID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
r.SendStreamUpdate(testID, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendCompletionUpdate 发送完成更新
|
||||||
|
func (r *TestRunner) sendCompletionUpdate(testID string, result *model.TestResult) {
|
||||||
|
update := model.StreamUpdate{
|
||||||
|
Type: "completion",
|
||||||
|
TestID: testID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Progress: 100,
|
||||||
|
Message: "测试完成",
|
||||||
|
Data: result,
|
||||||
|
}
|
||||||
|
r.SendStreamUpdate(testID, update)
|
||||||
|
}
|
||||||
|
|
||||||
// GetTestStatus 获取测试状态
|
// GetTestStatus 获取测试状态
|
||||||
func (r *TestRunner) GetTestStatus(testID string) *model.TestStatus {
|
func (r *TestRunner) GetTestStatus(testID string) *model.TestStatus {
|
||||||
r.testsMu.RLock()
|
r.testsMu.RLock()
|
||||||
@ -139,6 +216,72 @@ func (r *TestRunner) GetAllTestStatus() []*model.TestStatus {
|
|||||||
return statuses
|
return statuses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterStream 注册流式连接
|
||||||
|
func (r *TestRunner) RegisterStream(testID, clientID string, w http.ResponseWriter) {
|
||||||
|
r.streamsMu.Lock()
|
||||||
|
defer r.streamsMu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := r.streams[testID]; !ok {
|
||||||
|
r.streams[testID] = make(map[string]http.ResponseWriter)
|
||||||
|
}
|
||||||
|
r.streams[testID][clientID] = w
|
||||||
|
r.logger.Infof("客户端 %s 已连接到测试 %s 的流", clientID, testID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterStream 注销流式连接
|
||||||
|
func (r *TestRunner) UnregisterStream(testID, clientID string) {
|
||||||
|
r.streamsMu.Lock()
|
||||||
|
defer r.streamsMu.Unlock()
|
||||||
|
|
||||||
|
if clients, ok := r.streams[testID]; ok {
|
||||||
|
delete(clients, clientID)
|
||||||
|
r.logger.Infof("客户端 %s 已断开与测试 %s 的流连接", clientID, testID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendStreamUpdate 发送流式更新
|
||||||
|
func (r *TestRunner) SendStreamUpdate(testID string, update interface{}) {
|
||||||
|
r.streamsMu.RLock()
|
||||||
|
defer r.streamsMu.RUnlock()
|
||||||
|
|
||||||
|
clients, ok := r.streams[testID]
|
||||||
|
if !ok || len(clients) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(update)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Errorf("无法序列化流更新: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for clientID, w := range clients {
|
||||||
|
// 使用Server-Sent Events格式
|
||||||
|
_, err := fmt.Fprintf(w, "data: %s\n\n", data)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warnf("向客户端 %s 发送更新失败: %v", clientID, err)
|
||||||
|
} else {
|
||||||
|
if f, ok := w.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveIntegrityInfo 保存完整性信息
|
||||||
|
func (r *TestRunner) SaveIntegrityInfo(testID string, info *model.IntegrityInfo) {
|
||||||
|
r.integrityMu.Lock()
|
||||||
|
defer r.integrityMu.Unlock()
|
||||||
|
r.integrityInfo[testID] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIntegrityInfo 获取完整性信息
|
||||||
|
func (r *TestRunner) GetIntegrityInfo(testID string) *model.IntegrityInfo {
|
||||||
|
r.integrityMu.RLock()
|
||||||
|
defer r.integrityMu.RUnlock()
|
||||||
|
return r.integrityInfo[testID]
|
||||||
|
}
|
||||||
|
|
||||||
// StartServer 启动HTTP服务器
|
// StartServer 启动HTTP服务器
|
||||||
func StartServer(cfg *config.Config, runner *TestRunner, logger *logrus.Logger) *http.Server {
|
func StartServer(cfg *config.Config, runner *TestRunner, logger *logrus.Logger) *http.Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@ -217,6 +360,152 @@ func StartServer(cfg *config.Config, runner *TestRunner, logger *logrus.Logger)
|
|||||||
json.NewEncoder(w).Encode(status)
|
json.NewEncoder(w).Encode(status)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 新增: 实时数据进度流式API
|
||||||
|
mux.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testID := r.URL.Query().Get("test_id")
|
||||||
|
if testID == "" {
|
||||||
|
http.Error(w, "Missing test_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头,支持SSE (Server-Sent Events)
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
// 创建完成通道
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
defer close(doneCh)
|
||||||
|
|
||||||
|
// 注册客户端连接
|
||||||
|
clientID := r.URL.Query().Get("client_id")
|
||||||
|
runner.RegisterStream(testID, clientID, w)
|
||||||
|
defer runner.UnregisterStream(testID, clientID)
|
||||||
|
|
||||||
|
// 保持连接直到客户端断开
|
||||||
|
select {
|
||||||
|
case <-r.Context().Done():
|
||||||
|
runner.logger.Infof("connection closed by client %s", clientID)
|
||||||
|
return
|
||||||
|
case <-doneCh:
|
||||||
|
runner.logger.Infof("connection closed by server for client %s", clientID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 新增: 数据完整性检测API
|
||||||
|
mux.HandleFunc("/integrity", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testID := r.URL.Query().Get("test_id")
|
||||||
|
if testID == "" {
|
||||||
|
http.Error(w, "Missing test_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取测试的数据完整性信息
|
||||||
|
integrityInfo := runner.GetIntegrityInfo(testID)
|
||||||
|
if integrityInfo == nil {
|
||||||
|
http.Error(w, "Integrity info not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(integrityInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 新增: 恢复测试API,用于断电测试后的恢复与校验
|
||||||
|
mux.HandleFunc("/recovery", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
TestType string `json:"test_type"`
|
||||||
|
TestDir string `json:"test_dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("收到恢复测试请求: %+v", req)
|
||||||
|
|
||||||
|
// 创建恢复测试实例
|
||||||
|
test, err := runner.factory.CreateTestCase(req.TestType)
|
||||||
|
if err != nil || test == nil {
|
||||||
|
http.Error(w, fmt.Sprintf("无法创建测试实例: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取测试ID
|
||||||
|
testID := test.Status().TestID
|
||||||
|
|
||||||
|
// 执行恢复和数据完整性检查
|
||||||
|
go func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// 设置测试环境
|
||||||
|
logger.Info("设置恢复测试环境")
|
||||||
|
if err := test.Setup(ctx); err != nil {
|
||||||
|
logger.Errorf("设置恢复测试环境失败: %v", err)
|
||||||
|
runner.sendErrorUpdate(testID, fmt.Sprintf("设置恢复测试环境失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据完整性检查
|
||||||
|
logger.Info("执行数据完整性检查")
|
||||||
|
runner.sendStatusUpdate(test)
|
||||||
|
|
||||||
|
// 检查并获取数据完整性信息
|
||||||
|
if powerTest, ok := test.(*testcase.PowerLossTest); ok {
|
||||||
|
integrityInfo := powerTest.CheckIntegrity()
|
||||||
|
|
||||||
|
// 保存完整性信息
|
||||||
|
runner.SaveIntegrityInfo(testID, integrityInfo)
|
||||||
|
|
||||||
|
// 发送完整性信息
|
||||||
|
update := model.StreamUpdate{
|
||||||
|
Type: "integrity",
|
||||||
|
TestID: testID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Message: "数据完整性检查完成",
|
||||||
|
Data: integrityInfo,
|
||||||
|
}
|
||||||
|
runner.SendStreamUpdate(testID, update)
|
||||||
|
|
||||||
|
logger.Infof("恢复测试完成: 丢失数据: %.2f MB", integrityInfo.DataLossMB)
|
||||||
|
} else {
|
||||||
|
logger.Error("不是断电测试实例,无法执行数据完整性检查")
|
||||||
|
runner.sendErrorUpdate(testID, "不是断电测试实例,无法执行数据完整性检查")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理测试环境
|
||||||
|
logger.Info("清理恢复测试环境")
|
||||||
|
if err := test.Cleanup(ctx); err != nil {
|
||||||
|
logger.Errorf("清理恢复测试环境失败: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 返回接受响应
|
||||||
|
resp := model.TestResponse{
|
||||||
|
RequestID: testID,
|
||||||
|
Status: "accepted",
|
||||||
|
Message: "恢复测试已接受并开始执行",
|
||||||
|
ServerTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
})
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
addr := fmt.Sprintf("%s:%d", cfg.Server.ListenAddr, cfg.Server.Port)
|
addr := fmt.Sprintf("%s:%d", cfg.Server.ListenAddr, cfg.Server.Port)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
|
@ -114,3 +114,38 @@ type HealthStatus struct {
|
|||||||
MemoryUsage float64 `json:"memory_usage"`
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IntegrityInfo 表示数据完整性信息
|
||||||
|
type IntegrityInfo struct {
|
||||||
|
TestID string `json:"test_id"`
|
||||||
|
TestType string `json:"test_type"`
|
||||||
|
CheckTime time.Time `json:"check_time"`
|
||||||
|
TotalBlocks int `json:"total_blocks"`
|
||||||
|
ExpectedBlocks int `json:"expected_blocks"`
|
||||||
|
AvailableBlocks int `json:"available_blocks"`
|
||||||
|
CorruptedBlocks int `json:"corrupted_blocks"`
|
||||||
|
MissingBlocks int `json:"missing_blocks"`
|
||||||
|
DataLossMB float64 `json:"data_loss_mb"`
|
||||||
|
RecoverySuccess bool `json:"recovery_success"`
|
||||||
|
RecoveryDuration float64 `json:"recovery_duration_ms"`
|
||||||
|
BlocksMap map[int]BlockStatus `json:"blocks_map,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockStatus 表示数据块状态
|
||||||
|
type BlockStatus struct {
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Corrupted bool `json:"corrupted"`
|
||||||
|
Checksum string `json:"checksum,omitempty"`
|
||||||
|
FilePath string `json:"file_path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamUpdate 表示流式更新的数据
|
||||||
|
type StreamUpdate struct {
|
||||||
|
Type string `json:"type"` // "status", "progress", "integrity", "error"
|
||||||
|
TestID string `json:"test_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Progress float64 `json:"progress,omitempty"`
|
||||||
|
CurrentPhase string `json:"current_phase,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
@ -2,9 +2,13 @@ package testcase
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"plp-test/internal/config"
|
"plp-test/internal/config"
|
||||||
@ -26,6 +30,9 @@ type PowerLossTest struct {
|
|||||||
blocks []*model.TestBlock
|
blocks []*model.TestBlock
|
||||||
recoveryTimeMs float64
|
recoveryTimeMs float64
|
||||||
powerCutInfo *model.PowerCutInfo
|
powerCutInfo *model.PowerCutInfo
|
||||||
|
integrityInfo *model.IntegrityInfo
|
||||||
|
blocksMu sync.RWMutex // 保护数据块访问
|
||||||
|
blocksMap map[int]model.BlockStatus // 数据块状态映射
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPowerLossTest 创建断电测试
|
// NewPowerLossTest 创建断电测试
|
||||||
@ -39,8 +46,9 @@ func NewPowerLossTest(cfg *config.Config, logger *logrus.Logger) *PowerLossTest
|
|||||||
|
|
||||||
return &PowerLossTest{
|
return &PowerLossTest{
|
||||||
BaseTestCase: baseTest,
|
BaseTestCase: baseTest,
|
||||||
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
blockSize: utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
|
blocksMap: make(map[int]model.BlockStatus),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +103,21 @@ func (t *PowerLossTest) Setup(ctx context.Context) error {
|
|||||||
t.blocks = make([]*model.TestBlock, 0, t.totalBlocks)
|
t.blocks = make([]*model.TestBlock, 0, t.totalBlocks)
|
||||||
t.powerCutInfo = &model.PowerCutInfo{}
|
t.powerCutInfo = &model.PowerCutInfo{}
|
||||||
|
|
||||||
|
// 初始化完整性信息
|
||||||
|
t.integrityInfo = &model.IntegrityInfo{
|
||||||
|
TestID: t.testID,
|
||||||
|
TestType: t.name,
|
||||||
|
CheckTime: time.Time{},
|
||||||
|
TotalBlocks: t.totalBlocks,
|
||||||
|
ExpectedBlocks: t.totalBlocks,
|
||||||
|
AvailableBlocks: 0,
|
||||||
|
CorruptedBlocks: 0,
|
||||||
|
MissingBlocks: 0,
|
||||||
|
DataLossMB: 0,
|
||||||
|
RecoverySuccess: false,
|
||||||
|
BlocksMap: make(map[int]model.BlockStatus),
|
||||||
|
}
|
||||||
|
|
||||||
t.setProgress(10)
|
t.setProgress(10)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -105,18 +128,17 @@ func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
var totalBytesWritten int
|
var totalBytesWritten int
|
||||||
|
|
||||||
// 写入阶段 - 只写入一部分数据,断电前
|
// 第一阶段 - 持续写入数据,直到手动断电
|
||||||
t.setMessage("写入数据 (断电前)")
|
t.setMessage("写入数据 (请在适当时手动断电)")
|
||||||
blocksBeforePowerCut := t.totalBlocks / 2 // 写入一半的数据块
|
|
||||||
|
|
||||||
for i := 0; i < blocksBeforePowerCut; i++ {
|
for i := 0; i < t.totalBlocks; i++ {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
t.setStatus(StatusAborted)
|
t.setStatus(StatusAborted)
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
// 生成随机数据
|
// 生成随机数据
|
||||||
data, err := utils.GenerateRandomData(t.blockSize)
|
data, err := utils.GenerateRandomData(utils.KBToBytes(float64(t.blockSize)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.setStatus(StatusFailed)
|
t.setStatus(StatusFailed)
|
||||||
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
||||||
@ -124,11 +146,32 @@ func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) {
|
|||||||
|
|
||||||
// 创建测试数据块
|
// 创建测试数据块
|
||||||
block := model.NewTestBlock(data, i)
|
block := model.NewTestBlock(data, i)
|
||||||
|
|
||||||
|
// 添加到数据块列表
|
||||||
|
t.blocksMu.Lock()
|
||||||
t.blocks = append(t.blocks, block)
|
t.blocks = append(t.blocks, block)
|
||||||
|
|
||||||
|
// 记录数据块状态
|
||||||
|
blockStatus := model.BlockStatus{
|
||||||
|
Available: true,
|
||||||
|
Corrupted: false,
|
||||||
|
Checksum: block.Checksum,
|
||||||
|
FilePath: fmt.Sprintf("block_%d.dat", i),
|
||||||
|
}
|
||||||
|
t.blocksMap[i] = blockStatus
|
||||||
|
t.blocksMu.Unlock()
|
||||||
|
|
||||||
// 写入文件
|
// 写入文件
|
||||||
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
||||||
err = os.WriteFile(filePath, data, 0644)
|
// direct IO
|
||||||
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|syscall.O_DIRECT, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.setStatus(StatusFailed)
|
||||||
|
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.setStatus(StatusFailed)
|
t.setStatus(StatusFailed)
|
||||||
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
|
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
|
||||||
@ -137,182 +180,40 @@ func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) {
|
|||||||
t.writtenBlocks++
|
t.writtenBlocks++
|
||||||
totalBytesWritten += len(data)
|
totalBytesWritten += len(data)
|
||||||
|
|
||||||
// 更新进度
|
// 每写入一定数量的块后执行同步
|
||||||
progress := float64(i+1) / float64(t.totalBlocks) * 30 // 第一阶段占30%
|
if i > 0 && i%10 == 0 {
|
||||||
t.setProgress(progress)
|
t.setMessage(fmt.Sprintf("同步数据到磁盘 (已写入 %d/%d 块)", i, t.totalBlocks))
|
||||||
|
_, err := utils.ExecuteCommand("sync")
|
||||||
|
if err != nil {
|
||||||
|
t.logger.Warnf("执行sync命令失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录写入数据的时间点
|
// 更新进度
|
||||||
|
progress := float64(i+1) / float64(t.totalBlocks) * 100
|
||||||
|
t.setProgress(progress)
|
||||||
|
|
||||||
|
// 每写入一定数量的块后暂停一下,给用户断电的机会
|
||||||
|
if i > 0 && i%100 == 0 {
|
||||||
|
t.setMessage(fmt.Sprintf("已写入 %d/%d 块数据, 共 %.2f MB", i, t.totalBlocks, float64(i*t.blockSize)/(1024*1024)))
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录写入数据的信息
|
||||||
t.powerCutInfo.BlocksWritten = t.writtenBlocks
|
t.powerCutInfo.BlocksWritten = t.writtenBlocks
|
||||||
|
|
||||||
// 执行同步以确保部分数据已经写入到磁盘,但部分仍在缓存中
|
// 完成所有数据写入后,同步到磁盘
|
||||||
t.setMessage("执行sync同步部分数据到磁盘")
|
t.setMessage("同步所有数据到磁盘")
|
||||||
_, err := utils.ExecuteCommand("sync")
|
_, err := utils.ExecuteCommand("sync")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.logger.Warnf("执行sync命令失败: %v", err)
|
t.logger.Warnf("执行sync命令失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再写入一些数据但不同步,保证有缓存中的数据
|
|
||||||
t.setMessage("写入额外数据到缓存中 (这些数据可能会在断电后丢失)")
|
|
||||||
additionalBlocks := t.totalBlocks / 4 // 额外写入1/4的数据块
|
|
||||||
|
|
||||||
for i := blocksBeforePowerCut; i < blocksBeforePowerCut+additionalBlocks; i++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
t.setStatus(StatusAborted)
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
// 生成随机数据
|
|
||||||
data, err := utils.GenerateRandomData(t.blockSize)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建测试数据块
|
|
||||||
block := model.NewTestBlock(data, i)
|
|
||||||
t.blocks = append(t.blocks, block)
|
|
||||||
|
|
||||||
// 写入文件
|
|
||||||
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
|
||||||
err = os.WriteFile(filePath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.writtenBlocks++
|
|
||||||
totalBytesWritten += len(data)
|
|
||||||
|
|
||||||
// 更新进度
|
|
||||||
progress := 30 + float64(i-blocksBeforePowerCut+1)/float64(additionalBlocks)*10 // 额外写入占10%
|
|
||||||
t.setProgress(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟断电
|
|
||||||
t.setMessage("模拟断电...")
|
|
||||||
t.powerCutInfo.Timestamp = time.Now()
|
|
||||||
err = t.casManager.SimulatePowerCut(t.config.Server.CacheInstanceID)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("模拟断电失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟等待一段时间,就像真正断电后的重启
|
|
||||||
t.setMessage("模拟系统重启中...")
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
// 恢复阶段
|
|
||||||
t.setMessage("恢复阶段")
|
|
||||||
recoveryStart := time.Now()
|
|
||||||
|
|
||||||
// 重新创建缓存实例
|
|
||||||
id := t.config.Server.CacheInstanceID
|
|
||||||
nvme := t.config.Server.DevicesNVMe
|
|
||||||
hdd := t.config.Server.DevicesHDD
|
|
||||||
|
|
||||||
// 尝试修复/加载现有缓存
|
|
||||||
t.setMessage("尝试加载和修复缓存")
|
|
||||||
err = t.casManager.RepairCache(id, nvme)
|
|
||||||
if err != nil {
|
|
||||||
// 如果修复失败,尝试重新创建
|
|
||||||
t.logger.Warnf("修复缓存失败,尝试重新创建: %v", err)
|
|
||||||
err = t.casManager.CreateCacheInstance(id, nvme, hdd, "wb")
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("断电后重新创建缓存实例失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取缓存设备路径
|
|
||||||
cacheDevice := fmt.Sprintf("/dev/cas%s-1", id)
|
|
||||||
|
|
||||||
// 重新挂载缓存设备
|
|
||||||
mountPoint := t.config.Server.MountPoint
|
|
||||||
t.setMessage(fmt.Sprintf("重新挂载缓存设备到 %s", mountPoint))
|
|
||||||
err = t.casManager.MountDevice(cacheDevice, mountPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("断电后重新挂载缓存设备失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录恢复时间
|
|
||||||
t.recoveryTimeMs = float64(time.Since(recoveryStart).Milliseconds())
|
|
||||||
t.setProgress(50)
|
|
||||||
|
|
||||||
// 验证阶段 - 检查断电前写入的数据是否完整
|
|
||||||
t.setMessage("验证阶段 - 检查数据完整性")
|
|
||||||
t.corruptedBlocks = 0
|
|
||||||
|
|
||||||
for i, block := range t.blocks {
|
|
||||||
// 检查文件是否存在
|
|
||||||
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
|
||||||
if !utils.FileExists(filePath) {
|
|
||||||
t.logger.Warnf("文件 %s 在断电后丢失", filePath)
|
|
||||||
t.corruptedBlocks++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取文件数据
|
|
||||||
data, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Warnf("读取文件 %s 失败: %v", filePath, err)
|
|
||||||
t.corruptedBlocks++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证数据完整性
|
|
||||||
if string(data) != string(block.Data) {
|
|
||||||
t.logger.Warnf("文件 %s 数据损坏", filePath)
|
|
||||||
t.corruptedBlocks++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.verifiedBlocks++
|
|
||||||
|
|
||||||
// 更新进度
|
|
||||||
progress := 50 + float64(i+1)/float64(len(t.blocks))*40 // 验证占40%
|
|
||||||
t.setProgress(progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入断电后的额外数据
|
|
||||||
t.setMessage("断电后写入额外数据")
|
|
||||||
for i := t.writtenBlocks; i < t.totalBlocks; i++ {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
t.setStatus(StatusAborted)
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
// 生成随机数据
|
|
||||||
data, err := utils.GenerateRandomData(t.blockSize)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入文件
|
|
||||||
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
|
||||||
err = os.WriteFile(filePath, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.setStatus(StatusFailed)
|
|
||||||
return nil, fmt.Errorf("断电后写入文件 %s 失败: %v", filePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新进度
|
|
||||||
progress := 90 + float64(i-t.writtenBlocks+1)/float64(t.totalBlocks-t.writtenBlocks)*10 // 最后10%
|
|
||||||
t.setProgress(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录断电信息
|
|
||||||
t.powerCutInfo.RecoverySuccess = t.corruptedBlocks == 0
|
|
||||||
t.powerCutInfo.DataLossMB = utils.BytesToMB(t.corruptedBlocks * t.blockSize)
|
|
||||||
|
|
||||||
t.setProgress(100)
|
t.setProgress(100)
|
||||||
t.setStatus(StatusCompleted)
|
t.setStatus(StatusCompleted)
|
||||||
t.setMessage("断电测试完成")
|
t.setMessage("数据写入完成")
|
||||||
|
|
||||||
// 构造测试结果
|
// 构造测试结果
|
||||||
result := t.getTestResult()
|
result := t.getTestResult()
|
||||||
@ -328,6 +229,86 @@ func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckIntegrity 检查数据完整性
|
||||||
|
func (t *PowerLossTest) CheckIntegrity() *model.IntegrityInfo {
|
||||||
|
t.setMessage("开始检查数据完整性")
|
||||||
|
t.integrityInfo.CheckTime = time.Now()
|
||||||
|
|
||||||
|
// 重置计数器
|
||||||
|
t.integrityInfo.AvailableBlocks = 0
|
||||||
|
t.integrityInfo.CorruptedBlocks = 0
|
||||||
|
t.integrityInfo.MissingBlocks = 0
|
||||||
|
|
||||||
|
// 为所有块创建状态记录
|
||||||
|
for i := 0; i < t.totalBlocks; i++ {
|
||||||
|
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
|
||||||
|
|
||||||
|
if !utils.FileExists(filePath) {
|
||||||
|
// 文件不存在
|
||||||
|
t.integrityInfo.MissingBlocks++
|
||||||
|
t.integrityInfo.BlocksMap[i] = model.BlockStatus{
|
||||||
|
Available: false,
|
||||||
|
Corrupted: false,
|
||||||
|
FilePath: fmt.Sprintf("block_%d.dat", i),
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件数据
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
// 无法读取文件
|
||||||
|
t.integrityInfo.CorruptedBlocks++
|
||||||
|
t.integrityInfo.BlocksMap[i] = model.BlockStatus{
|
||||||
|
Available: true,
|
||||||
|
Corrupted: true,
|
||||||
|
FilePath: fmt.Sprintf("block_%d.dat", i),
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算和验证校验和
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
checksum := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
var blockChecksum string
|
||||||
|
t.blocksMu.RLock()
|
||||||
|
if i < len(t.blocks) && t.blocks[i] != nil {
|
||||||
|
blockChecksum = t.blocks[i].Checksum
|
||||||
|
}
|
||||||
|
t.blocksMu.RUnlock()
|
||||||
|
|
||||||
|
if blockChecksum != "" && checksum != blockChecksum {
|
||||||
|
// 数据损坏
|
||||||
|
t.integrityInfo.CorruptedBlocks++
|
||||||
|
t.integrityInfo.BlocksMap[i] = model.BlockStatus{
|
||||||
|
Available: true,
|
||||||
|
Corrupted: true,
|
||||||
|
Checksum: checksum,
|
||||||
|
FilePath: fmt.Sprintf("block_%d.dat", i),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 数据完好
|
||||||
|
t.integrityInfo.AvailableBlocks++
|
||||||
|
t.integrityInfo.BlocksMap[i] = model.BlockStatus{
|
||||||
|
Available: true,
|
||||||
|
Corrupted: false,
|
||||||
|
Checksum: checksum,
|
||||||
|
FilePath: fmt.Sprintf("block_%d.dat", i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算数据丢失量
|
||||||
|
t.integrityInfo.DataLossMB = utils.BytesToMB((t.integrityInfo.MissingBlocks + t.integrityInfo.CorruptedBlocks) * t.blockSize)
|
||||||
|
t.integrityInfo.RecoverySuccess = t.integrityInfo.CorruptedBlocks == 0 && t.integrityInfo.MissingBlocks == 0
|
||||||
|
|
||||||
|
t.setMessage(fmt.Sprintf("数据完整性检查完成: %d 个块正常, %d 个块丢失, %d 个块损坏",
|
||||||
|
t.integrityInfo.AvailableBlocks, t.integrityInfo.MissingBlocks, t.integrityInfo.CorruptedBlocks))
|
||||||
|
|
||||||
|
return t.integrityInfo
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup 清理测试环境
|
// Cleanup 清理测试环境
|
||||||
func (t *PowerLossTest) Cleanup(ctx context.Context) error {
|
func (t *PowerLossTest) Cleanup(ctx context.Context) error {
|
||||||
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
|
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
|
||||||
|
@ -40,8 +40,8 @@ func NewRandomWriteTest(cfg *config.Config, logger *logrus.Logger) *RandomWriteT
|
|||||||
|
|
||||||
return &RandomWriteTest{
|
return &RandomWriteTest{
|
||||||
BaseTestCase: baseTest,
|
BaseTestCase: baseTest,
|
||||||
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
blockSize: utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
blocks: make(map[int]*model.TestBlock),
|
blocks: make(map[int]*model.TestBlock),
|
||||||
writeSequence: make([]int, 0),
|
writeSequence: make([]int, 0),
|
||||||
writeLatencies: make([]float64, 0),
|
writeLatencies: make([]float64, 0),
|
||||||
@ -128,7 +128,7 @@ func (t *RandomWriteTest) Run(ctx context.Context) (*model.TestResult, error) {
|
|||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
// 生成随机数据
|
// 生成随机数据
|
||||||
data, err := utils.GenerateRandomData(t.blockSize)
|
data, err := utils.GenerateRandomData(t.blockSize * 1024)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.setStatus(StatusFailed)
|
t.setStatus(StatusFailed)
|
||||||
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
||||||
|
@ -38,8 +38,8 @@ func NewSequentialWriteTest(cfg *config.Config, logger *logrus.Logger) *Sequenti
|
|||||||
|
|
||||||
return &SequentialWriteTest{
|
return &SequentialWriteTest{
|
||||||
BaseTestCase: baseTest,
|
BaseTestCase: baseTest,
|
||||||
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
blockSize: utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
|
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.KBToBytes(float64(cfg.Test.BlockSize)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ func (t *SequentialWriteTest) Run(ctx context.Context) (*model.TestResult, error
|
|||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
// 生成随机数据
|
// 生成随机数据
|
||||||
data, err := utils.GenerateRandomData(t.blockSize)
|
data, err := utils.GenerateRandomData(t.blockSize * 1024)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.setStatus(StatusFailed)
|
t.setStatus(StatusFailed)
|
||||||
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
return nil, fmt.Errorf("生成随机数据失败: %v", err)
|
||||||
|
@ -57,6 +57,11 @@ func MBToBytes(mb float64) int {
|
|||||||
return int(mb * 1024 * 1024)
|
return int(mb * 1024 * 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KBToBytes 将KB转换为字节
|
||||||
|
func KBToBytes(kb float64) int {
|
||||||
|
return int(kb * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
// FormatDuration 格式化持续时间
|
// FormatDuration 格式化持续时间
|
||||||
func FormatDuration(d time.Duration) string {
|
func FormatDuration(d time.Duration) string {
|
||||||
if d < time.Minute {
|
if d < time.Minute {
|
||||||
|
Loading…
Reference in New Issue
Block a user