diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs index 10604912..86b88e0b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs @@ -250,14 +250,38 @@ public class ScriptRuntimeContext /// /// M2.12 (#25): fire-and-forget emission of a script Error site event /// for a recursion-limit violation. Mirrors the call shape used by - /// ScriptExecutionActor's catch blocks (WP-32 / M1.8). The returned - /// task is intentionally discarded by callers (_ =) so the event log - /// never blocks or faults the throw that follows. A null logger is a no-op. + /// ScriptExecutionActor's catch blocks (WP-32 / M1.8). A fault from + /// the site-event logger is observed-and-dropped (best-effort) via + /// ContinueWith(OnlyOnFaulted) — it never blocks or faults the + /// _logger.LogError + throw path that follows. A null logger is a no-op. /// - private Task EmitRecursionLimitEventAsync(string msg) => - _siteEventLogger?.LogEventAsync( - "script", "Error", _instanceName, _sourceScript ?? "ScriptRuntimeContext", msg) - ?? Task.CompletedTask; + private void EmitRecursionLimitEventAsync(string msg) + { + if (_siteEventLogger == null) + return; + + var source = string.IsNullOrEmpty(_instanceName) + ? "recursion-guard" + : $"InstanceScript:{_instanceName}"; + + var logTask = _siteEventLogger.LogEventAsync("script", "Error", _instanceName, source, msg); + if (!logTask.IsCompleted) + { + logTask.ContinueWith( + t => _logger.LogWarning(t.Exception, + "Site event log write failed for recursion-limit violation on instance '{Instance}'", + _instanceName), + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + else if (logTask.IsFaulted) + { + _logger.LogWarning(logTask.Exception, + "Site event log write failed for recursion-limit violation on instance '{Instance}'", + _instanceName); + } + } /// /// Gets the current value of an attribute from the Instance Actor. @@ -333,7 +357,7 @@ public class ScriptRuntimeContext $"CallScript('{scriptName}') rejected at depth {nextDepth}."; _logger.LogError(msg); // M2.12 (#25): emit to site event log in addition to ILogger; fire-and-forget. - _ = EmitRecursionLimitEventAsync(msg); + EmitRecursionLimitEventAsync(msg); throw new InvalidOperationException(msg); } @@ -498,7 +522,7 @@ public class ScriptRuntimeContext _logger.LogError(msg); // M2.12 (#25): emit to site event log via the parent context's // helper — single emission path, fire-and-forget. - _ = _context.EmitRecursionLimitEventAsync(msg); + _context.EmitRecursionLimitEventAsync(msg); throw new InvalidOperationException(msg); } diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/RecursionLimitSiteEventTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/RecursionLimitSiteEventTests.cs index 4fe09f7f..3843d08d 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/RecursionLimitSiteEventTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/RecursionLimitSiteEventTests.cs @@ -143,6 +143,7 @@ public class RecursionLimitSiteEventTests string? capturedCategory = null; string? capturedSeverity = null; string? capturedInstance = null; + string? capturedSource = null; string? capturedMessage = null; var loggerMock = new Mock(); @@ -157,6 +158,7 @@ public class RecursionLimitSiteEventTests capturedCategory = cat; capturedSeverity = sev; capturedInstance = inst; + capturedSource = src; capturedMessage = msg; }) .Returns(Task.CompletedTask); @@ -169,6 +171,8 @@ public class RecursionLimitSiteEventTests Assert.Equal("script", capturedCategory); Assert.Equal("Error", capturedSeverity); Assert.Equal(InstanceName, capturedInstance); + // Source fallback: no sourceScript wired, so the helper uses "InstanceScript:{instanceName}". + Assert.Equal($"InstanceScript:{InstanceName}", capturedSource); Assert.NotNull(capturedMessage); Assert.Contains("CallScript", capturedMessage); Assert.Contains("2", capturedMessage); // maxCallDepth in the message