mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-08 09:34:30 +08:00
fix(core): 修复上下文销毁解绑与并发一致性
- 修复 GameContext 的别名字典与当前活动上下文同步边界,避免解绑与读取路径出现状态漂移 - 修复 Architecture.Destroy() 缺少全局解绑的问题,并补充相关生命周期 XML 文档 - 更新回归测试、CQRS 注册断言与 single-context-priority 跟踪记录
This commit is contained in:
parent
ff04a4fbad
commit
ee8b6a4deb
@ -237,6 +237,26 @@ public class ArchitectureLifecycleBehaviorTests
|
||||
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>
|
||||
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
|
||||
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。
|
||||
|
||||
@ -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<DeterministicOrderNotification>)),
|
||||
Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -359,6 +359,11 @@ public abstract class Architecture : IArchitecture
|
||||
/// <summary>
|
||||
/// 异步销毁架构及所有组件
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 无论 <c>_lifecycle.DestroyAsync()</c> 是否抛出异常,该方法都会在 <see langword="finally" /> 中调用
|
||||
/// <see cref="GameContext.Unbind" />(<see cref="object.GetType" />()),移除当前架构类型在全局上下文表中的绑定。
|
||||
/// 这样可以阻止新的惰性上下文回退命中已销毁实例;但已经缓存上下文的对象不会被自动重置。
|
||||
/// </remarks>
|
||||
public virtual async ValueTask DestroyAsync()
|
||||
{
|
||||
try
|
||||
@ -376,10 +381,22 @@ public abstract class Architecture : IArchitecture
|
||||
/// <summary>
|
||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该同步兼容入口会与 <see cref="DestroyAsync" /> 保持相同的全局解绑语义;即使销毁过程抛出异常,
|
||||
/// 也会在 <see langword="finally" /> 中调用 <see cref="GameContext.Unbind" />(<see cref="object.GetType" />())。
|
||||
/// </remarks>
|
||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
||||
public virtual void Destroy()
|
||||
{
|
||||
_lifecycle.Destroy();
|
||||
try
|
||||
{
|
||||
_lifecycle.Destroy();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 同步销毁同样需要解除全局回退入口,避免兼容调用路径保留过期上下文。
|
||||
GameContext.Unbind(GetType());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@ -12,6 +12,12 @@ namespace GFramework.Core.Architectures;
|
||||
/// </summary>
|
||||
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
|
||||
= 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -58,8 +68,11 @@ public static class GameContext
|
||||
/// <exception cref="InvalidOperationException">当当前没有活动上下文时抛出。</exception>
|
||||
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
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定类型的架构上下文实例
|
||||
/// 获取指定类型的架构上下文实例。
|
||||
/// 该方法会优先复用当前活动上下文,再回退到显式注册的类型别名。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">架构上下文类型,必须实现IArchitectureContext接口</typeparam>
|
||||
/// <returns>指定类型的架构上下文实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当指定类型的架构上下文不存在时抛出</exception>
|
||||
public static T Get<T>() 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定类型的架构上下文实例
|
||||
/// 尝试获取指定类型的架构上下文实例。
|
||||
/// 该方法会优先检查当前活动上下文是否兼容目标类型,再回退到显式注册的类型别名。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">架构上下文类型,必须实现IArchitectureContext接口</typeparam>
|
||||
/// <param name="context">输出参数,如果找到则返回对应的架构上下文实例,否则返回null</param>
|
||||
@ -114,16 +135,19 @@ public static class GameContext
|
||||
public static bool TryGet<T>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定类型的架构上下文绑定
|
||||
/// 移除指定类型的架构上下文绑定。
|
||||
/// 当最后一个指向当前活动上下文的别名被移除时,也会同步清空当前活动上下文指针。
|
||||
/// </summary>
|
||||
/// <param name="architectureType">要移除的架构类型</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="architectureType" /> 为 <see langword="null" />。</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有架构上下文绑定
|
||||
/// 清空所有架构上下文绑定,并重置当前活动上下文。
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
ArchitectureDictionary.Clear();
|
||||
Interlocked.Exchange(ref _currentArchitectureContext, null);
|
||||
lock (SyncRoot)
|
||||
{
|
||||
ArchitectureDictionary.Clear();
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
/// 获取当前的架构上下文。
|
||||
/// </summary>
|
||||
/// <returns>架构上下文实例</returns>
|
||||
/// <exception cref="InvalidOperationException">当前没有已绑定的活动架构上下文时抛出。</exception>
|
||||
public IArchitectureContext GetContext()
|
||||
{
|
||||
return GameContext.GetFirstArchitectureContext();
|
||||
|
||||
@ -48,6 +48,8 @@ public abstract class ContextAwareBase : IContextAware
|
||||
/// <remarks>
|
||||
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" /> 返回的当前活动上下文。
|
||||
/// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。
|
||||
/// 一旦回退结果被写入 <see cref="Context" />,后续即使关联架构解除 <see cref="GameContext" /> 绑定,
|
||||
/// 该实例仍会保留原引用,调用方需要自行约束其生命周期或改用支持 provider 协调的生成实现。
|
||||
/// </remarks>
|
||||
IArchitectureContext IContextAware.GetContext()
|
||||
{
|
||||
|
||||
@ -10,13 +10,14 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-001`
|
||||
- 当前阶段:`Phase 2`
|
||||
- 恢复点编号:`SINGLE-CONTEXT-PRIORITY-RP-003`
|
||||
- 当前阶段:`Phase 3`
|
||||
- 当前结论:
|
||||
- `GameContext` 已从“字典首个枚举值即默认上下文”收敛为“单活动上下文 + 类型别名兼容查找”;同一全局上下文表不再允许并存两个不同上下文实例
|
||||
- `MicrosoftDiContainer` 的预冻结 `Get<T>()` / `Get(Type)` 已改为复用实例可见性收集逻辑,和 `GetAll*` 的实例暴露规则保持一致
|
||||
- `IIocContainer` XML 文档已明确预冻结查询与 `Contains<T>()` 的契约边界,避免把注册阶段查询误读为完整 DI 激活语义
|
||||
- `Architecture.DestroyAsync()` 现会在生命周期销毁完成后显式解除 `GameContext` 绑定,防止已销毁架构继续充当默认上下文回退入口
|
||||
- 当前 PR review 跟进中,`Architecture.Destroy()` 已补齐相同解绑语义,`GameContext` 也改为通过统一临界区维护别名字典与当前活动上下文的一致性
|
||||
- 当前分支从 `main` 创建,已完成 `git pull --ff-only origin main`
|
||||
|
||||
## 当前活跃事实
|
||||
@ -35,6 +36,7 @@
|
||||
- `Contains<T>()` 在预冻结阶段目前更接近“是否存在注册”,不等同于“是否能立即解析实例”;本轮若不改其行为,需要在文档和测试中明确这一点
|
||||
- `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”的设计与测试
|
||||
|
||||
@ -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` 锁兼容实现
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user