b5e5f5485b
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>
314 lines
13 KiB
Markdown
314 lines
13 KiB
Markdown
# Plan: Revision-Write Path (`AddRevisionValuesBegin/Value/End`)
|
||
|
||
Status: **ARCHITECTURALLY BLOCKED — verified 2026-05-05.** Same root
|
||
cause as `AddS2`: client-side cache rejects values for tags that
|
||
weren't registered through a configured IO server / Application Server
|
||
pipeline. Documented below; implementation deferred until / unless that
|
||
prerequisite is removed.
|
||
|
||
## Empirical finding (2026-05-05)
|
||
|
||
The native trace harness was extended with `--write-revision-values` to
|
||
drive the revision flow:
|
||
|
||
1. `HistorianAccess.CreateHistorianDataValueList(HistorianDataCategory.NonStreamedOriginal)`
|
||
succeeds — list is bound to the live `HistorianClient*` via
|
||
`GetClient(ConnectionIndex.Process)`.
|
||
2. `HistorianDataValueList.NonStreamedValuesBegin()` succeeds — list
|
||
batchID transitions 0 → 1.
|
||
3. `HistorianDataValueList.AddNonStreamedValue(value, validate=true, out error)`
|
||
**fails** with `ErrorCode=TagNotFoundInCache (129)`,
|
||
`ErrorDescription="error = 129 (Tag not found in cache)"` — the value
|
||
is never added to the list (`Count` stays 0).
|
||
4. `HistorianDataValueList.AddNonStreamedValuesEnd()` returns void.
|
||
5. `HistorianAccess.SendValues(list, out error)` returns `true` with
|
||
`ErrorCode=Success` — **but** no wire bytes left the client because
|
||
the list is empty. (Inspecting captured WriteMessage stream confirms
|
||
no `AddNonStreamValues*` Trx call appears.)
|
||
|
||
The validation that rejects the value is the same gate that blocks
|
||
`AddStreamedValue` (`AddS2`): the library's local tag cache only knows
|
||
about tags that were:
|
||
|
||
- Auto-populated from a configured IO server / Application Server pipeline, or
|
||
- Read via the existing read flow (which hits the cache as a side effect)
|
||
|
||
Tags created via `HistorianAccess.AddTag` populate `Runtime.dbo.Tag` but
|
||
are not added to the in-memory cache that AddStreamedValue /
|
||
AddNonStreamedValue consult. So writes from a managed client to a
|
||
client-created tag fail at the validation gate before any wire bytes
|
||
flow.
|
||
|
||
## Conclusion
|
||
|
||
The revision-write path **does not bypass the AddS2 blocker** — it
|
||
shares the same `TagNotFoundInCache` precondition.
|
||
|
||
### Follow-up probe (2026-05-05): SysTimeSec
|
||
|
||
To narrow the gate's scope, the harness was extended with
|
||
`--write-revision-target-tag <name>` (overrides the value's TagKey via
|
||
SQL lookup). Probed `SysTimeSec` (an auto-populated system tag whose
|
||
wwTagKey=12 is well-known in the runtime cache):
|
||
|
||
```
|
||
AddNonStreamedValue (TagKey=12 SysTimeSec):
|
||
Result=False
|
||
ErrorCode=TagNotFoundInCache
|
||
ErrorDescription="error = 129 (Tag not found in cache)"
|
||
```
|
||
|
||
Same failure. Then probed with `--write-revision-skip-validate` to set
|
||
the `validate` boolean to false on `AddNonStreamedValue` — same
|
||
`TagNotFoundInCache` failure. The cache check is intrinsic to the
|
||
function, not gated by the `validate` parameter.
|
||
|
||
So the gate is **per-(client-session, tag)**, not per-(server-cache, tag):
|
||
|
||
- Server-side, `SysTimeSec` IS in the runtime cache (it's auto-populated).
|
||
- Client-side, the managed library has its own per-connection tag list
|
||
that AddNonStreamedValue checks. That list is NOT populated by simply
|
||
knowing the wwTagKey — something else (likely a `RegisterTags2` call
|
||
during connection open, or the read flow as a side effect, or
|
||
IO-server-driven registration) populates it.
|
||
|
||
The harness opens with `ReadOnly=false` for the write scenario, which
|
||
may suppress the read-flow side effect that would otherwise populate
|
||
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.
|
||
|
||
### Cache gate is inside the native C++ HistorianClient
|
||
|
||
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)
|
||
|
||
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
|
||
|
||
Do **not** add public `WriteRevisionsAsync` / `BeginRevisionAsync` to
|
||
the SDK. The contract methods already exist in
|
||
`Wcf/Contracts/ITransactionServiceContract.cs`
|
||
(`AddNonStreamValuesBegin/AddNonStreamValues/AddNonStreamValuesEnd`)
|
||
for completeness, but the orchestrator and public surface stay absent.
|
||
|
||
Revisit if either of these changes:
|
||
|
||
1. AVEVA documents (or a customer demonstrates) a code path that
|
||
bypasses the cache validation for client-created tags.
|
||
2. The SDK's mission expands to include data correction for tags that
|
||
ARE in the runtime cache (i.e., tags managed by a real IO server),
|
||
in which case the harness extension below provides a starting point.
|
||
|
||
## Harness diagnostic (preserved)
|
||
|
||
The `--write-revision-values` flag in
|
||
`tools/AVEVA.Historian.NativeTraceHarness/Program.cs` reproduces the
|
||
above failure deterministically. Re-run it any time to verify the
|
||
blocker still holds:
|
||
|
||
```powershell
|
||
dotnet run --no-build --project tools\AVEVA.Historian.NativeTraceHarness -- `
|
||
--scenario write `
|
||
--write-sandbox-tag RetestSdkWriteRevSandbox `
|
||
--write-data-type Float `
|
||
--write-skip-add-tag --write-skip-add-value `
|
||
--write-revision-values
|
||
```
|
||
|
||
Look for the `AddNonStreamedValue` row's `ErrorCode` field in the JSON
|
||
output.
|
||
|
||
## Original plan (preserved for context if the blocker ever lifts)
|
||
|
||
## Context
|
||
|
||
The Historian's "revision write" path is the documented mechanism for
|
||
editing historized data after the fact (replaces the inferred
|
||
`ModifyData` / `DeleteData` use cases that don't exist as WCF ops). Native
|
||
managed surface (per Phase 1 findings of the write-commands plan):
|
||
|
||
| Public method | Token | Purpose |
|
||
|---|---|---|
|
||
| `ArchestrA.HistorianAccess.AddRevisionValuesBegin` | `0x06006175` | Open a revision-edit transaction |
|
||
| `ArchestrA.HistorianAccess.AddRevisionValue` | `0x06006176` | Append a value to the open transaction |
|
||
| `ArchestrA.HistorianAccess.AddRevisionValuesEnd` | `0x06006177` | Commit the transaction |
|
||
| `ArchestrA.HistorianAccess.AddRevisionValues` | `0x0600617F` | Single-shot variant |
|
||
| `ArchestrA.HistorianAccess.AddVersionedStreamedValue` | `0x0600616F` | Push one versioned value (related path) |
|
||
|
||
WCF surface is unknown — likely a new op group on `IHistoryServiceContract2`
|
||
or `IRetrievalServiceContract4` or a new contract.
|
||
|
||
## Goal
|
||
|
||
Public SDK API:
|
||
|
||
```csharp
|
||
public Task<HistorianRevisionTransaction> BeginRevisionAsync(string tag, CancellationToken ct);
|
||
// On the returned transaction:
|
||
public Task AddRevisionValueAsync(HistorianSampleEdit sample, CancellationToken ct);
|
||
public Task<bool> CommitAsync(CancellationToken ct);
|
||
// IDisposable / IAsyncDisposable for cancellation rollback if such a thing exists
|
||
```
|
||
|
||
Or a single batch convenience:
|
||
|
||
```csharp
|
||
public Task<bool> WriteRevisionsAsync(string tag, IReadOnlyList<HistorianSampleEdit> samples, CancellationToken ct);
|
||
```
|
||
|
||
The choice depends on the wire shape — if Begin/Value/End requires the
|
||
caller to maintain a server handle between calls, the disposable
|
||
transaction is necessary; if it's stateless, the batch convenience is fine.
|
||
|
||
## Workstreams
|
||
|
||
### A. Static analysis (1-2 hours)
|
||
|
||
- Inspect IL for the four managed public methods to identify the
|
||
underlying `CHistoryConnectionWCF.*` calls and their server-side WCF
|
||
contract methods.
|
||
- Add the contract methods to `Wcf/Contracts/IHistoryServiceContract2.cs`
|
||
(or a new contract if appropriate) with `[OperationContract(Name = "...")]`
|
||
+ `[MessageParameter]` attributes once names are known.
|
||
|
||
### B. Native harness extension (2-3 hours)
|
||
|
||
- Add `--scenario revision-write` to the harness.
|
||
- Refer to existing `--scenario write` plumbing for the AddTag wrapper
|
||
pattern.
|
||
- Sequence:
|
||
1. Open connection (probably write-enabled mode `0x401`)
|
||
2. AddTag for sandbox tag (re-uses existing harness flow)
|
||
3. AddStreamedValue for the initial sample (currently blocked
|
||
architecturally per Phase 2 findings — but may not be required if
|
||
the revision path operates directly on the historian engine state)
|
||
4. AddRevisionValuesBegin / AddRevisionValue × N / AddRevisionValuesEnd
|
||
5. Read back via existing read path; verify the samples reflect the
|
||
edits
|
||
|
||
### C. Wire capture (1 hour)
|
||
|
||
- Same `instrument-wcf-writemessage` + `instrument-wcf-readmessage`
|
||
IL-rewrite tooling already used for EnsT2 / DelT.
|
||
- Capture both Begin/Value/End and the single-shot AddRevisionValues
|
||
variant for byte-level diff.
|
||
|
||
### D. Decode + managed serializer (4-6 hours)
|
||
|
||
- Walk the captured InBuff bytes against the native serializer IL.
|
||
- The Begin payload likely seeds a server-side transaction handle that
|
||
Value calls reference. Look for an `out`-returned handle in the Begin
|
||
response.
|
||
- Value payload structure is likely similar to `AddS2`'s pBuf
|
||
(uint16 version + uint32 sampleCount + N × {tagId, FILETIME, quality,
|
||
typed value bytes}) but may include a per-sample revision/version field.
|
||
|
||
### E. Public API + tests (4-6 hours)
|
||
|
||
- New types: `HistorianSampleEdit` (sample + reason/version metadata),
|
||
`HistorianRevisionTransaction` (disposable handle).
|
||
- Public methods on `HistorianClient` per the Goal section.
|
||
- Unit tests: golden-byte fixtures for Begin/Value/End/Commit payloads.
|
||
- Live integration tests: write a known sample, edit it via the
|
||
revision path, read back and assert the new value appears.
|
||
|
||
## Risks
|
||
|
||
- **Server-cache prerequisite.** If the historian's revision path
|
||
also requires the tag to be "live in the runtime cache" (the same
|
||
blocker that killed `AddS2`), the entire path may be unimplementable
|
||
for the same architectural reason.
|
||
- **State across calls.** Begin/Value/End may store transaction state
|
||
on the server keyed by the WCF session GUID. WCF's session model
|
||
needs to be configured to keep the same channel alive across all
|
||
three calls — which is a different lifecycle from the existing
|
||
one-call-per-channel pattern in the SDK orchestrators.
|
||
- **Concurrent edits.** Server may reject concurrent revision
|
||
transactions on the same tag — needs probing.
|
||
- **Time bounds.** Revision likely respects the same `RealTimeWindow`
|
||
/ `FutureTimeThreshold` system parameters as `AddS2`. Out-of-window
|
||
edits silently drop or error — needs probing.
|
||
|
||
## Success Criteria
|
||
|
||
- Public `BeginRevisionAsync` (or batch variant) live-verified against
|
||
a sandbox tag created by `EnsureTagAsync`.
|
||
- Round-trip test: write initial value → revise it → read back → verify
|
||
the revised value persists in `History` extension table via SQL.
|
||
- Golden-byte fixtures for Begin / Value / End / Commit captured against
|
||
the sandbox tag.
|
||
- Decision documented for whether the `AddRevisionValues` single-shot
|
||
variant is exposed in addition to the Begin/Value/End sequence.
|
||
|
||
## Dependencies
|
||
|
||
- Existing analog write surface (`EnsureTagAsync`) — done.
|
||
- `AddS2` is **not** a prerequisite; the revision path may be an
|
||
independent code path that bypasses the runtime-cache gate. If it
|
||
doesn't, this plan is blocked the same way `AddS2` is.
|
||
|
||
## Out of scope
|
||
|
||
- Editing event tags. Events come from AVEVA AnE; the SDK only reads
|
||
them.
|
||
- Bulk schema changes. Forbidden over the wire per the Historian's
|
||
architecture.
|
||
|
||
## Trigger to start
|
||
|
||
A customer-driven request, or a real need to expose historical data
|
||
correction in the SDK's API. Without one, this remains the most
|
||
substantive remaining write-path workstream but isn't worth the 1-2
|
||
days of focused work speculatively.
|