Namespace matching¶
When a client connects, the server's wire-level namespace indices
(ns=0, ns=1, ns=2, ...) need to be mapped onto entries in the
process-wide o6.ns registry. This page describes how that
mapping is built. It does not cover how to use it — that is the job of
Namespace mapping in o6 and
Working with the address space.
Prerequisites
The user-facing pages Namespace and Namespace mapping in o6 describe the registry and shortname system from the application's side. This page assumes that surface and digs into the matching algorithm.
Why matching is non-trivial¶
A namespace URI is supposed to be unique, but a URI alone is not enough information. Two reasons:
- The same URI can be registered multiple times in the same process
with different scopes (
::globalversus a per-client or per-server scope) or with different versions of the model. A URI match alone would pick the wrong entry. - A server may publish a URI that no module in the registry has. The
client must still be able to read the server's
NamespaceArrayand talk to its nodes.
The algorithm below answers both. The result is one wire-index →
process-index mapping per Client, rebuilt only when the server's
NamespaceArray changes.
The wire-level flow¶
Client.connect() triggers the matching. The flow, in order, is:
- Open the secure channel and the session as usual.
- Register
ns=1. The client reads the server'sApplicationUrifrom aGetEndpointscall and registers it in the registry as<client.name>_ns1, scoped to the client's endpoint URL. The returned process-wide index is stored on the client as_application_namespace_index. - Read the server's
NamespaceArrayati=2255. The result is a list of URIs;uris[0]is alwayshttp://opcfoundation.org/UA/anduris[1]is the application URI. - For each URI at
wire_index >= 2, run the matching procedure described below. Build a candidate list per wire index and pick one. - Build a provisional mapping — the best non-version-aware guess —
and, if any wire index has more than one candidate, install that
provisional mapping and read the server's
NamespaceMetadatato resolve the ambiguity. If no wire index is ambiguous, the provisional mapping is the final mapping. - Build the final mapping. If it differs from the previous snapshot, install it.
Both the provisional and final installations are atomic "namespace snapshots" — Python mapping, SecureChannel decoder mapping, and the custom datatype chain are all replaced in one C-level transaction. A failed refresh leaves the previous snapshot in place. An unchanged snapshot is not rebuilt.
The matching procedure¶
For each (wire_index, uri) pair with wire_index >= 2 and a
non-empty uri, the client does the following:
1. Filter the registry by URI and scope¶
The filter combines as an AND — o6.ns.filter(uri=..., scope=...) —
but the client uses the two-step form to express the "either-or"
rule on scope: a hit is accepted when its scope is either the
client's own scope (self._name) or the global scope (::global).
Any other scope is rejected.
The default ordering of o6.ns.filter is by process-wide index, so
the first hit is the lowest-indexed match.
2. Auto-register on miss¶
If hits is empty, the URI is not in the registry at all. The client
auto-registers it under the client scope with a deterministic
shortname:
hits = [
o6.ns.register(
shortname=_remote_namespace_shortname(uri, scope),
uri=uri,
scope=scope,
)
]
The shortname is built from the last non-empty URI path segment, snake-cased, prefixed with the client scope, and disambiguated with a twelve-character SHA-256 suffix of the URI on collision. The result is a unique, process-wide shortname that round-trips: a second client that sees the same URI on the same server will compute the same shortname and re-use the existing registration.
3. Pick a candidate¶
_select_namespace_candidate implements the priority order. With no
server_version argument, the priority is:
- Exact version match. If the caller supplied a non-empty
server_version, candidates withhit.version == server_versionare kept and the rest are dropped. Without aserver_version, this step is a no-op and the fullhitslist is considered. - Scope preference. Among the surviving candidates, the one
whose
scopeequals the client's own scope (rather than::global) is preferred. The comparison is(hit.scope == scope, ...), which sortsTrue > False. - Version key. Among the survivors, the one with the highest
_version_key(hit.version)wins. The version key splits the version string on digit/letter boundaries and sorts numeric segments as integers (lower) and alphabetic segments as lower-cased strings (higher than integers), so"1.10.0"sorts above"1.9.0"and"1.05.06"sorts above"1.05.0RC".
The function uses max(..., key=...), so ties at the version key
are broken by process-wide index (lowest first), which is the order
of o6.ns.filter.
4. Resolve version ambiguity against the server¶
If a wire index has more than one candidate, the chosen candidate
might not be the version the server actually published. The client
applies the provisional mapping, then walks the server's
NamespaceMetadata (the NamespaceMetadataType object under the
Objects folder) to read NamespaceVersion for that wire index, and
re-runs _select_namespace_candidate(hits, scope, server_version).
This time the exact version match has data to work on. If the server is newer than anything the client has compiled, the match falls back to the highest available version, and the binding logs a warning naming the URI, the server's version, and the client's best match:
Namespace URI
<uri>from server has version<server_version>which is newer than the client's best match version<client_version>.
The mapping is then rebuilt as the final mapping and installed
atomically. The provisional mapping is in effect only for the
duration of the metadata walk; the user-visible Client always
exposes the final mapping.
Priority order — summary¶
In order from most to least specific, the algorithm uses:
| Step | Source of disambiguation | Applied to |
|---|---|---|
| 1 | Wire index fixed by server (ns=0, ns=1, ns>=2) |
The URI list itself |
| 2 | URI exact match against the registry | The registry |
| 3 | Scope must be the client's scope or ::global |
The candidate set |
| 4 | If URI is unknown, auto-register under client scope | The registry |
| 5 | Version exact match against NamespaceMetadata.NamespaceVersion |
The candidate set, when ambiguous |
| 6 | Scope preference (client > ::global) |
The candidate set, ties |
| 7 | _version_key(version) (numeric-aware) |
The candidate set, ties |
| 8 | Process-wide index (lowest first) | The candidate set, final ties |
URI and scope are the dominant filters. The version key and the exact-version probe only matter when the URI is shared by multiple compilations — which is rare, but expected for companion specs that ship in multiple historical releases.
ns=0 and ns=1 are special¶
ns=0 and ns=1 are handled outside the matching procedure:
ns=0always maps to thens0module, which is registered at import time and never has a per-client scope.ns=1maps to the<client.name>_ns1registration that step 2 of the wire-level flow installs, with the wire index1and the process index stored in_application_namespace_index.
Both mappings are pinned into the entries list before the candidates loop runs, so the candidate logic is never asked to decide between them.
Idempotence and runtime additions¶
Client.updateRemoteNamespaces is safe to call again. The snapshot
transaction is idempotent on equal snapshots: the Python mapping, the
SecureChannel decoder mapping, and the custom datatype chain are
all rebuilt together, and an unchanged snapshot is detected before
any work is done.
A connected server can add a new namespace at runtime. The new URI
appears in the server's NamespaceArray and the client sees it on
the next call. If the URI is already in the registry, the existing
mapping is reused; if not, the auto-register step creates a new
client-scoped entry and the snapshot is rebuilt.
The client never deletes an entry from the registry. A namespace that was registered for one server remains in the registry after disconnect and may be reused by a later connection.
Edge cases¶
- Empty URI at
ns>=2. The wire index is skipped. Empty URIs are not added to the mapping. - Empty
NamespaceArray.updateRemoteNamespacesreturns without modifying state. - Server version older than every compiled version. The exact match step finds nothing, the client picks the highest available version, and no warning is logged.
- Server version equal to the highest compiled version. The exact match step picks that hit, and the final mapping matches the provisional mapping. The snapshot is rebuilt only if it differs from the previous snapshot.
- Failed snapshot transaction. A failure inside
_apply_namespace_snapshotis propagated to the caller, but the previous snapshot remains in effect. The binding does not roll forward to a partial state. The testtest_failed_namespace_snapshot_preserves_previous_mappingin tests/namespaces/test_declarations_and_mapping.py exercises this path. - Two connected servers with the same URI but different
application URIs. Each server has its own client, so each
client has its own
ns=1registration. The shared URI is registered once at process level under the global scope (because both clients have a hit there) or under each client's scope (if not). The wire indices are server-local; the mapping is client-local.
Where to look in the code¶
Client._set_ns1_mappingino6/client.py— thens=1registration.Client.updateRemoteNamespacesino6/client.py— the procedure above, from readingi=2255to applying the final snapshot._select_namespace_candidate,_version_key, and_remote_namespace_shortnameino6/client.py— the priority logic, the version key, and the auto-register shortname._NamespacePackage.filterino6/ns/__init__.py— the AND-combined URI / scope / version filter._apply_namespace_snapshot(Python helper plus thepyClient_apply_namespace_snapshotC call) — the atomic snapshot transaction.
The priority order is exercised by tests/namespaces/test_client_mapping.py:
test_version_selection_prefers_exact_match— step 5.test_version_selection_defaults_to_latest— step 7.test_version_selection_prefers_client_scope— step 6.test_remote_namespace_shortname_is_client_scoped— step 4.test_remote_namespace_shortname_disambiguates_collisions— step 4.test_unknown_remote_namespace_is_registered_and_mapped— step 4 end-to-end against the test server.
See also¶
- Namespace mapping in o6 — the registry, shortnames, scopes, and the user-facing API.
- Working with the address space — the
user-facing description of the
ns=1registration andupdateRemoteNamespaces. - Loading and using nodesets
— the
Namespace.from_xmlcompanion to the client-side matching.