docs(historian-gateway): follow-up & deferred-items plan (gateway SendEvent source + tz, recorder override, propagation)
v2-ci / build (pull_request) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
v2-ci / build (pull_request) Failing after 40s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
Consolidates everything deferred or surfaced during live validation, with owning repo per item: P1 gateway bugs (FU-1 SendEvent doesn't populate Source_Object → alarm write-back-by-source; FU-2 WriteLiveValues +4h explicit-timestamp shift), P2 OtOpcUa items (FU-3 HistorianTagname-override recorder edge; FU-4 MaxAttempts test; FU-5 pre-existing Modbus Host.IntegrationTests failure), P3 cross-repo propagation. Includes the live-validation reproduction recipe + the dbo.Events INSQL-view caveat. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# OtOpcUa ↔ HistorianGateway — Follow-up & Deferred Items
|
||||
|
||||
**Status:** the 21-task integration (`feat/historian-gateway-backend`, Gitea PR
|
||||
[#423](https://gitea.dohertylan.com/dohertj2/lmxopcua/pulls/423)) + the continuous-historization
|
||||
ref-feed are complete and **live-validated** against `wonder-sql-vd03`. The offline suite is green;
|
||||
the live `Category=LiveIntegration` suite is green (read ✅, write-persist ✅, alarm-send ✅,
|
||||
alarm-readback ⏭ skip). This doc tracks everything deliberately deferred or surfaced during
|
||||
validation, with the **owning repo** for each.
|
||||
|
||||
**Live-validation harness recap (how to reproduce any of the live findings below):** run the
|
||||
HistorianGateway locally against the live historian, then point the OtOpcUa live tests (or `grpcurl`)
|
||||
at it. The gateway boots from env-var config (secrets from `~/.zshenv`):
|
||||
|
||||
```
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
Historian__Host=$HISTORIAN_GRPC_HOST Historian__Port=32565 Historian__GrpcUseTls=true
|
||||
Historian__UserName=$HISTORIAN_USER Historian__Password=$HISTORIAN_PASSWORD
|
||||
Historian__AllowUntrustedServerCertificate=true
|
||||
Galaxy__ConnectionString=$GALAXY_SQL_CONNECTION
|
||||
RuntimeDb__Enabled=true RuntimeDb__EventReadsEnabled=true
|
||||
RuntimeDb__ConnectionString="Server=$HISTORIAN_GRPC_HOST;Database=Runtime;User Id=$HISTORIAN_SQL_USER;Password=$HISTORIAN_SQL_PASSWORD;TrustServerCertificate=true;Encrypt=false"
|
||||
ApiKeys__Mode=Disabled
|
||||
# dotnet run the Server → gRPC h2c on localhost:5221, HTTP on :5220 (/healthz, /health/ready)
|
||||
```
|
||||
OtOpcUa live tests then read `HISTGW_GATEWAY_ENDPOINT=http://localhost:5221` +
|
||||
`HISTGW_GATEWAY_APIKEY=<any>` + `HISTGW_TEST_TAG`/`HISTGW_WRITE_SANDBOX_TAG`/`HISTGW_ALARM_SOURCE`.
|
||||
Direct SQL: `Runtime.dbo.Events` is an **INSQL linked-server view that rejects untimed queries** —
|
||||
always include an `EventTimeUtc` range. `sqlcmd -S $HISTORIAN_GRPC_HOST -d Runtime -U $HISTORIAN_SQL_USER -C`
|
||||
(password via `SQLCMDPASSWORD`).
|
||||
|
||||
---
|
||||
|
||||
## Priority 1 — Gateway-side bugs that block OtOpcUa write/read use cases
|
||||
**Owning repo: `~/Desktop/HistorianGateway` (HistorianGateway).** OtOpcUa code is correct for both;
|
||||
these are gateway defects that gate the "write OtOpcUa's own data, read it back" use case.
|
||||
|
||||
### FU-1 — `SendEvent` does not populate `Source_Object` (alarm write-back-by-source)
|
||||
**Symptom (live-proven):** OtOpcUa's `GatewayAlarmHistorianWriter.SendEvent` of an event with
|
||||
`source_name="HistGW.LiveTest.AlarmSource"` **acks** and **lands in `Runtime.dbo.Events`** with the
|
||||
correct `Type` (`LimitAlarm`) and `EventTimeUtc` (no shift) — but with **`Source_Object = NULL`** (and
|
||||
all other `Source_*`/`Provider_*` columns null). The gateway's `SqlEventReader` filters
|
||||
`WHERE Source_Object = @source`, so a source-filtered `ReadEvents` of a just-sent event returns 0.
|
||||
|
||||
**What works (so this is narrow, not "C2 won't-fix"):**
|
||||
- Time-only `ReadEvents` (no source filter) returns events (50 in a 2-day window during validation).
|
||||
- Source-filtered `ReadEvents` for a **real Galaxy event source** (`TableAlarms_006`) returns its
|
||||
history (`System.Deploy`/`Undeploy`/`Alarm.Set`, each with `source_name` populated). So the SQL
|
||||
reader + source filter are functional; only **ad-hoc SendEvents lack a `Source_Object`.**
|
||||
- ⇒ **Reading existing Galaxy alarm/event history by source already works** (the mxaccessgw read use
|
||||
case). Only round-tripping OtOpcUa's *own* sends by source is blocked.
|
||||
|
||||
**Investigation (gateway repo):**
|
||||
- Read the v8 event-send path: `RegisterCmEventTag` + the `ConnectionType=Event` send (CM_EVENT). Find
|
||||
where the event's source/tag is set on the wire payload and whether the historian maps any send-side
|
||||
field → the `Events.Source_Object` column. Start at the gateway `SendEvent` service + the vendored
|
||||
`AVEVA.Historian.Client` event session (`HistorianEventSession`), and the
|
||||
`event-session-reuse-spike` notes in `../histsdk/docs/reverse-engineering/`.
|
||||
- Determine whether the historian's CM_EVENT API even *allows* setting a `Source_Object` for an event
|
||||
not raised by a Galaxy object. If the source must be a registered event-tag/source name, decide how
|
||||
OtOpcUa's `EquipmentPath` should map to it.
|
||||
|
||||
**Proposed fix (one of):**
|
||||
1. If the send payload has a source/tag field that maps to `Source_Object`: populate it from the event's
|
||||
`source_name` in the gateway `SendEvent` handler. (Preferred — makes write-back-by-source work.)
|
||||
2. If the historian cannot carry a source for ad-hoc events: document it, and have the gateway's
|
||||
`SqlEventReader` optionally match the source in a fallback column the send *does* populate (if any),
|
||||
or expose a "read all events in window, filter client-side" mode. Update OtOpcUa's
|
||||
`GatewayHistorianDataSource.ReadEventsAsync` defensive client-side source filter accordingly (it
|
||||
currently drops events whose mapped `SourceName` ≠ requested source — which would also drop
|
||||
source-less sends even if the server returned them).
|
||||
|
||||
**Acceptance:** an OtOpcUa `SendEvent(source=X)` is readable back via `ReadEvents(source=X)` within the
|
||||
window. Then **un-skip** `Alarm_SendEvent_then_ReadEvents` in
|
||||
`tests/Drivers/.../Live/GatewayLiveIntegrationTests.cs` (it currently `Assert.Skip`s on a 0-result with
|
||||
the accurate reason).
|
||||
|
||||
### FU-2 — `WriteLiveValues` shifts an explicit timestamp by the local↔UTC offset (~+4h)
|
||||
**Symptom (live-proven, reproduces via raw `grpcurl` — no OtOpcUa code involved):** a `WriteLiveValues`
|
||||
with an **explicit** `timestamp=2026-06-27T03:45:00Z` lands in the historian at
|
||||
`2026-06-27T07:45:00Z` (+4h = the deployment's local↔UTC delta). A **server-stamped** write (null
|
||||
timestamp) lands correctly at the gateway's UTC now. The OtOpcUa value-writer sends correct UTC
|
||||
(`Timestamp.FromDateTime(SpecifyKind(ts, Utc))`), so the shift is in the gateway's SQL write path.
|
||||
|
||||
**Impact:** the continuous-historization recorder writes the driver's **source** timestamp (explicit),
|
||||
so historized values would carry timestamps offset by the host's UTC offset until fixed. (The OtOpcUa
|
||||
live write test currently uses a ±12h tz-tolerant readback window to validate *persistence* around
|
||||
this — see FU-2 acceptance.)
|
||||
|
||||
**Investigation (gateway repo):** `SqlLiveValueWriter` (the `aaAnalogTagInsert` + `INSERT INTO History`
|
||||
path). Inspect which `History` DateTime column is written (local vs `*UTC`) and the conversion applied
|
||||
to the incoming proto UTC `Timestamp`. The +4h (value lands *later* than supplied UTC) is consistent
|
||||
with writing a UTC value into a **local** column that `ReadRaw` then converts local→UTC, on a server
|
||||
whose offset is −4h (EDT). Compare against the **server-stamped** path (which is correct) to see what
|
||||
conversion the explicit path is missing.
|
||||
|
||||
**Proposed fix:** convert the supplied UTC timestamp to the historian server's local time before the
|
||||
`History` insert (or write the UTC-typed column), so an explicit UTC timestamp round-trips unchanged.
|
||||
Add a gateway unit/live test: write explicit `T`, read back, assert the sample timestamp == `T`.
|
||||
|
||||
**Acceptance:** an explicit-timestamp `WriteLiveValues` reads back at the supplied UTC time. Then
|
||||
**tighten** the OtOpcUa live write test (`Write_then_read_on_sandbox_tag`) back to a narrow recent
|
||||
window anchored on the write time.
|
||||
|
||||
---
|
||||
|
||||
## Priority 2 — OtOpcUa-side follow-ups
|
||||
**Owning repo: `~/Desktop/OtOpcUa` (this repo).**
|
||||
|
||||
### FU-3 — Continuous-historization `HistorianTagname` override edge case
|
||||
The `ContinuousHistorizationRecorder` registers `DependencyMuxActor` interest **by the resolved
|
||||
historian name** (`HistorianTagname` override else `FullName`) — the same key the EnsureTags hook and
|
||||
the writer use. The mux fans `DependencyValueChanged` **keyed by `FullReference`** (the driver's
|
||||
published ref). In the **common case (no override)** historian-name == `FullReference`, so it's fully
|
||||
consistent and works (live-validated path is the value writer; mux fan-out is the recorder's input).
|
||||
**When a `HistorianTagname` override is set** (override ≠ `FullReference`), the recorder registers
|
||||
interest under a key the mux never fans → that tag's values are never captured.
|
||||
**Fix options:** register mux interest by `FullReference` (the mux key) while writing to the historian
|
||||
under the resolved historian name — i.e. carry both identifiers through `IHistorizedTagSubscriptionSink`
|
||||
/ the recorder (a `(muxRef, historianName)` pair) instead of a single string. Add a recorder test with
|
||||
a divergent override. **Low urgency** (overrides are uncommon); only matters for non-Galaxy historized
|
||||
tags that set an explicit `HistorianTagname`.
|
||||
|
||||
### FU-4 — `AlarmHistorianOptions.Validate()` `MaxAttempts<=0` test coverage (minor)
|
||||
T19 pruned the Wonderware-shaped fields and reworked `AlarmHistorianRegistrationTests`. The
|
||||
`MaxAttempts <= 0` warning branch in `AlarmHistorianOptions.Validate()` is exercised in prod but not
|
||||
covered by a test (the sibling warnings for `DrainIntervalSeconds`/`Capacity`/`DeadLetterRetentionDays`
|
||||
are). Add a `Validate_warns_on_non_positive_max_attempts` case. Trivial.
|
||||
|
||||
### FU-5 — Pre-existing `Host.IntegrationTests` failure (NOT ours — track separately)
|
||||
`EquipmentNamespaceMaterializationTests.Deploying_an_equipment_namespace_carries_the_signal_into_the_artifact`
|
||||
fails (`Rejected` vs expected `Accepted`) on a **Modbus-only** namespace via `DraftValidator`/
|
||||
`ConfigComposer` — untouched by this branch. **Verified failing identically on `master`** (via
|
||||
`git stash`). Environment/pre-existing; out of scope for the historian work but worth a separate ticket.
|
||||
|
||||
---
|
||||
|
||||
## Priority 3 — Cross-repo propagation (after merges)
|
||||
- **FU-6 — scadaproj index + agent memory.** When PR #423 merges (and the Plan 1 client PR), update
|
||||
`../scadaproj/CLAUDE.md` (the HistorianGateway + OtOpcUa entries) and the agent memory notes
|
||||
(`otopcua-historian-backend`, `scadaproj-umbrella`) to record: OtOpcUa now consumes
|
||||
`ZB.MOM.WW.HistorianGateway.Client` as its historian backend; the Wonderware historian driver was
|
||||
retired; the two gateway follow-ups (FU-1/FU-2). Per the CLAUDE.md cross-repo propagation rule.
|
||||
|
||||
---
|
||||
|
||||
## Already resolved this effort (for the record — do NOT redo)
|
||||
- **Alarm SendEvent event-id bug** — `AlarmEventMapper` set the wire `Id` → gateway handler throws →
|
||||
every alarm send `PermanentFail`. **Fixed** (`44644ddc`): leave `Id` unset, carry the id as an
|
||||
`AlarmId` property. Live-validated (send acks).
|
||||
- **Continuous-historization ref-feed gap** — recorder spawned with an empty ref set. **Closed**
|
||||
(`2982cc4b`): `IHistorizedTagSubscriptionSink` + recorder `UpdateHistorizedRefs(added, removed)`
|
||||
converges mux interest on each `AddressSpaceApplier.Apply()`.
|
||||
- **Read path / use case 1** — live-validated PASS (ReadRaw through `GatewayHistorianDataSource`).
|
||||
- **C2 mis-attribution** — the alarm readback-0 was NOT the "C2 server-gated event reads" limitation;
|
||||
the SQL reader works (see FU-1).
|
||||
Reference in New Issue
Block a user