fix(lmxproxy): protect probe subscription from ReadAsync teardown, add instance configs
ReadAsync internally subscribes/unsubscribes the same ScanTime tag used by the persistent probe, which was tearing down the probe subscription and triggering false reconnects every ~5s. Guard UnsubscribeInternal and stored subscription state so the probe tag is never removed by other callers. Also removes DetailedHealthCheckService (redundant with the persistent probe), adds per-instance config files (appsettings.v2.json, appsettings.v2b.json) loaded via LMXPROXY_INSTANCE env var so deploys no longer overwrite port settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,109 +132,4 @@ namespace ZB.MOM.WW.LmxProxy.Host.Tests.Health
|
||||
}
|
||||
}
|
||||
|
||||
public class DetailedHealthCheckServiceTests
|
||||
{
|
||||
private class FakeScadaClient : IScadaClient
|
||||
{
|
||||
public bool IsConnected { get; set; } = true;
|
||||
public ConnectionState ConnectionState { get; set; } = ConnectionState.Connected;
|
||||
public Vtq? ReadResult { get; set; }
|
||||
public Exception? ReadException { get; set; }
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
public Task ConnectAsync(CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task DisconnectAsync(CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task<Vtq> ReadAsync(string address, CancellationToken ct = default)
|
||||
{
|
||||
if (ReadException != null) throw ReadException;
|
||||
return Task.FromResult(ReadResult ?? Vtq.Good(true));
|
||||
}
|
||||
public Task<IReadOnlyDictionary<string, Vtq>> ReadBatchAsync(IEnumerable<string> addresses, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyDictionary<string, Vtq>>(new Dictionary<string, Vtq>());
|
||||
public Task WriteAsync(string address, object value, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task WriteBatchAsync(IReadOnlyDictionary<string, object> values, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task<(bool flagReached, int elapsedMs)> WriteBatchAndWaitAsync(
|
||||
IReadOnlyDictionary<string, object> values, string flagTag, object flagValue,
|
||||
int timeoutMs, int pollIntervalMs, CancellationToken ct = default) =>
|
||||
Task.FromResult((false, 0));
|
||||
public Task UnsubscribeByAddressAsync(IEnumerable<string> addresses) => Task.CompletedTask;
|
||||
public Task<IAsyncDisposable> SubscribeAsync(IEnumerable<string> addresses, Action<string, Vtq> callback, CancellationToken ct = default) =>
|
||||
Task.FromResult<IAsyncDisposable>(new FakeHandle());
|
||||
public ValueTask DisposeAsync() => default;
|
||||
internal void FireEvent() => ConnectionStateChanged?.Invoke(this, null!);
|
||||
private class FakeHandle : IAsyncDisposable { public ValueTask DisposeAsync() => default; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsUnhealthy_WhenNotConnected()
|
||||
{
|
||||
var client = new FakeScadaClient { IsConnected = false };
|
||||
var svc = new DetailedHealthCheckService(client);
|
||||
|
||||
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.Should().Be(HealthStatus.Unhealthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsHealthy_WhenTestTagGoodAndRecent()
|
||||
{
|
||||
var client = new FakeScadaClient
|
||||
{
|
||||
IsConnected = true,
|
||||
ReadResult = Vtq.New(true, DateTime.UtcNow, Quality.Good)
|
||||
};
|
||||
var svc = new DetailedHealthCheckService(client);
|
||||
|
||||
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.Should().Be(HealthStatus.Healthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsDegraded_WhenTestTagQualityNotGood()
|
||||
{
|
||||
var client = new FakeScadaClient
|
||||
{
|
||||
IsConnected = true,
|
||||
ReadResult = Vtq.New(true, DateTime.UtcNow, Quality.Uncertain)
|
||||
};
|
||||
var svc = new DetailedHealthCheckService(client);
|
||||
|
||||
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.Should().Be(HealthStatus.Degraded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsDegraded_WhenTestTagTimestampStale()
|
||||
{
|
||||
var client = new FakeScadaClient
|
||||
{
|
||||
IsConnected = true,
|
||||
ReadResult = Vtq.New(true, DateTime.UtcNow.AddMinutes(-10), Quality.Good)
|
||||
};
|
||||
var svc = new DetailedHealthCheckService(client);
|
||||
|
||||
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.Should().Be(HealthStatus.Degraded);
|
||||
result.Description.Should().Contain("stale");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsDegraded_WhenTestTagReadThrows()
|
||||
{
|
||||
var client = new FakeScadaClient
|
||||
{
|
||||
IsConnected = true,
|
||||
ReadException = new InvalidOperationException("COM error")
|
||||
};
|
||||
var svc = new DetailedHealthCheckService(client);
|
||||
|
||||
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
||||
|
||||
result.Status.Should().Be(HealthStatus.Degraded);
|
||||
result.Description.Should().Contain("Could not read test tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user