fix(core): 修复上下文销毁解绑与并发一致性

- 修复 GameContext 的别名字典与当前活动上下文同步边界,避免解绑与读取路径出现状态漂移
- 修复 Architecture.Destroy() 缺少全局解绑的问题,并补充相关生命周期 XML 文档
- 更新回归测试、CQRS 注册断言与 single-context-priority 跟踪记录
This commit is contained in:
gewuyou 2026-05-07 10:43:07 +08:00
parent ff04a4fbad
commit ee8b6a4deb
8 changed files with 166 additions and 42 deletions

View File

@ -237,6 +237,26 @@ public class ArchitectureLifecycleBehaviorTests
Assert.Throws<InvalidOperationException>(() => probe.GetContext()); Assert.Throws<InvalidOperationException>(() => probe.GetContext());
} }
/// <summary>
/// 验证同步兼容销毁入口同样会解除全局 GameContext 绑定。
/// </summary>
[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<InvalidOperationException>(() => GameContext.GetByType(architecture.GetType()));
Assert.Throws<InvalidOperationException>(() => GameContext.GetFirstArchitectureContext());
}
/// <summary> /// <summary>
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。 /// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。 /// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。

View File

@ -494,6 +494,11 @@ public class MicrosoftDiContainerTests
{ {
Assert.DoesNotThrow(() => Assert.DoesNotThrow(() =>
_container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly])); _container.RegisterCqrsHandlersFromAssemblies([typeof(DeterministicOrderNotification).Assembly]));
Assert.That(
_container.GetServicesUnsafe.Any(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<DeterministicOrderNotification>)),
Is.True);
} }
/// <summary> /// <summary>

View File

@ -359,6 +359,11 @@ public abstract class Architecture : IArchitecture
/// <summary> /// <summary>
/// 异步销毁架构及所有组件 /// 异步销毁架构及所有组件
/// </summary> /// </summary>
/// <remarks>
/// 无论 <c>_lifecycle.DestroyAsync()</c> 是否抛出异常,该方法都会在 <see langword="finally" /> 中调用
/// <see cref="GameContext.Unbind" />(<see cref="object.GetType" />()),移除当前架构类型在全局上下文表中的绑定。
/// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。
/// </remarks>
public virtual async ValueTask DestroyAsync() public virtual async ValueTask DestroyAsync()
{ {
try try
@ -376,11 +381,23 @@ public abstract class Architecture : IArchitecture
/// <summary> /// <summary>
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容) /// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
/// </summary> /// </summary>
/// <remarks>
/// 该同步兼容入口会与 <see cref="DestroyAsync" /> 保持相同的全局解绑语义;即使销毁过程抛出异常,
/// 也会在 <see langword="finally" /> 中调用 <see cref="GameContext.Unbind" />(<see cref="object.GetType" />())。
/// </remarks>
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")] [Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
public virtual void Destroy() public virtual void Destroy()
{
try
{ {
_lifecycle.Destroy(); _lifecycle.Destroy();
} }
finally
{
// 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。
GameContext.Unbind(GetType());
}
}
#endregion #endregion
} }

View File

