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;
}
}