Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9b96fdf
Add shadow NodeManager reload support
marcschier Jul 21, 2026
09b0d50
Add lossless WoT conversion and source generation
marcschier Jul 21, 2026
30614a3
Add injectable WoT protocol bindings
marcschier Jul 21, 2026
ea63219
Add registry-first WoT Connectivity runtime
marcschier Jul 21, 2026
8d9d084
Fix WoT converter: mandatory digest, shared resolution context, resol…
marcschier Jul 21, 2026
e8c5fc4
Route ownership-sensitive monitored-item ops to owning NodeManager
marcschier Jul 21, 2026
d1236f1
Harden WoT protocol executors (MQTT TLS, Modbus validation, HTTP redi…
marcschier Jul 21, 2026
5b1a054
Make WoT registry commits atomically durable and populate typed events
marcschier Jul 21, 2026
b3164da
Sync corrected WoT Connectivity V2 model
marcschier Jul 21, 2026
4f48525
Reject oversized Modbus quantities
marcschier Jul 21, 2026
bf7eedd
Fix HTTP test server analyzer warning
marcschier Jul 21, 2026
0d9a3b5
Use explicit HTTP header search comparison
marcschier Jul 22, 2026
d91d553
Align WoT Connectivity proof to combined 1.1 spec model
marcschier Jul 22, 2026
835476e
Fix WoT proof test analyzer warnings
marcschier Jul 22, 2026
9572b83
Align WoT converter with portable Binding identity
marcschier Jul 23, 2026
ded7dea
Make WoT NodeSet conversion native-first
marcschier Jul 23, 2026
499d28e
Address WoT model concept review feedback
marcschier Jul 24, 2026
7f9dbdf
Address WoT mapping review feedback
marcschier Jul 24, 2026
ca88061
Prefer context prefixes for WoT QualifiedNames
marcschier Jul 24, 2026
49aaf29
Implement selectable WoT projection retirement
marcschier Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions UA.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
<Project Path="src/Opc.Ua.Redundancy.Client/Opc.Ua.Redundancy.Client.csproj" />
<Project Path="src/Opc.Ua.Redundancy.Kubernetes/Opc.Ua.Redundancy.Kubernetes.csproj" />
<Project Path="src/Opc.Ua.WotCon/Opc.Ua.WotCon.csproj" />
<Project Path="src/Opc.Ua.WotCon.Binding/Opc.Ua.WotCon.Binding.csproj" />
<Project Path="src/Opc.Ua.WotCon.Binding.Http/Opc.Ua.WotCon.Binding.Http.csproj" />
<Project Path="src/Opc.Ua.WotCon.Binding.Mqtt/Opc.Ua.WotCon.Binding.Mqtt.csproj" />
<Project Path="src/Opc.Ua.WotCon.Binding.Modbus/Opc.Ua.WotCon.Binding.Modbus.csproj" />
<Project Path="src/Opc.Ua.WotCon.Binding.OpcUa/Opc.Ua.WotCon.Binding.OpcUa.csproj" />
<Project Path="src/Opc.Ua.WotCon.Client/Opc.Ua.WotCon.Client.csproj" />
<Project Path="src/Opc.Ua.WotCon.Server/Opc.Ua.WotCon.Server.csproj" />
<Project Path="src/Opc.Ua.Bindings.Https/Opc.Ua.Bindings.Https.csproj" />
Expand Down Expand Up @@ -250,6 +255,7 @@
<Project Path="tests/Opc.Ua.MigrationAnalyzer.Tests/Opc.Ua.MigrationAnalyzer.Tests.csproj" />
<Project Path="tests/Opc.Ua.Types.Tests/Opc.Ua.Types.Tests.csproj" />
<Project Path="tests/Opc.Ua.WotCon.Tests/Opc.Ua.WotCon.Tests.csproj" />
<Project Path="tests/Opc.Ua.WotCon.Binding.Tests/Opc.Ua.WotCon.Binding.Tests.csproj" />
</Folder>
<Folder Name="/tests/TestFramework/">
<Project Path="tests/Opc.Ua.Test.Common/Opc.Ua.Test.Common.csproj" />
Expand Down
35 changes: 32 additions & 3 deletions docs/RuntimeNodeSets.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Use the [source-generated path](SourceGeneratedNodeManagers.md) when you want co

`AddRuntimeNodeSet` on `IOpcUaServerBuilder` remains the startup path: its factory is created before the server starts and its NodeSet is imported during `CreateAddressSpaceAsync`.

Running servers also expose `INodeManagerLifecycle`. Resolve it from dependency injection in a hosted server, or use `StandardServer.NodeManagerLifecycle` when constructing the server directly. The lifecycle provider can add, reload, and remove runtime NodeSets without restarting the server.
Running servers also expose `INodeManagerLifecycle`. Resolve it from dependency injection in a hosted server, or use `StandardServer.NodeManagerLifecycle` when constructing the server directly. The lifecycle provider can add, reload, shadow-reload, and remove runtime NodeSets without restarting the server.

```csharp
public sealed class ModelLoader(INodeManagerLifecycle lifecycle)
Expand Down Expand Up @@ -55,17 +55,46 @@ Each add returns an immutable `NodeManagerRegistration`. Reload returns the next

Reload and removal fail when the current NodeManager owns active monitored items. Delete those monitored items first, then retry. This fail-closed rule prevents a live subscription from retaining a stale manager handle.

### Shadow reload

`ShadowReloadRuntimeNodeSetAsync` (backed by `INodeManagerLifecycle.ShadowReloadAsync`) replaces a live registration the same way `ReloadRuntimeNodeSetAsync` does, but without the active-monitored-item guard:

```csharp
public async ValueTask ShadowReloadAsync(CancellationToken ct)
{
m_registration = await lifecycle.ShadowReloadRuntimeNodeSetAsync(
m_registration!,
new RuntimeNodeSetOptions
{
Sources = [RuntimeNodeSetSource.FromFile("Models/MyMachine.NodeSet2.xml")]
},
ct);
}
```

The replacement generation is prepared and published through the same transactional prepare/publish/commit/rollback path as `ReloadAsync`, so a failure during preparation, publication, or the routing switch leaves the current generation fully active and cleans up the replacement, exactly as a normal reload does. Once committed, every new service request is atomically routed to the replacement generation, including for namespaces the current and replacement generations share.

The current generation is not torn down immediately. It is moved to the same retired-generation bookkeeping used for an ordinary reload, but its existing monitored items and any request or continuation point that already captured it keep being served by it, unaffected by the routing switch. The retired generation is disposed automatically, without deleting any client subscription, once its monitored items and in-flight state drain; a later lifecycle operation (or shutdown) opportunistically retries that cleanup until it succeeds. `ShadowReloadAsync` returns the replacement `NodeManagerRegistration` immediately and invalidates the current handle for further lifecycle mutations, the same as `ReloadAsync`.

Use `ShadowReloadAsync` when a model update must take effect for new requests without waiting for existing subscriptions to unsubscribe first; use the fail-closed `ReloadAsync` when a stale generation must never remain reachable, even briefly, for already-open monitored items.

### Immediate reload

`ImmediateReloadRuntimeNodeSetAsync` (backed by `INodeManagerLifecycle.ImmediateReloadAsync`) performs the same atomic replacement but does not retain the previous generation until monitored items drain. After requests that already captured the old routing generation finish, every affected data-change monitored item is made publishable with `BadNodeIdUnknown`, event monitored items stop producing events, continuation points are invalidated, and the old NodeManager is disposed. The subscription and monitored-item records remain available so clients can receive the status and delete or recreate the affected items.

Use immediate reload only when continuity through the previous generation is not required. Durable monitored items are not eligible for immediate retirement because their terminal state would have to survive restart; choose shadow reload for any generation that owns them.

Treat `INodeManagerLifecycle` as a host control-plane API. Do not invoke reload or removal from inside an OPC UA service or Method callback: teardown waits for requests that already captured the retired routing generation to complete before disposing it.

The built-in runtime NodeSet manager implements `INodeManagerReloadParticipant`, which transfers inbound cross-manager references to retained NodeIds and removes counterparts for dropped nodes. A custom NodeManager can be added and removed through the lifecycle provider, but must implement this participant contract before it can be reloaded safely.

Reload and removal invalidate saved Browse continuation points owned by the retired manager. A later `BrowseNext` with one of those tokens returns `BadContinuationPointInvalid` instead of invoking a disposed generation.
Reload and removal invalidate saved Browse continuation points owned by the retired manager. A later `BrowseNext` with one of those tokens returns `BadContinuationPointInvalid` instead of invoking a disposed generation. A shadow reload defers this invalidation until the retired generation's monitored items have drained, so continuation points that already captured it keep working until then. Immediate reload invalidates them as soon as in-flight requests complete.

Namespace indexes are append-only for the lifetime of a running server. Removing a model removes its nodes and routing but leaves its namespace URI in `NamespaceArray`; a later reload or add reuses the same index. When a live add appends a URI, the server updates `NamespaceArray` and `UrisVersion`.

Runtime DataType registrations are also additive. Reload accepts an existing DataType only when its definition is structurally compatible, rejects incompatible changes, and retains removed stand-in encodeables so existing sessions and in-flight values remain decodable.

Every committed lifecycle transaction emits one compressed model-change notification. Reload also emits a semantic-change notification when values of properties marked with the `SemanticChange` access-level bit changed.
Every committed lifecycle transaction, including shadow and immediate reload, emits one compressed model-change notification. Reload also emits a semantic-change notification when values of properties marked with the `SemanticChange` access-level bit changed.

## Quick-start examples

Expand Down
Loading