diff --git a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs index e5eb0e1f..359a92bf 100644 --- a/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs +++ b/GFramework.Core.Tests/Architectures/ArchitectureLifecycleBehaviorTests.cs @@ -237,6 +237,26 @@ public class ArchitectureLifecycleBehaviorTests Assert.Throws(() => probe.GetContext()); } + /// + /// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。 + /// + [Test] + public async Task Destroy_Should_Unbind_Context_From_GameContext() + { + var architecture = new PhaseTrackingArchitecture(); + + await architecture.InitializeAsync(); + + Assert.That(GameContext.GetByType(architecture.GetType()), Is.SameAs(architecture.Context)); + +#pragma warning disable CS0618 + architecture.Destroy(); +#pragma warning restore CS0618 + + Assert.Throws(() => GameContext.GetByType(architecture.GetType())); + Assert.Throws(() => GameContext.GetFirstArchitectureContext()); + } + /// /// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。 /// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。 diff --git a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs index b1c5e336..38832cdd 100644 --- a/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs +++ b/GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs @@ -494,6 +494,11 @@ public class MicrosoftDiContainerTests { Assert.DoesNotThrow(() => _container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly])); + + Assert.That( + _container.GetServicesUnsafe.Any(static descriptor => + descriptor.ServiceType == typeof(INotificationHandler)), + Is.True); } /// diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index 7fa66b1d..20883e7b 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -359,6 +359,11 @@ public abstract class Architecture : IArchitecture /// /// 异步销毁架构及所有组件 /// + /// + /// 无论 _lifecycle.DestroyAsync() 是否抛出异常,该方法都会在 中调用 + /// (()),移除当前架构类型在全局上下文表中的绑定。 + /// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。 + /// public virtual async ValueTask DestroyAsync() { try @@ -376,10 +381,22 @@ public abstract class Architecture : IArchitecture /// /// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容) /// + /// + /// 该同步兼容入口会与 保持相同的全局解绑语义;即使销毁过程抛出异常, + /// 也会在 中调用 (())。 + /// [Obsolete("建议使用 DestroyAsync() 以支持异步清理")] public virtual void Destroy() { - _lifecycle.Destroy(); + try + { + _lifecycle.Destroy(); + } + finally + { + // 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。 + GameContext.Unbind(GetType()); + } } #endregion diff --git a/GFramework.Core/Architectures/GameContext.cs b/GFramework.Core/Architectures/GameContext.cs index a25a07ca..580b8661 100644 --- a/GFramework.Core/Architectures/GameContext.cs +++ b/GFramework.Core/Architectures/GameContext.cs @@ -12,6 +12,12 @@ namespace GFramework.Core.Architectures; /// public static class GameContext { + // ConcurrentDictionary 负责向外暴露安全的实时视图;该锁负责维护“别名字典 + 当前活动上下文”之间的组合不变式。 +#if NET9_0_OR_GREATER + private static readonly Lock SyncRoot = new(); +#else + private static readonly object SyncRoot = new(); +#endif private static readonly ConcurrentDictionary ArchitectureDictionary = new(); private static IArchitectureContext? _currentArchitectureContext; @@ -39,15 +45,19 @@ public static class GameContext ArgumentNullException.ThrowIfNull(architectureType); ArgumentNullException.ThrowIfNull(context); - var currentContext = Interlocked.CompareExchange(ref _currentArchitectureContext, context, comparand: null); - if (currentContext != null && !ReferenceEquals(currentContext, context)) - throw new InvalidOperationException( - $"GameContext already tracks active context '{currentContext.GetType().Name}'. " + - $"Cannot bind a different context '{context.GetType().Name}'."); + lock (SyncRoot) + { + if (_currentArchitectureContext != null && !ReferenceEquals(_currentArchitectureContext, context)) + throw new InvalidOperationException( + $"GameContext already tracks active context '{_currentArchitectureContext.GetType().Name}'. " + + $"Cannot bind a different context '{context.GetType().Name}'."); - if (!ArchitectureDictionary.TryAdd(architectureType, context)) - throw new InvalidOperationException( - $"Architecture context for '{architectureType.Name}' already exists"); + if (!ArchitectureDictionary.TryAdd(architectureType, context)) + throw new InvalidOperationException( + $"Architecture context for '{architectureType.Name}' already exists"); + + _currentArchitectureContext ??= context; + } } /// @@ -58,8 +68,11 @@ public static class GameContext /// 当当前没有活动上下文时抛出。 public static IArchitectureContext GetFirstArchitectureContext() { - if (_currentArchitectureContext is { } context) - return context; + lock (SyncRoot) + { + if (_currentArchitectureContext is { } context) + return context; + } throw new InvalidOperationException("No active architecture context is currently bound."); } @@ -76,11 +89,14 @@ public static class GameContext { ArgumentNullException.ThrowIfNull(type); - if (ArchitectureDictionary.TryGetValue(type, out var context)) - return context; + lock (SyncRoot) + { + if (ArchitectureDictionary.TryGetValue(type, out var context)) + return context; - if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) - return _currentArchitectureContext; + if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) + return _currentArchitectureContext; + } throw new InvalidOperationException( $"Architecture context for '{type.Name}' not found"); @@ -88,25 +104,30 @@ public static class GameContext /// - /// 获取指定类型的架构上下文实例 + /// 获取指定类型的架构上下文实例。 + /// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。 /// /// 架构上下文类型,必须实现IArchitectureContext接口 /// 指定类型的架构上下文实例 /// 当指定类型的架构上下文不存在时抛出 public static T Get() where T : class, IArchitectureContext { - if (_currentArchitectureContext is T currentContext) - return currentContext; + lock (SyncRoot) + { + if (_currentArchitectureContext is T currentContext) + return currentContext; - if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) - return (T)ctx; + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) + return (T)ctx; + } throw new InvalidOperationException( $"Architecture context '{typeof(T).Name}' not found"); } /// - /// 尝试获取指定类型的架构上下文实例 + /// 尝试获取指定类型的架构上下文实例。 + /// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。 /// /// 架构上下文类型,必须实现IArchitectureContext接口 /// 输出参数,如果找到则返回对应的架构上下文实例,否则返回null @@ -114,16 +135,19 @@ public static class GameContext public static bool TryGet(out T? context) where T : class, IArchitectureContext { - if (_currentArchitectureContext is T currentContext) + lock (SyncRoot) { - context = currentContext; - return true; - } + if (_currentArchitectureContext is T currentContext) + { + context = currentContext; + return true; + } - if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) - { - context = (T)ctx; - return true; + if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) + { + context = (T)ctx; + return true; + } } context = null; @@ -131,30 +155,54 @@ public static class GameContext } /// - /// 移除指定类型的架构上下文绑定 + /// 移除指定类型的架构上下文绑定。 + /// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。 /// /// 要移除的架构类型 + /// public static void Unbind(Type architectureType) { ArgumentNullException.ThrowIfNull(architectureType); - if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) - return; + lock (SyncRoot) + { + if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) + return; - if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) - return; + if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) + return; - if (!ArchitectureDictionary.Values.Any(current => ReferenceEquals(current, removedContext))) - Interlocked.CompareExchange(ref _currentArchitectureContext, null, removedContext); + if (!HasAliasForContext(removedContext)) + _currentArchitectureContext = null; + } } /// - /// 清空所有架构上下文绑定 + /// 清空所有架构上下文绑定,并重置当前活动上下文。 /// public static void Clear() { - ArchitectureDictionary.Clear(); - Interlocked.Exchange(ref _currentArchitectureContext, null); + lock (SyncRoot) + { + ArchitectureDictionary.Clear(); + _currentArchitectureContext = null; + } + } + + /// + /// 判断当前是否仍存在指向同一上下文实例的其他类型别名。 + /// + /// 被移除绑定原本指向的上下文实例。 + /// 如果还有其他别名指向同一实例则返回 ;否则返回 + private static bool HasAliasForContext(IArchitectureContext context) + { + foreach (var current in ArchitectureDictionary.Values) + { + if (ReferenceEquals(current, context)) + return true; + } + + return false; } } diff --git a/GFramework.Core/Architectures/GameContextProvider.cs b/GFramework.Core/Architectures/GameContextProvider.cs index 5725d00b..32a530da 100644 --- a/GFramework.Core/Architectures/GameContextProvider.cs +++ b/GFramework.Core/Architectures/GameContextProvider.cs @@ -15,6 +15,7 @@ public sealed class GameContextProvider : IArchitectureContextProvider /// 获取当前的架构上下文。 /// /// 架构上下文实例 + /// 当前没有已绑定的活动架构上下文时抛出。 public IArchitectureContext GetContext() { return GameContext.GetFirstArchitectureContext(); diff --git a/GFramework.Core/Rule/ContextAwareBase.cs b/GFramework.Core/Rule/ContextAwareBase.cs index 970f5992..85931faa 100644 --- a/GFramework.Core/Rule/ContextAwareBase.cs +++ b/GFramework.Core/Rule/ContextAwareBase.cs @@ -48,6 +48,8 @@ public abstract class ContextAwareBase : IContextAware /// /// 当 为空时,该实现会直接回退到 返回的当前活动上下文。 /// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。 + /// 一旦回退结果被写入 ,后续即使关联架构解除 绑定, + /// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。 /// IArchitectureContext IContextAware.GetContext() { diff --git a/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md b/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md index b25cc74f..11296e83 100644 --- a/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md +++ b/ai-plan/public/single-context-priority/todos/single-context-priority-tracking.md @@ -10,13 +10,14 @@ ## 当前恢复点 -- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-001` -- 当前阶段:`Phase 2` +- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-003` +- 当前阶段:`Phase 3` - 当前结论: - `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例 - `MicrosoftDiContainer` 的预冻结 `Get()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致 - `IIocContainer` XML 文档已明确预冻结查询与 `Contains()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义 - `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口 + - 当前 PR review 跟进中,`Architecture.Destroy()` 已补齐相同解绑语义,`GameContext` 也改为通过统一临界区维护别名字典与当前活动上下文的一致性 - 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main` ## 当前活跃事实 @@ -35,6 +36,7 @@ - `Contains()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点 - `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper - 现有解绑逻辑通过 `Architecture.GetType()` 移除初始化期绑定;若后续引入更多显式上下文别名,需同步评估是否要在销毁时额外移除这些别名 +- `GameContext` 当前通过粗粒度锁保护组合状态一致性;若该入口后续进入高频并发热点,需要基于真实性能数据再评估更细粒度方案 ## 最近权威验证 @@ -52,8 +54,15 @@ - 结果:通过,`32/32` passed - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - 结果:再次通过,`0 warning / 0 error` +- `python3 scripts/license-header.py --check` + - 结果:再次通过,所有受支持文件头合法 +- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~GameContextTests|FullyQualifiedName~ContextProviderTests|FullyQualifiedName~ContextAwareTests|FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~MicrosoftDiContainerTests"` + - 结果:通过,`87/87` passed +- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` + - 结果:再次通过,`0 warning / 0 error` ## 下一推荐步骤 1. 若后续继续推进,可评估是否要把 `GameContext.ArchitectureReadOnlyDictionary` 标记为兼容层,并收口其公开使用面 -2. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试 +2. 若 `GameContext` 的组合锁成为瓶颈,再基于压测结果讨论更细粒度的原子状态模型 +3. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试 diff --git a/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md b/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md index 44273c0b..ec2f32d1 100644 --- a/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md +++ b/ai-plan/public/single-context-priority/traces/single-context-priority-trace.md @@ -42,3 +42,25 @@ ### 当前下一步 1. 若后续要进一步彻底移除全局回退,可单独评估 `GameContext` 公开别名字典的收口策略与生成器默认 provider 的进一步简化空间 + +### 阶段:PR review 跟进修复(SINGLE-CONTEXT-PRIORITY-RP-003) + +- Review 来源: + - PR `#332`(`refactor/single-context-priority`)上的 CodeRabbit unresolved outside-diff / nitpick comments + - PR `#332` 上 Greptile 的两个 latest-head open threads +- 本轮确认并接受的结论: + - `Architecture.Destroy()` 与 `DestroyAsync()` 存在语义漂移,前者缺少 `GameContext.Unbind(GetType())` 清理,仍然会泄漏过期全局回退入口 + - `GameContext` 仅靠 `ConcurrentDictionary` + `_currentArchitectureContext` 的分离更新无法保证组合状态一致;`Bind` / `Unbind` / `Clear` 与读取路径需要统一同步边界 + - `GameContextProvider.GetContext()`、`ContextAwareBase.GetContext()`、`Architecture.DestroyAsync()` 等处的 XML 文档仍未完全反映最新生命周期语义 + - `RegisterCqrsHandlersFromAssemblies_Should_Resolve_Registration_Service_When_Registered_As_Instance` 需要从“仅不抛异常”加强为“确实注册了 handler” +- 本轮实现摘要: + - 为 `GameContext` 新增统一临界区,保证别名字典与当前活动上下文指针的状态切换一致 + - 为 `Architecture.Destroy()` 增加 `finally` 解绑,和异步销毁路径对齐 + - 补充 `GameContextProvider`、`ContextAwareBase`、`Architecture`、`GameContext` 的 XML 文档说明 + - 新增同步 `Destroy()` 解绑回归测试,并加强 CQRS 注册断言 +- 验证与环境备注: + - `python3 scripts/license-header.py --check` 通过 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~GameContextTests|FullyQualifiedName~ContextProviderTests|FullyQualifiedName~ContextAwareTests|FullyQualifiedName~ArchitectureLifecycleBehaviorTests|FullyQualifiedName~MicrosoftDiContainerTests"` 通过,`87/87` passed + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` 通过,`0 warning / 0 error` + - 中途尝试把 `dotnet test` 与 `dotnet build` 并行执行时再次触发 `GFramework.Core.pdb` 占用重试;按仓库规则改为串行直跑后结果稳定通过 + - `System.Threading.Lock` 在 `net8.0` 目标下不可用,因此最终采用 `#if NET9_0_OR_GREATER` 条件锁声明,同时保留 `net8.0` 的 `object` 锁兼容实现