fix(concurrency): 修复维度和场景创建的竞态条件
This commit is contained in:
@@ -1,82 +1,138 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using WCTDataMiner.Core.Data;
|
||||||
using WCTDataMiner.Core.Models;
|
using WCTDataMiner.Core.Models;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace WCTDataMiner.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 维度表管理服务 - 负责维度表的CRUD操作
|
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DimensionService
|
public class DimensionService
|
||||||
{
|
{
|
||||||
private readonly WctMinerDbContext _context;
|
private readonly WctMinerDbContext _context;
|
||||||
|
private readonly ILogger<DimensionService> _logger;
|
||||||
|
|
||||||
public DimensionService(WctMinerDbContext context)
|
public DimensionService(WctMinerDbContext context, ILogger<DimensionService> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或创建TX面板维度
|
/// 获取或创建TX面板维度(并发安全)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TxPanel> GetOrCreateTxPanelAsync(string name)
|
public async Task<TxPanel> GetOrCreateTxPanelAsync(string name)
|
||||||
{
|
{
|
||||||
var existing = await _context.TxPanels
|
return await GetOrCreateDimensionAsync(
|
||||||
.FirstOrDefaultAsync(p => p.Name == name);
|
() => _context.TxPanels.AsNoTracking().FirstOrDefaultAsync(p => p.Name == name),
|
||||||
|
() => new TxPanel { Name = name },
|
||||||
if (existing != null) return existing;
|
entity => _context.TxPanels.Add(entity),
|
||||||
|
name,
|
||||||
var newPanel = new TxPanel { Name = name };
|
"TX Panel"
|
||||||
_context.TxPanels.Add(newPanel);
|
);
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return newPanel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或创建TX硬件版本维度
|
/// 获取或创建TX硬件版本维度(并发安全)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TxHardware> GetOrCreateTxHardwareAsync(string version)
|
public async Task<TxHardware> GetOrCreateTxHardwareAsync(string version)
|
||||||
{
|
{
|
||||||
var existing = await _context.TxHardwares
|
return await GetOrCreateDimensionAsync(
|
||||||
.FirstOrDefaultAsync(h => h.Version == version);
|
() => _context.TxHardwares.AsNoTracking().FirstOrDefaultAsync(h => h.Version == version),
|
||||||
|
() => new TxHardware { Version = version },
|
||||||
if (existing != null) return existing;
|
entity => _context.TxHardwares.Add(entity),
|
||||||
|
version,
|
||||||
var newHardware = new TxHardware { Version = version };
|
"TX Hardware"
|
||||||
_context.TxHardwares.Add(newHardware);
|
);
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return newHardware;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或创建TX软件版本维度
|
/// 获取或创建TX软件版本维度(并发安全)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TxSoftware> GetOrCreateTxSoftwareAsync(string version)
|
public async Task<TxSoftware> GetOrCreateTxSoftwareAsync(string version)
|
||||||
{
|
{
|
||||||
var existing = await _context.TxSoftwares
|
return await GetOrCreateDimensionAsync(
|
||||||
.FirstOrDefaultAsync(s => s.Version == version);
|
() => _context.TxSoftwares.AsNoTracking().FirstOrDefaultAsync(s => s.Version == version),
|
||||||
|
() => new TxSoftware { Version = version },
|
||||||
if (existing != null) return existing;
|
entity => _context.TxSoftwares.Add(entity),
|
||||||
|
version,
|
||||||
var newSoftware = new TxSoftware { Version = version };
|
"TX Software"
|
||||||
_context.TxSoftwares.Add(newSoftware);
|
);
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
return newSoftware;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或创建RX类型维度
|
/// 获取或创建RX类型维度(并发安全)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<RxType> GetOrCreateRxTypeAsync(string name)
|
public async Task<RxType> GetOrCreateRxTypeAsync(string name)
|
||||||
{
|
{
|
||||||
var existing = await _context.RxTypes
|
return await GetOrCreateDimensionAsync(
|
||||||
.FirstOrDefaultAsync(r => r.Name == name);
|
() => _context.RxTypes.AsNoTracking().FirstOrDefaultAsync(r => r.Name == name),
|
||||||
|
() => new RxType { Name = name },
|
||||||
|
entity => _context.RxTypes.Add(entity),
|
||||||
|
name,
|
||||||
|
"RX Type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (existing != null) return existing;
|
/// <summary>
|
||||||
|
/// 通用的并发安全 GetOrCreate 模式
|
||||||
|
/// </summary>
|
||||||
|
private async Task<T> GetOrCreateDimensionAsync<T>(
|
||||||
|
Func<Task<T?>> findExisting,
|
||||||
|
Func<T> createNew,
|
||||||
|
Action<T> addToContext,
|
||||||
|
string identifier,
|
||||||
|
string dimensionName) where T : class
|
||||||
|
{
|
||||||
|
const int maxRetries = 3;
|
||||||
|
|
||||||
var newRxType = new RxType { Name = name };
|
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||||
_context.RxTypes.Add(newRxType);
|
{
|
||||||
await _context.SaveChangesAsync();
|
// 使用 AsNoTracking 避免 ChangeTracker 缓存
|
||||||
return newRxType;
|
var existing = await findExisting();
|
||||||
|
|
||||||
|
if (existing != null) return existing;
|
||||||
|
|
||||||
|
var newEntity = createNew();
|
||||||
|
addToContext(newEntity);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogDebug("创建新 {DimensionName} 维度: {Identifier}", dimensionName, identifier);
|
||||||
|
return newEntity;
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||||
|
{
|
||||||
|
// Detach 实体以避免 ChangeTracker 问题
|
||||||
|
_context.Entry(newEntity).State = EntityState.Detached;
|
||||||
|
_logger.LogDebug("{DimensionName} 并发创建冲突,重试: {Identifier}", dimensionName, identifier);
|
||||||
|
|
||||||
|
if (attempt < maxRetries - 1)
|
||||||
|
{
|
||||||
|
// 随机退避,减少并发冲突
|
||||||
|
await Task.Delay(Random.Shared.Next(5, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一次尝试:必然存在(唯一约束已生效)
|
||||||
|
var finalEntity = await findExisting();
|
||||||
|
if (finalEntity == null)
|
||||||
|
{
|
||||||
|
// 极端情况:重试全部失败但数据不存在,抛出异常
|
||||||
|
throw new InvalidOperationException($"无法获取或创建 {dimensionName} 维度: {identifier}");
|
||||||
|
}
|
||||||
|
return finalEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测 SQLite 唯一约束违反错误
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
||||||
|
{
|
||||||
|
return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx
|
||||||
|
&& sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using WCTDataMiner.Core.Data;
|
||||||
using WCTDataMiner.Core.Models;
|
using WCTDataMiner.Core.Models;
|
||||||
using WCTDataMiner.Core.Parsers;
|
using WCTDataMiner.Core.Parsers;
|
||||||
@@ -6,32 +7,98 @@ using WCTDataMiner.Core.Parsers;
|
|||||||
namespace WCTDataMiner.Core.Services;
|
namespace WCTDataMiner.Core.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试场景服务 - 负责测试场景的创建和查询
|
/// 测试场景服务 - 负责测试场景的创建和查询(并发安全版本)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ScenarioService
|
public class ScenarioService
|
||||||
{
|
{
|
||||||
private readonly WctMinerDbContext _context;
|
private readonly WctMinerDbContext _context;
|
||||||
private readonly DimensionService _dimensionService;
|
private readonly DimensionService _dimensionService;
|
||||||
|
private readonly ILogger<ScenarioService> _logger;
|
||||||
|
|
||||||
public ScenarioService(WctMinerDbContext context, DimensionService dimensionService)
|
public ScenarioService(WctMinerDbContext context, DimensionService dimensionService, ILogger<ScenarioService> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_dimensionService = dimensionService;
|
_dimensionService = dimensionService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据场景信息获取或创建测试场景
|
/// 根据场景信息获取或创建测试场景(并发安全)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
|
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("查找/创建场景: TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}",
|
||||||
|
info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType);
|
||||||
|
|
||||||
// 1. 获取或创建各维度
|
// 1. 获取或创建各维度
|
||||||
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
|
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
|
||||||
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
|
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
|
||||||
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
|
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
|
||||||
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
|
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
|
||||||
|
|
||||||
// 2. 检查场景是否已存在(利用唯一约束)
|
// 2. 并发安全的场景创建
|
||||||
var existing = await _context.TestScenarios
|
const int maxRetries = 3;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
// 使用 AsNoTracking 避免 ChangeTracker 缓存
|
||||||
|
var existing = await _context.TestScenarios
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(s =>
|
||||||
|
s.TxPanelId == txPanel.Id &&
|
||||||
|
s.TxHardwareId == txHardware.Id &&
|
||||||
|
s.TxSoftwareId == txSoftware.Id &&
|
||||||
|
s.RxTypeId == rxType.Id &&
|
||||||
|
s.TestPurpose == info.TestPurpose &&
|
||||||
|
s.TestDate == info.TestDate &&
|
||||||
|
s.TestSequence == info.TestSequence);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("场景已存在: {ScenarioId}", existing.Id);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建新场景
|
||||||
|
var scenario = new TestScenario
|
||||||
|
{
|
||||||
|
TxPanelId = txPanel.Id,
|
||||||
|
TxHardwareId = txHardware.Id,
|
||||||
|
TxSoftwareId = txSoftware.Id,
|
||||||
|
RxTypeId = rxType.Id,
|
||||||
|
TestPurpose = info.TestPurpose,
|
||||||
|
TestDate = info.TestDate,
|
||||||
|
TestSequence = info.TestSequence
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.TestScenarios.Add(scenario);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("创建新测试场景: {ScenarioId}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, TestDate={TestDate}",
|
||||||
|
scenario.Id, info.TxPanel, info.TxHardware, info.RxType, info.TestDate);
|
||||||
|
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||||
|
{
|
||||||
|
// Detach 实体以避免 ChangeTracker 问题
|
||||||
|
_context.Entry(scenario).State = EntityState.Detached;
|
||||||
|
_logger.LogDebug("场景并发创建冲突,重试: TxPanel={TxPanel}", info.TxPanel);
|
||||||
|
|
||||||
|
if (attempt < maxRetries - 1)
|
||||||
|
{
|
||||||
|
// 随机退避,减少并发冲突
|
||||||
|
await Task.Delay(Random.Shared.Next(5, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后一次尝试:必然存在(唯一约束已生效)
|
||||||
|
var finalScenario = await _context.TestScenarios
|
||||||
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(s =>
|
.FirstOrDefaultAsync(s =>
|
||||||
s.TxPanelId == txPanel.Id &&
|
s.TxPanelId == txPanel.Id &&
|
||||||
s.TxHardwareId == txHardware.Id &&
|
s.TxHardwareId == txHardware.Id &&
|
||||||
@@ -41,24 +108,12 @@ public class ScenarioService
|
|||||||
s.TestDate == info.TestDate &&
|
s.TestDate == info.TestDate &&
|
||||||
s.TestSequence == info.TestSequence);
|
s.TestSequence == info.TestSequence);
|
||||||
|
|
||||||
if (existing != null) return existing;
|
if (finalScenario == null)
|
||||||
|
|
||||||
// 3. 创建新场景
|
|
||||||
var scenario = new TestScenario
|
|
||||||
{
|
{
|
||||||
TxPanelId = txPanel.Id,
|
throw new InvalidOperationException($"无法获取或创建场景: {info.TxPanel}/{info.TxHardware}/{info.RxType}");
|
||||||
TxHardwareId = txHardware.Id,
|
}
|
||||||
TxSoftwareId = txSoftware.Id,
|
|
||||||
RxTypeId = rxType.Id,
|
|
||||||
TestPurpose = info.TestPurpose,
|
|
||||||
TestDate = info.TestDate,
|
|
||||||
TestSequence = info.TestSequence
|
|
||||||
};
|
|
||||||
|
|
||||||
_context.TestScenarios.Add(scenario);
|
return finalScenario;
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
return scenario;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -73,4 +128,13 @@ public class ScenarioService
|
|||||||
.Include(s => s.RxType)
|
.Include(s => s.RxType)
|
||||||
.FirstOrDefaultAsync(s => s.Id == scenarioId);
|
.FirstOrDefaultAsync(s => s.Id == scenarioId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测 SQLite 唯一约束违反错误
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
||||||
|
{
|
||||||
|
return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx
|
||||||
|
&& sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user