diff --git a/GameFrameX.Discovery/Discovery/DiscoveryRegistry.cs b/GameFrameX.Discovery/Discovery/DiscoveryRegistry.cs
index 35634336a..ce3b54361 100644
--- a/GameFrameX.Discovery/Discovery/DiscoveryRegistry.cs
+++ b/GameFrameX.Discovery/Discovery/DiscoveryRegistry.cs
@@ -355,11 +355,7 @@ public void Dispose()
try
{
_loopCancellation.Cancel();
- WriteStoppedAsync().GetAwaiter().GetResult();
- }
- catch (Exception)
- {
- // 尽力而为:进程退出路径上数据库不可达时无法补写终态,交给 TTL 兜底清除。
+ WriteTerminalHeartbeatSafely();
}
finally
{
@@ -470,13 +466,32 @@ private static async Task WaitLoopAsync(Task loopTask)
/// 事件参数 / The event arguments
private void OnProcessExit(object sender, EventArgs eventArguments)
{
- try
- {
- WriteStoppedAsync().GetAwaiter().GetResult();
- }
- catch (Exception)
+ WriteTerminalHeartbeatSafely();
+ }
+
+ ///
+ /// 终态心跳的同步尽力写入:2 秒超时 + Task.Run 脱离可能的 SynchronizationContext,
+ /// 避免存在同步上下文时死锁;失败/超时记日志不阻塞退出(TTL 兜底清除)。
+ ///
+ ///
+ /// Best-effort synchronous write of the terminal heartbeat: a 2-second timeout and
+ /// Task.Run to escape any SynchronizationContext (deadlock guard); failures and
+ /// timeouts are logged without blocking exit (TTL cleanup is the fallback).
+ ///
+ private void WriteTerminalHeartbeatSafely()
+ {
+ using (var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(2)))
{
- // 进程退出路径上数据库不可达时无法补写终态,交给 TTL 兜底清除。
+ try
+ {
+ Task.Run(() => WriteStoppedAsync(), cancellation.Token).GetAwaiter().GetResult();
+ }
+ catch (Exception exception)
+ {
+ // 尽力而为:进程退出路径上数据库不可达/超时时无法补写终态,交给 TTL 兜底清除。
+ // Localization: Discovery.Registry.TerminalHeartbeatFailed - [DiscoveryRegistry] 终态心跳写入失败(超时 2s 内未完成),交给 TTL 兜底清除
+ LogHelper.Error(exception, LocalizationService.GetString(Localization.Keys.Discovery.Registry.TerminalHeartbeatFailed));
+ }
}
}
diff --git a/GameFrameX.Localization/Keys.Discovery.cs b/GameFrameX.Localization/Keys.Discovery.cs
index 436a4ef24..ff29e9574 100644
--- a/GameFrameX.Localization/Keys.Discovery.cs
+++ b/GameFrameX.Localization/Keys.Discovery.cs
@@ -235,6 +235,15 @@ public static class Registry
///
public const string TtlCleanupFailed = "Discovery.Registry.TtlCleanupFailed";
+ ///
+ /// [DiscoveryRegistry] 终态心跳写入失败(超时 2s 内未完成),交给 TTL 兜底清除
+ ///
+ ///
+ /// 键名: Discovery.Registry.TerminalHeartbeatFailed
+ /// 用途: Dispose/ProcessExit 终态心跳尽力写入失败或超时时的错误日志
+ ///
+ public const string TerminalHeartbeatFailed = "Discovery.Registry.TerminalHeartbeatFailed";
+
///
/// 附加广播端点未被发布:服务进程类型 '{0}' 不是本进程心跳承载的首选类型 '{1}'(心跳文档单 Role 模型,非首选类型不跨进程可达)
///
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.resx b/GameFrameX.Localization/Localization/Messages/Resources.resx
index f7a878775..65350f2b5 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.resx
@@ -1984,6 +1984,10 @@
[AdvertiseEndpointEnvironment] egress address detection failed; falling back to the machine name {0} as the advertise host. Set {1} explicitly when cross-process routing is required.
+
+
+ [DiscoveryRegistry] terminal heartbeat write failed (not completed within the 2s timeout); leaving it to TTL cleanup
+
[DiscoveryRegistry] heartbeat upsert failed for instance {0}; will retry next interval
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
index d06b145de..24e45d32b 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
@@ -1978,6 +1978,10 @@
[AdvertiseEndpointEnvironment] 出口地址探测失败;回退使用机器名 {0} 作为广播主机。需要跨进程路由时请显式设置 {1}
+
+
+ [DiscoveryRegistry] 终态心跳写入失败(超时 2s 内未完成),交给 TTL 兜底清除
+
[DiscoveryRegistry] 实例 {0} 心跳写入失败;将在下个间隔重试