diff --git a/src/BootstrapBlazor/Dynamic/ChangeDetectionTask.cs b/src/BootstrapBlazor/Dynamic/ChangeDetectionTask.cs index 61fe923d086..c6f939d5645 100644 --- a/src/BootstrapBlazor/Dynamic/ChangeDetectionTask.cs +++ b/src/BootstrapBlazor/Dynamic/ChangeDetectionTask.cs @@ -21,7 +21,7 @@ static class ChangeDetectionCleanTask #endif private static ConcurrentDictionary _cache = new(); - private static CancellationTokenSource? _cancellationTokenSource; + private static PeriodicTimer? _timer; private static Task? _cleanTask; private static ConcurrentDictionary _tableCache = new(); @@ -82,7 +82,17 @@ 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)); } } @@ -90,38 +100,18 @@ 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(); } } diff --git a/test/UnitTest/Dynamic/ChangeDetectionCleanTaskTest.cs b/test/UnitTest/Dynamic/ChangeDetectionCleanTaskTest.cs index fce7fc4847a..808122cb7f0 100644 --- a/test/UnitTest/Dynamic/ChangeDetectionCleanTaskTest.cs +++ b/test/UnitTest/Dynamic/ChangeDetectionCleanTaskTest.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Reflection; +using System.Reflection.Emit; namespace UnitTest.Dynamic; @@ -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(); + 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); + } + } + + /// + /// 构造一个隶属于动态程序集 BootstrapBlazor_DynamicAssembly 的类型 + /// 程序集名称需与 DataTableDynamicContext.DynamicAssemblyName 常量保持一致 + /// + 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