Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 19 additions & 29 deletions src/BootstrapBlazor/Dynamic/ChangeDetectionTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ static class ChangeDetectionCleanTask
#endif

private static ConcurrentDictionary<Type, bool> _cache = new();
private static CancellationTokenSource? _cancellationTokenSource;
private static PeriodicTimer? _timer;
private static Task? _cleanTask;
private static ConcurrentDictionary<ITable, byte> _tableCache = new();

Expand Down Expand Up @@ -82,46 +82,36 @@ public static void Run()
return;
}

_cleanTask = Task.Run(Clean);
// 组件变更检测清理方法执行间隔,默认 5000 毫秒,最小 500 毫秒
var interval = 5000;
if (CacheManager.Options != null)
{
interval = Math.Max(500, CacheManager.Options.ChangeDetectionTaskInterval);
}

// 在锁内创建 PeriodicTimer,与 Stop 互斥,确保其创建与销毁不会发生竞态
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(interval));
_timer = timer;
_cleanTask = Task.Run(() => Clean(timer));
}
}

private static void Stop()
{
lock (_locker)
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
// 释放定时器后 WaitForNextTickAsync 返回 false,Clean 循环正常退出
_timer?.Dispose();
_timer = null;
}
}

private static async Task Clean()
private static async Task Clean(PeriodicTimer timer)
{
_cancellationTokenSource ??= new();

// 组件变更检测清理方法执行间隔,默认 5000 毫秒,最小 500 毫秒
var interval = 5000;
if (CacheManager.Options != null)
// 每隔 interval 毫秒执行一次清理方法;调用 Stop 释放定时器后 WaitForNextTickAsync 返回 false 退出循环
while (await timer.WaitForNextTickAsync())
{
interval = Math.Max(500, CacheManager.Options.ChangeDetectionTaskInterval);
}
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(interval));
while (_cancellationTokenSource is { IsCancellationRequested: false })
{
try
{
// 每隔 interval 毫秒执行一次清理方法
await timer.WaitForNextTickAsync(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{

}
finally
{
DoTask();
}
DoTask();
}
}

Expand Down
96 changes: 80 additions & 16 deletions test/UnitTest/Dynamic/ChangeDetectionCleanTaskTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Collections.Concurrent;
using System.Reflection;
using System.Reflection.Emit;

namespace UnitTest.Dynamic;

Expand Down Expand Up @@ -55,33 +56,96 @@ public void Stop_Ok()
}

[Fact]
public async Task Clean_Ok()
public void Clean_Ok()
{
var type = typeof(Table<>).Assembly.GetType("BootstrapBlazor.Components.ChangeDetectionCleanTask");
Assert.NotNull(type);

var methodInfo = type.GetMethod("Clean", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(methodInfo);
// 先停止并清空 _cleanTask,确保 Run 会重新创建 PeriodicTimer,保证测试隔离
var stopMethodInfo = type.GetMethod("Stop", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(stopMethodInfo);
stopMethodInfo.Invoke(null, null);

// 开启 Clean 任务
methodInfo.Invoke(null, null);
var cleanTaskFieldInfo = type.GetField("_cleanTask", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(cleanTaskFieldInfo);
cleanTaskFieldInfo.SetValue(null, null);

// 反射获得 _cancellationTokenSource 值
var fieldInfo = type.GetField("_cancellationTokenSource", BindingFlags.NonPublic | BindingFlags.Static);
// 调用 Run 在锁内同步创建 PeriodicTimer 并开启 Clean 任务
var runMethodInfo = type.GetMethod("Run", BindingFlags.Public | BindingFlags.Static);
Assert.NotNull(runMethodInfo);
runMethodInfo.Invoke(null, null);

// 反射获得 _timer 值
var fieldInfo = type.GetField("_timer", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(fieldInfo);

var cancellationTokenSource = fieldInfo.GetValue(null) as CancellationTokenSource;
Assert.NotNull(cancellationTokenSource);
var timer = fieldInfo.GetValue(null) as PeriodicTimer;
Assert.NotNull(timer);

// 反射获得 Stop 方法
methodInfo = type.GetMethod("Stop", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(methodInfo);
// 调用 Stop 后 _timer 应该被释放并置空
stopMethodInfo.Invoke(null, null);

methodInfo.Invoke(null, null);
timer = fieldInfo.GetValue(null) as PeriodicTimer;
Assert.Null(timer);
}

[Fact]
public async Task CleanLoop_Ok()
{
var type = typeof(Table<>).Assembly.GetType("BootstrapBlazor.Components.ChangeDetectionCleanTask");
Assert.NotNull(type);

// _cancellationTokenSource 应该被取消
cancellationTokenSource = fieldInfo.GetValue(null) as CancellationTokenSource;
Assert.Null(cancellationTokenSource);
var cacheFieldInfo = type.GetField("_cache", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(cacheFieldInfo);

var cleanMethodInfo = type.GetMethod("Clean", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(cleanMethodInfo);

// 构造一个隶属于动态程序集 (DataTableDynamicContext.DynamicAssemblyName) 的类型,DoTask 执行后应将其移除
var dynamicType = CreateDynamicType();
var cache = new ConcurrentDictionary<Type, bool>();
cache[dynamicType] = true;

// 临时替换 _cache,避免污染 ASP.NET Core 共享缓存;测试结束后还原
var originalCache = cacheFieldInfo.GetValue(null);
cacheFieldInfo.SetValue(null, cache);
try
{
// 使用自建的高频定时器直接驱动 Clean 循环,确定性地触发 DoTask
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
var task = (Task)cleanMethodInfo.Invoke(null, new object[] { timer })!;

// 轮询等待至少一次 DoTask 执行,将动态程序集类型移除
var removed = false;
for (var i = 0; i < 100 && !removed; i++)
{
await Task.Delay(50);
removed = !cache.ContainsKey(dynamicType);
}
Assert.True(removed);

// 释放定时器后 WaitForNextTickAsync 返回 false,Clean 循环正常退出
timer.Dispose();
await task;
Assert.True(task.IsCompletedSuccessfully);
}
finally
{
cacheFieldInfo.SetValue(null, originalCache);
}
}

/// <summary>
/// 构造一个隶属于动态程序集 BootstrapBlazor_DynamicAssembly 的类型
/// 程序集名称需与 DataTableDynamicContext.DynamicAssemblyName 常量保持一致
/// </summary>
private static Type CreateDynamicType()
{
var assemblyName = new AssemblyName("BootstrapBlazor_DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
var typeBuilder = moduleBuilder.DefineType("TestDynamicType", TypeAttributes.Public);
return typeBuilder.CreateType();
}

class MockTable : ITable
Expand Down
Loading