feat: depth-aware channel derivation (leaf-preserving truncation) - #6
Merged
Conversation
MTConnect component paths go deeper than a UNS topic can carry. The demo
Mazak's `stock` sits on Resources[resources]/Materials[materials]/Stock[stock]
- four channel tokens where an instance-scoped data topic has room for three -
so under `selection: { "mode": "all" }` the library refused its topic with
DEPTH_EXCEEDED and the signal never published at all.
A derived channel is now the LAST k component-path segments plus the signal id,
with k the largest value that fits the instance's real UNS budget. The leaf-most
segments are the informative ones, so root-side segments drop first; the id is
terminal and never dropped, which is what keeps every derived channel unique
(signal ids are unique per instance - enforced by validate_bindings for the
explicit half, by the -2/-3 suffix chain for the derived half).
The budget is measured, not assumed: `app::channel_budget_of` mints a probe
topic through the library's own `Uns` builder and takes what the prefix did not
spend, so the 7-separator and 256-byte limits are never copied here. It is
per instance and includes the device/component/instance token lengths, so a
verbose identity costs channel depth instead of silently breaking the topic.
`ChannelBudgets` resolves once at startup and is stamped by both
`compile_mtconnect` and `SignalRegistry::new`, so startup and every reload
derive identical channels; it is hashed into the signal-set generation.
Nothing is lost: the full component path stays in the probe model and is served
as `signal.address.componentPath` on sb/signals and sb/browse.
Ordinary truncation is normal derivation on a deep machine - counted in
`ServedSet::channel_truncated`, logged at DEBUG, no event. The only warning is
the pathological floor, where not even the bare id fits: that raises the
existing MtconnectSignalSetEvent with reason "channelBudget" and leaves the
channel as the id, so the library's own validation refuses it loudly on publish.
A hand-set `channel` is untouched however deep.
Verified live against demo.mtconnect.org: the Mazak stock signal now publishes
on
ecv1/gw-01/MtconnectAdapter/cnc-1/data/materials-materials/stock-stock/stock
Design: DESIGN.md D-MtconnectAdapter-L12.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
MTConnect component paths go deeper than a UNS topic can carry. The live demo Mazak's
stockdataitem sits on
Resources[resources]/Materials[materials]/Stock[stock], which with its id is fourchannel tokens — an instance-scoped data topic (
ecv1/{device}/{component}/{instance}/data/…) hasalready spent four of the seven allowed
/separators, so it has room for three. Underselection: { "mode": "all" }the library refused the topic withDEPTH_EXCEEDEDand the signalnever published at all.
The rule
A derived channel is the last k UNS-sanitized component-path segments plus the signal id, where
kis the largest value that fits the instance's real UNS budget.stockController[controller]controller-controller/estopAxes/Linear[X]axes/linear-x/xabsResources[resources]/Materials[materials]/Stock[stock]materials-materials/stock-stock/stockSystems[systems]/Hydraulic[hydraulic]/Pump[pump]/Motor[motor]/Sensor[sensor]motor-motor/sensor-sensor/ptempResources[resources]aboveMaterials[materials]/Stock[stock]barely narrows anything.unique per instance (
validate_bindingsenforces it for the explicit half, the-2/-3suffixchain for the derived half), so however much path is dropped two derived channels cannot collide.
No extra suffix rule is needed and none was added — there is a test that asserts uniqueness at
every
k.app::channel_budget_ofmints a probe topic through thelibrary's own
Unsbuilder and takes what the prefix did not spend, soMAX_TOPIC_SLASHES(7)and
MAX_TOPIC_UTF8_BYTES(256) are never copied into this repo. It is per instance andincludes the device/component/instance token lengths, so a verbose identity costs channel depth
rather than silently breaking the topic.
src/mtconnect/**stays EdgeCommons-free: it receives aplain
ChannelBudget { max_tokens, max_bytes }.ProbeModeland is servedas
signal.address.componentPathonsb/signalsand on everysb/browseentry. Only the topicis shaped.
Observability
Ordinary truncation is normal derivation on a deep machine: counted in
ServedSet::channel_truncatedand logged once per recompile at DEBUG — no event. The onlywarning is the pathological floor, where not even the bare id fits (the identity itself has consumed
the topic): that raises the existing
MtconnectSignalSetEventwithreason: "channelBudget", onceper distinct count, and leaves the channel as the bare id so the library's own validation refuses it
loudly on publish.
A hand-set
channelon an explicitsignals[]entry is untouched, however deep. An explicitentry that omits
channeltakes the derived one and is shaped like any other.Plumbing
ChannelBudgetsresolves once at startup (identity is fixed for the process; adding an instance isalready
RESTART_REQUIRED) and is stamped by bothcompile_mtconnectandSignalRegistry::new, so the startup compile and every live reload derive identical channels. Thebudget is hashed into the signal-set generation, because it changes what every derived channel is
and therefore what a browse cursor was minted against.
Live evidence
Run against the real
demo.mtconnect.orgMazak withmode: "all", HOST/MQTT into a local broker.The signal that used to be dropped now publishes:
134 distinct topics observed, every one within budget; no
publish failed/DEPTH_EXCEEDEDin theadapter log. Other deep paths on the same device shaped the same way
(
systems-systems/coolant-coolant/cooltemp,controller-controller/path-path/execution, …).Gates
cargo test— 377 passed, 0 failed (+15 new: 8 unit inselection.rs, 5 inapp.rs, 2end-to-end in
poll_acquisition.rs; 2 more inreload.rs).cargo clippy --all-targets -- -D warnings— clean.cargo llvm-cov --fail-under-lines 90(CI's exclusions) — 96.71% total;selection.rs97.92%,app.rs95.28%,reload.rs98.30%.New fixtures:
tests/fixtures/devices_deep_2.7.xml(the demo Mazakstockshape reproducedverbatim, plus a five-level path and a long-token path that exhausts bytes before levels) and
its
/currentcompanion.Docs
DESIGN.mdgains D-MtconnectAdapter-L12 (supersedes L10's channel clause);docs/reference/configuration.mdgains a Deep component paths section;docs/explanation.mdgains A machine model is deeper than a topic.