2feb56d52c
Drove the revision-write flow via reflection in the native trace harness (--write-revision-values) to see whether it bypasses the AddS2 architectural blocker. It doesn't. Findings: - HistorianAccess.CreateHistorianDataValueList(NonStreamedOriginal) succeeds - HistorianDataValueList.NonStreamedValuesBegin() succeeds (batchID 0->1) - HistorianDataValueList.AddNonStreamedValue(value, validate=true, out err) FAILS with ErrorCode=TagNotFoundInCache (129) — same client-side validation gate that blocks AddS2 - AddNonStreamedValuesEnd() returns void; SendValues() returns true with Success because the list is empty (no value was ever added) - No AddNonStreamValues* WCF calls reach the wire Conclusion: the revision-write path requires the tag to be in the library's runtime tag cache, which is only populated by configured IO server / Application Server pipelines, not by HistorianAccess.AddTag. This matches the architectural blocker documented for AddS2 and means no public WriteRevisionsAsync / BeginRevisionAsync should be added to the SDK — the path is unreachable for client-created sandbox tags. The Wcf/Contracts/ITransactionServiceContract methods (AddNonStream- ValuesBegin/AddNonStreamValues/AddNonStreamValuesEnd) remain declared for completeness; no orchestrator or public surface is added. The harness extension is preserved as a deterministic reproducer for the blocker: re-run --write-revision-values to verify the gate any time. docs/plans/revision-write-path.md updated with the empirical finding plus the original plan retained as historical context. 177/177 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
9.8 KiB
Markdown
233 lines
9.8 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. There is no path
|
||
from a managed client to a successful AddNonStreamValues call against
|
||
a client-created sandbox tag.
|
||
|
||
To validate this conclusion further (not done in this pass — too risky
|
||
for the production Historian) one could try AddNonStreamedValue against
|
||
a system tag like `SysTimeSec` whose key IS in the cache from upstream
|
||
registration. If that succeeds, the path is implementable in principle
|
||
for IO-registered tags; if it also fails, the prerequisite is even
|
||
stricter.
|
||
|
||
## 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.
|