diff --git a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs index 4705357f..6e7b0581 100644 --- a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs +++ b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs @@ -394,6 +394,35 @@ public class PauseStackManagerTests Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False); } + /// + /// 验证销毁时会向所有仍暂停的组补发恢复通知。 + /// + [Test] + public async Task DestroyAsync_Should_NotifyResumedGroups() + { + var resumedGroups = new List(); + var mockHandler = new MockPauseHandler(); + + _manager.RegisterHandler(mockHandler); + _manager.OnPauseStateChanged += (_, e) => + { + if (!e.IsPaused) + { + resumedGroups.Add(e.Group); + } + }; + + _manager.Push("Global", PauseGroup.Global); + _manager.Push("Gameplay", PauseGroup.Gameplay); + mockHandler.Reset(); + + await _manager.DestroyAsync(); + + Assert.That(mockHandler.CallCount, Is.EqualTo(2)); + Assert.That(mockHandler.LastIsPaused, Is.False); + Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay })); + } + /// /// 验证并发Push是线程安全的 /// @@ -469,4 +498,4 @@ public class PauseStackManagerTests LastIsPaused = null; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureLifecycle.cs b/GFramework.Core/Architectures/ArchitectureLifecycle.cs index d503f68c..c0fed21f 100644 --- a/GFramework.Core/Architectures/ArchitectureLifecycle.cs +++ b/GFramework.Core/Architectures/ArchitectureLifecycle.cs @@ -145,69 +145,34 @@ internal sealed class ArchitectureLifecycle( { logger.Info($"Initializing {_pendingInitializableList.Count} components"); - // 按类型分组初始化(保持原有的阶段划分) - var utilities = _pendingInitializableList.OfType().ToList(); - var models = _pendingInitializableList.OfType().ToList(); - var systems = _pendingInitializableList.OfType().ToList(); + var initializationPlan = CreateInitializationPlan(); - // 1. 工具初始化阶段 - EnterPhase(ArchitecturePhase.BeforeUtilityInit); + await InitializePhaseComponentsAsync( + initializationPlan.Utilities, + ArchitecturePhase.BeforeUtilityInit, + ArchitecturePhase.AfterUtilityInit, + "context utilities", + "utility", + asyncMode) + .ConfigureAwait(false); + await InitializePhaseComponentsAsync( + initializationPlan.Models, + ArchitecturePhase.BeforeModelInit, + ArchitecturePhase.AfterModelInit, + "models", + "model", + asyncMode) + .ConfigureAwait(false); + await InitializePhaseComponentsAsync( + initializationPlan.Systems, + ArchitecturePhase.BeforeSystemInit, + ArchitecturePhase.AfterSystemInit, + "systems", + "system", + asyncMode) + .ConfigureAwait(false); - if (utilities.Count != 0) - { - logger.Info($"Initializing {utilities.Count} context utilities"); - - foreach (var utility in utilities) - { - logger.Debug($"Initializing utility: {utility.GetType().Name}"); - await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false); - } - - logger.Info("All context utilities initialized"); - } - - EnterPhase(ArchitecturePhase.AfterUtilityInit); - - // 2. 模型初始化阶段 - EnterPhase(ArchitecturePhase.BeforeModelInit); - - if (models.Count != 0) - { - logger.Info($"Initializing {models.Count} models"); - - foreach (var model in models) - { - logger.Debug($"Initializing model: {model.GetType().Name}"); - await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false); - } - - logger.Info("All models initialized"); - } - - EnterPhase(ArchitecturePhase.AfterModelInit); - - // 3. 系统初始化阶段 - EnterPhase(ArchitecturePhase.BeforeSystemInit); - - if (systems.Count != 0) - { - logger.Info($"Initializing {systems.Count} systems"); - - foreach (var system in systems) - { - logger.Debug($"Initializing system: {system.GetType().Name}"); - await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false); - } - - logger.Info("All systems initialized"); - } - - EnterPhase(ArchitecturePhase.AfterSystemInit); - - _pendingInitializableList.Clear(); - _pendingInitializableSet.Clear(); - _initialized = true; - logger.Info("All components initialized"); + MarkInitializationCompleted(); } /// @@ -223,6 +188,67 @@ internal sealed class ArchitectureLifecycle( component.Initialize(); } + /// + /// 按架构既有阶段语义把待初始化组件拆分为 utility、model 和 system 三个批次。 + /// 这样可以在压缩主流程复杂度的同时,继续复用注册顺序和接口类型决定的初始化分层。 + /// + /// 当前待初始化组件的阶段化批次。 + private InitializationPlan CreateInitializationPlan() + { + return new InitializationPlan( + _pendingInitializableList.OfType().ToList(), + _pendingInitializableList.OfType().ToList(), + _pendingInitializableList.OfType().ToList()); + } + + /// + /// 执行单个生命周期阶段的批量初始化,并统一维护阶段切换、日志输出和异步初始化策略。 + /// + /// 当前阶段要初始化的组件类型。 + /// 当前阶段的组件列表。 + /// 阶段开始前要进入的生命周期状态。 + /// 阶段结束后要进入的生命周期状态。 + /// 用于批量日志的组件组名称。 + /// 用于单个组件日志的组件角色名称。 + /// 是否允许优先走异步初始化契约。 + private async Task InitializePhaseComponentsAsync( + IReadOnlyList components, + ArchitecturePhase beforePhase, + ArchitecturePhase afterPhase, + string componentGroupName, + string componentLogName, + bool asyncMode) + where TComponent : class, IInitializable + { + EnterPhase(beforePhase); + + if (components.Count != 0) + { + logger.Info($"Initializing {components.Count} {componentGroupName}"); + + foreach (var component in components) + { + logger.Debug($"Initializing {componentLogName}: {component.GetType().Name}"); + await InitializeComponentAsync(component, asyncMode).ConfigureAwait(false); + } + + logger.Info($"All {componentGroupName} initialized"); + } + + EnterPhase(afterPhase); + } + + /// + /// 在所有阶段初始化完成后清理挂起列表,并把生命周期状态切换到“已初始化”。 + /// + private void MarkInitializationCompleted() + { + _pendingInitializableList.Clear(); + _pendingInitializableSet.Clear(); + _initialized = true; + logger.Info("All components initialized"); + } + /// /// 立即初始化在常规初始化批次完成后新增的组件。 /// 当启用 AllowLateRegistration 时,生命周期层需要和注册层保持一致, @@ -258,6 +284,17 @@ internal sealed class ArchitectureLifecycle( #endregion + /// + /// 保存一次完整初始化流程所需的三个阶段批次。 + /// + /// Utility 初始化批次。 + /// Model 初始化批次。 + /// System 初始化批次。 + private readonly record struct InitializationPlan( + IReadOnlyList Utilities, + IReadOnlyList Models, + IReadOnlyList Systems); + #region Ready State /// diff --git a/GFramework.Core/Pause/PauseStackManager.cs b/GFramework.Core/Pause/PauseStackManager.cs index dd15fe3c..665c2d3a 100644 --- a/GFramework.Core/Pause/PauseStackManager.cs +++ b/GFramework.Core/Pause/PauseStackManager.cs @@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs public ValueTask DestroyAsync() { if (_disposed) + { return ValueTask.CompletedTask; - - List pausedGroups; - IPauseHandler[] handlersSnapshot; - - _lock.EnterWriteLock(); - try - { - if (_disposed) - return ValueTask.CompletedTask; - - _disposed = true; - - // 收集所有当前暂停的组 - pausedGroups = _pauseStacks - .Where(kvp => kvp.Value.Count > 0) - .Select(kvp => kvp.Key) - .ToList(); - - // 获取处理器快照 - handlersSnapshot = _handlers.ToArray(); - - // 清理所有数据结构 - _pauseStacks.Clear(); - _tokenMap.Clear(); - _handlers.Clear(); - - _logger.Debug("PauseStackManager destroyed"); - } - finally - { - _lock.ExitWriteLock(); } - // 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致 - foreach (var group in pausedGroups) + var destroySnapshot = TryBeginDestroy(); + if (destroySnapshot == null) { - _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false"); - - foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority)) - { - try - { - handler.OnPauseStateChanged(group, false); - } - catch (Exception ex) - { - _logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex); - } - } - - // 触发事件 - try - { - RaisePauseStateChanged(group, false); - } - catch (Exception ex) - { - _logger.Error($"Event subscriber failed during destruction for group {group}", ex); - } + return ValueTask.CompletedTask; } - // 释放锁资源 + NotifyDestroyedGroups(destroySnapshot.Value); _lock.Dispose(); return ValueTask.CompletedTask; @@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs public bool Pop(PauseToken token) { if (!token.IsValid) + { return false; - - bool found; - bool shouldNotify = false; - PauseGroup notifyGroup = PauseGroup.Global; - - _lock.EnterWriteLock(); - try - { - ThrowIfDisposed(); - - if (!_tokenMap.TryGetValue(token.Id, out var entry)) - { - _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}"); - return false; - } - - var group = entry.Group; - var stack = _pauseStacks[group]; - var wasPaused = stack.Count > 0; - - // 从栈中移除 - var tempStack = new Stack(); - found = false; - - while (stack.Count > 0) - { - var current = stack.Pop(); - if (current.TokenId == token.Id) - { - found = true; - break; - } - - tempStack.Push(current); - } - - // 恢复栈结构 - while (tempStack.Count > 0) - { - stack.Push(tempStack.Pop()); - } - - if (found) - { - _tokenMap.Remove(token.Id); - _logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})"); - - // 状态变化检测:从暂停 → 未暂停 - if (wasPaused && stack.Count == 0) - { - shouldNotify = true; - notifyGroup = group; - } - } - } - finally - { - _lock.ExitWriteLock(); } - // 在锁外通知处理器,避免死锁 - if (shouldNotify) + var result = TryPopEntry(token); + if (result.ShouldNotify) { - NotifyHandlers(notifyGroup, false); + NotifyHandlers(result.NotifyGroup, false); } - return found; + return result.Found; } /// @@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs } } + /// + /// 采集销毁所需的快照并清空内部状态。 + /// + /// + /// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 。 + /// + /// + /// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行, + /// 以避免在生命周期结束阶段持锁调用用户代码。 + /// + private DestroySnapshot? TryBeginDestroy() + { + _lock.EnterWriteLock(); + try + { + if (_disposed) + { + return null; + } + + _disposed = true; + + var pausedGroups = CollectPausedGroups(); + var handlersSnapshot = CreateHandlerSnapshot(); + + _pauseStacks.Clear(); + _tokenMap.Clear(); + _handlers.Clear(); + + _logger.Debug("PauseStackManager destroyed"); + + return new DestroySnapshot(pausedGroups, handlersSnapshot); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。 + /// + /// 销毁阶段采集的分组与处理器快照。 + private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot) + { + foreach (var group in destroySnapshot.PausedGroups) + { + _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false"); + + NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true); + RaiseDestroyStateChanged(group); + } + } + + /// + /// 在锁内执行令牌移除,并返回锁外通知所需的信息。 + /// + /// 要移除的暂停令牌。 + /// 包含本次弹出结果和后续通知决策的快照。 + /// + /// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序, + /// 只在最后一个暂停请求被移除时触发恢复通知。 + /// + private PopResult TryPopEntry(PauseToken token) + { + _lock.EnterWriteLock(); + try + { + ThrowIfDisposed(); + + if (!_tokenMap.TryGetValue(token.Id, out var entry)) + { + _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}"); + return PopResult.NotFound; + } + + var stack = _pauseStacks[entry.Group]; + var wasPaused = stack.Count > 0; + var found = RemoveEntryFromStack(stack, token.Id); + if (!found) + { + return PopResult.NotFound; + } + + _tokenMap.Remove(token.Id); + _logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})"); + + return new PopResult(true, wasPaused && stack.Count == 0, entry.Group); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。 + /// + /// 要修改的暂停栈。 + /// 目标令牌标识。 + /// 如果找到了目标令牌则返回 + private static bool RemoveEntryFromStack(Stack stack, Guid tokenId) + { + var tempStack = new Stack(); + var found = false; + + while (stack.Count > 0) + { + var current = stack.Pop(); + if (current.TokenId == tokenId) + { + found = true; + break; + } + + tempStack.Push(current); + } + + while (tempStack.Count > 0) + { + stack.Push(tempStack.Pop()); + } + + return found; + } + + /// + /// 收集当前仍处于暂停状态的分组列表。 + /// + /// 包含所有暂停中的分组的数组。 + private PauseGroup[] CollectPausedGroups() + { + return _pauseStacks + .Where(kvp => kvp.Value.Count > 0) + .Select(kvp => kvp.Key) + .ToArray(); + } + + /// + /// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。 + /// + /// 已按优先级排序的处理器快照。 + private IPauseHandler[] CreateHandlerSnapshot() + { + return _handlers + .OrderBy(handler => handler.Priority) + .ToArray(); + } + + /// + /// 统一使用给定的处理器快照派发暂停状态变化通知。 + /// + /// 发生状态变化的暂停组。 + /// 新的暂停状态。 + /// 要通知的处理器快照。 + /// 是否处于销毁补发路径。 + private void NotifyHandlersSnapshot( + PauseGroup group, + bool isPaused, + IReadOnlyList handlersSnapshot, + bool isDestroying) + { + foreach (var handler in handlersSnapshot) + { + try + { + handler.OnPauseStateChanged(group, isPaused); + } + catch (Exception ex) + { + var message = isDestroying + ? $"Handler {handler.GetType().Name} failed during destruction" + : $"Handler {handler.GetType().Name} failed"; + _logger.Error(message, ex); + } + } + } + + /// + /// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。 + /// + /// 需要补发恢复事件的暂停组。 + private void RaiseDestroyStateChanged(PauseGroup group) + { + try + { + RaisePauseStateChanged(group, false); + } + catch (Exception ex) + { + _logger.Error($"Event subscriber failed during destruction for group {group}", ex); + } + } + /// /// 内部查询暂停状态的方法,不加锁。 /// @@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs _lock.EnterReadLock(); try { - handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray(); + handlersSnapshot = CreateHandlerSnapshot(); } finally { @@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs } // 在锁外遍历快照并通知处理器 - foreach (var handler in handlersSnapshot) - { - try - { - handler.OnPauseStateChanged(group, isPaused); - } - catch (Exception ex) - { - _logger.Error($"Handler {handler.GetType().Name} failed", ex); - } - } + NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false); // 触发事件 RaisePauseStateChanged(group, isPaused); @@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs protected override void OnInit() { } -} \ No newline at end of file + + /// + /// 锁内采集的销毁快照,供锁外补发恢复通知使用。 + /// + /// 销毁前仍处于暂停状态的分组。 + /// 按优先级排序后的处理器快照。 + private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot); + + /// + /// Pop 操作的锁内结果快照。 + /// + /// 是否成功移除了目标令牌。 + /// 是否需要在锁外发出恢复通知。 + /// 需要通知的暂停组。 + private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup) + { + /// + /// 表示未找到目标令牌时的默认结果。 + /// + public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global); + } +} diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index c6fd3909..1bef8ef0 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -1,6 +1,7 @@ using GFramework.Core.Abstractions.Ioc; using GFramework.Core.Abstractions.Logging; using GFramework.Cqrs.Abstractions.Cqrs; +using System.Diagnostics.CodeAnalysis; using System.Reflection.Emit; namespace GFramework.Cqrs.Internal; @@ -88,63 +89,14 @@ internal static class CqrsHandlerRegistrar if (registryTypes.Count == 0) return GeneratedRegistrationResult.NoGeneratedRegistry(); - var registries = new List(registryTypes.Count); - foreach (var registryType in registryTypes) - { - var activationMetadata = RegistryActivationMetadataCache.GetOrAdd( - registryType, - AnalyzeRegistryActivation); + if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries)) + return GeneratedRegistrationResult.NoGeneratedRegistry(); - if (!activationMetadata.ImplementsRegistryContract) - { - logger.Warn( - $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); - return GeneratedRegistrationResult.NoGeneratedRegistry(); - } - - if (activationMetadata.IsAbstract) - { - logger.Warn( - $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); - return GeneratedRegistrationResult.NoGeneratedRegistry(); - } - - if (activationMetadata.Factory is null) - { - logger.Warn( - $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor."); - return GeneratedRegistrationResult.NoGeneratedRegistry(); - } - - var registry = activationMetadata.Factory(); - registries.Add(registry); - } - - foreach (var registry in registries) - { - logger.Debug( - $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); - registry.Register(services, logger); - } - - var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata; - if (reflectionFallbackMetadata is not null) - { - if (reflectionFallbackMetadata.HasExplicitTypes) - { - logger.Debug( - $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s)."); - } - else - { - logger.Debug( - $"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers."); - } - - return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata); - } - - return GeneratedRegistrationResult.FullyHandled(); + RegisterGeneratedRegistries(services, registries, assemblyName, logger); + return BuildGeneratedRegistrationResult( + assemblyMetadata.ReflectionFallbackMetadata, + assemblyName, + logger); } catch (Exception exception) { @@ -186,12 +138,138 @@ internal static class CqrsHandlerRegistrar // Request/notification handlers receive context injection before every dispatch. // Transient registration avoids sharing mutable Context across concurrent requests. services.AddTransient(handlerInterface, implementationType); - logger.Debug( + logger.Debug( $"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}."); } } } + /// + /// 激活当前程序集声明的所有 generated registry;若任一 registry 不满足运行时契约,则整批回退到反射扫描。 + /// + /// 程序集声明的 generated registry 类型列表。 + /// 用于诊断的程序集稳定名称。 + /// 日志记录器。 + /// 成功激活后的 registry 实例。 + /// 当全部 registry 都可安全激活时返回 ;否则返回 + private static bool TryCreateGeneratedRegistries( + IReadOnlyList registryTypes, + string assemblyName, + ILogger logger, + out IReadOnlyList registries) + { + var activatedRegistries = new List(registryTypes.Count); + foreach (var registryType in registryTypes) + { + if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry)) + { + registries = Array.Empty(); + return false; + } + + activatedRegistries.Add(registry); + } + + registries = activatedRegistries; + return true; + } + + /// + /// 激活单个 generated registry,并在契约不满足时输出与原先完全一致的回退诊断。 + /// + /// 要分析的 generated registry 类型。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// 激活成功后的 registry 实例。 + /// 当 registry 可安全使用时返回 ;否则返回 + private static bool TryCreateGeneratedRegistry( + Type registryType, + string assemblyName, + ILogger logger, + [NotNullWhen(true)] out ICqrsHandlerRegistry? registry) + { + var activationMetadata = RegistryActivationMetadataCache.GetOrAdd( + registryType, + AnalyzeRegistryActivation); + + if (!activationMetadata.ImplementsRegistryContract) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}."); + registry = null; + return false; + } + + if (activationMetadata.IsAbstract) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract."); + registry = null; + return false; + } + + if (activationMetadata.Factory is null) + { + logger.Warn( + $"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor."); + registry = null; + return false; + } + + registry = activationMetadata.Factory(); + return true; + } + + /// + /// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。 + /// + /// 目标服务集合。 + /// 已通过契约校验的 registry 实例。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void RegisterGeneratedRegistries( + IServiceCollection services, + IReadOnlyList registries, + string assemblyName, + ILogger logger) + { + foreach (var registry in registries) + { + logger.Debug( + $"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}."); + registry.Register(services, logger); + } + } + + /// + /// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。 + /// + /// 生成注册器声明的反射补扫元数据。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + /// 描述 generated registry 是否已完全处理当前程序集的结果对象。 + private static GeneratedRegistrationResult BuildGeneratedRegistrationResult( + ReflectionFallbackMetadata? reflectionFallbackMetadata, + string assemblyName, + ILogger logger) + { + if (reflectionFallbackMetadata is null) + return GeneratedRegistrationResult.FullyHandled(); + + if (reflectionFallbackMetadata.HasExplicitTypes) + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s)."); + } + else + { + logger.Debug( + $"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers."); + } + + return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata); + } + /// /// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。 /// @@ -255,6 +333,29 @@ internal static class CqrsHandlerRegistrar return null; var resolvedTypes = new List(); + AppendDirectFallbackTypes(fallbackAttributes, resolvedTypes, assemblyName, logger); + AppendNamedFallbackTypes(assembly, fallbackAttributes, resolvedTypes, assemblyName, logger); + + return new ReflectionFallbackMetadata( + resolvedTypes + .Distinct() + .OrderBy(GetTypeSortKey, StringComparer.Ordinal) + .ToArray()); + } + + /// + /// 追加 attribute 里直接携带的 fallback 类型,并过滤掉跨程序集误声明的条目。 + /// + /// 当前程序集上的 fallback attribute 集合。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void AppendDirectFallbackTypes( + IReadOnlyList fallbackAttributes, + ICollection resolvedTypes, + string assemblyName, + ILogger logger) + { foreach (var fallbackType in fallbackAttributes .SelectMany(static attribute => attribute.FallbackHandlerTypes) .Where(static type => type is not null) @@ -273,37 +374,65 @@ internal static class CqrsHandlerRegistrar resolvedTypes.Add(fallbackType); } + } + /// + /// 追加 attribute 里以类型名声明的 fallback 条目,并保留逐项失败的诊断能力。 + /// + /// 当前待解析的程序集。 + /// 当前程序集上的 fallback attribute 集合。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 日志记录器。 + private static void AppendNamedFallbackTypes( + Assembly assembly, + IReadOnlyList fallbackAttributes, + ICollection resolvedTypes, + string assemblyName, + ILogger logger) + { foreach (var typeName in fallbackAttributes .SelectMany(static attribute => attribute.FallbackHandlerTypeNames) .Where(static name => !string.IsNullOrWhiteSpace(name)) .Distinct(StringComparer.Ordinal) .OrderBy(static name => name, StringComparer.Ordinal)) { - try - { - var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false); - if (type is null) - { - logger.Warn( - $"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry."); - continue; - } + TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger); + } + } - resolvedTypes.Add(type); - } - catch (Exception exception) + /// + /// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。 + /// + /// 当前待解析的程序集。 + /// 待补充的已解析类型集合。 + /// 当前程序集的稳定名称。 + /// 要解析的完整类型名。 + /// 日志记录器。 + private static void TryAppendNamedFallbackType( + Assembly assembly, + ICollection resolvedTypes, + string assemblyName, + string typeName, + ILogger logger) + { + try + { + var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false); + if (type is null) { logger.Warn( - $"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}"); + $"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry."); + return; } - } - return new ReflectionFallbackMetadata( - resolvedTypes - .Distinct() - .OrderBy(GetTypeSortKey, StringComparer.Ordinal) - .ToArray()); + resolvedTypes.Add(type); + } + catch (Exception exception) + { + logger.Warn( + $"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}"); + } } /// diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 7fa5cf09..2e7b710f 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -7,28 +7,36 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001` -- 当前阶段:`Phase 1` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005` +- 当前阶段:`Phase 5` - 当前焦点: - - 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`,active 入口只保留当前恢复信息 - - 基于现有剩余热点,评估 `MA0051`、`MA0048`、`MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理 - - 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点 + - 已完成 `GFramework.Core/Pause/PauseStackManager.cs` 的 `MA0051` 收口:将 `DestroyAsync` 与 `Pop` 拆分为锁内状态迁移、 + 栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变 + - 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知” + - 下一轮若继续推进,优先在 `CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的 + `PauseStackManager` ## 当前状态摘要 - 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理 - 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险 -- 当前剩余 warning 已集中到长方法、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题 +- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在 + `GFramework.Core/Coroutine/CoroutineScheduler.cs`、`GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、 + delegate 形状和少量公共集合抽象接口问题 ## 当前活跃事实 - 当前主题仍是 active topic,因为剩余结构性 warning 是否继续推进尚未决策 - `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/` +- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证 +- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证 +- `RP-004` 已完成当前 PR review follow-up:修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题 +- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 -- 结构性重构风险:剩余 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名 +- 结构性重构风险:剩余 `GFramework.Core` 侧 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名 - 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面 - 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定 - 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证 @@ -43,10 +51,25 @@ ## 验证说明 - `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档 +- `RP-002` 的定向验证结果: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=` +- `RP-003` 的定向验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=` +- `RP-004` 的定向验证结果: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` + - 结果:`0 Warning(s)`,`0 Error(s)` +- `RP-005` 的定向验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表中 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=` + - 结果:`25 Passed`,`0 Failed` - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 从 `MA0051`、`MA0048`、`MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面 +2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` 与 `GFramework.Core/StateManagement/Store.cs` + 的 `MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面 3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 5e42fb93..33fb83c8 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,82 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-21 — RP-005 + +### 阶段:PauseStackManager `MA0051` 收口(RP-005) + +- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择 + `GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler` 和 `Store`, + 且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为 +- 先用 `warnings-only` 定向构建确认 `DestroyAsync` 与 `Pop` 仍分别命中 `MA0051`,再把逻辑拆分为: + - `TryBeginDestroy` + - `NotifyDestroyedGroups` + - `TryPopEntry` + - `RemoveEntryFromStack` +- 额外抽出 `CreateHandlerSnapshot` 与 `NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志, + 保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变 +- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时, + 处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号 +- 验证通过: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=` + - 结果:`25 Passed`,`0 Failed` +- 下一步保持原节奏:只在 `CoroutineScheduler` 或 `Store` 中二选一继续,不与其他 warning 家族混做 + +## 2026-04-21 — RP-003 + +### 阶段:Architecture 生命周期 `MA0051` 收口(RP-003) + +- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,选定 + `GFramework.Core/Architectures/ArchitectureLifecycle.cs`,因为文件体量适中且已有 + `ArchitectureLifecycleBehaviorTests` 覆盖阶段流转、销毁顺序和 late registration 行为 +- 先用 `warnings-only` 定向构建确认 `ArchitectureLifecycle.InitializeAllComponentsAsync` 仍在报 + `MA0051`,随后把主流程拆成: + - `CreateInitializationPlan` + - `InitializePhaseComponentsAsync` + - `MarkInitializationCompleted` +- 保持原有阶段顺序 `Before* -> After*`、批量日志文本和异步初始化策略不变,只压缩主方法长度 +- 修正新增 `InitializationPlan` 记录类型的 XML `` 名称大小写,避免引入文档告警 +- 验证通过: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly` + - 结果:`29 Warning(s)`,`0 Error(s)`;`ArchitectureLifecycle.cs` 已不再出现在 warning 列表 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=` + - 结果:`6 Passed`,`0 Failed` + +## 2026-04-21 — RP-004 + +### 阶段:PR review follow-up(RP-004) + +- 使用 `gframework-pr-review` 抓取当前分支 PR #263 的最新 CodeRabbit review threads、MegaLinter 摘要与 CTRF 测试结果, + 只接受仍能在本地工作树复现的 review 点 +- 在 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 中将 `TryCreateGeneratedRegistry` 的 `out` 参数改为 + `[NotNullWhen(true)] out ICqrsHandlerRegistry?`,移除三处 `null!` 抑制,保持激活失败时的日志文本与回退语义不变 +- 修正 active trace 中重复的 `## 2026-04-21` 二级标题,消除 CodeRabbit 报告的 markdownlint `MD024` +- 核实 PR 信号后确认:当前 CTRF 报告为 `2134 passed / 0 failed`;MegaLinter 唯一告警来自 CI 环境中的 `dotnet-format` + restore 失败,不是本地代码格式问题 +- 验证通过: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` + - 结果:`0 Warning(s)`,`0 Error(s)` + +## 2026-04-21 — RP-002 + +### 阶段:CQRS `MA0051` 收口(RP-002) + +- 依据 active tracking 中“先只选一个结构性切入点”的约束,选定 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` + 作为低风险下一步,因为它已有稳定的 targeted test 覆盖 generated registry、reflection fallback、缓存和重复注册行为 +- 将 `TryRegisterGeneratedHandlers` 拆分为 registry 激活、批量注册和 fallback 结果构建三个辅助阶段,同时把 + `GetReflectionFallbackMetadata` 的直接类型解析与按名称解析拆开,降低长方法复杂度但不改日志文本与回退语义 +- 顺手修正 `RegisterAssemblyHandlers` 内部调试日志的缩进,未改注册顺序、生命周期或服务描述符写入逻辑 +- 验证通过: + - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` + - 结果:`0 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=` + - 结果:`11 Passed`,`0 Failed` +- 新发现的环境注意事项: + - 当前 WSL worktree 下若不显式传入 `-p:RestoreFallbackFolders=`,Linux `dotnet` 会读取不存在的 Windows fallback package + folder 并导致 `ResolvePackageAssets` 失败 + - sandbox 内运行 `dotnet` 会因 MSBuild named-pipe 限制失败;需要在提权上下文中执行 .NET 验证 + ## 2026-04-19 ### 阶段:local-plan 迁移收口(RP-001) @@ -28,5 +105,5 @@ ### 下一步 -1. 后续若继续 analyzer warning reduction,只从 `ai-plan/public/analyzer-warning-reduction/` 进入,不再恢复 `local-plan/` -2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/` +1. 若继续 analyzer warning reduction,优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏 +2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归