@ -12,6 +12,12 @@ namespace GFramework.Core.Architectures;
/// </summary> /// </summary>
public static class GameContext 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<Type, IArchitectureContext> ArchitectureDictionary private static readonly ConcurrentDictionary<Type, IArchitectureContext> ArchitectureDictionary
= new(); = new();
private static IArchitectureContext? _currentArchitectureContext; private static IArchitectureContext? _currentArchitectureContext;
@ -39,15 +45,19 @@ public static class GameContext
ArgumentNullException.ThrowIfNull(architectureType); ArgumentNullException.ThrowIfNull(architectureType);
ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(context);
var currentContext = Interlocked.CompareExchange(ref _currentArchitectureContext, context, comparand: null); lock (SyncRoot)
if (currentContext != null && !ReferenceEquals(currentContext, context)) {
if (_currentArchitectureContext != null && !ReferenceEquals(_currentArchitectureContext, context))
throw new InvalidOperationException( throw new InvalidOperationException(
$"GameContext already tracks active context '{currentContext.GetType().Name}'. " + $"GameContext already tracks active context '{_currentArchitectureContext.GetType().Name}'. " +
$"Cannot bind a different context '{context.GetType().Name}'."); $"Cannot bind a different context '{context.GetType().Name}'.");
if (!ArchitectureDictionary.TryAdd(architectureType, context)) if (!ArchitectureDictionary.TryAdd(architectureType, context))
throw new InvalidOperationException( throw new InvalidOperationException(
$"Architecture context for '{architectureType.Name}' already exists"); $"Architecture context for '{architectureType.Name}' already exists");
_currentArchitectureContext ??= context;
}
} }
/// <summary> /// <summary>
@ -57,9 +67,12 @@ public static class GameContext
/// <returns>当前活动的架构上下文实例。</returns> /// <returns>当前活动的架构上下文实例。</returns>
/// <exception cref="InvalidOperationException">当当前没有活动上下文时抛出。</exception> /// <exception cref="InvalidOperationException">当当前没有活动上下文时抛出。</exception>
public static IArchitectureContext GetFirstArchitectureContext() public static IArchitectureContext GetFirstArchitectureContext()
{
lock (SyncRoot)
{ {
if (_currentArchitectureContext is { } context) if (_currentArchitectureContext is { } context)
return context; return context;
}
throw new InvalidOperationException("No active architecture context is currently bound."); throw new InvalidOperationException("No active architecture context is currently bound.");
} }
@ -76,11 +89,14 @@ public static class GameContext
{ {
ArgumentNullException.ThrowIfNull(type); ArgumentNullException.ThrowIfNull(type);
lock (SyncRoot)
{
if (ArchitectureDictionary.TryGetValue(type, out var context)) if (ArchitectureDictionary.TryGetValue(type, out var context))
return context; return context;
if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext)) if (_currentArchitectureContext != null && type.IsInstanceOfType(_currentArchitectureContext))
return _currentArchitectureContext; return _currentArchitectureContext;
}
throw new InvalidOperationException( throw new InvalidOperationException(
$"Architecture context for '{type.Name}' not found"); $"Architecture context for '{type.Name}' not found");
@ -88,31 +104,38 @@ public static class GameContext
/// <summary> /// <summary>
/// 获取指定类型的架构上下文实例 /// 获取指定类型的架构上下文实例。
/// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。
/// </summary> /// </summary>
/// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam> /// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam>
/// <returns>指定类型的架构上下文实例</returns> /// <returns>指定类型的架构上下文实例</returns>
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception> /// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
public static T Get<T>() where T : class, IArchitectureContext public static T Get<T>() where T : class, IArchitectureContext
{
lock (SyncRoot)
{ {
if (_currentArchitectureContext is T currentContext) if (_currentArchitectureContext is T currentContext)
return currentContext; return currentContext;
if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx)) if (ArchitectureDictionary.TryGetValue(typeof(T), out var ctx))
return (T)ctx; return (T)ctx;
}
throw new InvalidOperationException( throw new InvalidOperationException(
$"Architecture context '{typeof(T).Name}' not found"); $"Architecture context '{typeof(T).Name}' not found");
} }
/// <summary> /// <summary>
/// 尝试获取指定类型的架构上下文实例 /// 尝试获取指定类型的架构上下文实例。
/// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。
/// </summary> /// </summary>
/// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam> /// <typeparam name="T">架构上下文类型必须实现IArchitectureContext接口</typeparam>
/// <param name="context">输出参数如果找到则返回对应的架构上下文实例否则返回null</param> /// <param name="context">输出参数如果找到则返回对应的架构上下文实例否则返回null</param>
/// <returns>如果找到指定类型的架构上下文则返回true否则返回false</returns> /// <returns>如果找到指定类型的架构上下文则返回true否则返回false</returns>
public static bool TryGet<T>(out T? context) public static bool TryGet<T>(out T? context)
where T : class, IArchitectureContext where T : class, IArchitectureContext
{
lock (SyncRoot)
{ {
if (_currentArchitectureContext is T currentContext) if (_currentArchitectureContext is T currentContext)
{ {
@ -125,36 +148,61 @@ public static class GameContext
context = (T)ctx; context = (T)ctx;
return true; return true;
} }
}
context = null; context = null;
return false; return false;
} }
/// <summary> /// <summary>
/// 移除指定类型的架构上下文绑定 /// 移除指定类型的架构上下文绑定。
/// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。
/// </summary> /// </summary>
/// <param name="architectureType">要移除的架构类型</param> /// <param name="architectureType">要移除的架构类型</param>
/// <exception cref="ArgumentNullException"><paramref name="architectureType" /> 为 <see langword="null" />。</exception>
public static void Unbind(Type architectureType) public static void Unbind(Type architectureType)
{ {
ArgumentNullException.ThrowIfNull(architectureType); ArgumentNullException.ThrowIfNull(architectureType);
lock (SyncRoot)
{
if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext)) if (!ArchitectureDictionary.TryRemove(architectureType, out var removedContext))
return; return;
if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext)) if (_currentArchitectureContext == null || !ReferenceEquals(_currentArchitectureContext, removedContext))
return; return;
if (!ArchitectureDictionary.Values.Any(current => ReferenceEquals(current, removedContext))) if (!HasAliasForContext(removedContext))
Interlocked.CompareExchange(ref _currentArchitectureContext, null, removedContext); _currentArchitectureContext = null;
}
} }
/// <summary> /// <summary>
/// 清空所有架构上下文绑定 /// 清空所有架构上下文绑定,并重置当前活动上下文。
/// </summary> /// </summary>
public static void Clear() public static void Clear()
{
lock (SyncRoot)
{ {
ArchitectureDictionary.Clear(); ArchitectureDictionary.Clear();
Interlocked.Exchange(ref _currentArchitectureContext, null); _currentArchitectureContext = null;
}
}
/// <summary>
/// 判断当前是否仍存在指向同一上下文实例的其他类型别名。
/// </summary>
/// <param name="context">被移除绑定原本指向的上下文实例。</param>
/// <returns>如果还有其他别名指向同一实例则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool HasAliasForContext(IArchitectureContext context)
{
foreach (var current in ArchitectureDictionary.Values)
{
if (ReferenceEquals(current, context))
return true;
}
return false;
} }
} }

