build-flow.api/tests/BuildFlow.Monitoring.Tests/Sync/CountingUpsertInterceptor.cs
2026-07-24 09:22:17 +08:00

126 lines
4.2 KiB
C#

using System.Data.Common;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace BuildFlow.Monitoring.Tests.Sync;
/// <summary>
/// 统计同步记录查询和 UPSERT 命令,并可让两个旧式先查后写查询在同一点汇合以稳定复现竞争。
/// </summary>
public sealed class CountingUpsertInterceptor(int concurrentSelectParticipants = 0) : DbCommandInterceptor
{
private readonly TaskCompletionSource _selectBarrier =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private int _recordSelectCount;
private int _upsertCommandCount;
private int _parameterlessUpsertCount;
private int _selectBarrierArrivals;
/// <summary>访问同步记录表的 SELECT 命令数。</summary>
public int RecordSelectCount => Volatile.Read(ref _recordSelectCount);
/// <summary>包含 ON CONFLICT 的同步记录 INSERT 命令数。</summary>
public int UpsertCommandCount => Volatile.Read(ref _upsertCommandCount);
/// <summary>所有 UPSERT 是否都通过数据库参数传值。</summary>
public bool AllUpsertsParameterized => Volatile.Read(ref _parameterlessUpsertCount) == 0;
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
Count(command);
return result;
}
public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
Count(command);
if (concurrentSelectParticipants > 0 && IsRecordSelect(command))
{
var arrivals = Interlocked.Increment(ref _selectBarrierArrivals);
if (arrivals == concurrentSelectParticipants)
{
_selectBarrier.TrySetResult();
}
if (arrivals <= concurrentSelectParticipants)
{
await _selectBarrier.Task.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken);
}
}
return result;
}
public override InterceptionResult<int> NonQueryExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result)
{
Count(command);
return result;
}
public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
Count(command);
return ValueTask.FromResult(result);
}
public override InterceptionResult<object> ScalarExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<object> result)
{
Count(command);
return result;
}
public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<object> result,
CancellationToken cancellationToken = default)
{
Count(command);
return ValueTask.FromResult(result);
}
private void Count(DbCommand command)
{
if (IsRecordSelect(command))
{
Interlocked.Increment(ref _recordSelectCount);
}
if (!IsUpsert(command))
{
return;
}
Interlocked.Increment(ref _upsertCommandCount);
if (command.Parameters.Count == 0)
{
Interlocked.Increment(ref _parameterlessUpsertCount);
}
}
private static bool IsRecordSelect(DbCommand command)
=> command.CommandText.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
&& command.CommandText.Contains("b_synced_records", StringComparison.OrdinalIgnoreCase);
private static bool IsUpsert(DbCommand command)
=> command.CommandText.TrimStart().StartsWith("INSERT", StringComparison.OrdinalIgnoreCase)
&& command.CommandText.Contains("b_synced_records", StringComparison.OrdinalIgnoreCase)
&& command.CommandText.Contains("ON CONFLICT", StringComparison.OrdinalIgnoreCase);
}