R1.10 RenameTagsAsync: async tag rename via History StartJob (StJb)

Tag rename has no dedicated WCF op — the (old,new) name batch rides the
generic History StartJob (StJb) job buffer; the server returns a job id and
applies renames asynchronously. Handle is the uppercase storage-session GUID,
Open2 in write mode; reuses the write orchestrator's open+priming chain.

jobBuffer layout (decoded + server-validated): byte[7] zero prefix + uint32
pairCount + per pair (uint32 oldCharCount + UTF-16 oldName + uint32
newCharCount + UTF-16 newName), order (old,new). The raw instrument capture
mangles the final byte with MDAS chunk markers (the R1.1 lesson), so the golden
fixture pins the CLEAN byte[] the SDK handed the channel (dumped via
AVEVA_HISTORIAN_RENAME_DUMP) — the exact buffer the live server accepted and
renamed with.

Gated server-side by the AllowRenameTags system parameter (default 0): when
disabled the native client rejects pre-wire (err 132); the managed SDK surfaces
it as StartJob=false -> Accepted=false. Enabling needs a Historian config
reload, not just a storage-engine restart.

Shipped: HistorianClient.RenameTagAsync/RenameTagsAsync -> HistorianTagRenameResult;
HistorianTagRenameProtocol; orchestrator RenameTags/SendStartJobRename; golden
WcfTagRenameProtocolTests (4, pins server-accepted buffer); gated live test
RenameTagsAsync_AgainstLocalHistorian_RenamesSandboxTag (passed end-to-end).
Native-harness `rename` scenario + Capture-RenameTags.ps1 + decode-rename-capture.py.
Doc: docs/reverse-engineering/wcf-rename-tags.md. 213 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-21 01:18:41 -04:00
parent 362fcb0ef4
commit bc353df8c4
12 changed files with 794 additions and 3 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ blob needs RE).
| Create analog tag | `AddTag` | `History.EnsureTags` (EnsT2) | ✅ | DONE | Float/Double/Int2/Int4/UInt2/UInt4 + scaling |
| Create string/discrete tag | `AddTag` | `History.EnsureTags` | ⬜ | GATED/BOUNDED | native AddTag rejects these types server-side; needs different metadata path |
| Delete tag(s) | `DeleteTags` | `History.DeleteTags` | ✅ | DONE | |
| Rename tag(s) | `RenameTags` | (History op) | | BOUNDED | `AllowRenameTags` param already probed |
| Rename tag(s) | `RenameTags` | `History.StartJob` (StJb) | | DONE | `RenameTagsAsync`/`RenameTagAsync`; async rename job via StJb; gated by `AllowRenameTags`. See `wcf-rename-tags.md` |
| Add/Delete extended properties | `AddTagExtendedProperties`, `DeleteTagExtendedPropertiesByName` | `History.AddTagExtendedProperties` / `DeleteTagExtendedProperties` | ⬜ | BOUNDED | gRPC op + TEP serialize |
| Add/Delete localized properties | `AddTagLocalizedProperties`, `DeleteTagLocalizedPropertiesByName` | `History.AddTagLocalizedProperties` / `DeleteTagLocalizedProperties` | ⬜ | BOUNDED | |
+1 -1
View File
@@ -111,7 +111,7 @@ read/browse/status surface is Windows-free and the gRPC stack is the default pat
### 1c. Bounded config writes (SM each)
| ID | Capability | gRPC op | Payload | Notes |
|---|---|---|---|---|
| R1.10 | `RenameTagsAsync` | History rename op | rename request buffer | `AllowRenameTags` already probed |
| ~~R1.10~~ | `RenameTagsAsync` | History `StartJob` (StJb) | ✅ **DONE (2026-06-21), live-verified.** Rename has no dedicated op — the batch of (old,new) name pairs rides the generic **`StartJob`/StJb** job buffer (string handle = uppercase storage GUID, Open2 write mode); server returns a job id and applies renames async. jobBuffer = `byte[7] zero prefix + uint32 pairCount + per pair(uint32 oldCharCount+UTF-16 + uint32 newCharCount+UTF-16)`. Gated server-side by **`AllowRenameTags`** (default 0; needs a Historian config reload to enable — storage-engine-only restart is insufficient). Shipped: `RenameTagAsync`/`RenameTagsAsync``HistorianTagRenameResult`, `HistorianTagRenameProtocol`, golden `WcfTagRenameProtocolTests` (pins server-accepted buffer), gated live test. See `docs/reverse-engineering/wcf-rename-tags.md`. |
| R1.11 | Extended-property **write** | `History.AddTagExtendedProperties` (+ groups) / `DeleteTagExtendedProperties` | TEP serialize | mirror analog CTagMetadata discipline |
| R1.12 | Localized-property **write** | `History.AddTagLocalizedProperties` / `DeleteTagLocalizedProperties` | localized serialize | |
| R1.13 | Non-analog tag create (string/discrete) | `History.EnsureTags` | distinct CTagMetadata variant | ⚠ native AddTag rejected some types — confirm server path first; may be GATED |
@@ -0,0 +1,82 @@
# Tag rename over 2020 WCF — StartJob (StJb) rename job (HCAL R1.10)
**Status: ✅ DONE + live-verified (2026-06-21).** `HistorianClient.RenameTagsAsync` /
`RenameTagAsync` renames tags by submitting an asynchronous rename **job** to the Historian. Decoded
from an instrumented native `RenameTags` capture and verified end-to-end from the pure-managed .NET 10
client against the local 2020 Historian (sandbox tag created → renamed → new name visible → cleaned up).
## The op — rename rides the generic job framework
There is **no dedicated rename WCF operation**. The native `RenameTags(Tuple<string,string>[] pairs,
ref HistorianTagRenameStatus, out error)` packs the batch into the generic History **`StartJob`**
(`StJb`) buffer; the server returns a job id and applies the renames in the background. The native
client then polls `GetJobStatus` (`GtJb`) until the job reports done.
```
bool StartJob(string handle, byte[] jobBuffer, out string jobId, out byte[] errorBuffer) // StJb
bool GetJobStatus(string handle, string jobId, out byte[] jobStatus, out byte[] errorBuffer) // GtJb
```
Both already existed in `IHistoryServiceContract2`. `StartJob` takes a **string handle** = the Open2
storage-session GUID formatted `storageSessionId.ToString("D").ToUpperInvariant()` (the same uppercase
handle used by the other string-handle ops). The connection must be **write-enabled** (Open2 mode
`0x401`); the SDK reuses the write orchestrator's open + priming chain.
## The rename jobBuffer (decoded + server-validated)
```
byte[7] reserved / job-descriptor prefix (all zero in every capture)
uint32 pairCount
repeated pairCount times:
uint32 oldNameCharCount + UTF-16LE oldName (the tag being renamed)
uint32 newNameCharCount + UTF-16LE newName (its new name)
```
Char counts are UTF-16 code-unit counts. Pair order is **(old, new)**. ⚠️ A **raw** instrument
capture mangles the buffer's final byte with MDAS chunk markers (`9E`/`9F`) — the same hazard noted
for R1.1. So the golden fixture is the **clean** byte[] the SDK hands the WCF channel, dumped via the
`AVEVA_HISTORIAN_RENAME_DUMP` env hook on `HistorianWcfTagWriteOrchestrator`. That exact buffer was
accepted by the live server and the tag was renamed, so it is server-validated, not hand-stitched.
## Server gate — `AllowRenameTags`
Rename is gated by the **`AllowRenameTags`** system parameter (default **0/disabled**). When disabled,
the **native** client library rejects the call *before the wire* (`error 132 OperationNotEnabled`,
component `aahClientCommon::CClientCommon::RenameTags`); the managed SDK has no such pre-check, so a
disabled gate surfaces as `StartJob` returning false (reported as `Accepted = false`).
To enable for testing: `EXEC Runtime.dbo.aaSystemParameterUpdate @name='AllowRenameTags', @value=1`
**and reload the Historian config** — the running services cache system parameters, so the value only
takes effect after the Historian reloads (a Historian restart; a storage-engine-only restart is **not**
enough — the value is served from the `InSQLConfiguration` cache). Restore to `0` when done.
## Async completion
`RenameTagsAsync` submits the job and returns `HistorianTagRenameResult { Accepted, JobId, PairCount,
Error }`. The renames apply asynchronously server-side (observed: the native `GetTagRenameStatus` went
`Pending=true``false` within ~1.5 s for a single rename on the local box). The SDK does **not** poll
`GtJb` for completion: only the *pending* `jobstatus` buffer (6 zero bytes) was captured — the
done-state encoding was not, so polling is intentionally left out rather than guessing it. The gated
live test confirms completion by polling the new name's metadata after submission.
## Shipped surface
- `HistorianClient.RenameTagAsync(old, new)` / `RenameTagsAsync(IReadOnlyList<(string,string)>)`
`HistorianTagRenameResult`.
- `HistorianTagRenameProtocol.SerializeRenameJob` (the jobBuffer serializer);
`HistorianWcfTagWriteOrchestrator.RenameTags`/`SendStartJobRename` (open → write-priming → StJb).
- Golden `WcfTagRenameProtocolTests` (pins the server-accepted buffer + layout); gated live test
`RenameTagsAsync_AgainstLocalHistorian_RenamesSandboxTag` (needs `HISTORIAN_HOST=localhost`,
`HISTORIAN_RENAME_SANDBOX=RetestSdkWrite…`, and `AllowRenameTags` enabled).
## Capture / decode tooling
`scripts/Capture-RenameTags.ps1` (native-harness `rename` scenario + instrument-wcf-{write,read}message;
sandbox-guarded create→rename→cleanup) and `scripts/decode-rename-capture.py`.
## Scope notes
- String-valued, original tag renames only. The multi-pair batch framing is captured (count-prefixed)
and unit-tested; the live test exercises a single pair.
- `RenameSourceTags` (replication/source-server rename) is **not** shipped — different op signature
(adds a source-server string), not captured.