Files
ScadaBridge/docs/components/KpiHistory.md
T

8.1 KiB
Raw Blame History

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

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.