D2: gate is in the C++ HistorianClient, not the managed wrapper

Direct HistorianAccess.AddNonStreamedValue (the 4-param overload that
bypasses HistorianDataValueList and goes straight to
HistorianClient.AddNonStreamedValueAsync) ALSO fails with 129
TagNotFoundInCache against SysTimeSec, even with validate=false.

So the cache check is inside the native C++ HistorianClient's
per-connection tag list — there's no managed-callable bypass.

Critical insight discovered: the SDK doesn't use the C++ HistorianClient
at all. It talks WCF directly. The cache gate that blocks the native
wrapper may not block a managed WCF client because the gate is enforced
by aahClientManaged, not by the WCF server.

This shifts the recommendation for any future D2 attempt from "wrap the
native API" (which is genuinely blocked) to "implement the wire path
directly on top of the existing ITransactionServiceContract methods and
test against the live server" (unverified but plausibly viable). The
harness can't help with that path — the wrapper itself is the blocker
we'd be bypassing.

177/177 tests still pass; harness gains --write-revision-direct flag
for further probing of the native-wrapper path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 02:34:02 -04:00
parent 3af8a13059
commit b5e5f5485b
2 changed files with 106 additions and 9 deletions
@@ -541,6 +541,63 @@ internal static class Program
snapshots["DataValueListBeforeSend"] = SnapshotObject(listInstance);
// Try the DIRECT public AddNonStreamedValue overload on HistorianAccess —
// (ConnectionIndex, HistorianDataValue, bool, ref error). This bypasses
// the DataValueList layer and goes straight to HistorianClient.AddNonStreamedValueAsync.
// If it succeeds where the list path failed, the cache gate is in the list-side
// ValidateValue rather than the native client.
if (HasFlag(args, "--write-revision-direct"))
{
try
{
MethodInfo[] directCandidates = accessType.GetMethods(allInstance)
.Where(m => m.Name == "AddNonStreamedValue")
.ToArray();
rows.Add(new
{
Kind = "DirectAddNonStreamedValueCandidates",
Count = directCandidates.Length,
Sigs = directCandidates.Select(m => string.Join(",", m.GetParameters().Select(p => p.ParameterType.Name))).ToArray(),
});
// Pick the 4-param overload — (ConnectionIndex, HistorianDataValue,
// bool, error&). Drop the IsPublic filter; reflection with
// NonPublic binding flags can call internal methods.
MethodInfo direct = directCandidates.First(m => m.GetParameters().Length == 4);
object directError = Activator.CreateInstance(errorType)!;
object?[] directArgs = new object?[4];
// ConnectionIndex enum values are internal — list with NonPublic
// flags first, then probe both 0 and 1 (most enums use these for
// primary connection slots). For Process scenario it's typically 0.
System.Reflection.FieldInfo[] ciFields = connectionIndexEnum.GetFields(
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
rows.Add(new
{
Kind = "ConnectionIndexFields",
Fields = ciFields.Where(f => f.IsLiteral)
.Select(f => $"{f.Name}={Convert.ToInt32(f.GetRawConstantValue())}").ToArray(),
});
// Default: index 0 (cast int -> enum)
directArgs[0] = Enum.ToObject(connectionIndexEnum, 0);
directArgs[1] = revValue;
directArgs[2] = false; // skip validate
directArgs[3] = directError;
bool directSuccess = (bool)direct.Invoke(access, directArgs)!;
object directErrorAfter = directArgs[3]!;
rows.Add(new
{
Kind = "DirectAddNonStreamedValue",
Success = directSuccess,
ErrorDescription = GetPropertyText(directErrorAfter, "ErrorDescription"),
ErrorCode = GetPropertyText(directErrorAfter, "ErrorCode"),
ErrorType = GetPropertyText(directErrorAfter, "ErrorType"),
});
}
catch (Exception ex)
{
rows.Add(new { Kind = "DirectAddNonStreamedValueException", Type = ex.GetType().Name, Message = ex.Message, Inner = ex.InnerException?.Message });
}
}
// Safety: require explicit --write-revision-commit to actually fire
// SendValues. Without it, the harness validates the path (cache gate,
// value validation) but does NOT push anything to the wire. Important