b3907efa6e
Re-review at 7286d320. -014 (Medium): ReadAtTimeAsync didn't classify StartQuery failures,
so a connection-class failure left a dead connection, re-failed every timestamp, and returned
Success=true with all-Bad (no failover); now resets+fails over via a shared classifier + tests.
-015: refresh stale named-pipe comments to TCP (no wire change). -013 (silent cap truncation,
ties OpcUaServer-002/Core.Abstractions-009) deferred cross-module. NOTE: the SDK-touching tests
are net48 + native aahClientManaged and run only on Windows; macOS verifies build + the SDK-free
subset only.
105 lines
5.7 KiB
C#
105 lines
5.7 KiB
C#
using ArchestrA;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.Backend;
|
|
|
|
/// <summary>
|
|
/// Driver.Historian.Wonderware-008 regression. The previous implementation unconditionally
|
|
/// called <c>HandleConnectionError()</c> whenever <c>StartQuery</c> returned <c>false</c>,
|
|
/// which tore down the (relatively expensive) shared SDK connection on a query-class error
|
|
/// such as a bad tag name. A burst of bad-tag queries could therefore push an otherwise
|
|
/// healthy cluster node into cooldown via the picker's <c>MarkFailed</c>. The fix
|
|
/// classifies the SDK error code: connection-class codes drop the connection; query-class
|
|
/// codes leave it intact.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class HistorianDataSourceStartQueryClassificationTests
|
|
{
|
|
// ── Connection-class codes — the connection should be reset ───────────
|
|
|
|
/// <summary>Verifies that connection-class error codes are classified as connection errors.</summary>
|
|
/// <param name="code">The historian error code to test.</param>
|
|
[Theory]
|
|
[InlineData(HistorianAccessError.ErrorValue.FailedToConnect)]
|
|
[InlineData(HistorianAccessError.ErrorValue.FailedToCreateSession)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NoReply)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NotReady)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NotInitialized)]
|
|
[InlineData(HistorianAccessError.ErrorValue.Stopping)]
|
|
[InlineData(HistorianAccessError.ErrorValue.Win32Exception)]
|
|
[InlineData(HistorianAccessError.ErrorValue.InvalidResponse)]
|
|
public void Connection_class_codes_are_classified_as_connection_errors(HistorianAccessError.ErrorValue code)
|
|
{
|
|
HistorianDataSource.IsConnectionClassError(code).ShouldBeTrue(
|
|
$"{code} is a connection/server failure — the SDK connection should be reset");
|
|
}
|
|
|
|
// ── Query-class codes — the connection should NOT be reset ────────────
|
|
|
|
/// <summary>Verifies that query-class error codes are NOT classified as connection errors.</summary>
|
|
/// <param name="code">The historian error code to test.</param>
|
|
[Theory]
|
|
[InlineData(HistorianAccessError.ErrorValue.InvalidArgument)] // bad tag name, etc.
|
|
[InlineData(HistorianAccessError.ErrorValue.ValidationFailed)] // bad query args
|
|
[InlineData(HistorianAccessError.ErrorValue.NotApplicable)] // wrong tag kind for query
|
|
[InlineData(HistorianAccessError.ErrorValue.NotImplemented)] // unsupported aggregate
|
|
[InlineData(HistorianAccessError.ErrorValue.NoData)] // empty range
|
|
public void Query_class_codes_are_NOT_classified_as_connection_errors(HistorianAccessError.ErrorValue code)
|
|
{
|
|
HistorianDataSource.IsConnectionClassError(code).ShouldBeFalse(
|
|
$"{code} is a query payload problem — must NOT tear down the SDK connection");
|
|
}
|
|
|
|
// ── Driver.Historian.Wonderware-014: the at-time loop must classify a per-timestamp
|
|
// StartQuery failure the same way the raw / aggregate / event paths do. The SDK
|
|
// HistoryQuery type is sealed-by-non-virtual + has no interface, so the loop itself
|
|
// can't be driven offline; the per-failure decision is therefore extracted into a
|
|
// pure helper that the at-time loop calls and these tests pin directly. ──────────
|
|
|
|
/// <summary>
|
|
/// A connection-class StartQuery error in the at-time loop must signal "reset the
|
|
/// connection and abort the read" (true) — not silently record a Bad sample and keep
|
|
/// hammering the dead connection for every remaining timestamp.
|
|
/// </summary>
|
|
/// <param name="code">The connection-class error code.</param>
|
|
[Theory]
|
|
[InlineData(HistorianAccessError.ErrorValue.FailedToConnect)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NoReply)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NotReady)]
|
|
public void AtTime_StartQuery_failure_with_connection_class_code_requests_connection_reset(
|
|
HistorianAccessError.ErrorValue code)
|
|
{
|
|
var error = new HistorianAccessError { ErrorCode = code };
|
|
HistorianDataSource.ShouldResetConnectionForStartQueryFailure(error).ShouldBeTrue(
|
|
$"{code} is a connection failure — the at-time loop must reset the connection, not record Bad");
|
|
}
|
|
|
|
/// <summary>
|
|
/// A query-class StartQuery error (or a missing error) in the at-time loop must NOT
|
|
/// reset the connection (false): a single bad/empty timestamp records a per-timestamp
|
|
/// Bad sample and continues to the next without tearing down the shared connection.
|
|
/// </summary>
|
|
/// <param name="code">The query-class error code.</param>
|
|
[Theory]
|
|
[InlineData(HistorianAccessError.ErrorValue.InvalidArgument)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NoData)]
|
|
[InlineData(HistorianAccessError.ErrorValue.NotApplicable)]
|
|
public void AtTime_StartQuery_failure_with_query_class_code_does_not_request_reset(
|
|
HistorianAccessError.ErrorValue code)
|
|
{
|
|
var error = new HistorianAccessError { ErrorCode = code };
|
|
HistorianDataSource.ShouldResetConnectionForStartQueryFailure(error).ShouldBeFalse(
|
|
$"{code} is a query/no-data problem — the at-time loop keeps the connection and records Bad");
|
|
}
|
|
|
|
/// <summary>A null error defaults to query-class (no reset) — the caller still records a Bad sample.</summary>
|
|
[Fact]
|
|
public void AtTime_StartQuery_failure_with_null_error_defaults_to_no_reset()
|
|
{
|
|
HistorianDataSource.ShouldResetConnectionForStartQueryFailure(null).ShouldBeFalse(
|
|
"a null error must not be promoted to a connection reset");
|
|
}
|
|
}
|