package testcase import ( "context" "fmt" "os" "path/filepath" "time" "plp-test/internal/config" "plp-test/internal/model" "plp-test/internal/utils" "github.com/sirupsen/logrus" ) // PowerLossTest 断电测试 type PowerLossTest struct { *BaseTestCase testDir string blockSize int totalBlocks int writtenBlocks int verifiedBlocks int corruptedBlocks int blocks []*model.TestBlock recoveryTimeMs float64 powerCutInfo *model.PowerCutInfo } // NewPowerLossTest 创建断电测试 func NewPowerLossTest(cfg *config.Config, logger *logrus.Logger) *PowerLossTest { baseTest := NewBaseTestCase( "power_loss", "测试在断电情况下Open-CAS的数据完整性保护能力", cfg, logger, ) return &PowerLossTest{ BaseTestCase: baseTest, blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)), totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)), } } // Setup 设置测试环境 func (t *PowerLossTest) Setup(ctx context.Context) error { if err := t.BaseTestCase.Setup(ctx); err != nil { return err } t.setMessage("创建Open-CAS缓存实例") id := t.config.Server.CacheInstanceID nvme := t.config.Server.DevicesNVMe hdd := t.config.Server.DevicesHDD // 创建缓存实例 - 使用Write-Back模式以测试断电恢复 err := t.casManager.CreateCacheInstance(id, nvme, hdd, "wb") if err != nil { t.setStatus(StatusFailed) return fmt.Errorf("创建缓存实例失败: %v", err) } // 获取缓存设备路径 cacheDevice := fmt.Sprintf("/dev/cas%s-1", id) t.setMessage(fmt.Sprintf("格式化缓存设备 %s", cacheDevice)) // 格式化缓存设备 err = t.casManager.FormatDevice(cacheDevice, "ext4") if err != nil { t.setStatus(StatusFailed) return fmt.Errorf("格式化缓存设备失败: %v", err) } // 挂载缓存设备 mountPoint := t.config.Server.MountPoint t.setMessage(fmt.Sprintf("挂载缓存设备到 %s", mountPoint)) err = t.casManager.MountDevice(cacheDevice, mountPoint) if err != nil { t.setStatus(StatusFailed) return fmt.Errorf("挂载缓存设备失败: %v", err) } // 创建测试目录 t.testDir = filepath.Join(mountPoint, "power_loss_test") t.setMessage(fmt.Sprintf("创建测试目录 %s", t.testDir)) err = utils.CreateDirIfNotExist(t.testDir) if err != nil { t.setStatus(StatusFailed) return fmt.Errorf("创建测试目录失败: %v", err) } // 初始化测试数据 t.blocks = make([]*model.TestBlock, 0, t.totalBlocks) t.powerCutInfo = &model.PowerCutInfo{} t.setProgress(10) return nil } // Run 运行测试 func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) { t.setMessage("开始断电测试") startTime := time.Now() var totalBytesWritten int // 写入阶段 - 只写入一部分数据,断电前 t.setMessage("写入数据 (断电前)") blocksBeforePowerCut := t.totalBlocks / 2 // 写入一半的数据块 for i := 0; i < blocksBeforePowerCut; 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 := float64(i+1) / float64(t.totalBlocks) * 30 // 第一阶段占30% t.setProgress(progress) } } // 记录写入数据的时间点 t.powerCutInfo.BlocksWritten = t.writtenBlocks // 执行同步以确保部分数据已经写入到磁盘,但部分仍在缓存中 t.setMessage("执行sync同步部分数据到磁盘") _, err := utils.ExecuteCommand("sync") if err != nil { 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.setStatus(StatusCompleted) t.setMessage("断电测试完成") // 构造测试结果 result := t.getTestResult() result.BlocksWritten = t.writtenBlocks result.BlocksVerified = t.verifiedBlocks result.DataWrittenMB = utils.BytesToMB(totalBytesWritten) result.WriteSpeedMBs = utils.BytesToMB(totalBytesWritten) / time.Since(startTime).Seconds() result.Metrics = model.TestMetrics{ DataIntegrityLoss: t.corruptedBlocks, RecoveryTimeMs: t.recoveryTimeMs, } return result, nil } // Cleanup 清理测试环境 func (t *PowerLossTest) Cleanup(ctx context.Context) error { if err := t.BaseTestCase.Cleanup(ctx); err != nil { return err } t.setMessage("卸载缓存设备") err := t.casManager.UnmountDevice(t.config.Server.MountPoint) if err != nil { t.logger.Warnf("卸载缓存设备失败: %v", err) } t.setMessage("停止缓存实例") err = t.casManager.StopCacheInstance(t.config.Server.CacheInstanceID) if err != nil { t.logger.Warnf("停止缓存实例失败: %v", err) } return nil }