feat(historian): backend registry, ServerHistorian:Backend, Host wiring via provider
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
|
||||
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Maps <c>ServerHistorian:Backend</c> to an <see cref="IHistorianBackendProvider"/>. Names are
|
||||
/// case-insensitive. To add a backend, add one entry to <see cref="Factories"/>.
|
||||
/// <see cref="Resolve"/> only constructs the provider; option validation belongs to
|
||||
/// <see cref="ServerHistorianOptionsValidator"/>.
|
||||
/// </summary>
|
||||
public static class HistorianBackendRegistry
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, Func<ServerHistorianOptions, IConfiguration, IHistorianBackendProvider>> Factories =
|
||||
new Dictionary<string, Func<ServerHistorianOptions, IConfiguration, IHistorianBackendProvider>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[ServerHistorianOptions.GatewayBackendName] = (o, _) => new GatewayHistorianProvider(o),
|
||||
};
|
||||
|
||||
/// <summary>The registered backend names, for validation messages.</summary>
|
||||
public static IReadOnlyCollection<string> KnownBackends { get; } = Factories.Keys.ToArray();
|
||||
|
||||
/// <summary>True when <paramref name="backend"/> names a registered backend.</summary>
|
||||
public static bool IsKnown(string? backend) =>
|
||||
!string.IsNullOrWhiteSpace(backend) && Factories.ContainsKey(backend.Trim());
|
||||
|
||||
/// <summary>Builds the provider for <c>options.Backend</c>; throws <see cref="OptionsValidationException"/> if unknown.</summary>
|
||||
public static IHistorianBackendProvider Resolve(ServerHistorianOptions options, IConfiguration configuration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
if (!IsKnown(options.Backend))
|
||||
throw new OptionsValidationException(
|
||||
ServerHistorianOptions.SectionName, typeof(ServerHistorianOptions),
|
||||
[$"ServerHistorian:Backend '{options.Backend}' is not a known historian backend. Allowed values: {string.Join(", ", KnownBackends)}."]);
|
||||
return Factories[options.Backend.Trim()](options, configuration);
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,22 @@ public sealed class ServerHistorianOptionsValidator : OptionsValidatorBase<Serve
|
||||
/// <inheritdoc />
|
||||
protected override void Validate(ValidationBuilder builder, ServerHistorianOptions options)
|
||||
{
|
||||
// Unknown backend always fails, with no I/O and regardless of the consumer gate.
|
||||
builder.RequireThat(
|
||||
HistorianBackendRegistry.IsKnown(options.Backend),
|
||||
$"ServerHistorian:Backend '{options.Backend}' is not a known historian backend. "
|
||||
+ $"Allowed values: {string.Join(", ", HistorianBackendRegistry.KnownBackends)}.");
|
||||
|
||||
var alarmHistorianEnabled = _configuration.GetValue<bool>("AlarmHistorian:Enabled");
|
||||
var consumed = options.Enabled || alarmHistorianEnabled;
|
||||
|
||||
// A section nobody consumes may legitimately be empty (the docker-dev default) — no checks.
|
||||
if (!consumed) return;
|
||||
|
||||
// Gateway connection checks apply only to the Gateway backend; other backends validate their own
|
||||
// options (added with the backend) here, under the same consumer gate.
|
||||
if (!options.IsGatewayBackend) return;
|
||||
|
||||
var endpointValid = Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||
|
||||
|
||||
@@ -229,11 +229,21 @@ if (hasDriver)
|
||||
if (builder.Configuration.GetValue<bool>("AlarmHistorian:Enabled") && !serverHistorianOptions.Enabled)
|
||||
Log.Warning(
|
||||
"AlarmHistorian is enabled but ServerHistorian is disabled — the alarm-history sink is sourcing its " +
|
||||
"gateway connection (endpoint/key/TLS) from the disabled ServerHistorian section; HistoryReads stay GoodNoData.");
|
||||
"{Backend} backend connection from the disabled ServerHistorian section; HistoryReads stay GoodNoData.",
|
||||
serverHistorianOptions.Backend);
|
||||
|
||||
// Backend selection: resolve the provider once (unknown name => OptionsValidationException naming the
|
||||
// allowed values) and build every historian seam through it.
|
||||
var historianProvider = HistorianBackendRegistry.Resolve(serverHistorianOptions, builder.Configuration);
|
||||
builder.Services.AddSingleton<IHistorianBackendProvider>(historianProvider);
|
||||
// HistoryUpdate is gated like HistoryRead: None unconditionally, the real holder only when enabled.
|
||||
builder.Services.AddSingleton(HistorianUpdaterHolder.None);
|
||||
if (serverHistorianOptions.Enabled)
|
||||
builder.Services.AddSingleton(sp => new HistorianUpdaterHolder(historianProvider.CreateUpdater(sp)));
|
||||
|
||||
builder.Services.AddAlarmHistorian(
|
||||
builder.Configuration,
|
||||
(_, sp) => GatewayHistorian.CreateAlarmWriter(serverHistorianOptions, sp));
|
||||
(_, sp) => historianProvider.CreateAlarmWriter(sp));
|
||||
|
||||
// Node-local LocalDb: caches the deployed-configuration artifact so this node can boot from
|
||||
// its last-known-good config when central SQL is unreachable, and optionally replicates that
|
||||
@@ -260,7 +270,7 @@ if (hasDriver)
|
||||
// and the ServerHistorianOptions -> client-options mapping); Runtime owns the gating.
|
||||
builder.Services.AddServerHistorian(
|
||||
builder.Configuration,
|
||||
(opts, sp) => GatewayHistorian.CreateDataSource(opts, sp));
|
||||
(_, 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
|
||||
@@ -271,7 +281,7 @@ if (hasDriver)
|
||||
// ServerHistorian:Enabled flag as the read path — both target the same single gateway.
|
||||
builder.Services.AddHistorianProvisioning(
|
||||
builder.Configuration,
|
||||
(opts, sp) => GatewayHistorian.CreateProvisioner(opts, sp));
|
||||
(_, 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
|
||||
@@ -309,11 +319,7 @@ if (hasDriver)
|
||||
|
||||
// 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).
|
||||
builder.Services.AddSingleton<IHistorianValueWriter>(sp =>
|
||||
new GatewayHistorianValueWriter(
|
||||
HistorianGatewayClientAdapter.Create(
|
||||
serverHistorianOptions, sp.GetRequiredService<ILoggerFactory>()),
|
||||
sp.GetRequiredService<ILogger<GatewayHistorianValueWriter>>()));
|
||||
builder.Services.AddSingleton<IHistorianValueWriter>(sp => historianProvider.CreateValueWriter(sp));
|
||||
}
|
||||
|
||||
// Bind every cross-platform driver factory before AddAkka resolves IDriverFactory — replaces
|
||||
|
||||
Reference in New Issue
Block a user