View File

@ -15,6 +15,7 @@ public sealed class GameContextProvider : IArchitectureContextProvider
/// 获取当前的架构上下文。 /// 获取当前的架构上下文。
/// </summary> /// </summary>
/// <returns>架构上下文实例</returns> /// <returns>架构上下文实例</returns>
/// <exception cref="InvalidOperationException">当前没有已绑定的活动架构上下文时抛出。</exception>
public IArchitectureContext GetContext() public IArchitectureContext GetContext()
{ {
return GameContext.GetFirstArchitectureContext(); return GameContext.GetFirstArchitectureContext();

View File

@ -48,6 +48,8 @@ public abstract class ContextAwareBase : IContextAware
/// <remarks> /// <remarks>
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" /> 返回的当前活动上下文。 /// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" /> 返回的当前活动上下文。
/// 该回退过程不执行额外同步,也不支持替换 provider如需这些能力请改用生成的 ContextAware 实现。 /// 该回退过程不执行额外同步,也不支持替换 provider如需这些能力请改用生成的 ContextAware 实现。
/// 一旦回退结果被写入 <see cref="Context" />,后续即使关联架构解除 <see cref="GameContext" /> 绑定,
/// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。
/// </remarks> /// </remarks>
IArchitectureContext IContextAware.GetContext() IArchitectureContext IContextAware.GetContext()
{ {

View File

@ -10,13 +10,14 @@
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-001` - 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-003`
- 当前阶段:`Phase 2` - 当前阶段:`Phase 3`
- 当前结论: - 当前结论:
- `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例 - `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例
- `MicrosoftDiContainer` 的预冻结 `Get<T>()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致 - `MicrosoftDiContainer` 的预冻结 `Get<T>()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致
- `IIocContainer` XML 文档已明确预冻结查询与 `Contains<T>()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义 - `IIocContainer` XML 文档已明确预冻结查询与 `Contains<T>()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义
- `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口 - `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口
- 当前 PR review 跟进中,`Architecture.Destroy()` 已补齐相同解绑语义,`GameContext` 也改为通过统一临界区维护别名字典与当前活动上下文的一致性
- 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main` - 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main`
## 当前活跃事实 ## 当前活跃事实
@ -35,6 +36,7 @@
- `Contains<T>()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点 - `Contains<T>()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点
- `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper - `ResolveCqrsRegistrationService()` 仍要求注册阶段对 `ICqrsRegistrationService` 可见的是实例绑定;若后续改成工厂或实现类型注册,需要额外设计注册阶段激活 helper
- 现有解绑逻辑通过 `Architecture.GetType()` 移除初始化期绑定;若后续引入更多显式上下文别名,需同步评估是否要在销毁时额外移除这些别名 - 现有解绑逻辑通过 `Architecture.GetType()` 移除初始化期绑定;若后续引入更多显式上下文别名,需同步评估是否要在销毁时额外移除这些别名
- `GameContext` 当前通过粗粒度锁保护组合状态一致性;若该入口后续进入高频并发热点,需要基于真实性能数据再评估更细粒度方案
## 最近权威验证 ## 最近权威验证
@ -52,8 +54,15 @@
- 结果:通过,`32/32` passed - 结果:通过,`32/32` passed
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release` - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:再次通过,`0 warning / 0 error` - 结果:再次通过,`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` 标记为兼容层,并收口其公开使用面 1. 若后续继续推进,可评估是否要把 `GameContext.ArchitectureReadOnlyDictionary` 标记为兼容层,并收口其公开使用面
2. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试 2. 若 `GameContext` 的组合锁成为瓶颈,再基于压测结果讨论更细粒度的原子状态模型
3. 若 CQRS runtime seam 计划改成工厂式注册,再单独补“注册阶段激活 helper”的设计与测试

View File

@ -42,3 +42,25 @@
### 当前下一步 ### 当前下一步
1. 若后续要进一步彻底移除全局回退,可单独评估 `GameContext` 公开别名字典的收口策略与生成器默认 provider 的进一步简化空间 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` 锁兼容实现