Historian: no read-only binding to an existing AVEVA tag; EnsureTags upserts (retypes, re-describes) it on every add #899
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
There is no way to expose an existing AVEVA historian tag's history on an OtOpcUa node without OtOpcUa rewriting that tag.
isHistorized: true(+historianTagname).IHistorianProvisioning.EnsureTagsAsync→GatewayTagProvisioner→ gatewayEnsureTags→Tags.AddAsync. That is an upsert: it sets the DataType (Boolean→Int2,Int32→Int4, …), StorageRate 1000 ms, IntegralDivisor 1 and Description = the OtOpcUa tag name.ServerHistorian:Enabled=false, which also disables HistoryRead. Typical gateway keys carryhistorian:tags:write(the deployedOtOpcUa-wonderkey on vd03 does).Example. Binding an
Int32node toSysTimeSec(AVEVA UInt2 system tag) would re-ensure it as Int4 with OtOpcUa's description. Binding aBooleannode toSysPulse(discrete) would push it as an Int2 analog. This happens again on every host start, because the first plan adds every tag.This blocked the Int32/Boolean part of the #700 deployed-endpoint gate on DEV vd03. That gate used only types the applier happens not to provision (#898) and therefore stayed zero-write. Evidence:
docs/plans/evidence/2026-11-13-historian-700-vd03/vd03-endpoint-gate.md.Proposal. A per-tag opt-out in TagConfig, e.g.
"historianProvision": false(or a moderead-only), that keeps HistoryRead and skips EnsureTags and continuous-historization capture for that tag. Optionally also a server switchServerHistorian:ProvisionTags(defaulttrue). Either should be honoured by the provisioner, the recorder and the validator/AdminUI (CSV column + editor toggle). Consider also making the default safer: refuse to upsert a tag that already exists with a different type unless explicitly allowed.Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
Design note and fix (lands together with #898)
Problem.
EnsureTagsis an upsert. Every added historized raw tag, which on a host start means every one, was provisioned. Binding a node to an existing AVEVA tag throughhistorianTagnametherefore re-typed and re-described that tag. The only off switch wasServerHistorian:Enabled=false, which also turns off reads.Design. There are two orthogonal opt-outs. The default for ordinary owned tags is unchanged.
TagConfig.historianReadOnly: true. This is a read-only binding.Historizing=trueand the HistoryRead bit, and reads the named historian tag.EnsureTagsrequest, whether it was added or changed (AddressSpaceApplier.IsOwnedHistorizedValue).WriteLiveValuesreach it. OtOpcUa issues no write of any kind for that tag.TagConfigIntent.ParsesetsHistorianReadOnly(only a JSONtruecounts).RawTagPlan.HistorianReadOnly, which takes part in equality and the hash.TagConfigverbatim. The deploy-artifact parity test asserts the flag survives artifact decode, and two entries were added to the golden corpus.UnsReferenceBackingParityTestsrecords the reason."true") is the deploy errorHistorianReadOnlyInvalid. The runtime would read such a value as owned and upsert someone else's tag, so it must not deploy.HistorianReadOnlynext toHistorianTagname. The export writes it, and theTagConfigJsonresidual strips the key. The import parses it strictly: a non-boolean cell is a row error.TagHistorizeConfig.Readuses the runtime's ownTagConfigIntent.Parse, so what the AdminUI shows is what the runtime does.isHistorizedandhistorianTagnamehave none in the v3 raw modal either; they are authored in the CSV or the raw JSON. This keeps the new flag consistent with them.ServerHistorian:ProvisionTags. The values are case-insensitive, and an unknown value fails host start.CreateOrUpdate(default) is the upsert, as before.CreateOnlyis Gateway only; any other backend fails host start. Each tag is checked withTagExistsAsync(a read) first. An existing tag is left alone and countedskipped. A lookup that fails is countedfailedand the tag is not sent, so the policy fails closed.Offkeeps the no-op provisioner. Reads and recording are unaffected.CreateOnlystill records into existing tags. OnlyhistorianReadOnlystops recording.Default for an explicit
historianTagname: unchanged (owned). I considered making an override read-only by default and decided against it. An override does not mean "someone else's tag". The AdminUI's raw-rename warning tells authors to sethistorianTagnameto pin an owned tag's history across RawPath renames, so overrides on owned tags are common. A read-only default would silently stop both provisioning and recording for all of them. Binding to a foreign tag is therefore an explicit opt-in, per tag or server-wide.Not covered. On the SqlServer and QuestDB backends, HistoryUpdate on a read-only node is unchanged. Those stores are OtOpcUa's own tables, and HistoryUpdate is an explicit client operation governed by ACLs. The Gateway backend has no HistoryUpdate, so on AVEVA the binding is fully read-only.
Tests (all with a capturing or fake provisioner and gateway; nothing calls
EnsureTagsagainst a real historian):AddressSpaceApplierProvisioningTests:GatewayTagProvisionerTestsfor CreateOnly:GatewayHistorianProviderTests,AddHistorianProvisioningTests(Offkeeps the Null provisioner),ServerHistorianOptionsValidatorTests(unknown value; CreateOnly on SqlServer or QuestDB).TagConfigIntentTestsDraftValidatorHistorianReadOnlyTestsTagHistorizeConfigTestsRawTagCsvRoundTripTestsAddressSpaceChangeClassifierDualNamespaceTests: a flag-only change is surgical.DeploymentArtifactRawUnsParityTestsDocs:
docs/Historian.mdgains §"Read-only bindings to existing historian tags (#899)", theProvisionTagsrow and the upgrade warning..claude/rules/historian.md,docs/Raw.mdanddocs/Uns.mdare updated too.Mutation results. Each guard was reverted on its own; every one was caught. The test projects were built and then run filtered.
IsOwnedHistorizedValueHistorianReadOnlyinRawTagPlan.EqualsOffkeeps the Null provisionerHistorianReadOnlyInvalidruleVerification.
DriverHostActorSourceRemovedTests) failed under load and passes when run alone (5/5).Landed on master as
6c4d2990. No live run:EnsureTagsis a write, so the provisioning paths were verified only against fakes and recording doubles. Nothing was deployed.