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:
+25
-14
@@ -6,7 +6,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
/// <summary>Status codes owned by the updater (named consts so <c>StatusCodeParityTests</c> sees them).</summary>
|
||||
internal static class SqlHistorianUpdateStatusCodes
|
||||
{
|
||||
/// <summary>A value that cannot be stored (Double NaN / ±Infinity).</summary>
|
||||
/// <summary>
|
||||
/// A value that cannot be stored: a Double NaN / ±Infinity, an Empty (null) value with a non-Bad status, or a
|
||||
/// tag name longer than <see cref="SqlServerTagCatalog.MaxNameLength"/>.
|
||||
/// </summary>
|
||||
public const uint BadInvalidArgument = 0x80AB0000u;
|
||||
}
|
||||
|
||||
@@ -19,7 +22,13 @@ internal static class SqlHistorianUpdateStatusCodes
|
||||
/// DeleteAtTime — the first occurrence reports the real outcome, later ones <c>BadNoEntryExists</c>.</para>
|
||||
/// <para><b>Invalid items.</b> A null or <see cref="DateTime.MinValue"/> timestamp is
|
||||
/// <c>BadInvalidTimestamp</c>. A Double NaN/±Infinity cannot live in the SQL <c>float</c> column and is
|
||||
/// rejected with <c>BadInvalidArgument</c> (not silently stored as Empty). Other items still proceed.</para>
|
||||
/// rejected with <c>BadInvalidArgument</c> (not silently stored as Empty). An Empty (null) value is only
|
||||
/// accepted with a Bad status (an operator-authored outage marker); with a Good or Uncertain status it is
|
||||
/// <c>BadInvalidArgument</c>, because a stored sample is never served better than "no value". Other items
|
||||
/// still proceed.</para>
|
||||
/// <para><b>Tag names over <see cref="SqlServerTagCatalog.MaxNameLength"/> characters</b> are refused, never
|
||||
/// truncated: every item of the call (and DeleteRaw's single result) reports <c>BadInvalidArgument</c> and nothing
|
||||
/// is written.</para>
|
||||
/// <para><b>Unknown tag.</b> Insert/Update create it via <see cref="SqlServerTagCatalog"/>; Replace and the
|
||||
/// deletes report <c>BadNoEntryExists</c> (DeleteRaw: <c>BadNoData</c>) without creating anything.</para>
|
||||
/// </summary>
|
||||
@@ -44,6 +53,11 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
ArgumentNullException.ThrowIfNull(tag);
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
var results = new uint[values.Count];
|
||||
if (!SqlServerTagCatalog.IsValidName(tag))
|
||||
{
|
||||
Array.Fill(results, SqlHistorianUpdateStatusCodes.BadInvalidArgument);
|
||||
return results;
|
||||
}
|
||||
var winner = new Dictionary<long, int>(); // tick -> index of the item actually sent
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
@@ -53,7 +67,8 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
results[i] = SqlHistorianStatusCodes.BadInvalidTimestamp;
|
||||
continue;
|
||||
}
|
||||
if (v.Value.Kind == HistorianValueKind.Double && !double.IsFinite(v.Value.Number))
|
||||
if ((v.Value.Kind == HistorianValueKind.Double && !double.IsFinite(v.Value.Number))
|
||||
|| (v.Value.Kind == HistorianValueKind.Empty && !StatusBits.IsBad(v.StatusCode)))
|
||||
{
|
||||
results[i] = SqlHistorianUpdateStatusCodes.BadInvalidArgument;
|
||||
continue;
|
||||
@@ -71,7 +86,7 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
|
||||
var tagId = mode == HistoryUpdateMode.Replace
|
||||
? await _tags.TryGetTagIdAsync(tag, ct).ConfigureAwait(false)
|
||||
: await _tags.GetOrCreateTagIdAsync(tag, DataTypeFor(values[send[0]].Value.Kind), ct).ConfigureAwait(false);
|
||||
: await _tags.GetOrCreateTagIdAsync(tag, SqlHistorianDataTypes.FromKind(values[send[0]].Value.Kind), ct).ConfigureAwait(false);
|
||||
|
||||
var outcome = tagId is null
|
||||
? new Dictionary<long, uint>()
|
||||
@@ -98,6 +113,7 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
public async Task<uint> DeleteRawAsync(string tag, DateTime startUtc, DateTime endUtc, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(tag);
|
||||
if (!SqlServerTagCatalog.IsValidName(tag)) return SqlHistorianUpdateStatusCodes.BadInvalidArgument;
|
||||
var tagId = await _tags.TryGetTagIdAsync(tag, ct).ConfigureAwait(false);
|
||||
if (tagId is null) return SqlHistorianStatusCodes.BadNoData;
|
||||
await using var conn = await _conn.OpenAsync(ct).ConfigureAwait(false);
|
||||
@@ -121,6 +137,11 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
ArgumentNullException.ThrowIfNull(tag);
|
||||
ArgumentNullException.ThrowIfNull(timesUtc);
|
||||
var results = new uint[timesUtc.Count];
|
||||
if (!SqlServerTagCatalog.IsValidName(tag))
|
||||
{
|
||||
Array.Fill(results, SqlHistorianUpdateStatusCodes.BadInvalidArgument);
|
||||
return results;
|
||||
}
|
||||
var first = new Dictionary<long, int>();
|
||||
for (var i = 0; i < timesUtc.Count; i++)
|
||||
{
|
||||
@@ -177,14 +198,4 @@ internal sealed class SqlServerHistorianDataUpdater : IHistorianDataUpdater
|
||||
await tx.CommitAsync(ct).ConfigureAwait(false);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
// T9 owns the canonical HistorianValueKind -> Tag.DataType mapping; a later fix wave will dedupe the two.
|
||||
private static byte DataTypeFor(HistorianValueKind kind) => (byte)(kind switch
|
||||
{
|
||||
HistorianValueKind.Int64 => DriverDataType.Int64,
|
||||
HistorianValueKind.Boolean => DriverDataType.Boolean,
|
||||
HistorianValueKind.String => DriverDataType.String,
|
||||
HistorianValueKind.DateTime => DriverDataType.DateTime,
|
||||
_ => DriverDataType.Float64,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user