using System.Collections.Generic; namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian; /// /// Binds the ServerHistorian configuration section that gates the server-side /// HistoryRead backend. When is true, AddServerHistorian /// registers a read-only HistorianGateway-backed IHistorianDataSource (supplied by /// the Host) in place of the NullHistorianDataSource default; otherwise the Null default /// survives and HistoryRead returns GoodNoData-empty. /// /// The read client talks gRPC to the ZB.MOM.WW.HistorianGateway sidecar /// (historian_gateway.v1) over , authenticating with the /// peppered-HMAC (histgw_<id>_<secret>) in the /// Authorization: Bearer header. This is the READ path only — there are no /// DatabasePath / drain / capacity / retention knobs (those belong to the write-side /// AlarmHistorian store-and-forward sink). The client's own /// bounds each read; the node manager adds no extra timeout. /// /// public sealed class ServerHistorianOptions { /// The configuration section name this options class binds. public const string SectionName = "ServerHistorian"; /// /// When true, the HistorianGateway read client is registered as the /// IHistorianDataSource; when false (the default) the no-op /// NullHistorianDataSource stays in place and HistoryRead returns empty. /// public bool Enabled { get; init; } /// /// Absolute gateway endpoint URI the read client dials (e.g. https://host:5222). /// The scheme selects the transport: https:// = TLS, http:// = h2c plaintext. /// Required when is true. /// public string Endpoint { get; init; } = ""; /// /// The peppered-HMAC API key (histgw_<id>_<secret>) the gateway validates /// in the Authorization: Bearer header. Supply via the environment variable /// ServerHistorian__ApiKey — never commit it to config. Required when /// is true. /// public string ApiKey { get; init; } = ""; /// When true (the default), the client connects over TLS; must match the scheme. public bool UseTls { get; init; } = true; /// When true, the client accepts a self-signed / untrusted server certificate (dev / on-prem only). public bool AllowUntrustedServerCertificate { get; init; } /// Path to a PEM CA certificate that pins the gateway's TLS trust chain. Null or empty uses the OS trust store. public string? CaCertificatePath { get; init; } /// Per-call deadline applied to each unary gateway read. Defaults to 30 seconds. public TimeSpan CallTimeout { get; init; } = TimeSpan.FromSeconds(30); /// /// The upper bound on the bounded over-fetch the HistoryRead-Raw paging uses to page WITHIN an /// oversized "tie cluster" (more raw samples sharing one SourceTimestamp than the client's per-page /// cap). When a resume read stalls on such a cluster, the node manager over-fetches up to /// MaxTieClusterOverfetch + 1 ties at that single timestamp (a start == end read) and /// pages through them; this bounds how large a single-timestamp burst the server will buffer in /// memory before it gives up and surfaces BadHistoryOperationUnsupported for that node. The /// default (65536) comfortably covers any realistic same-millisecond burst. /// public int MaxTieClusterOverfetch { get; init; } = 65536; /// /// Maximum number of historized nodes served CONCURRENTLY within one HistoryRead batch (arch-review /// 03/S3). The SDK's HistoryRead* override surface is synchronous, so each arm block-bridges /// once at its boundary; this bound turns the per-node fan-out inside that boundary from sequential /// (worst case N × CallTimeout) into ⌈N / P⌉ × CallTimeout. It also caps the gateway-load /// multiplication a single client can command. Default 4. /// public int HistoryReadBatchParallelism { get; init; } = 4; /// /// Process-wide limit on the number of HistoryRead batches served CONCURRENTLY (arch-review 03/S3). /// A flood of history clients degrades to BadTooManyOperations rather than exhausting the SDK /// request-thread pool. Together with this caps total /// in-flight gateway reads at MaxConcurrentHistoryReadBatches × HistoryReadBatchParallelism /// (default 16 × 4 = 64). Default 16. /// public int MaxConcurrentHistoryReadBatches { get; init; } = 16; /// /// Per-request wall-clock deadline for a HistoryRead batch (arch-review 03/S3). A single request can /// no longer hold an SDK request thread longer than this regardless of node count; a node still /// in flight at the deadline surfaces BadTimeout. Non-positive = unbounded (a /// warning). Default 60 seconds. /// public TimeSpan HistoryReadDeadline { get; init; } = TimeSpan.FromSeconds(60); /// Returns operator-facing misconfiguration warnings for an Enabled historian /// (empty when disabled or correctly configured). Pure — the registration logs each entry. /// /// These are warnings only. The Host layers a fail-fast startup validator /// (ServerHistorianOptionsValidator, wired via AddValidatedOptions) on top of this /// for the provably-crashing subset — an empty/malformed Endpoint that is consumed /// by an enabled historian or an enabled AlarmHistorian — so that config fails host start /// with a named error instead of a raw UriFormatException crash-loop (archreview 06/S-11). /// The empty-ApiKey / non-positive-MaxTieClusterOverfetch entries stay warnings /// because they degrade rather than crash. /// /// Zero or more human-readable warning messages (never carrying secret values). public IReadOnlyList Validate() { var warnings = new List(); if (!Enabled) return warnings; if (string.IsNullOrWhiteSpace(Endpoint)) warnings.Add("ServerHistorian:Endpoint is empty while the historian is enabled — the read client has no gateway address to dial."); // archreview 06/S-11 warning parity: a non-empty but unparseable / non-absolute-http(s) Endpoint // is just as fatal (it throws in the gateway client factory). Warn on it here so the Runtime-side // registration logs describe malformed endpoints too, consistent with the Host's fail-fast // ServerHistorianOptionsValidator (which turns this same condition into a startup failure). else if (!Uri.TryCreate(Endpoint, UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) warnings.Add($"ServerHistorian:Endpoint ('{Endpoint}') is not an absolute http(s) URI while the historian is enabled — the read client cannot dial it (e.g. https://host:5222)."); if (string.IsNullOrWhiteSpace(ApiKey)) warnings.Add("ServerHistorian:ApiKey is empty while the historian is enabled — the gateway gRPC surface will reject unauthenticated calls."); // MaxTieClusterOverfetch is intentionally checked AFTER the Enabled early-return above: // the over-fetch code path only runs when a real IHistorianDataSource is wired in, // so a zero/negative value is harmless (and noise-free) when the historian is disabled. if (MaxTieClusterOverfetch <= 0) warnings.Add($"ServerHistorian:MaxTieClusterOverfetch is {MaxTieClusterOverfetch} — must be > 0; HistoryRead-Raw cannot page within an oversized tie cluster and will surface BadHistoryOperationUnsupported for those reads."); // S3 HistoryRead concurrency knobs — a non-positive value disables the corresponding bound, so warn // (these gate the per-node fan-out / batch limiter that only run when a real data source is wired). if (HistoryReadBatchParallelism <= 0) warnings.Add($"ServerHistorian:HistoryReadBatchParallelism is {HistoryReadBatchParallelism} — must be > 0; HistoryRead falls back to serving each node sequentially."); if (MaxConcurrentHistoryReadBatches <= 0) warnings.Add($"ServerHistorian:MaxConcurrentHistoryReadBatches is {MaxConcurrentHistoryReadBatches} — must be > 0; the process-wide HistoryRead batch limiter is disabled."); if (HistoryReadDeadline <= TimeSpan.Zero) warnings.Add($"ServerHistorian:HistoryReadDeadline is {HistoryReadDeadline} — must be > 0; HistoryRead requests run unbounded (no per-request deadline)."); return warnings; } }