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>
136 lines
6.0 KiB
C#
136 lines
6.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using Xunit;
|
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
|
using ZB.MOM.WW.LmxProxy.Host.Health;
|
|
using HealthCheckService = ZB.MOM.WW.LmxProxy.Host.Health.HealthCheckService;
|
|
using ZB.MOM.WW.LmxProxy.Host.Metrics;
|
|
using ZB.MOM.WW.LmxProxy.Host.Subscriptions;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.Tests.Health
|
|
{
|
|
public class HealthCheckServiceTests
|
|
{
|
|
private class FakeScadaClient : IScadaClient
|
|
{
|
|
public bool IsConnected { get; set; } = true;
|
|
public ConnectionState ConnectionState { get; set; } = ConnectionState.Connected;
|
|
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) =>
|
|
Task.FromResult(Vtq.Good(42.0));
|
|
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 ReturnsHealthy_WhenConnectedAndNormalMetrics()
|
|
{
|
|
var client = new FakeScadaClient { IsConnected = true, ConnectionState = ConnectionState.Connected };
|
|
using var sm = new SubscriptionManager(client);
|
|
using var pm = new PerformanceMetrics();
|
|
pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true);
|
|
|
|
var svc = new HealthCheckService(client, sm, pm);
|
|
|
|
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
result.Status.Should().Be(HealthStatus.Healthy);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReturnsUnhealthy_WhenNotConnected()
|
|
{
|
|
var client = new FakeScadaClient { IsConnected = false, ConnectionState = ConnectionState.Disconnected };
|
|
using var sm = new SubscriptionManager(client);
|
|
using var pm = new PerformanceMetrics();
|
|
|
|
var svc = new HealthCheckService(client, sm, pm);
|
|
|
|
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
result.Status.Should().Be(HealthStatus.Unhealthy);
|
|
result.Description.Should().Contain("not connected");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReturnsDegraded_WhenSuccessRateBelow50Percent()
|
|
{
|
|
var client = new FakeScadaClient { IsConnected = true };
|
|
using var sm = new SubscriptionManager(client);
|
|
using var pm = new PerformanceMetrics();
|
|
|
|
// Record 200 operations with 40% success rate
|
|
for (int i = 0; i < 80; i++)
|
|
pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), true);
|
|
for (int i = 0; i < 120; i++)
|
|
pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false);
|
|
|
|
var svc = new HealthCheckService(client, sm, pm);
|
|
|
|
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
result.Status.Should().Be(HealthStatus.Degraded);
|
|
result.Description.Should().Contain("success rate");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReturnsDegraded_WhenClientCountOver100()
|
|
{
|
|
var client = new FakeScadaClient { IsConnected = true };
|
|
using var sm = new SubscriptionManager(client);
|
|
using var pm = new PerformanceMetrics();
|
|
|
|
// Create 101 subscriptions to exceed the threshold
|
|
for (int i = 0; i < 101; i++)
|
|
{
|
|
using var cts = new CancellationTokenSource();
|
|
await sm.SubscribeAsync("client-" + i, new[] { "tag1" }, cts.Token);
|
|
}
|
|
|
|
var svc = new HealthCheckService(client, sm, pm);
|
|
|
|
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
result.Status.Should().Be(HealthStatus.Degraded);
|
|
result.Description.Should().Contain("client count");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DoesNotFlagLowSuccessRate_Under100Operations()
|
|
{
|
|
var client = new FakeScadaClient { IsConnected = true };
|
|
using var sm = new SubscriptionManager(client);
|
|
using var pm = new PerformanceMetrics();
|
|
|
|
// Record 50 operations with 0% success rate (under 100 threshold)
|
|
for (int i = 0; i < 50; i++)
|
|
pm.RecordOperation("Read", TimeSpan.FromMilliseconds(10), false);
|
|
|
|
var svc = new HealthCheckService(client, sm, pm);
|
|
|
|
var result = await svc.CheckHealthAsync(new HealthCheckContext());
|
|
|
|
result.Status.Should().Be(HealthStatus.Healthy);
|
|
}
|
|
}
|
|
|
|
}
|