refactor(deployment): move PendingUpdatesCache to DeploymentManager so the CLI shares it (ScadaBridge#91)

`health pending-updates` (#63) called IPendingUpdatesEvaluator.EvaluateAsync directly
from ManagementActor because the cache lived in CentralUI, so every CLI call was a full
uncached fleet flatten. IPendingUpdatesCache/PendingUpdatesCache now live in
DeploymentManager.PendingUpdates and are registered by AddDeploymentManager; the
SingleFlightMemo<T> primitive moves to Commons.Caching (public) so both CentralUI's KPI /
alarm-summary caches and DeploymentManager can use it. HandleGetPendingUpdates reads the
shared cache; the UI circuits and the CLI now share one memo per node.

Tests: ManagementActor repeated-calls test (fails against the direct evaluator call:
2 evaluations instead of 1), AddDeploymentManager singleton-registration test; the cache
and memo tests move with their types.

(ScadaBridge#91)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 13:30:38 -05:00
parent c7d868c52e
commit 1ee2e171cf
16 changed files with 102 additions and 33 deletions
@@ -551,14 +551,20 @@ the evaluator swallows an exception: a faulted evaluation propagates so the
cache's single-flight memo discards the flight and the Central UI renders the
aggregate as unavailable, rather than manufacturing a false "up to date".
The Central UI holds the snapshot behind a process-wide singleton
`IPendingUpdatesCache` (`PendingUpdatesCache`, mirroring `KpiSnapshotCache`): one
The snapshot is held behind a process-wide singleton `IPendingUpdatesCache`
(`PendingUpdatesCache`, `DeploymentManager.PendingUpdates`, registered by
`AddDeploymentManager`, mirroring the Central UI's `KpiSnapshotCache`). It lives
here rather than in CentralUI so the Central UI circuits **and** the management
API's `GetPendingUpdatesCommand` (CLI `health pending-updates`) share one memo
(Gitea #91 — before, every CLI call ran a full uncached flatten). It is one
`SingleFlightMemo<PendingUpdatesSnapshot>` keyed by
`ScadaBridge:DeploymentManager:PendingUpdatesCacheTtl` (default 60 s, validated
positive by `DeploymentManagerOptionsValidator`), each flight running the
evaluator in its own DI scope. However many Blazor circuits poll
evaluator in its own DI scope (the primitive, `SingleFlightMemo<T>`, lives in
Commons `Caching`). However many Blazor circuits or CLI calls hit
`GetAsync(forceRefresh)`, the fleet is flattened **at most once per TTL**, and not
at all when no Deployer/Admin circuit is open. See
at all when nobody asks. A deployment that finishes on this node drops the memo
at once; one run through the peer central clears within the TTL. See
`docs/plans/2026-09-14-pending-updates-design.md` for the full design, including
the Sites-page badge/re-check UI and the rail-footer indicator
(Component-CentralUI.md → *Site & Data Connection Management* and *Technology*).
@@ -227,7 +227,7 @@ The two-person authorization workflow for writes through the MxAccess Gateway. B
- **GetHealthSummary**: Query current health status of all sites.
- **GetSiteHealth**: Query detailed health for a specific site.
- **GetPendingUpdates**: Fleet-wide pending-updates counts (stale instances / sites with stale artifacts) with a per-site breakdown. Deployer role; site-scoped callers see only their permitted sites (Gitea #63).
- **GetPendingUpdates**: Fleet-wide pending-updates counts (stale instances / sites with stale artifacts) with a per-site breakdown. Deployer role; site-scoped callers see only their permitted sites (Gitea #63). Served from the same process-wide `IPendingUpdatesCache` the Central UI reads, so repeated calls inside the cache TTL cost one fleet flatten (Gitea #91).
### Remote Queries