feat(lmxproxy): replace subscribe/unsubscribe health probe with persistent subscription
The old probe did a subscribe-read-unsubscribe cycle every 5 seconds to check connection health. This created unnecessary churn and didn't detect the failure mode where long-lived subscriptions silently stop receiving COM callbacks (e.g. stalled STA message pump). The new approach keeps a persistent subscription on the health check tag and forces reconnect if no value update arrives within a configurable threshold (ProbeStaleThresholdMs, default 5s). Also adds STA message pump debug logging (5-min heartbeat with message counters) and fixes log file path resolution for Windows services. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,16 +34,13 @@ namespace ZB.MOM.WW.LmxProxy.Host.Configuration
|
|||||||
/// <summary>Health check / probe configuration.</summary>
|
/// <summary>Health check / probe configuration.</summary>
|
||||||
public class HealthCheckConfiguration
|
public class HealthCheckConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>Tag address to probe for connection liveness. Default: DevPlatform.Scheduler.ScanTime.</summary>
|
/// <summary>Tag address to subscribe to for connection liveness. Default: DevPlatform.Scheduler.ScanTime.</summary>
|
||||||
public string TestTagAddress { get; set; } = "DevPlatform.Scheduler.ScanTime";
|
public string TestTagAddress { get; set; } = "DevPlatform.Scheduler.ScanTime";
|
||||||
|
|
||||||
/// <summary>Probe timeout in milliseconds. Default: 5000.</summary>
|
/// <summary>
|
||||||
public int ProbeTimeoutMs { get; set; } = 5000;
|
/// Maximum time (ms) without a value update on the test tag before forcing reconnect.
|
||||||
|
/// Default: 5000 (5 seconds).
|
||||||
/// <summary>Consecutive transport failures before forced reconnect. Default: 3.</summary>
|
/// </summary>
|
||||||
public int MaxConsecutiveTransportFailures { get; set; } = 3;
|
public int ProbeStaleThresholdMs { get; set; } = 5000;
|
||||||
|
|
||||||
/// <summary>Probe interval while in degraded state (ms). Default: 30000 (30s).</summary>
|
|
||||||
public int DegradedProbeIntervalMs { get; set; } = 30000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,12 +57,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
|||||||
int pollIntervalMs,
|
int pollIntervalMs,
|
||||||
CancellationToken ct = default);
|
CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Probes connection health by reading a test tag.
|
|
||||||
/// Returns a classified result: Healthy, TransportFailure, or DataDegraded.
|
|
||||||
/// </summary>
|
|
||||||
Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribes specific tag addresses. Removes from stored subscriptions
|
/// Unsubscribes specific tag addresses. Removes from stored subscriptions
|
||||||
/// and COM state. Safe to call after reconnect -- uses current handle mappings.
|
/// and COM state. Safe to call after reconnect -- uses current handle mappings.
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ namespace ZB.MOM.WW.LmxProxy.Host
|
|||||||
nodeName: _config.Connection.NodeName,
|
nodeName: _config.Connection.NodeName,
|
||||||
galaxyName: _config.Connection.GalaxyName,
|
galaxyName: _config.Connection.GalaxyName,
|
||||||
probeTestTagAddress: _config.HealthCheck.TestTagAddress,
|
probeTestTagAddress: _config.HealthCheck.TestTagAddress,
|
||||||
probeTimeoutMs: _config.HealthCheck.ProbeTimeoutMs,
|
probeStaleThresholdMs: _config.HealthCheck.ProbeStaleThresholdMs,
|
||||||
maxConsecutiveTransportFailures: _config.HealthCheck.MaxConsecutiveTransportFailures,
|
|
||||||
degradedProbeIntervalMs: _config.HealthCheck.DegradedProbeIntervalMs,
|
|
||||||
clientName: _config.ClientName);
|
clientName: _config.ClientName);
|
||||||
|
|
||||||
// 5. Connect to MxAccess synchronously (with timeout)
|
// 5. Connect to MxAccess synchronously (with timeout)
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
// Recreate any stored subscriptions from a previous connection
|
// Recreate any stored subscriptions from a previous connection
|
||||||
await RecreateStoredSubscriptionsAsync();
|
await RecreateStoredSubscriptionsAsync();
|
||||||
|
|
||||||
|
// Start persistent probe subscription
|
||||||
|
await StartProbeSubscriptionAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -172,87 +175,61 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Probes the connection by reading a test tag with a timeout.
|
/// Subscribes to the configured probe test tag so that OnDataChange
|
||||||
/// Classifies the result as transport failure vs data degraded.
|
/// callbacks update <see cref="_lastProbeValueTime"/>. Called after
|
||||||
|
/// connect (and reconnect). The subscription is stored for reconnect
|
||||||
|
/// replay like any other subscription.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs,
|
private async Task StartProbeSubscriptionAsync()
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
if (!IsConnected)
|
if (_probeTestTagAddress == null) return;
|
||||||
return ProbeResult.TransportFailed("Not connected");
|
|
||||||
|
|
||||||
try
|
_lastProbeValueTime = DateTime.UtcNow;
|
||||||
{
|
|
||||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
|
|
||||||
{
|
|
||||||
cts.CancelAfter(timeoutMs);
|
|
||||||
|
|
||||||
Vtq vtq;
|
await _staThread.RunAsync(() =>
|
||||||
try
|
|
||||||
{
|
{
|
||||||
vtq = await ReadAsync(testTagAddress, cts.Token);
|
lock (_lock)
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
// Our timeout fired, not the caller's -- treat as transport failure
|
if (!IsConnected || _lmxProxy == null) return;
|
||||||
return ProbeResult.TransportFailed("Probe read timed out after " + timeoutMs + "ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vtq.Quality == Domain.Quality.Bad_NotConnected ||
|
// Subscribe (skips if already subscribed from reconnect replay)
|
||||||
vtq.Quality == Domain.Quality.Bad_CommFailure)
|
SubscribeInternal(_probeTestTagAddress);
|
||||||
{
|
|
||||||
return ProbeResult.TransportFailed("Probe returned " + vtq.Quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!vtq.Quality.IsGood())
|
// Store a no-op callback — the real work happens in OnProbeDataChange
|
||||||
{
|
// which is called from OnDataChange before the stored callback
|
||||||
return ProbeResult.Degraded(vtq.Quality, vtq.Timestamp,
|
_storedSubscriptions[_probeTestTagAddress] = (_, __) => { };
|
||||||
"Probe quality: " + vtq.Quality);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (DateTime.UtcNow - vtq.Timestamp > TimeSpan.FromMinutes(5))
|
Log.Information("Probe subscription started for {Tag} (stale threshold={ThresholdMs}ms)",
|
||||||
{
|
_probeTestTagAddress, _probeStaleThresholdMs);
|
||||||
return ProbeResult.Degraded(vtq.Quality, vtq.Timestamp,
|
|
||||||
"Probe data stale (>" + 5 + "min)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProbeResult.Healthy(vtq.Quality, vtq.Timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (System.Runtime.InteropServices.COMException ex)
|
|
||||||
{
|
|
||||||
return ProbeResult.TransportFailed("COM exception: " + ex.Message, ex);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex) when (ex.Message.Contains("Not connected"))
|
|
||||||
{
|
|
||||||
return ProbeResult.TransportFailed(ex.Message, ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return ProbeResult.TransportFailed("Probe failed: " + ex.Message, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Auto-reconnect monitor loop with active health probing.
|
/// Called from <see cref="OnDataChange"/> when a value arrives for the probe tag.
|
||||||
/// - If IsConnected is false: immediate reconnect (existing behavior).
|
/// Updates the last-seen timestamp so the monitor loop can detect staleness.
|
||||||
/// - If IsConnected is true and probe configured: read test tag each interval.
|
/// </summary>
|
||||||
/// - TransportFailure for N consecutive probes -> forced disconnect + reconnect.
|
internal void OnProbeDataChange(string address, Vtq vtq)
|
||||||
/// - DataDegraded -> stay connected, back off probe interval, report degraded.
|
{
|
||||||
/// - Healthy -> reset counters and resume normal interval.
|
_lastProbeValueTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-reconnect monitor loop with persistent subscription probe.
|
||||||
|
/// - If disconnected: attempt reconnect.
|
||||||
|
/// - If connected and probe configured: check time since last probe value update.
|
||||||
|
/// If stale beyond threshold, force disconnect and reconnect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task MonitorConnectionAsync(CancellationToken ct)
|
private async Task MonitorConnectionAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
Log.Information("Connection monitor loop started (interval={IntervalMs}ms, probe={ProbeEnabled})",
|
Log.Information("Connection monitor loop started (interval={IntervalMs}ms, probe={ProbeEnabled}, staleThreshold={StaleMs}ms)",
|
||||||
_monitorIntervalMs, _probeTestTagAddress != null);
|
_monitorIntervalMs, _probeTestTagAddress != null, _probeStaleThresholdMs);
|
||||||
|
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var interval = _isDegraded ? _degradedProbeIntervalMs : _monitorIntervalMs;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(interval, ct);
|
await Task.Delay(_monitorIntervalMs, ct);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -262,50 +239,20 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
// -- Case 1: Already disconnected --
|
// -- Case 1: Already disconnected --
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
_isDegraded = false;
|
|
||||||
_consecutiveTransportFailures = 0;
|
|
||||||
await AttemptReconnectAsync(ct);
|
await AttemptReconnectAsync(ct);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Case 2: Connected, no probe configured -- legacy behavior --
|
// -- Case 2: Connected, no probe configured --
|
||||||
if (_probeTestTagAddress == null)
|
if (_probeTestTagAddress == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// -- Case 3: Connected, probe configured -- active health check --
|
// -- Case 3: Connected, check probe staleness --
|
||||||
var probe = await ProbeConnectionAsync(_probeTestTagAddress, _probeTimeoutMs, ct);
|
var elapsed = DateTime.UtcNow - _lastProbeValueTime;
|
||||||
|
if (elapsed.TotalMilliseconds > _probeStaleThresholdMs)
|
||||||
switch (probe.Status)
|
|
||||||
{
|
{
|
||||||
case ProbeStatus.Healthy:
|
Log.Warning("Probe tag {Tag} stale for {ElapsedMs}ms (threshold={ThresholdMs}ms) — forcing reconnect",
|
||||||
if (_isDegraded)
|
_probeTestTagAddress, (int)elapsed.TotalMilliseconds, _probeStaleThresholdMs);
|
||||||
{
|
|
||||||
Log.Information("Probe healthy -- exiting degraded mode");
|
|
||||||
_isDegraded = false;
|
|
||||||
}
|
|
||||||
_consecutiveTransportFailures = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ProbeStatus.DataDegraded:
|
|
||||||
_consecutiveTransportFailures = 0;
|
|
||||||
if (!_isDegraded)
|
|
||||||
{
|
|
||||||
Log.Warning("Probe degraded: {Message} -- entering degraded mode (probe interval {IntervalMs}ms)",
|
|
||||||
probe.Message, _degradedProbeIntervalMs);
|
|
||||||
_isDegraded = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ProbeStatus.TransportFailure:
|
|
||||||
_isDegraded = false;
|
|
||||||
_consecutiveTransportFailures++;
|
|
||||||
Log.Warning("Probe transport failure ({Count}/{Max}): {Message}",
|
|
||||||
_consecutiveTransportFailures, _maxConsecutiveTransportFailures, probe.Message);
|
|
||||||
|
|
||||||
if (_consecutiveTransportFailures >= _maxConsecutiveTransportFailures)
|
|
||||||
{
|
|
||||||
Log.Warning("Max consecutive transport failures reached -- forcing reconnect");
|
|
||||||
_consecutiveTransportFailures = 0;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -314,13 +261,10 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Warning(ex, "Error during forced disconnect before reconnect");
|
Log.Warning(ex, "Error during forced disconnect before reconnect");
|
||||||
// DisconnectAsync already calls CleanupComObjectsAsync on error path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await AttemptReconnectAsync(ct);
|
await AttemptReconnectAsync(ct);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Connection monitor loop exited");
|
Log.Information("Connection monitor loop exited");
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update probe timestamp if this is the probe tag
|
||||||
|
if (_probeTestTagAddress != null &&
|
||||||
|
string.Equals(address, _probeTestTagAddress, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
OnProbeDataChange(address, vtq);
|
||||||
|
}
|
||||||
|
|
||||||
callback.Invoke(address, vtq);
|
callback.Invoke(address, vtq);
|
||||||
|
|
||||||
// Also route to the SubscriptionManager's global handler
|
// Also route to the SubscriptionManager's global handler
|
||||||
|
|||||||
@@ -48,13 +48,10 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
// Probe configuration
|
// Probe configuration
|
||||||
private readonly string? _probeTestTagAddress;
|
private readonly string? _probeTestTagAddress;
|
||||||
private readonly int _probeTimeoutMs;
|
private readonly int _probeStaleThresholdMs;
|
||||||
private readonly int _maxConsecutiveTransportFailures;
|
|
||||||
private readonly int _degradedProbeIntervalMs;
|
|
||||||
|
|
||||||
// Probe state
|
// Probe state — updated by OnDataChange callback, read by monitor loop
|
||||||
private int _consecutiveTransportFailures;
|
private DateTime _lastProbeValueTime;
|
||||||
private bool _isDegraded;
|
|
||||||
|
|
||||||
// Stored subscriptions for reconnect replay
|
// Stored subscriptions for reconnect replay
|
||||||
private readonly Dictionary<string, Action<string, Vtq>> _storedSubscriptions
|
private readonly Dictionary<string, Action<string, Vtq>> _storedSubscriptions
|
||||||
@@ -80,9 +77,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
string? nodeName = null,
|
string? nodeName = null,
|
||||||
string? galaxyName = null,
|
string? galaxyName = null,
|
||||||
string? probeTestTagAddress = null,
|
string? probeTestTagAddress = null,
|
||||||
int probeTimeoutMs = 5000,
|
int probeStaleThresholdMs = 5000,
|
||||||
int maxConsecutiveTransportFailures = 3,
|
|
||||||
int degradedProbeIntervalMs = 30000,
|
|
||||||
string? clientName = null)
|
string? clientName = null)
|
||||||
{
|
{
|
||||||
_maxConcurrentOperations = maxConcurrentOperations;
|
_maxConcurrentOperations = maxConcurrentOperations;
|
||||||
@@ -93,9 +88,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
_nodeName = nodeName;
|
_nodeName = nodeName;
|
||||||
_galaxyName = galaxyName;
|
_galaxyName = galaxyName;
|
||||||
_probeTestTagAddress = probeTestTagAddress;
|
_probeTestTagAddress = probeTestTagAddress;
|
||||||
_probeTimeoutMs = probeTimeoutMs;
|
_probeStaleThresholdMs = probeStaleThresholdMs;
|
||||||
_maxConsecutiveTransportFailures = maxConsecutiveTransportFailures;
|
|
||||||
_degradedProbeIntervalMs = degradedProbeIntervalMs;
|
|
||||||
_clientName = clientName ?? "LmxProxy-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
_clientName = clientName ?? "LmxProxy-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||||
|
|
||||||
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
private const uint PM_NOREMOVE = 0x0000;
|
private const uint PM_NOREMOVE = 0x0000;
|
||||||
|
|
||||||
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
|
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
|
||||||
|
private static readonly TimeSpan PumpLogInterval = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
private readonly Thread _thread;
|
private readonly Thread _thread;
|
||||||
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
|
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
|
||||||
@@ -26,6 +27,12 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
private volatile uint _nativeThreadId;
|
private volatile uint _nativeThreadId;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
private long _totalMessages;
|
||||||
|
private long _appMessages;
|
||||||
|
private long _dispatchedMessages;
|
||||||
|
private long _workItemsExecuted;
|
||||||
|
private DateTime _lastLogTime;
|
||||||
|
|
||||||
public StaComThread()
|
public StaComThread()
|
||||||
{
|
{
|
||||||
_thread = new Thread(ThreadEntry)
|
_thread = new Thread(ThreadEntry)
|
||||||
@@ -125,12 +132,18 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
|
PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
|
||||||
|
|
||||||
_ready.TrySetResult(true);
|
_ready.TrySetResult(true);
|
||||||
|
_lastLogTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Log.Debug("STA message pump entering loop");
|
||||||
|
|
||||||
// Run the message loop — blocks until WM_QUIT
|
// Run the message loop — blocks until WM_QUIT
|
||||||
while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
|
while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
|
||||||
{
|
{
|
||||||
|
_totalMessages++;
|
||||||
|
|
||||||
if (msg.message == WM_APP)
|
if (msg.message == WM_APP)
|
||||||
{
|
{
|
||||||
|
_appMessages++;
|
||||||
DrainQueue();
|
DrainQueue();
|
||||||
}
|
}
|
||||||
else if (msg.message == WM_APP + 1)
|
else if (msg.message == WM_APP + 1)
|
||||||
@@ -141,10 +154,16 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_dispatchedMessages++;
|
||||||
TranslateMessage(ref msg);
|
TranslateMessage(ref msg);
|
||||||
DispatchMessage(ref msg);
|
DispatchMessage(ref msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogPumpStatsIfDue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.Information("STA message pump exited loop (Total={Total}, App={App}, Dispatched={Dispatched}, WorkItems={WorkItems})",
|
||||||
|
_totalMessages, _appMessages, _dispatchedMessages, _workItemsExecuted);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -157,6 +176,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
while (_workItems.TryDequeue(out var workItem))
|
while (_workItems.TryDequeue(out var workItem))
|
||||||
{
|
{
|
||||||
|
_workItemsExecuted++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
workItem();
|
workItem();
|
||||||
@@ -168,6 +188,16 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LogPumpStatsIfDue()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastLogTime < PumpLogInterval) return;
|
||||||
|
|
||||||
|
Log.Debug("STA pump alive: Total={Total}, App={App}, Dispatched={Dispatched}, WorkItems={WorkItems}, Pending={Pending}",
|
||||||
|
_totalMessages, _appMessages, _dispatchedMessages, _workItemsExecuted, _workItems.Count);
|
||||||
|
_lastLogTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
#region Win32 PInvoke
|
#region Win32 PInvoke
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ namespace ZB.MOM.WW.LmxProxy.Host
|
|||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// 2. Configure Serilog
|
// 2. Set working directory to exe location so relative log paths resolve correctly
|
||||||
|
Environment.CurrentDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
// 3. Configure Serilog
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.ReadFrom.Configuration(configuration)
|
.ReadFrom.Configuration(configuration)
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
@@ -27,11 +30,11 @@ namespace ZB.MOM.WW.LmxProxy.Host
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 3. Bind configuration
|
// 4. Bind configuration
|
||||||
var config = new LmxProxyConfiguration();
|
var config = new LmxProxyConfiguration();
|
||||||
configuration.Bind(config);
|
configuration.Bind(config);
|
||||||
|
|
||||||
// 4. Configure Topshelf
|
// 5. Configure Topshelf
|
||||||
var exitCode = HostFactory.Run(host =>
|
var exitCode = HostFactory.Run(host =>
|
||||||
{
|
{
|
||||||
host.UseSerilog();
|
host.UseSerilog();
|
||||||
|
|||||||
@@ -34,9 +34,7 @@
|
|||||||
|
|
||||||
"HealthCheck": {
|
"HealthCheck": {
|
||||||
"TestTagAddress": "DevPlatform.Scheduler.ScanTime",
|
"TestTagAddress": "DevPlatform.Scheduler.ScanTime",
|
||||||
"ProbeTimeoutMs": 5000,
|
"ProbeStaleThresholdMs": 5000
|
||||||
"MaxConsecutiveTransportFailures": 3,
|
|
||||||
"DegradedProbeIntervalMs": 30000
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"ServiceRecovery": {
|
"ServiceRecovery": {
|
||||||
@@ -58,7 +56,8 @@
|
|||||||
"Override": {
|
"Override": {
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"System": "Warning",
|
"System": "Warning",
|
||||||
"Grpc": "Information"
|
"Grpc": "Information",
|
||||||
|
"ZB.MOM.WW.LmxProxy.Host.MxAccess.StaComThread": "Debug"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WriteTo": [
|
"WriteTo": [
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.Tests.Health
|
|||||||
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
||||||
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
||||||
Task.FromResult((false, 0));
|
Task.FromResult((false, 0));
|
||||||
public Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs, CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(ProbeResult.Healthy(Quality.Good, DateTime.UtcNow));
|
|
||||||
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
||||||
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
||||||
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
||||||
@@ -158,8 +156,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.Tests.Health
|
|||||||
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
||||||
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
||||||
Task.FromResult((false, 0));
|
Task.FromResult((false, 0));
|
||||||
public Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs, CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(ProbeResult.Healthy(Quality.Good, DateTime.UtcNow));
|
|
||||||
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
||||||
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
||||||
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.Tests.Status
|
|||||||
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
||||||
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
||||||
Task.FromResult((false, 0));
|
Task.FromResult((false, 0));
|
||||||
public Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs, CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(ProbeResult.Healthy(Quality.Good, DateTime.UtcNow));
|
|
||||||
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
||||||
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
||||||
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.Tests.Subscriptions
|
|||||||
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
||||||
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
||||||
Task.FromResult((false, 0));
|
Task.FromResult((false, 0));
|
||||||
public Task<ProbeResult> ProbeConnectionAsync(string testTagAddress, int timeoutMs, CancellationToken ct = default) =>
|
|
||||||
Task.FromResult(ProbeResult.Healthy(Quality.Good, DateTime.UtcNow));
|
|
||||||
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
||||||
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
||||||
Task.FromResult<IAsyncDisposable>(new FakeSubscriptionHandle());
|
Task.FromResult<IAsyncDisposable>(new FakeSubscriptionHandle());
|
||||||
|
|||||||
Reference in New Issue
Block a user