diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 76a560f5..9db0bd3e 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -8,9 +8,15 @@ using GFramework.Core.Abstractions.Systems; namespace GFramework.Core.Abstractions.Ioc; /// -/// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作 +/// 依赖注入容器接口,定义服务注册、解析与生命周期管理的统一入口。 /// -public interface IIocContainer : IContextAware +/// +/// 实现者必须在 中释放容器拥有的根 及其 +/// 关联同步资源,并保证释放操作幂等。 +/// 容器一旦释放,后续任何注册、解析、查询或作用域创建调用都必须抛出 +/// ,避免消费者继续访问失效的运行时状态。 +/// +public interface IIocContainer : IContextAware, IDisposable { #region Register Methods diff --git a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs index d0dcdd08..c36aba30 100644 --- a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs +++ b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs @@ -3,6 +3,9 @@ using GFramework.Core.Ioc; using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; namespace GFramework.Core.Tests.Ioc; @@ -22,6 +25,18 @@ public class IocContainerLifetimeTests public Guid Id { get; } = Guid.NewGuid(); } + private sealed class DisposableTestService : ITestService, IDisposable + { + public Guid Id { get; } = Guid.NewGuid(); + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + [Test] public void RegisterSingleton_Should_Return_Same_Instance() { @@ -207,4 +222,112 @@ public class IocContainerLifetimeTests scope2.Dispose(); scope3.Dispose(); } -} \ No newline at end of file + + [Test] + public void Dispose_Should_Dispose_Resolved_Singleton_And_Block_Further_Use() + { + // Arrange + var container = new MicrosoftDiContainer(); + container.RegisterSingleton(); + container.Freeze(); + var service = container.GetRequired(); + + // Act + container.Dispose(); + + // Assert + Assert.That(service.IsDisposed, Is.True); + Assert.Throws(() => container.Get()); + Assert.Throws(() => container.CreateScope()); + } + + [Test] + public void Dispose_Should_Be_Idempotent() + { + var container = new MicrosoftDiContainer(); + + Assert.DoesNotThrow(container.Dispose); + Assert.DoesNotThrow(container.Dispose); + } + + [Test] + public void Dispose_Should_Be_Idempotent_When_Called_Concurrently() + { + var container = new MicrosoftDiContainer(); + var containerLock = GetContainerLock(container); + var releasedGate = false; + + containerLock.EnterWriteLock(); + try + { + var firstDisposeTask = Task.Run(container.Dispose); + Thread.Sleep(50); + var secondDisposeTask = Task.Run(container.Dispose); + Thread.Sleep(50); + + containerLock.ExitWriteLock(); + releasedGate = true; + + Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing); + } + finally + { + if (!releasedGate) + { + containerLock.ExitWriteLock(); + } + } + } + + [Test] + public void Dispose_Should_Only_Attempt_Lock_Disposal_Once_When_Called_Concurrently() + { + var container = new MicrosoftDiContainer(); + var containerLock = GetContainerLock(container); + var releasedGate = false; + + containerLock.EnterWriteLock(); + try + { + var firstDisposeTask = Task.Run(container.Dispose); + Thread.Sleep(50); + var secondDisposeTask = Task.Run(container.Dispose); + Thread.Sleep(50); + + containerLock.ExitWriteLock(); + releasedGate = true; + + Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing); + Assert.That(GetLockDisposalStarted(container), Is.EqualTo(1)); + } + finally + { + if (!releasedGate) + { + containerLock.ExitWriteLock(); + } + } + } + + /// + /// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。 + /// + private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(lockField, Is.Not.Null); + return (ReaderWriterLockSlim)lockField!.GetValue(container)!; + } + + /// + /// 读取锁销毁启动标记,验证并发释放路径不会重复执行底层锁销毁。 + /// + private static int GetLockDisposalStarted(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + var flagField = typeof(MicrosoftDiContainer).GetField("_lockDisposalStarted", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(flagField, Is.Not.Null); + return (int)flagField!.GetValue(container)!; + } +} diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index 820cfa66..a2627a06 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; @@ -760,4 +762,73 @@ public class MicrosoftDiContainerTests Assert.That(((IPrioritizedService)services[0]).Priority, Is.EqualTo(10)); Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30)); } + + /// + /// 测试容器释放后会阻止后续注册与解析,避免 benchmark 或短生命周期宿主继续使用已回收状态。 + /// + [Test] + public void Dispose_Should_Block_Subsequent_Registration_And_Query_Operations() + { + _container.Dispose(); + + Assert.Throws(() => _container.Register(new TestService())); + Assert.Throws(() => _container.Contains()); + Assert.Throws(() => _container.GetAll()); + } + + /// + /// 测试等待中的读取线程在容器释放后也会收到稳定的容器级释放异常,而不是底层锁异常。 + /// + [Test] + public async Task Dispose_Should_Translate_Waiting_Readers_To_Container_ObjectDisposedException() + { + _container.RegisterSingleton(new TestService()); + _container.Freeze(); + + var containerLock = GetContainerLock(_container); + var releasedGate = false; + using var queryStarted = new ManualResetEventSlim(false); + + containerLock.EnterWriteLock(); + try + { + var queryTask = Task.Run(() => + { + queryStarted.Set(); + return _container.Get(); + }); + + Assert.That(queryStarted.Wait(TimeSpan.FromSeconds(1)), Is.True); + + var disposeTask = Task.Run(_container.Dispose); + Thread.Sleep(50); + + containerLock.ExitWriteLock(); + releasedGate = true; + + await disposeTask.ConfigureAwait(false); + + var exception = Assert.ThrowsAsync(async () => await queryTask.ConfigureAwait(false)); + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.ObjectName, Is.EqualTo(nameof(MicrosoftDiContainer))); + } + finally + { + if (!releasedGate) + { + containerLock.ExitWriteLock(); + } + } + } + + /// + /// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。 + /// + private static ReaderWriterLockSlim GetContainerLock(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + var lockField = typeof(MicrosoftDiContainer).GetField("_lock", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.That(lockField, Is.Not.Null); + return (ReaderWriterLockSlim)lockField!.GetValue(container)!; + } } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 5a6d728c..ad362b76 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Reflection; +using System.Threading; using GFramework.Core.Abstractions.Bases; using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; @@ -17,11 +18,143 @@ namespace GFramework.Core.Ioc; /// 将 Microsoft DI 包装为 IIocContainer 接口实现 /// 提供线程安全的依赖注入容器功能 /// +/// +/// 该适配器负责维护服务注册表、冻结后的根 以及并发访问控制。 +/// 容器释放后会阻止任何进一步访问,并统一抛出 , +/// 以避免 benchmark、测试宿主或短生命周期架构误用失效的 DI 状态。 +/// /// 可选的IServiceCollection实例,默认创建新的ServiceCollection public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) : ContextAwareBase, IIocContainer { #region Helper Methods + /// + /// 抛出统一的容器释放异常,避免并发路径泄露底层锁类型的实现细节。 + /// + /// 始终抛出,且对象名固定为当前容器类型。 + private void ThrowDisposedException() + { + const string objectName = nameof(MicrosoftDiContainer); + _logger.Warn("Attempted to use a disposed MicrosoftDiContainer."); + throw new ObjectDisposedException(objectName); + } + + /// + /// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。 + /// + /// 当容器已释放时抛出。 + private void ThrowIfDisposed() + { + if (!_disposed) return; + ThrowDisposedException(); + } + + /// + /// 进入读锁,并在获取锁前后都复核释放状态,确保等待中的线程也能稳定得到容器级异常。 + /// + /// 当容器已释放,或等待期间被其他线程释放时抛出。 + private void EnterReadLockOrThrowDisposed() + { + var lockTaken = false; + + try + { + _lock.EnterReadLock(); + lockTaken = true; + } + catch (ObjectDisposedException) when (_disposed) + { + ThrowDisposedException(); + } + + if (!_disposed) + { + return; + } + + if (lockTaken) + { + _lock.ExitReadLock(); + } + + ThrowDisposedException(); + } + + /// + /// 进入写锁,并在获取锁前后都复核释放状态,确保等待中的线程不会泄露底层锁异常。 + /// + /// 当容器已释放,或等待期间被其他线程释放时抛出。 + private void EnterWriteLockOrThrowDisposed() + { + var lockTaken = false; + + try + { + _lock.EnterWriteLock(); + lockTaken = true; + } + catch (ObjectDisposedException) when (_disposed) + { + ThrowDisposedException(); + } + + if (!_disposed) + { + return; + } + + if (lockTaken) + { + _lock.ExitWriteLock(); + } + + ThrowDisposedException(); + } + + /// + /// 在释放标志已经对外可见后,等待遗留 waiter 退场,再尝试释放底层锁。 + /// + /// + /// 容器会先把 置为 并退出写锁, + /// 这样所有已在等待队列中的线程都能醒来并通过统一路径抛出容器级 + /// 。只有当这些线程退场后,底层锁才可安全释放。 + /// 该步骤只允许一个释放调用者执行,避免并发 重复销毁同一个 + /// 并破坏幂等契约。 + /// + private void DisposeLockWhenQuiescent() + { + if (Interlocked.CompareExchange(ref _lockDisposalStarted, 1, 0) != 0) + { + return; + } + + const int maxDisposeSpinAttempts = 512; + var spinWait = new SpinWait(); + + for (var attempt = 0; attempt < maxDisposeSpinAttempts; attempt++) + { + if (_lock.CurrentReadCount == 0 && + _lock.WaitingReadCount == 0 && + _lock.WaitingWriteCount == 0 && + _lock.WaitingUpgradeCount == 0) + { + try + { + _lock.Dispose(); + return; + } + catch (SynchronizationLockException) + { + // 等待中的线程刚好在本轮检查后切换状态;继续自旋直到锁真正静默。 + } + } + + spinWait.SpinOnce(); + } + + _logger.Warn("MicrosoftDiContainer lock disposal was skipped because waiters did not quiesce in time."); + } + /// /// 检查容器是否已冻结,如果已冻结则抛出异常 /// 用于保护注册操作的安全性 @@ -57,6 +190,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// private volatile bool _frozen; + /// + /// 容器释放状态标志,true 表示容器已释放,不允许继续访问。 + /// + private volatile bool _disposed; + + /// + /// 标记底层读写锁的销毁流程是否已经启动,确保并发释放时最多只有一个线程尝试销毁锁实例。 + /// + private int _lockDisposalStarted; + /// /// 读写锁,确保多线程环境下的线程安全操作 /// @@ -85,8 +228,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结或类型已被注册时抛出 public void RegisterSingleton(T instance) { + ThrowIfDisposed(); var type = typeof(T); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -119,7 +263,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -142,7 +287,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -165,7 +311,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TImpl : class, TService where TService : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -187,10 +334,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void RegisterPlurality(object instance) { + ThrowIfDisposed(); var concreteType = instance.GetType(); var interfaces = concreteType.GetInterfaces(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -219,7 +367,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void RegisterPlurality() where T : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -262,7 +411,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void Register(T instance) { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -284,7 +434,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器已冻结时抛出 public void Register(Type type, object instance) { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -307,7 +458,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterFactory( Func factory) where TService : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -328,7 +480,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 行为类型,必须是引用类型 public void RegisterCqrsPipelineBehavior() where TBehavior : class { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -392,6 +545,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterCqrsHandlersFromAssemblies(IEnumerable assemblies) { ArgumentNullException.ThrowIfNull(assemblies); + ThrowIfDisposed(); var assemblyArray = assemblies.ToArray(); foreach (var assembly in assemblyArray) { @@ -401,7 +555,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -419,7 +573,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务配置委托 public void ExecuteServicesHook(Action? configurator = null) { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -464,7 +619,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务实例或null public T? Get() where T : class { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -503,7 +659,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 服务实例或null public object? Get(Type type) { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -593,7 +750,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 只读的服务实例列表 public IReadOnlyList GetAll() where T : class { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -620,8 +778,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public IReadOnlyList GetAll(Type type) { ArgumentNullException.ThrowIfNull(type); + ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -750,6 +909,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 排序后的只读服务实例列表 public IReadOnlyList GetAllSorted(Comparison comparison) where T : class { + ThrowIfDisposed(); var list = GetAll().ToList(); list.Sort(comparison); return list; @@ -816,7 +976,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// true表示包含该类型实例,false表示不包含 public bool Contains() where T : class { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -838,7 +999,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// true表示包含该实例,false表示不包含 public bool ContainsInstance(object instance) { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { return _registeredInstances.Contains(instance); @@ -855,7 +1017,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void Clear() { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { // 冻结的容器不允许清空操作 @@ -865,9 +1028,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) return; } + // 未冻结的容器不会构建根 ServiceProvider,因此这里仅重置注册状态即可。 GetServicesUnsafe.Clear(); _registeredInstances.Clear(); _provider = null; + _frozen = false; _logger.Info("Container cleared"); } finally @@ -882,7 +1047,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// public void Freeze() { - _lock.EnterWriteLock(); + ThrowIfDisposed(); + EnterWriteLockOrThrowDisposed(); try { // 防止重复冻结 @@ -917,7 +1083,8 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// 当容器未冻结时抛出 public IServiceScope CreateScope() { - _lock.EnterReadLock(); + ThrowIfDisposed(); + EnterReadLockOrThrowDisposed(); try { // 在锁内检查,避免竞态条件 @@ -938,5 +1105,58 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } + /// + /// 释放容器持有的服务提供者、注册状态和同步原语。 + /// + /// + /// 冻结后的根 会拥有 DI 创建的单例与作用域根缓存,因此 benchmark、 + /// 测试宿主或短生命周期架构在结束时需要显式释放容器,避免这些对象与内部 + /// 一起滞留。 + /// 释放是幂等的;首次释放后所有后续访问都会抛出 。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + var lockTaken = false; + + try + { + try + { + _lock.EnterWriteLock(); + lockTaken = true; + } + catch (ObjectDisposedException) when (_disposed) + { + return; + } + + if (_disposed) + { + return; + } + + _disposed = true; + (_provider as IDisposable)?.Dispose(); + _provider = null; + GetServicesUnsafe.Clear(); + _registeredInstances.Clear(); + _frozen = false; + _logger.Info("IOC Container disposed"); + } + finally + { + if (lockTaken) + { + _lock.ExitWriteLock(); + DisposeLockWhenQuiescent(); + } + } + } + #endregion } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkCleanupHelper.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkCleanupHelper.cs new file mode 100644 index 00000000..27e5b3b9 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkCleanupHelper.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 统一处理 benchmark 宿主的资源释放,避免前一个 抛错后中断后续清理。 +/// +internal static class BenchmarkCleanupHelper +{ + /// + /// 按顺序释放一组 benchmark 资源,并在全部资源都尝试释放后再回抛异常。 + /// + /// 当前 benchmark 宿主拥有并负责释放的资源。 + /// + /// 当且仅当至少一个资源释放失败时抛出。 + /// 单个失败会回抛原始异常,多个失败会聚合为 。 + /// + public static void DisposeAll(params IDisposable?[] disposables) + { + List? exceptions = null; + + foreach (var disposable in disposables) + { + if (disposable is null) + { + continue; + } + + try + { + disposable.Dispose(); + } + catch (Exception exception) + { + exceptions ??= []; + exceptions.Add(exception); + } + } + + if (exceptions is null) + { + return; + } + + if (exceptions.Count == 1) + { + ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); + } + + throw new AggregateException("One or more benchmark resources failed to dispose cleanly.", exceptions); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 42ee16b9..30208feb 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -83,7 +83,7 @@ public class NotificationBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 731f2a23..25c6af14 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -85,7 +85,7 @@ public class RequestBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index a943e095..87e326e5 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -103,8 +103,14 @@ public class RequestInvokerBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); - BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + try + { + BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider); + } + finally + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index ece2b977..07dbc0c5 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -101,8 +101,14 @@ public class RequestPipelineBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); - BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + try + { + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + } + finally + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs index 494e6296..b37fa20c 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -29,6 +29,7 @@ public class RequestStartupBenchmarks private static readonly ILogger RuntimeLogger = CreateLogger(nameof(RequestStartupBenchmarks)); private static readonly BenchmarkRequest Request = new(Guid.NewGuid()); + private MicrosoftDiContainer _container = null!; private ServiceProvider _serviceProvider = null!; private IMediator _mediatr = null!; private ICqrsRuntime _runtime = null!; @@ -62,7 +63,8 @@ public class RequestStartupBenchmarks _serviceProvider = CreateMediatRServiceProvider(); _mediatr = _serviceProvider.GetRequiredService(); - _runtime = CreateGFrameworkRuntime(); + _container = CreateGFrameworkContainer(); + _runtime = CreateGFrameworkRuntime(_container); } /// @@ -84,7 +86,7 @@ public class RequestStartupBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// @@ -124,10 +126,11 @@ public class RequestStartupBenchmarks /// [Benchmark] [BenchmarkCategory("ColdStart")] - public ValueTask ColdStart_GFrameworkCqrs() + public async ValueTask ColdStart_GFrameworkCqrs() { - var runtime = CreateGFrameworkRuntime(); - return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None); + using var container = CreateGFrameworkContainer(); + var runtime = CreateGFrameworkRuntime(container); + return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false); } /// @@ -137,12 +140,21 @@ public class RequestStartupBenchmarks /// 该 benchmark 故意保持与 MediatR 对照组同样的“单 handler 最小宿主”模型, /// 因此这里继续使用单点手工注册,而不引入依赖完整 CQRS 注册协调器的程序集扫描路径。 /// - private static ICqrsRuntime CreateGFrameworkRuntime() + private static MicrosoftDiContainer CreateGFrameworkContainer() { - var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer => + return BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer => { currentContainer.RegisterTransient, BenchmarkRequestHandler>(); }); + } + + /// + /// 基于已冻结的 benchmark 容器构建最小 GFramework.CQRS runtime。 + /// + /// 当前 benchmark 拥有并负责释放的容器。 + private static ICqrsRuntime CreateGFrameworkRuntime(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index d6ce01ba..a552a233 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -103,8 +103,14 @@ public class StreamInvokerBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); - BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + try + { + BenchmarkCleanupHelper.DisposeAll(_reflectionContainer, _generatedContainer, _serviceProvider); + } + finally + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs index 1c22d5f3..8b3b1d94 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -86,7 +86,7 @@ public class StreamingBenchmarks [GlobalCleanup] public void Cleanup() { - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index 096db6b6..4ee6c551 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -38,6 +38,10 @@ help the current worktree land on the right recovery documents without scanning - Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog. - Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md` - Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md` +- `microsoft-di-container-disposal` + - Purpose: track `PR #330` disposal-contract fixes for `MicrosoftDiContainer`, related benchmark cleanup hardening, and review follow-up. + - Tracking: `ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md` + - Trace: `ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md` ## Worktree To Active Topic Map @@ -58,3 +62,5 @@ help the current worktree land on the right recovery documents without scanning - Branch: `docs/sdk-update-documentation` - Worktree hint: `GFramework-update-documentation` - Priority 1: `documentation-full-coverage-governance` +- Branch: `fix/microsoft-di-container-disposal` + - Priority 1: `microsoft-di-container-disposal` diff --git a/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md b/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md new file mode 100644 index 00000000..4cef31b4 --- /dev/null +++ b/ai-plan/public/archive/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md @@ -0,0 +1,48 @@ +# MicrosoftDiContainer Disposal Tracking + +## Goal + +Fix issue `#327` by making `MicrosoftDiContainer` explicitly disposable, ensuring frozen service providers and lock +state are released deterministically, and updating CQRS benchmarks so every owned container is disposed in cleanup or +cold-start paths. + +## Current Recovery Point + +- Recovery point: `MDC-DISPOSE-RP-001` +- Phase: completed and ready to archive +- Focus: + - keep the final validated implementation and archive handoff concise + +## Active Risks + +- No active implementation blockers remain after validation. + +## Completed In This Stage + +- Extended `IIocContainer` to inherit `IDisposable` so callers holding the abstraction can release container-owned + resources explicitly. +- Implemented `MicrosoftDiContainer.Dispose()` with idempotent root-provider release, lock cleanup, state clearing, and + `ObjectDisposedException` guards for post-disposal access. +- Updated `Clear()` to dispose the currently built root provider before resetting container state. +- Added Core regression tests that verify resolved DI-owned singletons are disposed and that disposed containers reject + further registration, lookup, and scope creation. +- Fixed CQRS benchmark cleanup so every benchmark-owned `MicrosoftDiContainer` is disposed, including the temporary + cold-start container path in `RequestStartupBenchmarks`. + +## Validation Target + +- `python3 scripts/license-header.py --check` +- `dotnet test GFramework.Core.Tests -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` +- `dotnet build GFramework.sln -c Release` + +## Latest Validation Result + +- `python3 scripts/license-header.py --check` passed on 2026-05-06. +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` passed on 2026-05-06 with `55` tests passed. +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` passed on 2026-05-06 with `0 warnings` and `0 errors`. +- `dotnet build GFramework.sln -c Release` passed on 2026-05-06 with `0 warnings` and `0 errors`. + +## Next Recommended Resume Step + +Archive this topic under `ai-plan/public/archive/` and push the fix branch for review. diff --git a/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md b/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md new file mode 100644 index 00000000..80ffce6d --- /dev/null +++ b/ai-plan/public/archive/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md @@ -0,0 +1,31 @@ +# MicrosoftDiContainer Disposal Trace + +## 2026-05-06 + +### MDC-DISPOSE-RP-001 Issue #327 disposal repair + +- Trigger: + - issue `#327` reports that `MicrosoftDiContainer` holds a `ReaderWriterLockSlim` and a frozen `IServiceProvider` + but never releases either resource explicitly + - CQRS benchmark types keep `MicrosoftDiContainer` fields alive across runs and currently only dispose the MediatR + `ServiceProvider` side + - `RequestStartupBenchmarks` also creates temporary GFramework runtimes whose backing containers are never surfaced + for cleanup +- Decisions: + - treat the fix as a container lifetime contract update, not only a benchmark workaround + - add the disposal contract at the `IIocContainer` abstraction so callers holding interface references can release the + container explicitly + - keep runtime ownership unchanged; benchmarks that create containers remain responsible for disposing them +- Implementation notes: + - `MicrosoftDiContainer` now releases its frozen root `IServiceProvider`, clears registration state, disposes the + internal `ReaderWriterLockSlim`, and rejects all later operations with `ObjectDisposedException` + - `RequestStartupBenchmarks` was rewritten so the steady-state runtime keeps an explicit container field and the + cold-start benchmark disposes its temporary container in the same measured invocation + - other benchmark classes that own `MicrosoftDiContainer` fields now dispose them during `GlobalCleanup` +- Validation milestone: + - `python3 scripts/license-header.py --check` passed + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainer|FullyQualifiedName~IocContainerLifetimeTests"` passed (`55/55`) + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` passed with `0 warnings / 0 errors` + - `dotnet build GFramework.sln -c Release` passed with `0 warnings / 0 errors` +- Immediate next step: + - archive the topic and push the branch diff --git a/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md b/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md new file mode 100644 index 00000000..22acd952 --- /dev/null +++ b/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md @@ -0,0 +1,54 @@ +# Microsoft DI Container Disposal 跟踪 + +## 目标 + +围绕 `PR #330` 收敛 `MicrosoftDiContainer` 的释放契约、并发释放竞态,以及 `GFramework.Cqrs.Benchmarks` 的宿主清理鲁棒性。 + +## 当前恢复点 + +- 恢复点编号:`MICROSOFT-DI-DISPOSAL-RP-001` +- 当前阶段:`Phase 1` +- 当前 PR 锚点:`PR #330` +- 当前结论: + - `$gframework-pr-review` 已确认 latest-head review 仍存在 5 条 open AI thread,其中 `IIocContainer` 文档契约、`MicrosoftDiContainer.Clear()` 的不可达释放逻辑、`Dispose()` 并发竞态,以及 benchmark `Cleanup()` 缺乏异常隔离均已在本地补齐 + - `CodeRabbit` 关于 `GFramework.Cqrs.Benchmarks` 的 cleanup 问题虽然标在单个文件上,但同类模式实际覆盖 `RequestBenchmarks`、`NotificationBenchmarks`、`RequestPipelineBenchmarks`、`RequestStartupBenchmarks`、`StreamingBenchmarks`、`RequestInvokerBenchmarks`、`StreamInvokerBenchmarks`,当前已通过共享 helper 一次性收敛 + - `MicrosoftDiContainer.Dispose()` 现会先对外发布 `_disposed` 状态并释放写锁,让等待线程统一抛出容器级 `ObjectDisposedException`;随后仅在锁静默后才销毁底层 `ReaderWriterLockSlim` + - 针对剩余的 `greptile` P1,本轮进一步将底层锁销毁收敛为单次执行,避免两个并发 `Dispose()` 调用都进入 `DisposeLockWhenQuiescent()` 时触发双重 `ReaderWriterLockSlim.Dispose()` + +## 当前活跃事实 + +- 当前分支:`fix/microsoft-di-container-disposal` +- 当前分支对应 `PR #330`,状态为 `OPEN` +- 已决定的最小修复面: + - `GFramework.Core.Abstractions/Ioc/IIocContainer.cs` + - `GFramework.Core/Ioc/MicrosoftDiContainer.cs` + - `GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs` + - `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/*.cs` 的 7 个 benchmark cleanup + +## 当前风险 + +- 若极端情况下存在长时间不退出的遗留 waiter,`DisposeLockWhenQuiescent()` 会在有限自旋后跳过底层锁销毁并记录警告,以优先保证 `Dispose()` 不被无限阻塞 +- 并发释放回归测试依赖对内部 `_lock` 的反射访问,需要保持断言目标明确,避免把实现细节暴露成对外契约 + +## 最近权威验证 + +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #330`,open AI review 重点已收敛到释放契约、并发竞态和 benchmark cleanup +- `python3 scripts/license-header.py --check` + - 结果:通过 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~IocContainerLifetimeTests|FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`57/57` passed +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +## 待补最新验证 + +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~IocContainerLifetimeTests"` +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + +## 下一推荐步骤 + +1. 运行 `IocContainerLifetimeTests` 与 `GFramework.Core` Release build,确认单次锁销毁修复没有引入新的 warning 或回归 +2. 再次运行 `$gframework-pr-review` 或检查生成的 JSON,确认当前 latest-head open threads 是否只剩待推送的 GitHub 状态差异 diff --git a/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md b/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md new file mode 100644 index 00000000..d81d66e9 --- /dev/null +++ b/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md @@ -0,0 +1,40 @@ +# Microsoft DI Container Disposal 追踪 + +## 2026-05-06 + +### 阶段:PR #330 review triage 与修复面收敛(MICROSOFT-DI-DISPOSAL-RP-001) + +- 使用 `$gframework-pr-review` 抓取当前分支对应的 `PR #330` latest-head review 后,主线程确认仍有效的 open AI 反馈集中在四类: + - `IIocContainer` 缺少显式的释放生命周期文档 + - `MicrosoftDiContainer.Clear()` 在 `_frozen == false` 路径下仍保留不可达的 `_provider.Dispose()` 调用 + - `MicrosoftDiContainer.Dispose()` 会让等待中的读写线程泄露 `ReaderWriterLockSlim` 的 `ObjectDisposedException` + - 多个 `GFramework.Cqrs.Benchmarks` cleanup 顺序释放资源但缺乏异常隔离,前一个 `Dispose()` 失败会阻断后续资源回收 +- 本轮决策: + - 先补 `ai-plan/public/microsoft-di-container-disposal` 的 tracking / trace,保证该跨模块 PR follow-up 有明确恢复入口 + - 通过 `EnterReadLockOrThrowDisposed` / `EnterWriteLockOrThrowDisposed` 收口 `MicrosoftDiContainer` 的等待中竞态,而不是零散修补个别 API + - 通过共享 `BenchmarkCleanupHelper` 一次性收敛 benchmark 宿主 cleanup 的同类风险 +- 实现补充: + - `IIocContainer` 现已补充释放契约文档,明确 `Dispose()` 幂等性、根 `IServiceProvider` 与同步资源归属,以及释放后的统一异常语义 + - `MicrosoftDiContainer.Clear()` 已移除未冻结路径下不可达的 `_provider.Dispose()` 调用 + - `MicrosoftDiContainer.Dispose()` 现先发布 `_disposed`,再等待遗留 waiter 退场后释放底层锁;若锁在有限自旋内未静默,则记录 warning 并跳过锁销毁,避免 `Dispose()` 无限阻塞 + - `GFramework.Cqrs.Benchmarks` 新增 `BenchmarkCleanupHelper`,并统一接入 7 个 `GlobalCleanup` 入口 +- 回归验证: + - `Dispose_Should_Translate_Waiting_Readers_To_Container_ObjectDisposedException` + - `Dispose_Should_Be_Idempotent_When_Called_Concurrently` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~IocContainerLifetimeTests|FullyQualifiedName~MicrosoftDiContainerTests"` 通过,`57/57` passed + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` 通过,`0 warning / 0 error` + +### 当前下一步 + +1. 推送当前分支后重新运行 `$gframework-pr-review`,确认 latest-head open threads 是否已与本地修复对齐 + +### 阶段:收敛剩余并发 Dispose 双重锁销毁竞态(MICROSOFT-DI-DISPOSAL-RP-001) + +- 根据用户补充的 `greptile` P1,重新核对 `MicrosoftDiContainer.Dispose()` 的尾部流程后确认还存在一个更窄的窗口: + - 线程 A 与线程 B 都可能通过最外层 `_disposed` 快速路径 + - 线程 A 完成主释放并退出写锁后,线程 B 仍可能拿到写锁、因为 `_disposed == true` 直接返回,但 `finally` 仍会调用 `DisposeLockWhenQuiescent()` + - 这样两个线程都可能执行 `_lock.Dispose()`;第二次调用会抛出 `ObjectDisposedException` +- 本轮修复决策: + - 在 `DisposeLockWhenQuiescent()` 入口增加 `Interlocked.CompareExchange` 守卫,把底层锁销毁流程收敛为单次执行 + - 保持现有“先发布 `_disposed`、再等待 waiter 退场”的语义不变,只修复重复销毁底层锁的尾部竞态 + - 在 `IocContainerLifetimeTests` 增加更直接的回归断言,验证并发 `Dispose()` 后锁销毁启动标记只会变为 `1`