fix(mesh-phase5): telemetry service — graceful client-disconnect + null-safe required-string mapping
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -67,17 +67,21 @@ public static class TelemetryProtoMapNode
|
||||
|
||||
private static AlarmTransition MapAlarm(AlarmTransitionEvent e)
|
||||
{
|
||||
// Required (non-optional) proto string setters throw ArgumentNullException on null, and the
|
||||
// domain records carry no runtime null guard (nullable-ref annotations are compile-time only).
|
||||
// Coalesce every required string to "" so a stray null can never throw out of the pump and kill
|
||||
// the stream. The `optional` fields below keep their null-vs-"" presence guards.
|
||||
var msg = new AlarmTransition
|
||||
{
|
||||
AlarmId = e.AlarmId,
|
||||
EquipmentPath = e.EquipmentPath,
|
||||
AlarmName = e.AlarmName,
|
||||
TransitionKind = e.TransitionKind,
|
||||
AlarmId = e.AlarmId ?? "",
|
||||
EquipmentPath = e.EquipmentPath ?? "",
|
||||
AlarmName = e.AlarmName ?? "",
|
||||
TransitionKind = e.TransitionKind ?? "",
|
||||
Severity = e.Severity,
|
||||
Message = e.Message,
|
||||
User = e.User,
|
||||
Message = e.Message ?? "",
|
||||
User = e.User ?? "",
|
||||
TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
|
||||
AlarmTypeName = e.AlarmTypeName,
|
||||
AlarmTypeName = e.AlarmTypeName ?? "",
|
||||
};
|
||||
|
||||
if (e.Comment is not null)
|
||||
@@ -93,9 +97,9 @@ public static class TelemetryProtoMapNode
|
||||
{
|
||||
var msg = new ScriptLog
|
||||
{
|
||||
ScriptId = e.ScriptId,
|
||||
Level = e.Level,
|
||||
Message = e.Message,
|
||||
ScriptId = e.ScriptId ?? "",
|
||||
Level = e.Level ?? "",
|
||||
Message = e.Message ?? "",
|
||||
TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
|
||||
};
|
||||
|
||||
@@ -113,9 +117,9 @@ public static class TelemetryProtoMapNode
|
||||
{
|
||||
var msg = new DriverHealth
|
||||
{
|
||||
ClusterId = e.ClusterId,
|
||||
DriverInstanceId = e.DriverInstanceId,
|
||||
State = e.State,
|
||||
ClusterId = e.ClusterId ?? "",
|
||||
DriverInstanceId = e.DriverInstanceId ?? "",
|
||||
State = e.State ?? "",
|
||||
ErrorCount5Min = e.ErrorCount5Min,
|
||||
PublishedUtc = ToUtcTimestamp(e.PublishedUtc),
|
||||
};
|
||||
@@ -132,8 +136,8 @@ public static class TelemetryProtoMapNode
|
||||
{
|
||||
var msg = new DriverResilienceStatus
|
||||
{
|
||||
DriverInstanceId = e.DriverInstanceId,
|
||||
HostName = e.HostName,
|
||||
DriverInstanceId = e.DriverInstanceId ?? "",
|
||||
HostName = e.HostName ?? "",
|
||||
BreakerOpen = e.BreakerOpen,
|
||||
ConsecutiveFailures = e.ConsecutiveFailures,
|
||||
CurrentInFlight = e.CurrentInFlight,
|
||||
|
||||
@@ -103,6 +103,15 @@ public sealed class TelemetryStreamGrpcService : GeneratedServiceBase
|
||||
// Normal termination: the client disconnected (context token) or the max-lifetime cap
|
||||
// fired (linked token). Neither is an error — swallow and let the stream close cleanly.
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or RpcException)
|
||||
{
|
||||
// Routine mid-stream disconnect: central's TelemetryStreamClient reconnects on ANY
|
||||
// non-Cancelled error, so a broken pipe / connection-reset here is expected churn, not a
|
||||
// fault. Swallow at Debug so grpc-dotnet doesn't log it at Error on every reconnect.
|
||||
// (A genuinely unexpected exception still escapes to surface as an error.)
|
||||
_log.LogDebug(
|
||||
ex, "Telemetry client disconnected mid-stream (correlationId={CorrelationId})", correlationId);
|
||||
}
|
||||
|
||||
_log.LogDebug("Telemetry stream closed (correlationId={CorrelationId})", correlationId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user