fix(health): evict deleted sites from the aggregator on the periodic site refresh
This commit is contained in:
+32
-1
@@ -10,6 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Actors;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;
|
||||
|
||||
@@ -37,7 +38,37 @@ public class CentralCommunicationActorClientLifecycleTests : TestKit
|
||||
|
||||
private static SiteAddressCacheLoaded Load(string siteId, params string[] addrs) =>
|
||||
new(new Dictionary<string, IReadOnlyList<string>>
|
||||
{ [siteId] = addrs.ToList().AsReadOnly() });
|
||||
{ [siteId] = addrs.ToList().AsReadOnly() },
|
||||
new[] { siteId });
|
||||
|
||||
[Fact]
|
||||
public void PeriodicRefresh_PrunesDeletedSites_FromHealthAggregator()
|
||||
{
|
||||
// PLAN-01 Task 18: the periodic site refresh feeds the currently-configured
|
||||
// site ids to the aggregator so a deleted site is evicted within one
|
||||
// refresh interval. Repo returns only "site-a".
|
||||
var siteRepo = Substitute.For<ISiteRepository>();
|
||||
siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Site> { new("Site A", "site-a") });
|
||||
var aggregator = Substitute.For<ICentralHealthAggregator>();
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => siteRepo);
|
||||
services.AddSingleton(aggregator);
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var actor = Sys.ActorOf(Props.Create(() => new CentralCommunicationActor(
|
||||
provider, new DefaultSiteClientFactory(), null)));
|
||||
|
||||
// Trigger the refresh (also fires at PreStart, but drive it explicitly so
|
||||
// the assertion is deterministic). The load runs on a detached task and
|
||||
// pipes SiteAddressCacheLoaded back to the actor, which then prunes.
|
||||
actor.Tell(new RefreshSiteAddresses());
|
||||
|
||||
AwaitAssert(
|
||||
() => aggregator.Received().PruneUnknownSites(
|
||||
Arg.Is<IReadOnlyCollection<string>>(ids => ids.Contains("site-a"))),
|
||||
TimeSpan.FromSeconds(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddressEdit_RecreatesClient_WithoutRestartLoop()
|
||||
|
||||
@@ -473,6 +473,27 @@ public class CentralHealthAggregatorTests
|
||||
Assert.False(aggregator.GetSiteState("site-a")!.IsMetricsStale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 01 [Low]: <c>_siteStates</c> only ever grew — a site deleted from
|
||||
/// configuration remained a permanently-offline dashboard tile (and a KPI
|
||||
/// sample source) forever. Pruning against the known-site set evicts it; the
|
||||
/// synthetic central id is always retained.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PruneUnknownSites_RemovesDeletedSites_KeepsKnownAndCentral()
|
||||
{
|
||||
var (aggregator, _) = NewAggregator();
|
||||
aggregator.ProcessReport(MakeReport("site-a", 1));
|
||||
aggregator.ProcessReport(MakeReport("site-b", 1));
|
||||
aggregator.ProcessReport(MakeReport(CentralHealthReportLoop.CentralSiteId, 1));
|
||||
|
||||
aggregator.PruneUnknownSites(new[] { "site-a" });
|
||||
|
||||
Assert.NotNull(aggregator.GetSiteState("site-a"));
|
||||
Assert.Null(aggregator.GetSiteState("site-b"));
|
||||
Assert.NotNull(aggregator.GetSiteState(CentralHealthReportLoop.CentralSiteId)); // synthetic id always kept
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 01 underdeveloped #7: "when did the site drop" was unanswerable.
|
||||
/// Every online↔offline flip stamps <see cref="SiteHealthState.LastStatusChangeAt"/>.
|
||||
|
||||
@@ -27,6 +27,7 @@ public class CentralHealthReportLoopTests
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() =>
|
||||
new Dictionary<string, SiteHealthState>();
|
||||
public SiteHealthState? GetSiteState(string siteId) => null;
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -256,6 +257,7 @@ public class CentralHealthReportLoopTests
|
||||
public IReadOnlyDictionary<string, SiteHealthState> GetAllSiteStates() =>
|
||||
new Dictionary<string, SiteHealthState>();
|
||||
public SiteHealthState? GetSiteState(string siteId) => null;
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+3
@@ -166,5 +166,8 @@ public class SiteHealthKpiSampleSourceTests
|
||||
|
||||
public void MarkHeartbeat(string siteId, DateTimeOffset receivedAt) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public void PruneUnknownSites(IReadOnlyCollection<string> knownSiteIds) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user