fix(historian): SQL quality contract, long-name refusal, info-bit masking, AtTime-after-now, bootstrap hardening
- I-1: an Empty sample is always Bad. The writer stores a non-Bad NaN/Inf as Empty+BadOutOfRange and any other non-Bad Empty as BadNoData; the updater refuses a non-Bad Empty with BadInvalidArgument; reads clamp a legacy non-Bad Empty row to BadNoData (Raw and AtTime). - Tag/source names over 400 chars are refused, never truncated: writer per-tag failure, updater BadInvalidArgument, reads not-found, events none. - HistorianInfoBits masks the historian bits before setting one. - AtTime past now, stepped from the last sample => UncertainDataSubNormal. - Bootstrapper: logger, 5 s negative cache, ClearPool + Warning when sp_releaseapplock fails. - Provider builds its shared state lazily (an invalid Schema no longer throws at Resolve); Kinds is a FrozenSet; Gateway reuses the recorder's DefaultSupportedKinds. - Cleanup: SqlHistorianDataTypes in its own file (updater dup removed), dead Invalidate and unused status consts deleted, named severity masks, alarm writer brace. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+61
-22
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Frozen;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -11,23 +12,26 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
/// <see cref="HistorianValueKind"/> and HistoryUpdate. All seams share ONE connection helper, ONE schema
|
||||
/// bootstrapper and ONE tag catalog, so the schema is ensured once and tag ids are cached once.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The shared state is built on the first <c>Create*</c> call, not in the constructor: the constructor does no
|
||||
/// validation and no I/O, so resolving the provider for a backend nobody consumes never fails on its options
|
||||
/// (they are validated, consumer-gated, by the host's <c>ServerHistorianOptionsValidator</c>). The first
|
||||
/// <c>Create*</c> call's service provider supplies the logger factory the bootstrapper logs through.
|
||||
/// </remarks>
|
||||
public sealed class SqlServerHistorianProvider : IHistorianBackendProvider
|
||||
{
|
||||
private static readonly IReadOnlySet<HistorianValueKind> Kinds =
|
||||
new HashSet<HistorianValueKind>(Enum.GetValues<HistorianValueKind>());
|
||||
private static readonly FrozenSet<HistorianValueKind> Kinds =
|
||||
Enum.GetValues<HistorianValueKind>().ToFrozenSet();
|
||||
|
||||
private readonly SqlHistorianConnection _conn;
|
||||
private readonly SqlServerSchemaBootstrapper _bootstrap;
|
||||
private readonly SqlServerTagCatalog _catalog;
|
||||
private readonly SqlServerHistorianOptions _options;
|
||||
private readonly object _initLock = new();
|
||||
private Shared? _shared;
|
||||
|
||||
/// <summary>Creates the provider over validated options.</summary>
|
||||
/// <summary>Creates the provider over the <c>ServerHistorian:SqlServer</c> options (validated elsewhere).</summary>
|
||||
/// <param name="options">The <c>ServerHistorian:SqlServer</c> options.</param>
|
||||
public SqlServerHistorianProvider(SqlServerHistorianOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_conn = new SqlHistorianConnection(options);
|
||||
_bootstrap = new SqlServerSchemaBootstrapper(_conn, options);
|
||||
_catalog = new SqlServerTagCatalog(_conn, _bootstrap);
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -36,26 +40,61 @@ public sealed class SqlServerHistorianProvider : IHistorianBackendProvider
|
||||
/// <inheritdoc />
|
||||
public IReadOnlySet<HistorianValueKind> SupportedKinds => Kinds;
|
||||
|
||||
private static ILogger Log<T>(IServiceProvider sp) =>
|
||||
((sp.GetService(typeof(ILoggerFactory)) as ILoggerFactory) ?? NullLoggerFactory.Instance).CreateLogger<T>();
|
||||
private static ILoggerFactory Loggers(IServiceProvider sp) =>
|
||||
(sp.GetService(typeof(ILoggerFactory)) as ILoggerFactory) ?? NullLoggerFactory.Instance;
|
||||
|
||||
private Shared Get(IServiceProvider sp)
|
||||
{
|
||||
if (Volatile.Read(ref _shared) is { } built) return built;
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_shared is { } raced) return raced;
|
||||
var conn = new SqlHistorianConnection(_options);
|
||||
var bootstrap = new SqlServerSchemaBootstrapper(conn, _options,
|
||||
Loggers(sp).CreateLogger<SqlServerSchemaBootstrapper>());
|
||||
var shared = new Shared(conn, bootstrap, new SqlServerTagCatalog(conn, bootstrap));
|
||||
Volatile.Write(ref _shared, shared);
|
||||
return shared;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHistorianDataSource CreateDataSource(IServiceProvider sp) =>
|
||||
new SqlServerHistorianDataSource(_conn, _bootstrap, _catalog, Log<SqlServerHistorianDataSource>(sp));
|
||||
public IHistorianDataSource CreateDataSource(IServiceProvider sp)
|
||||
{
|
||||
var s = Get(sp);
|
||||
return new SqlServerHistorianDataSource(s.Conn, s.Bootstrap, s.Catalog,
|
||||
Loggers(sp).CreateLogger<SqlServerHistorianDataSource>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHistorianValueWriter CreateValueWriter(IServiceProvider sp) =>
|
||||
new SqlServerHistorianValueWriter(_conn, _bootstrap, _catalog, Log<SqlServerHistorianValueWriter>(sp));
|
||||
public IHistorianValueWriter CreateValueWriter(IServiceProvider sp)
|
||||
{
|
||||
var s = Get(sp);
|
||||
return new SqlServerHistorianValueWriter(s.Conn, s.Bootstrap, s.Catalog,
|
||||
Loggers(sp).CreateLogger<SqlServerHistorianValueWriter>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAlarmHistorianWriter CreateAlarmWriter(IServiceProvider sp) =>
|
||||
new SqlServerAlarmHistorianWriter(_conn, _bootstrap, Log<SqlServerAlarmHistorianWriter>(sp));
|
||||
public IAlarmHistorianWriter CreateAlarmWriter(IServiceProvider sp)
|
||||
{
|
||||
var s = Get(sp);
|
||||
return new SqlServerAlarmHistorianWriter(s.Conn, s.Bootstrap,
|
||||
Loggers(sp).CreateLogger<SqlServerAlarmHistorianWriter>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHistorianProvisioning CreateProvisioning(IServiceProvider sp) =>
|
||||
new SqlServerTagProvisioner(_conn, _bootstrap, Log<SqlServerTagProvisioner>(sp));
|
||||
public IHistorianProvisioning CreateProvisioning(IServiceProvider sp)
|
||||
{
|
||||
var s = Get(sp);
|
||||
return new SqlServerTagProvisioner(s.Conn, s.Bootstrap, Loggers(sp).CreateLogger<SqlServerTagProvisioner>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IHistorianDataUpdater? CreateUpdater(IServiceProvider sp) =>
|
||||
new SqlServerHistorianDataUpdater(_conn, _catalog);
|
||||
public IHistorianDataUpdater? CreateUpdater(IServiceProvider sp)
|
||||
{
|
||||
var s = Get(sp);
|
||||
return new SqlServerHistorianDataUpdater(s.Conn, s.Catalog);
|
||||
}
|
||||
|
||||
private sealed record Shared(SqlHistorianConnection Conn, SqlServerSchemaBootstrapper Bootstrap, SqlServerTagCatalog Catalog);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user