Files
lmxopcua/docs/v2/Architecture-v2.md
Joseph Doherty 1689901c0e docs(v2): Architecture-v2 + Cluster + ControlPlane + Runtime overviews (Task 65)
Four new docs at docs/v2/ giving a single-page tour of each v2 piece:
- Architecture-v2.md: top-level mental model (fused Host + roles + cluster + live-edit)
- Cluster.md: AkkaClusterOptions + IClusterRoleInfo + WithOtOpcUaClusterBootstrap
- ControlPlane.md: 5 admin singletons + DPS topics + deploy flow + failover recovery
- Runtime.md: per-node actor tree + state machines + engine-wiring follow-up map

Each links back to the design doc for depth. Architecture-v2 cross-references
the other three + ServiceHosting + Redundancy + security.
2026-05-26 06:41:48 -04:00

7.0 KiB

OtOpcUa v2 Architecture

Single-page tour of the v2 layout. For decision history + tradeoffs, see 2026-05-26-akka-hosting-alignment-design.md.

Big picture

                    ┌─────────────────────────────────────────────┐
                    │                  OtOpcUa.Host               │  (fused binary)
                    │                                             │
                    │  reads OTOPCUA_ROLES env, mounts:           │
                    │  ┌─────────────────────────────────────┐    │
                    │  │  admin → Blazor + auth + control-  │    │
                    │  │          plane singletons          │    │
                    │  │  driver → OPC UA endpoint +        │    │
                    │  │           per-node actors          │    │
                    │  └─────────────────────────────────────┘    │
                    └─────────────────────────────────────────────┘
                                          │
                                          │  joins
                                          ▼
                    ┌─────────────────────────────────────────────┐
                    │              Akka.NET cluster               │
                    │  (split-brain resolver: keep-oldest, 15s)   │
                    └─────────────────────────────────────────────┘

shared by every node:  ┌─────────────────┐
                       │  ConfigDb (SQL) │   live-edit + Deployment artifacts + audit
                       └─────────────────┘

The v1 setup was two separate Windows services (OtOpcUa.Server + OtOpcUa.Admin) talking through the DB. v2 collapses them into one binary with role gating, and adds an Akka cluster so admin singletons can drive deploys and the redundancy story is automatic.

Project layout

src/Core/                       shared abstractions, no Server deps
  ZB.MOM.WW.OtOpcUa.Commons             types + Akka message contracts + interfaces
  ZB.MOM.WW.OtOpcUa.Cluster             HOCON, AkkaClusterOptions, IClusterRoleInfo
  ZB.MOM.WW.OtOpcUa.Configuration       EF Core DbContext + entities

src/Server/                     server-side projects
  ZB.MOM.WW.OtOpcUa.Security            cookie+JWT auth, LDAP, JwtTokenService
  ZB.MOM.WW.OtOpcUa.ControlPlane        admin-role cluster singletons
  ZB.MOM.WW.OtOpcUa.Runtime             driver-role per-node actors
  ZB.MOM.WW.OtOpcUa.OpcUaServer         OPC UA endpoint facade + Phase7Composer
  ZB.MOM.WW.OtOpcUa.AdminUI             Blazor Razor class library
  ZB.MOM.WW.OtOpcUa.Host                fused binary (Program.cs)
Project Role Doc
Cluster Bootstrap + cluster topology view Cluster.md
ControlPlane Admin singletons (deploy, audit, fleet, redundancy) ControlPlane.md
Runtime Driver-role actor tree Runtime.md
Security Cookie+JWT auth, LDAP, /auth/* endpoints ../security.md
OpcUaServer OPC UA endpoint host + composer ../OpcUaServer.md
Host Role-gated DI graph + Program.cs ../ServiceHosting.md

Role gating

Program.cs reads OTOPCUA_ROLES once (per process) and decides what to wire:

var roles = RoleParser.Parse(Environment.GetEnvironmentVariable("OTOPCUA_ROLES"));
var hasAdmin = roles.Contains("admin");
var hasDriver = roles.Contains("driver");

builder.Services.AddOtOpcUaConfigDb(builder.Configuration);
builder.Services.AddOtOpcUaCluster(builder.Configuration);

builder.Services.AddAkka("otopcua", (ab, sp) =>
{
    ab.WithOtOpcUaClusterBootstrap(sp);            // HOCON + remote + cluster options
    if (hasAdmin)  ab.WithOtOpcUaControlPlaneSingletons();
    if (hasDriver) ab.WithOtOpcUaRuntimeActors();
});

if (hasAdmin)
{
    builder.Services.AddOtOpcUaAuth(builder.Configuration);
    builder.Services.AddAdminUI();
    // SignalR, AdminOpsClient, etc.
}

builder.Services.AddOtOpcUaHealth();

There is a single ActorSystem. Cluster singletons + per-node actors share it via the Akka.Hosting registry. This was a v2 fix (the initial Phase 9 wiring ran two ActorSystems by mistake; see commit d6fac2d).

Live-edit vs draft/publish

v1 had ConfigGeneration(Draft|Published) with every live-edit entity FK'd to a generation. Edits accumulated in a Draft until Publish promoted them.

v2 removes that entirely:

  • No ConfigGeneration table, no GenerationId columns.
  • Every live-edit entity has a RowVersion (IsRowVersion()) for last-write-wins.
  • Audit goes to ConfigEdit (per-row delta) and ConfigAuditLog (event-level).
  • Deploys snapshot the current DB state into an immutable Deployment.ArtifactBlob + its RevisionHash. That artifact is what driver nodes apply.

See ControlPlane.md § Deploy flow for the end-to-end dispatch + ACK + seal sequence.

NodeId

Each cluster member has a NodeId derived as {PublicHostname}:{Port} of the Akka remote endpoint. ClusterRoleInfo.LocalNode + ConfigPublishCoordinator.DiscoverDriverNodes() use the same formula so they always agree. The port suffix makes loopback test deployments distinguishable (commit 5cfbe8b); in production the hostname alone is already unique.

Health endpoints

Path Returns 200 when…
/healthz Process is alive (no checks).
/health/ready DB reachable + this node is Up in the cluster.
/health/active This node is the admin role-leader (used by Traefik/HA-LB to pin traffic).

What lives where (quick map)

Concern Project Entry point
Read OTOPCUA_ROLES Cluster.RoleParser static Parse(string?)
Cluster lifecycle Cluster.WithOtOpcUaClusterBootstrap extension on AkkaConfigurationBuilder
Local node identity Cluster.IClusterRoleInfo.LocalNode DI singleton
Admin singletons ControlPlane.WithOtOpcUaControlPlaneSingletons extension on AkkaConfigurationBuilder
Driver actors Runtime.WithOtOpcUaRuntimeActors extension on AkkaConfigurationBuilder
Auth pipeline Security.AddOtOpcUaAuth + MapOtOpcUaAuth extensions on IServiceCollection / IEndpointRouteBuilder
OPC UA facade OpcUaServer.OpcUaApplicationHost runtime host, started by driver-role startup
Health endpoints Host.Health.AddOtOpcUaHealth + MapOtOpcUaHealth extensions on IServiceCollection / IEndpointRouteBuilder