docs(historian): fix-wave docs and stale comments

- Historian.md: Empty samples always Bad (writer/updater/read clamp),
  long-name refusal, 5 s bootstrap failure cache, AtTime after now, info-bit
  masking, HistoryUpdate does NOT type-check (M-1 decision), DeleteRaw
  needs both bounds, reverse Raw paging, the historian/historization metric
  prefixes, and provisioning numeric-only lines qualified as Gateway.
- CLAUDE.md numeric-only lines qualified as Gateway backend.
- Stale 'sole historian' lines in DriverLifecycle, ServiceHosting and the
  retired Wonderware stub; Client.CLI exit-code contract.
- ServerHistorianOptions: Endpoint/Backend XML docs untangled, Validate()
  indented; Program.cs comments are backend-neutral; T10 week-test day count.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 14:33:30 -04:00
parent 7fafeb433c
commit 60a1e7e669
9 changed files with 104 additions and 59 deletions
+16 -16
View File
@@ -265,30 +265,29 @@ if (hasDriver)
// Config-gated server-side HistoryRead backend. When the ServerHistorian section is enabled this
// overrides the NullHistorianDataSource default from AddOtOpcUaRuntime (last registration wins) with
// a read-only HistorianGateway-backed data source the node manager's HistoryRead overrides
// block-bridge to. The factory lives in the Gateway driver (which owns the package-client adapter
// and the ServerHistorianOptions -> client-options mapping); Runtime owns the gating.
// the selected backend's data source (Gateway or SqlServer, built by the resolved provider) that the
// node manager's HistoryRead overrides block-bridge to. Runtime owns the gating.
builder.Services.AddServerHistorian(
builder.Configuration,
(_, sp) => historianProvider.CreateDataSource(sp));
// Config-gated historian tag provisioning. When the ServerHistorian section is enabled this registers the
// gateway-backed GatewayTagProvisioner (overriding the NullHistorianProvisioning default) so that deploying
// a historized tag auto-ensures it in the historian via the gateway's EnsureTags. The AddressSpaceApplier
// (constructed in WithOtOpcUaRuntimeActors) resolves IHistorianProvisioning and fires a non-blocking
// EnsureTagsAsync for added historized tags on every deploy; without this registration the applier falls
// back to the no-op NullHistorianProvisioning and provisioning never reaches the gateway. Gated on the SAME
// ServerHistorian:Enabled flag as the read path — both target the same single gateway.
// selected backend's provisioner (GatewayTagProvisioner -> the gateway's EnsureTags, or the SqlServer Tag-row
// MERGE), overriding the NullHistorianProvisioning default, so deploying a historized tag auto-ensures it in
// the historian. The AddressSpaceApplier (constructed in WithOtOpcUaRuntimeActors) resolves
// IHistorianProvisioning and fires a non-blocking EnsureTagsAsync for added/changed historized tags on every
// deploy; without this registration the applier falls back to the no-op NullHistorianProvisioning. Gated on
// the SAME ServerHistorian:Enabled flag as the read path — both use the one resolved backend.
builder.Services.AddHistorianProvisioning(
builder.Configuration,
(_, sp) => historianProvider.CreateProvisioning(sp));
// Continuous historization of driver (non-Galaxy) tag values. Gated on ContinuousHistorization:Enabled
// AND the ServerHistorian gateway being configured: the recorder drains driver-tag live values to the
// SAME single gateway's WriteLiveValues SQL path, sourcing endpoint/key/TLS from the ServerHistorian
// section (this section carries only the recorder + outbox knobs). When both are on, register the durable
// crash-safe outbox + the gateway-backed live-value writer here; WithOtOpcUaRuntimeActors (below) spawns
// the recorder actor itself, gated on the same options.
// AND ServerHistorian:Enabled: the recorder drains driver-tag live values through the selected backend's
// value writer (the gateway's WriteLiveValues, or the SqlServer Sample table), which takes its connection
// from the ServerHistorian section (this section carries only the recorder + outbox knobs). When both are
// on, register the durable crash-safe outbox + the backend's live-value writer here;
// WithOtOpcUaRuntimeActors (below) spawns the recorder actor itself, gated on the same options.
var continuousHistorizationOptions = builder.Configuration
.GetSection(ContinuousHistorizationOptions.SectionName).Get<ContinuousHistorizationOptions>()
?? new ContinuousHistorizationOptions();
@@ -317,8 +316,9 @@ if (hasDriver)
return outbox;
});
// Gateway-backed live-value writer over its OWN gRPC channel to the same single gateway (a second
// channel to a co-located sidecar is cheap — the gateway pools the historian sessions server-side).
// The backend's live-value writer. On Gateway it opens its OWN gRPC channel to the same gateway (a second
// channel to a co-located sidecar is cheap — the gateway pools the historian sessions server-side); on
// SqlServer it shares the provider's connection helper, schema bootstrapper and tag catalog.
builder.Services.AddSingleton<IHistorianValueWriter>(sp => historianProvider.CreateValueWriter(sp));
}
@@ -4,12 +4,13 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Historian;
/// <summary>
/// Binds the <c>ServerHistorian</c> configuration section that gates the server-side
/// HistoryRead backend. When <see cref="Enabled"/> is <c>true</c>, <c>AddServerHistorian</c>
/// registers a read-only <c>HistorianGateway</c>-backed <c>IHistorianDataSource</c> (supplied by
/// historian. <see cref="Backend"/> selects the provider (<c>Gateway</c> by default, or <c>SqlServer</c>,
/// whose own keys live in the <c>ServerHistorian:SqlServer</c> sub-section). When <see cref="Enabled"/> is
/// <c>true</c>, <c>AddServerHistorian</c> registers that backend's <c>IHistorianDataSource</c> (supplied by
/// the Host) in place of the <c>NullHistorianDataSource</c> default; otherwise the Null default
/// survives and HistoryRead returns <c>GoodNoData</c>-empty.
/// <para>
/// The read client talks gRPC to the <c>ZB.MOM.WW.HistorianGateway</c> sidecar
/// On the Gateway backend the read client talks gRPC to the <c>ZB.MOM.WW.HistorianGateway</c> sidecar
/// (<c>historian_gateway.v1</c>) over <see cref="Endpoint"/>, authenticating with the
/// peppered-HMAC <see cref="ApiKey"/> (<c>histgw_&lt;id&gt;_&lt;secret&gt;</c>) in the
/// <c>Authorization: Bearer</c> header. This is the READ path only — there are no
@@ -24,17 +25,12 @@ public sealed class ServerHistorianOptions
public const string SectionName = "ServerHistorian";
/// <summary>
/// When <c>true</c>, the HistorianGateway read client is registered as the
/// When <c>true</c>, the selected backend's data source is registered as the
/// <c>IHistorianDataSource</c>; when <c>false</c> (the default) the no-op
/// <c>NullHistorianDataSource</c> stays in place and HistoryRead returns empty.
/// </summary>
public bool Enabled { get; init; }
/// <summary>
/// Absolute gateway endpoint URI the read client dials (e.g. <c>https://host:5222</c>).
/// The scheme selects the transport: <c>https://</c> = TLS, <c>http://</c> = h2c plaintext.
/// Required when <see cref="Enabled"/> is <c>true</c>.
/// </summary>
/// <summary>
/// Selects the historian backend provider (case-insensitive). Default <c>"Gateway"</c>; the Host's
/// <c>HistorianBackendRegistry</c> owns the set of valid names. Gateway-specific keys
@@ -49,6 +45,12 @@ public sealed class ServerHistorianOptions
public bool IsGatewayBackend =>
string.Equals(Backend?.Trim(), GatewayBackendName, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gateway backend only: absolute gateway endpoint URI the read client dials (e.g.
/// <c>https://host:5222</c>). The scheme selects the transport: <c>https://</c> = TLS, <c>http://</c> = h2c
/// plaintext. Required when the Gateway backend is consumed (<see cref="Enabled"/>, or an enabled
/// <c>AlarmHistorian</c>).
/// </summary>
public string Endpoint { get; init; } = "";
/// <summary>
@@ -129,17 +131,17 @@ public sealed class ServerHistorianOptions
// Gateway-specific checks apply only to the Gateway backend (other backends carry their own options).
if (IsGatewayBackend)
{
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.");
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,