Files
histsdk/docs/plans/revision-write-path.md
T
Joseph Doherty 3af8a13059 D2 (revision-write): probe SysTimeSec — same gate, narrower scope
Extended the harness with --write-revision-target-tag <name> (overrides
the value's TagKey via SQL lookup) and --write-revision-skip-validate
(passes false to AddNonStreamedValue's `validate` boolean). Added
--write-revision-commit gate so the harness validates without actually
calling SendValues by default — important when targeting system tags.

Probed SysTimeSec (wwTagKey=12, server-cache-resident system tag):
- AddNonStreamedValue: ErrorCode=TagNotFoundInCache (129) — same failure
- With validate=false: same failure (the cache check is intrinsic, not
  gated by the boolean)

Conclusion: the gate is per-(client-session, tag), not per-server-cache.
Even tags the SERVER cache knows about are rejected because the LIBRARY
maintains a separate per-connection tag list that AddNonStreamedValue
checks. That list isn't populated by knowing the wwTagKey alone — it
needs whatever mechanism (RegisterTags2 / read flow side effect / IO
server registration) that we haven't reverse-engineered.

The revision-write path remains architecturally blocked for managed
clients. Plan doc updated with the SysTimeSec finding.

177/177 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:27:58 -04:00

274 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
### Decisive blocker
Both `AddStreamedValue` (AddS2) and `AddNonStreamedValue` (revision
write) hit the same client-side cache gate. That 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
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.
## 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.