diff --git a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs index 03525b55..9db0bd3e 100644 --- a/GFramework.Core.Abstractions/Ioc/IIocContainer.cs +++ b/GFramework.Core.Abstractions/Ioc/IIocContainer.cs @@ -8,8 +8,14 @@ using GFramework.Core.Abstractions.Systems; namespace GFramework.Core.Abstractions.Ioc; /// -/// 依赖注入容器接口,定义了服务注册、解析和管理的基本操作 +/// 依赖注入容器接口,定义服务注册、解析与生命周期管理的统一入口。 /// +/// +/// 实现者必须在 中释放容器拥有的根 及其 +/// 关联同步资源,并保证释放操作幂等。 +/// 容器一旦释放,后续任何注册、解析、查询或作用域创建调用都必须抛出 +/// ,避免消费者继续访问失效的运行时状态。 +/// 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 a3325ff6..720b212a 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; @@ -246,4 +249,44 @@ public class IocContainerLifetimeTests 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(); + } + } + } + + /// + /// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。 + /// + 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.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index bc16b6d2..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; @@ -773,4 +775,60 @@ public class MicrosoftDiContainerTests 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 4c5fe90b..aef15f04 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,27 @@ 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); + } + /// /// 检查容器是否已释放,避免访问已经失效的服务提供者与同步原语。 /// @@ -29,10 +46,106 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) private void ThrowIfDisposed() { if (!_disposed) return; + ThrowDisposedException(); + } - const string objectName = nameof(MicrosoftDiContainer); - _logger.Warn("Attempted to use a disposed MicrosoftDiContainer."); - throw new ObjectDisposedException(objectName); + /// + /// 进入读锁,并在获取锁前后都复核释放状态,确保等待中的线程也能稳定得到容器级异常。 + /// + /// 当容器已释放,或等待期间被其他线程释放时抛出。 + 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() + { + 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."); } /// @@ -105,7 +218,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) { ThrowIfDisposed(); var type = typeof(T); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -139,7 +252,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TService : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -163,7 +276,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TService : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -187,7 +300,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) where TService : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -213,7 +326,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) var concreteType = instance.GetType(); var interfaces = concreteType.GetInterfaces(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -243,7 +356,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterPlurality() where T : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -287,7 +400,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void Register(T instance) { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -310,7 +423,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void Register(Type type, object instance) { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -334,7 +447,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) Func factory) where TService : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -356,7 +469,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void RegisterCqrsPipelineBehavior() where TBehavior : class { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -430,7 +543,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } } - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -449,7 +562,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void ExecuteServicesHook(Action? configurator = null) { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { ThrowIfFrozen(); @@ -495,7 +608,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public T? Get() where T : class { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -535,7 +648,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public object? Get(Type type) { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -626,7 +739,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public IReadOnlyList GetAll() where T : class { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -655,7 +768,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) ArgumentNullException.ThrowIfNull(type); ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -852,7 +965,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public bool Contains() where T : class { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { if (_provider == null) @@ -875,7 +988,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public bool ContainsInstance(object instance) { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { return _registeredInstances.Contains(instance); @@ -893,7 +1006,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void Clear() { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { // 冻结的容器不允许清空操作 @@ -903,9 +1016,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) return; } + // 未冻结的容器不会构建根 ServiceProvider,因此这里仅重置注册状态即可。 GetServicesUnsafe.Clear(); _registeredInstances.Clear(); - (_provider as IDisposable)?.Dispose(); _provider = null; _frozen = false; _logger.Info("Container cleared"); @@ -923,7 +1036,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public void Freeze() { ThrowIfDisposed(); - _lock.EnterWriteLock(); + EnterWriteLockOrThrowDisposed(); try { // 防止重复冻结 @@ -959,7 +1072,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) public IServiceScope CreateScope() { ThrowIfDisposed(); - _lock.EnterReadLock(); + EnterReadLockOrThrowDisposed(); try { // 在锁内检查,避免竞态条件 @@ -996,9 +1109,20 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) return; } - _lock.EnterWriteLock(); + var lockTaken = false; + try { + try + { + _lock.EnterWriteLock(); + lockTaken = true; + } + catch (ObjectDisposedException) when (_disposed) + { + return; + } + if (_disposed) { return; @@ -1014,8 +1138,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) } finally { - _lock.ExitWriteLock(); - _lock.Dispose(); + if (lockTaken) + { + _lock.ExitWriteLock(); + DisposeLockWhenQuiescent(); + } } } 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 46929154..30208feb 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -83,8 +83,7 @@ public class NotificationBenchmarks [GlobalCleanup] public void Cleanup() { - _container.Dispose(); - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 6b4f060b..25c6af14 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -85,8 +85,7 @@ public class RequestBenchmarks [GlobalCleanup] public void Cleanup() { - _container.Dispose(); - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index 72cb423e..87e326e5 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -103,10 +103,14 @@ public class RequestInvokerBenchmarks [GlobalCleanup] public void Cleanup() { - _reflectionContainer.Dispose(); - _generatedContainer.Dispose(); - _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 69ea20c0..07dbc0c5 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -101,9 +101,14 @@ public class RequestPipelineBenchmarks [GlobalCleanup] public void Cleanup() { - _container.Dispose(); - _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 81fa96b6..b37fa20c 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -86,8 +86,7 @@ public class RequestStartupBenchmarks [GlobalCleanup] public void Cleanup() { - _container.Dispose(); - _serviceProvider.Dispose(); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index 7dd4db28..a552a233 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -103,10 +103,14 @@ public class StreamInvokerBenchmarks [GlobalCleanup] public void Cleanup() { - _reflectionContainer.Dispose(); - _generatedContainer.Dispose(); - _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 9cdadcfe..8b3b1d94 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -86,8 +86,7 @@ public class StreamingBenchmarks [GlobalCleanup] public void Cleanup() { - _container.Dispose(); - _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/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..0655b3be --- /dev/null +++ b/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md @@ -0,0 +1,48 @@ +# 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` + +## 当前活跃事实 + +- 当前分支:`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` + +## 下一推荐步骤 + +1. 再次运行 `$gframework-pr-review` 或检查生成的 JSON,确认当前 latest-head open threads 是否只剩待推送的 GitHub 状态差异 +2. 关注 push 后若仍有 review thread 未关闭,优先核对其是否属于 stale comment 还是需要额外文档/测试补充 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..b4a28617 --- /dev/null +++ b/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md @@ -0,0 +1,29 @@ +# 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 是否已与本地修复对齐