From 416789c6abc09f8e0faf8e9b5398cd2c06cb336e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 19:31:47 -0400 Subject: [PATCH] docs(archreview): R2-06 live gate 5/6 + U-7 retype policy answered - StorageRateMs bug found+fixed (PR #439); live suite 3/6 -> 5/6 - U-7: EnsureTags is a real upsert; supported-type retype ACCEPTED (Float->Double); Int1 not creatable on this histsdk (ProtocolEvidenceMissingException, Int1-only) - record the observed retype policy in docs/Historian.md (fills the placeholder) --- ...erverhistorian-failfast-plan.md.tasks.json | 2 +- archreview/plans/STATUS.md | 84 ++++++++++--------- docs/Historian.md | 26 ++++-- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json index 72512374..985c2394 100644 --- a/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json +++ b/archreview/plans/R2-06-serverhistorian-failfast-plan.md.tasks.json @@ -94,7 +94,7 @@ "T9", "T11" ], - "note": "ATTEMPTED 2026-07-13 on VPN vs a real 0.2.0 gateway (deployed locally in Docker → real AVEVA historian on wonder-sql-vd03). 3/6 green (read, alarm SendEvent-ack, EnsureTags-observable). Blocked: RuntimeDb SQL path needs Windows-auth as NAM\\dohertj2 (a Linux container can't SQL-auth a WINDOWS_LOGIN) → tests 2/3; and gateway EnsureTags returns ArgumentOutOfRangeException on this deployment (gateway-side) → test 5. U-7 retype policy STILL UNDETERMINED live (EnsureTags errors before a real Float→Int1 accept/reject). Deployment fixes made: FQDN Historian__Host + libgssapi-krb5-2/gss-ntlmssp in the gateway image. Full record in STATUS.md." + "note": "DONE 2026-07-13 on VPN vs a real 0.2.0 gateway (deployed locally in Docker → real AVEVA historian on wonder-sql-vd03 + Runtime SQL). 5/6 green. Found+FIXED a real OtOpcUa bug (PR #439, master 666908d7): GatewayTagProvisioner sent StorageRateMs=0 → gateway EnsureTags threw ArgumentOutOfRangeException → ALL historized-tag provisioning silently failed; stamp 1000ms + regression test. U-7 retype policy ANSWERED: EnsureTags is a real upsert; in-place retype between SUPPORTED types is ACCEPTED (Float→Double=success); but Int1 (0.2.0 Boolean target) is NOT creatable on this histsdk (ProtocolEvidenceMissingException — Int1-only; Int2/UInt2/Int4/UInt4/Float/Double all provision), so a pre-bump Float Boolean asked to become Int1 FAILS the provision (not silently retyped/data-lost). The 1 red test (Boolean→Int1) is a gateway histsdk type-support gap, not OtOpcUa. Deployment fixes: FQDN host + libgssapi-krb5-2/gss-ntlmssp in the gateway image + wonderapp SQL login for Runtime. Full record in STATUS.md." } ] } diff --git a/archreview/plans/STATUS.md b/archreview/plans/STATUS.md index a5c375a8..539a35fb 100644 --- a/archreview/plans/STATUS.md +++ b/archreview/plans/STATUS.md @@ -294,47 +294,55 @@ gates (R2-06 VPN, R2-08 live LDAP, R2-01 S7 blackhole) still need their infra. --- -## R2-06 live gate (T12) — attempted on the VPN, 3/6 green (2026-07-13) +## R2-06 live gate (T12) — 5/6 green on the VPN + a real provisioning bug fixed (2026-07-13) -The R2-06 `Category=LiveIntegration` gate was run against a **real 0.2.0 `ZB.MOM.WW.HistorianGateway`** deployed +The R2-06 `Category=LiveIntegration` gate ran against a **real 0.2.0 `ZB.MOM.WW.HistorianGateway`** deployed locally in Docker (`~/Desktop/HistorianGateway/docker`, republished at 0.2.0) pointed at the **real AVEVA -Historian** on `wonder-sql-vd03.zmr.zimmer.com:32565` + Galaxy SQL on `wonder-app-vd03`, over the corporate VPN. -OtOpcUa's `GatewayLiveFixture` connected h2c to the local gateway (`http://localhost:5221`, -`HISTGW_GATEWAY_ALLOW_UNTRUSTED=true` since h2c ⇒ no cert validation; gateway `ApiKeys:Mode=Disabled`, placeholder key). +Historian** on `wonder-sql-vd03.zmr.zimmer.com:32565` + Galaxy SQL on `wonder-app-vd03` + the `Runtime` SQL DB, +over the corporate VPN. OtOpcUa's `GatewayLiveFixture` connected h2c to the local gateway +(`http://localhost:5221`, `HISTGW_GATEWAY_ALLOW_UNTRUSTED=true` since h2c ⇒ no cert validation; gateway +`ApiKeys:Mode=Disabled`, placeholder key). See `[[reference_histgw_local_deploy]]` for the full recipe. -**Two real deployment fixes were needed** (both in the sibling `HistorianGateway/docker/`, local-only): -1. **DNS** — the base compose set the *short* name `Historian__Host: wonder-sql-vd03`, which does not resolve - inside the container; overrode it to the FQDN (Galaxy/RuntimeDb already used FQDNs). -2. **Kerberos/NTLM libs** — the historian **v6 session path** (reads + `EnsureTags`) and the SQL path authenticate - the Windows account `Historian__UserName=NAM\dohertj2` via .NET Negotiate/GSSAPI, which `aspnet:10.0` doesn't - ship. Without `libgssapi_krb5.so.2` every read/tag op failed with `InvalidOperationException`; adding - `libgssapi-krb5-2 gss-ntlmssp` to `docker/Dockerfile` fixed the historian path. (The **v8 event path** — - `SendEvent` — uses ECDH, not GSSAPI, so it worked before the fix.) +**Three deployment fixes (sibling `HistorianGateway/docker/`, local-only):** +1. **DNS** — base compose set the *short* `Historian__Host: wonder-sql-vd03`, which doesn't resolve in the + container; overrode to the FQDN. +2. **Kerberos/NTLM libs** — the historian **v6 session path** (reads + `EnsureTags`) + the SQL path authenticate + the Windows account `NAM\dohertj2` via .NET Negotiate/GSSAPI, which `aspnet:10.0` lacks → every read/tag op + failed `InvalidOperationException` (`Cannot load library libgssapi_krb5.so.2`). Added `libgssapi-krb5-2 + gss-ntlmssp` to `docker/Dockerfile`. (The **v8 event path**, `SendEvent`, uses ECDH — worked without it.) +3. **RuntimeDb SQL auth** — the `Runtime` DB SQL login had to be a **SQL login** (not the Windows account, which + a Linux container can't SQL-auth). The user supplied `wonderapp` (SQL_LOGIN with INSERT on `Runtime.dbo.History` + + SELECT on `dbo.Events`); wiring it made `WriteLiveValues` + SQL `ReadEvents` work. -**Result — 3/6 PASS:** -- ✅ `Galaxy_tag_read_round_trip` (ReadRaw `SysTimeSec`) — historian gRPC read path live. -- ✅ `Alarm_SendEvent_is_acked` — the alarm `SendEvent` (v8 event) path live. -- ✅ `EnsureTags_type_change_outcome_is_observable` — passes on the *observability* contract (see caveat below). +**🐞 Real OtOpcUa bug found + fixed (PR #439, master `666908d7`):** `GatewayTagProvisioner` built +`HistorianTagDefinition` **without `StorageRateMs`**, so the proto `uint32` defaulted to `0`; the gateway maps it +straight to the AVEVA SDK's `storageRateMs`, which **throws `ArgumentOutOfRangeException` ("Storage rate must be +> 0 ms")** on `0`. So **every** deploy-time historized-tag auto-provision (`IHistorianProvisioning.EnsureTagsAsync`) +was silently failing at the gateway (`failed=N`). Fixed by stamping `DefaultStorageRateMs=1000` (gateway +convention) + a regression unit test + `StorageRateMs` on the live-suite inline defs. Gateway driver unit +`103/103`. -**3/6 blocked (not OtOpcUa/R2-06 code defects):** -- ❌ `Write_then_read_on_sandbox_tag`, `Alarm_SendEvent_then_ReadEvents` — the **RuntimeDb SQL path** - (`WriteLiveValues` / SQL `ReadEvents`) must authenticate as `NAM\dohertj2`, a **WINDOWS_LOGIN** (verified via - `sys.server_principals`). A Linux container can't SQL-auth as a Windows login (host `sqlcmd` works only because - the go driver auto-negotiates NTLM); even with `gss-ntlmssp` present, `Encrypt=True` + explicit domain creds - didn't connect (`SQL event read failed` / write not acked). The account is *correct* (it has INSERT on - `Runtime.dbo.History` + SELECT on `dbo.Events`); the **auth mechanism** isn't available to a Mac container without - a `kinit` TGT against the NAM realm. Environmental, not a code defect. -- ❌ `EnsureTags_boolean_sandbox_provisions_as_Int1` — the gateway's `EnsureTags` returns - `ArgumentOutOfRangeException` for **every** definition (even a fresh Float create, reproduced directly via - `grpcurl`), *after* auth succeeds. This is a **gateway-side** issue (HistorianGateway's own live tests report - `EnsureTags` provisioning works against `wonder-sql-vd03`, so it's env/transport/version-specific — possibly the - `RemoteGrpc` transport not serving tag-config mutations); the gateway maps it to a bare type-name and logs no - stack even at Debug, so the detail isn't observable without instrumenting the gateway/histsdk itself. +**Result — 5/6 PASS** (was 3/6 before the fixes): +- ✅ `Galaxy_tag_read_round_trip` — historian gRPC read path. +- ✅ `Write_then_read_on_sandbox_tag` — `EnsureTags(Float)` + `WriteLiveValues` (SQL) + read-back. +- ✅ `Alarm_SendEvent_is_acked` + ✅ `Alarm_SendEvent_then_ReadEvents` — alarm `SendEvent` + SQL `ReadEvents`. +- ✅ `EnsureTags_type_change_outcome_is_observable` — now observes a **real** retype outcome (not a storage-rate error). +- ❌ `EnsureTags_boolean_sandbox_provisions_as_Int1` — **Int1 (Boolean) tag creation is not supported by the + gateway's histsdk on this historian**: `EnsureTags(Int1)` returns `ProtocolEvidenceMissingException` (the vendored + client's "no capture evidence for this type" guard → gRPC `Unimplemented`), both fresh and as a retype target. + A **type sweep** confirmed it is Int1-specific: **Int2/UInt2/Int4/UInt4/Float/Double all provision `success=True`; + only Int1 fails.** This is a **gateway/histsdk type-support gap**, not an OtOpcUa defect. -**⚠️ U-7 retype policy — still UNDETERMINED live.** Because `EnsureTags` errors on this deployment, the retype -probe (test 6) passes only by observing the generic `ArgumentOutOfRangeException` — it does **not** exercise a real -Float→Int1 accept/reject. So the 06/U-7 assessment note's three hypotheses (accepted-versioned / -accepted-retyped / rejected) remain open pending a working gateway `EnsureTags` on a real historian. The R2-06 -**code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; U-7 Boolean→Int1 mapping + upsert -semantics; C-7 doc) are all unit-covered and merged; this note records that the live *policy capture* leg is -still outstanding, gated on the gateway `EnsureTags` fix + a SQL-auth path for the RuntimeDb tests. +**✅ U-7 retype policy — ANSWERED (via `grpcurl` against the real historian):** +- `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type → `success`. +- An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns `success` + (of the three 06/U-7 hypotheses: **accepted-retyped**, not rejected). +- **But the specific 0.2.0 Boolean case (Float→Int1) can't retype** — Int1 isn't a creatable type here, so a + pre-0.2.0 **Float Boolean tag asked to become Int1 fails the provision** with `ProtocolEvidenceMissingException` + — it is **neither silently retyped nor data-lost**; the provision just fails (and is metered `failed=N`). That is + the practically-relevant U-7 answer for Boolean tags on this deployment. + +The R2-06 **code** deliverables (S-11 fail-fast + `ServerHistorianOptionsValidator`; U-7 Boolean→Int1 mapping + +upsert; C-7 doc) were already unit-covered + merged; this live gate additionally **found + fixed the StorageRateMs +provisioning bug** and **answered the U-7 retype policy**. The one remaining red test is a documented gateway +histsdk Int1-support gap. diff --git a/docs/Historian.md b/docs/Historian.md index 21a88b84..29a2f8d8 100644 --- a/docs/Historian.md +++ b/docs/Historian.md @@ -157,13 +157,25 @@ mapper code change is needed or wanted). Consequences: 1. **New Boolean tags provision correctly as Int1.** This is the fix arriving as a silent, zero-diff semantic change from the bump. 2. **Pre-bump Boolean tags exist historian-side as Float.** On the next deploy that dispatches provisioning - for such a tag, the gateway is asked (via the upsert) to update it to Int1. The AVEVA-side outcome of an - in-place analog retype is the one thing the contract does not pin; the candidate behaviors are - (i) **accepted with tag versioning** (history preserved under the old version, new writes under Int1), - (ii) **accepted as a plain retype**, or (iii) **rejected per-tag** → `TagOperationResult.Success=false` - → counted in the provisioner's `failed=N` dispatch tally. Only a live run distinguishes these — see the - **retype probe** (`EnsureTags_type_change_outcome_is_observable`) in the [Live /run gate](#live-run-gate). - *(This list is replaced with the observed answer after the first live run.)* + for such a tag, the gateway is asked (via the upsert) to update it to Int1. + + **Observed live (2026-07-13, real 0.2.0 gateway → real AVEVA historian on `wonder-sql-vd03`, probed via + `grpcurl` — archreview R2-06 T12):** + - `EnsureTags` is a **real create-or-update upsert** — re-`EnsureTags` of an existing tag at the same type + returns `Success=true`. + - An **in-place retype between SUPPORTED types is ACCEPTED** — `Float→Double` on an existing tag returns + `Success=true` (hypothesis **(ii) accepted as a plain retype**, not rejected). + - **BUT `Int1` — the 0.2.0 Boolean target — is *not a creatable type* on this historian's histsdk:** + `EnsureTags(Int1)` returns `Success=false, Error="ProtocolEvidenceMissingException"` (the vendored client's + "no capture evidence for this type" guard → gRPC `Unimplemented`), both for a fresh tag and as a retype + target. A type sweep confirmed it is **Int1-specific**: `Int2/UInt2/Int4/UInt4/Float/Double` all provision + `Success=true`; only `Int1` fails. + - **Net for a pre-bump Float Boolean tag asked to become Int1:** the provision **fails** (per-tag + `Success=false` → `failed=N` tally). It is **neither silently retyped nor data-lost** — the tag stays Float + and remains readable; the Int1 provision simply doesn't land until the gateway's histsdk gains `Int1` + create-evidence. (Prerequisite for reaching this outcome at all: the provisioner must stamp a **positive + `StorageRateMs`** — a `0` made `EnsureTags` throw `ArgumentOutOfRangeException` and fail *every* provision; + fixed in `GatewayTagProvisioner`, PR #439.) 3. **The failure mode is bounded and observable either way.** Provisioning is fire-and-forget and never blocks/fails a deploy; a per-tag rejection surfaces only as the tally's `failed` count. Values written to a still-Float tag by the recorder remain readable (Float supersets Int1's 0/1 range); nothing is lost