docs(components): reference docs for ScriptAnalysis, KpiHistory and DelmiaNotifier (ScadaBridge#72)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:02:49 -05:00
parent baf89b3df7
commit 69a2017000
6 changed files with 363 additions and 6 deletions
+114
View File
@@ -0,0 +1,114 @@
# KPI History
KPI History (#26) is a central-only component that records periodic point-in-time KPI samples into the configuration database, folds them into hourly rollups, and purges both on a retention schedule so the Central UI can draw trend charts. The project owns the recording singleton and its options; the sample sources, repository contract and query service live elsewhere.
## Overview
The component lives in `src/ZB.MOM.WW.ScadaBridge.KpiHistory/` and references only Commons (plus Akka and `ZB.MOM.WW.Configuration`). Its pieces are:
- `KpiHistoryRecorderActor` — a `ReceiveActor` with `IWithTimers`, created as the `kpi-history-recorder` cluster singleton by `AkkaHostedService` on the central role. The Host logs it as "not readiness-gated", so it never affects `/health/ready`.
- `KpiHistoryOptions` and `KpiHistoryOptionsValidator` — the `ScadaBridge:KpiHistory` section and its startup validation.
- `KpiSourceAdmission` — root-provider-owned admission so a replaced actor cannot start a second collection while an earlier one is still running.
- `ServiceCollectionExtensions.AddKpiHistory` — binds and validates the options and registers `KpiSourceAdmission` as a singleton. The actor itself is not registered in DI.
## Key Concepts
### Sources, samples, repository
The recorder is generic over sources: each pass it enumerates every DI-registered `IKpiSampleSource` (`Source` name plus `CollectAsync(capturedAtUtc, ct)` returning `KpiSample` rows) and writes the combined batch via `IKpiHistoryRepository.RecordSamplesAsync`. The sources registered in the codebase are `NotificationOutboxKpiSampleSource`, `SiteCallAuditKpiSampleSource`, `AuditLogKpiSampleSource` and `SiteHealthKpiSampleSource` (each a scoped registration via `TryAddEnumerable`); the matching names are the constants in `KpiSources` (`NotificationOutbox`, `SiteCallAudit`, `AuditLog`, `SiteHealth`). The repository implementation is `KpiHistoryRepository` in the Configuration Database project.
### Four timers
`PreStart` arms four timers:
| Timer | First fire | Cadence | Work |
|---|---|---|---|
| `kpi-sample` | 5 s | `SampleInterval` | Collect from all sources and record one batch stamped with a single `capturedAtUtc`. |
| `kpi-purge` | `PurgeTimerSchedule.InitialDelay(PurgeInterval)` (the interval, capped at 5 minutes) | `PurgeInterval` | `PurgeOlderThanAsync` for raw rows older than `RetentionDays`, then `PurgeRollupsOlderThanAsync` for rollups older than `RollupRetentionDays`. |
| `kpi-rollup` | 10 s | `RollupInterval` | `FoldHourlyRollupsAsync` over `[now truncated to hour − RollupLookbackHours, now truncated to hour)`; the in-progress hour is excluded. |
| `kpi-rollup-backfill` | 15 s, single-shot | once per actor lifetime | Fold the raw-retention window in slices. |
The fold is an idempotent upsert, so a tick missed during a singleton handover self-heals on the next fold.
### In-flight guards
A sample tick is skipped (debug log) while a previous sample pass is still running, and a rollup tick is skipped while either a rollup fold or the backfill is running. Every pass is piped back to the actor with a `...Complete` message on success or failure, so a faulted pass cannot wedge the guard.
### Source isolation
Each source is collected in its own DI scope through `KpiSourceAdmission.TryStart`, keyed by registration index. If an earlier collection for that index has not completed, no new one starts and the running task is awaited instead. The wait is bounded by `SourceTimeout` (via `WaitAsync`); the collection also gets a linked token that cancels after `SourceTimeout`. On timeout the source is skipped with a warning while its work stays owned; any other source exception is logged and the pass continues. If no samples were collected nothing is written. A failed batch write is logged and the batch dropped.
### Backfill
The backfill first reads `GetLatestRollupHourAsync` (the watermark; failures fall back to a null watermark, meaning the full window). `HandleBackfillPlan` sets the floor to the later of the watermark and `now − RetentionDays`. If that floor is at or after `now − RollupLookbackHours`, rollups are already current and the backfill is skipped. Otherwise the range is cut into 24-hour slices and folded one per tick, with the periodic fold interleaving between slices (a slice tick re-arms after 5 seconds while a periodic fold runs). The backfill latches done when the queue empties.
### Shutdown
`PostStop` cancels a shared `CancellationTokenSource`; cancellation-triggered exits are treated as non-failures throughout.
## Architecture
### Cross-validation in Host
`KpiRateFoldIntervalCrossValidator` (Host project) is registered against `IValidateOptions<KpiHistoryOptions>` and fails startup unless `KpiHistory:SampleInterval`, `NotificationOutbox:DeliveredKpiWindow` and `SiteCallAudit:KpiInterval` are all equal. The Notification Outbox and Site Call Audit sample sources rescale their delivered/failed-last-interval counts onto `SampleInterval`, so the hourly sum stays correct regardless; the equality check keeps charted values matching the live Health tiles.
### Query side
The Central UI registers `KpiHistoryQueryService` (scoped, in `CentralUI/ServiceCollectionExtensions.cs`), which reads `KpiHistoryOptions` and the repository. `RollupThresholdHours` and `DefaultMaxSeriesPoints` are consumed there; the recorder does not use them. Per the option docs, windows up to `RollupThresholdHours` read raw `KpiSample` rows and wider windows read hourly rollups.
## Usage
The component exposes no message API. Wiring is `services.AddKpiHistory(configuration)` in the Host's central composition, and the singleton is started by `AkkaHostedService` with `Props.Create(() => new KpiHistoryRecorderActor(serviceProvider, options, logger))`. To add a metric source, implement `IKpiSampleSource` and register it as a scoped service with `TryAddEnumerable`.
## Configuration
`KpiHistoryOptions` is bound from `ScadaBridge:KpiHistory` and validated on start.
| Key | Default | Validation |
|---|---|---|
| `SampleInterval` | `00:01:00` | > 0 |
| `SourceTimeout` | `00:00:10` | > 0 and <= 1 day |
| `RetentionDays` | `90` | [1, 3650] |
| `PurgeInterval` | `1.00:00:00` | > 0 |
| `DefaultMaxSeriesPoints` | `200` | [2, 5000] |
| `RollupInterval` | `01:00:00` | > 0 |
| `RollupLookbackHours` | `3` | [1, 168] |
| `RollupRetentionDays` | `365` | [1, 3650] and >= `RetentionDays` |
| `RollupThresholdHours` | `168` | >= 24 |
## Dependencies & Interactions
- [Commons (#16)](./Commons.md) — `IKpiSampleSource`, `IKpiHistoryRepository`, `KpiSample`, `KpiSources`, `PurgeTimerSchedule`.
- [Configuration Database (#17)](./ConfigurationDatabase.md) — `KpiHistoryRepository`.
- [Notification Outbox (#21)](./NotificationOutbox.md), [Site Call Audit (#22)](./SiteCallAudit.md), [Audit Log (#23)](./AuditLog.md), [Health Monitoring (#11)](./HealthMonitoring.md) — each contributes an `IKpiSampleSource`.
- [Central UI (#9)](./CentralUI.md) — trend charts through `KpiHistoryQueryService`.
- [Host (#15)](./Host.md) — registration, singleton creation and the rate-fold cross-validator.
- [Cluster Infrastructure (#13)](./ClusterInfrastructure.md) — singleton failover.
## Troubleshooting
### Host fails to start with "KPI rate-fold interval mismatch"
`SampleInterval`, `DeliveredKpiWindow` and `KpiInterval` differ; set all three equal.
### Startup fails on `RollupRetentionDays`
It must be at least `RetentionDays`, otherwise long-range charts would hit a window where raw rows are purged and no rollup exists.
### A source never appears in charts
Look for "exceeded its collection deadline" warnings. A source that ignores cancellation keeps ownership of its slot and is not restarted until the earlier call completes.
### Long-range charts blank after a deploy
The backfill runs 15 seconds after singleton start and processes one 24-hour slice per tick; check for the "KPI rollup backfill" log lines.
## Related Documentation
- [KPI History design specification](../requirements/Component-KpiHistory.md)
- [Notification Outbox](./NotificationOutbox.md)
- [Site Call Audit](./SiteCallAudit.md)
- [Audit Log](./AuditLog.md)
- [Configuration Database](./ConfigurationDatabase.md)
- [Commons](./Commons.md)
- [Host](./Host.md)