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:
@@ -78,21 +78,61 @@ the local cache. Without further RE on what populates the local cache,
|
||||
no path is reachable for a managed client to write either streaming or
|
||||
revision values.
|
||||
|
||||
### Decisive blocker
|
||||
### Cache gate is inside the native C++ HistorianClient
|
||||
|
||||
Both `AddStreamedValue` (AddS2) and `AddNonStreamedValue` (revision
|
||||
write) hit the same client-side cache gate. That gate isn't bypassed by:
|
||||
Followup probe (2026-05-05) tested the **direct** public overload
|
||||
`HistorianAccess.AddNonStreamedValue(ConnectionIndex, HistorianDataValue, bool validate, ref error)`
|
||||
which bypasses the `HistorianDataValueList` layer entirely and goes
|
||||
straight to `HistorianClient.AddNonStreamedValueAsync` (a C++ method).
|
||||
|
||||
Even with `validate=false` and `TagKey=12 (SysTimeSec)`, the call
|
||||
fails: `ErrorCode=TagNotFoundInCache (129)`.
|
||||
|
||||
So the gate isn't bypassed by:
|
||||
|
||||
1. Using a real wwTagKey from SQL
|
||||
2. Targeting a server-cache-resident tag (SysTimeSec)
|
||||
3. Setting `validate=false` on AddNonStreamedValue
|
||||
4. Bypassing the `HistorianDataValueList` layer (calling the direct
|
||||
`HistorianAccess.AddNonStreamedValue` overload)
|
||||
|
||||
There is no managed-client path to a successful write against this
|
||||
server architecture without first reverse-engineering and exercising
|
||||
whatever populates the per-connection local cache. That's a much larger
|
||||
investigation — likely involving the WCF `RegisterTags2` op,
|
||||
HistorianClient C++ internals, and/or IO-server-driven cache
|
||||
registration that managed clients can't trigger directly.
|
||||
The check is inside the **native C++ `HistorianClient`'s per-connection
|
||||
tag cache**, not in the managed wrapper. No managed-callable path exists
|
||||
to populate that cache.
|
||||
|
||||
### Critical insight: the SDK doesn't use the C++ HistorianClient
|
||||
|
||||
The SDK's production code talks **WCF directly** — no C++ HistorianClient
|
||||
instance, no per-connection local cache to gate against. The cache check
|
||||
is enforced by the `aahClientManaged.dll` wrapper, not by the WCF server.
|
||||
|
||||
This means the SDK could **plausibly** implement the revision-write
|
||||
path against the existing
|
||||
`ITransactionServiceContract.AddNonStreamValuesBegin/AddNonStreamValues/AddNonStreamValuesEnd`
|
||||
contract methods and have the server accept it directly — bypassing the
|
||||
gate that blocks the native wrapper.
|
||||
|
||||
**Unverified assumptions:**
|
||||
|
||||
- The server may have its own cache requirement that mirrors the
|
||||
C++ wrapper's. If yes, the SDK is also blocked. If no, the SDK
|
||||
can write where the wrapper can't.
|
||||
- The server may require `RTag2` (RegisterTags2) to be called per-tag
|
||||
before AddNonStreamValues — that's a known WCF op, already declared
|
||||
in `IHistoryServiceContract2`, used by the existing event flow. The
|
||||
SDK could call it.
|
||||
- The server may require an IO-server-style registration that's not
|
||||
exposable over the WCF surface at all.
|
||||
|
||||
**Recommendation:** if D2 is ever pursued, do it as a **direct
|
||||
WCF-level implementation in the SDK**, NOT as a wrapper over the
|
||||
native HistorianAccess methods. The harness can no longer help (the
|
||||
wrapper itself is gated). Test paths against the live server by
|
||||
calling the contract methods directly and observing what the server
|
||||
returns. If `AddNonStreamValues` succeeds without registration, the
|
||||
path is implementable. If it fails with a server-side cache error,
|
||||
try `RTag2` first. If it still fails, the path is genuinely blocked
|
||||
server-side.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user