diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/PendingUpdatesEvaluator.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/PendingUpdatesEvaluator.cs index db8a7cea..b2b3a1e3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/PendingUpdatesEvaluator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/PendingUpdates/PendingUpdatesEvaluator.cs @@ -78,10 +78,15 @@ public sealed class PendingUpdatesEvaluator : IPendingUpdatesEvaluator var staleness = await _deploymentService.GetInstanceStalenessAsync( deployedInstances.Select(i => i.Id).ToList(), cancellationToken); + // Grouped ONCE, not re-scanned per site: tallying by filtering the whole + // deployed set inside the site loop would make the aggregate O(sites x + // instances) — the one genuinely per-entity loop in an otherwise batched pass. + var instancesBySite = deployedInstances.ToLookup(i => i.SiteId); + var rows = new List(sites.Count); foreach (var site in sites.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)) { - var (deployed, stale, unknown) = TallyInstances(site.Id, deployedInstances, staleness); + var (deployed, stale, unknown) = TallyInstances(instancesBySite[site.Id], staleness); rows.Add(new SitePendingUpdates( site.Id, site.SiteIdentifier, @@ -114,9 +119,16 @@ public sealed class PendingUpdatesEvaluator : IPendingUpdatesEvaluator /// Decides a site's artifact state by comparing the hash stamped by its last /// successful artifact deploy against the hash the current definitions would /// produce. A site with no stamp has never had artifacts deployed, which is - /// , NOT out of date. A site - /// missing from the computed map (it appeared between the two reads) is reported - /// out of date, the side that prompts an operator to look rather than hiding it. + /// , NOT out of date. + /// + /// + /// A stamped site missing from the computed map is reported out of date. That is a + /// defensive default, not a reachable race: this call path hands the SAME site list + /// to , + /// which sets an entry for every input site or throws. It is kept so that a future + /// producer of a partial map fails toward VISIBLE (out of date, an operator looks) + /// rather than hidden (current, a stale site silently reads clean). + /// /// /// The site to classify. /// Site id to the hash a deploy would send right now. @@ -137,26 +149,25 @@ public sealed class PendingUpdatesEvaluator : IPendingUpdatesEvaluator /// /// Counts one site's deployed, stale and un-evaluatable instances. Only instances /// present in the staleness result are counted — the batch OMITS an instance with - /// no deployed snapshot, which is exactly the "nothing to be stale against" - /// exclusion the counts need. + /// no deployed snapshot at evaluation time (including one that was undeployed + /// mid-batch), which is exactly the "nothing to be stale against" exclusion the + /// counts need. /// - /// Site whose instances are being tallied. - /// All instances in a state other than NotDeployed. + /// + /// This site's instances in a state other than NotDeployed, pre-grouped by the caller. + /// /// Instance id to stale (true) / current (false) / unknown (null). /// The deployed, stale and unknown counts for the site. private static (int Deployed, int Stale, int Unknown) TallyInstances( - int siteId, - IReadOnlyList deployedInstances, + IEnumerable siteInstances, IReadOnlyDictionary staleness) { var deployed = 0; var stale = 0; var unknown = 0; - foreach (var instance in deployedInstances) + foreach (var instance in siteInstances) { - if (instance.SiteId != siteId) - continue; if (!staleness.TryGetValue(instance.Id, out var isStale)) continue;