# Component: Host ## Purpose The Host component is the single deployable executable for the entire ScadaBridge system. The same binary runs on every node — central and site alike. The node's role is determined entirely by configuration (`appsettings.json`), not by which binary is deployed. On central nodes the Host additionally bootstraps ASP.NET Core to serve the Central UI and Inbound API web endpoints. ## Location All nodes (central and site). ## Responsibilities - Serve as the single entry point (`Program.cs`) for the ScadaBridge process. - Read and validate node configuration at startup before any actor system is created. - Register the correct set of component services and actors based on the configured node role. - Bootstrap the Akka.NET actor system with Remoting, Clustering, and split-brain resolution from a hand-assembled, injection-safe HOCON document (`AkkaHostedService.BuildHocon`; see REQ-HOST-6). - Host ASP.NET Core web endpoints on central nodes only. - Configure structured logging (Serilog) with environment-specific enrichment. - Support running as a Windows Service in production and as a console application during development. - Perform graceful shutdown via Akka.NET CoordinatedShutdown when the service is stopped. --- ## Requirements ### REQ-HOST-1: Single Binary Deployment The same compiled binary must be deployable to both central and site nodes. The node's role (Central or Site) is determined solely by configuration values in `appsettings.json` (or environment-specific overrides). There must be no separate build targets, projects, or conditional compilation symbols for central vs. site. ### REQ-HOST-2: Role-Based Service Registration At startup the Host must inspect the configured node role and register only the component services appropriate for that role: - **Shared** (both Central and Site): ClusterInfrastructure, Communication, HealthMonitoring, ExternalSystemGateway. - **Central only**: TemplateEngine, DeploymentManager, Security, AuditLogging, CentralUI, InboundAPI, ManagementService, NotificationService, NotificationOutbox, SiteCallAudit. - **Site only**: SiteRuntime, DataConnectionLayer, StoreAndForward, SiteEventLogging. Components not applicable to the current role must not be registered in the DI container or the Akka.NET actor system. ### REQ-HOST-3: Configuration Binding The Host must bind configuration sections from `appsettings.json` to strongly-typed options classes using the .NET **Options pattern** (`IOptions` / `IOptionsSnapshot`). Each component has its own configuration section under `ScadaBridge`, mapped to a dedicated configuration class owned by that component. #### Infrastructure Sections | Section | Options Class | Owner | Contents | |---------|--------------|-------|----------| | `ScadaBridge:Node` | `NodeOptions` | Host | Role, NodeHostname, SiteId, RemotingPort, GrpcPort (site: hosts `SiteCommandService` + `SiteStreamGrpcServer`, default 8083), CentralGrpcPort (central: hosts `CentralControlService`, default 8083) | | `ScadaBridge:Cluster` | `ClusterOptions` | ClusterInfrastructure | SeedNodes, SplitBrainResolverStrategy, StableAfter, HeartbeatInterval, FailureDetectionThreshold, MinNrOfMembers | | `ScadaBridge:Database` | `DatabaseOptions` | Host | Central: ConfigurationDb, MachineDataDb connection strings; Site: SQLite paths | #### Per-Component Sections | Section | Options Class | Owner | Contents | |---------|--------------|-------|----------| | `ScadaBridge:DataConnection` | `DataConnectionOptions` | Data Connection Layer | ReconnectInterval, TagResolutionRetryInterval, WriteTimeout | | `ScadaBridge:StoreAndForward` | `StoreAndForwardOptions` | Store-and-Forward | SqliteDbPath, ReplicationEnabled | | `ScadaBridge:HealthMonitoring` | `HealthMonitoringOptions` | Health Monitoring | ReportInterval, OfflineTimeout | | `ScadaBridge:SiteEventLog` | `SiteEventLogOptions` | Site Event Logging | RetentionDays, MaxStorageMb, PurgeScheduleCron | | `ScadaBridge:Communication` | `CommunicationOptions` | Communication | DeploymentTimeout, LifecycleTimeout, QueryTimeout, TransportHeartbeatInterval, TransportFailureThreshold | | `ScadaBridge:Security` | `SecurityOptions` | Security & Auth | LdapServer, LdapPort, LdapUseTls, JwtSigningKey, JwtExpiryMinutes, IdleTimeoutMinutes | | `ScadaBridge:InboundApi` | `InboundApiOptions` | Inbound API | DefaultMethodTimeout | | `ScadaBridge:Notification` | `NotificationOptions` | Notification Service | (SMTP config is stored in the central config DB, not in appsettings) | | `ScadaBridge:NotificationOutbox` | `NotificationOutboxOptions` | Notification Outbox | Dispatcher poll interval, stuck-age threshold, retention window (delivery retry settings reuse the central SMTP configuration) | | `ScadaBridge:SiteCallAudit` | `SiteCallAuditOptions` | Site Call Audit | Reconciliation pull interval, stuck-age threshold, retention window | | `ScadaBridge:ManagementService` | `ManagementServiceOptions` | Management Service | (Reserved for future configuration) | | `ScadaBridge:Logging` | `LoggingOptions` | Host | Serilog sink configuration, log level overrides | #### Convention - Each component defines its own options class (e.g., `DataConnectionOptions`) in its own project. The class is a plain POCO with properties matching the JSON section keys. - The Host binds each section during startup via `services.Configure(configuration.GetSection("ScadaBridge:"))`. - Each component's `AddXxx()` extension method accepts `IServiceCollection` and reads its options via `IOptions` — the component never reads `IConfiguration` directly. - Options classes live in the component project, not in Commons, because they are component-specific configuration — not shared contracts. - Startup validation (REQ-HOST-4) validates all required options before the actor system starts. ### REQ-HOST-4: Startup Validation Before the Akka.NET actor system is created, the Host must validate all required configuration values and fail fast with a clear error message if any are missing or invalid. Validation rules include: - `NodeConfiguration.Role` must be a valid `NodeRole` value. - `NodeConfiguration.NodeHostname` must not be null or empty. - `NodeConfiguration.RemotingPort` must be in valid port range (1–65535). - Site nodes must have `GrpcPort` in valid port range (1–65535) and different from `RemotingPort`. - Site nodes must have a non-empty `SiteId`. - Central nodes must have non-empty `ConfigurationDb` and `MachineDataDb` connection strings. - Site nodes must have non-empty SQLite path values. Site nodes do **not** require a `ConfigurationDb` connection string — all configuration is received via artifact deployment and read from local SQLite. - At least two seed nodes must be configured. ### REQ-HOST-4a: Readiness Gating On central nodes, the ASP.NET Core web endpoints (Central UI, Inbound API) must **not accept traffic** until the node is fully operational: - Akka.NET cluster membership is established. - Database connectivity (MS SQL) is verified. - Required cluster singletons are running (if applicable). These are implemented as three `Ready`-tagged health checks registered in the Central-role branch of `Program.cs` (so they are naturally role-scoped — site nodes do not run them): `database` (`DatabaseHealthCheck`), `akka-cluster` (`AkkaClusterHealthCheck`), and `required-singletons` (`RequiredSingletonsHealthCheck`). The last verifies each *required-always* central singleton is reachable by Asking its local `ClusterSingletonProxy` an `Identify` with a short bounded timeout (~2s, probes run concurrently) and treating a non-null `ActorIdentity.Subject` as reachable; any unreachable required singleton degrades the check to **Unhealthy**, naming it. The required-always set is the five unconditional central singletons: notification-outbox, audit-log-ingest, site-call-audit, audit-log-purge, and site-audit-reconciliation. Feature-gated singletons are the "if applicable" case and are not probed when their feature is off. The check is leadership-agnostic — the proxy reaches the singleton from either central node, so a ready standby still reports ready (readiness must NOT require cluster leadership; that is the `Active` tier's job). During a brief singleton handover the probe may momentarily time out and the node may flap to not-ready, which is correct: a node mid-handover is legitimately not fully ready (no retries are used, to keep readiness polling fast). A standard ASP.NET Core health check endpoint (`/health/ready`) reports readiness status. The load balancer uses this endpoint to determine when to route traffic to the node. During startup or failover, the node returns `503 Service Unavailable` until ready. ### REQ-HOST-5: Windows Service Hosting The Host must support running as a Windows Service via `UseWindowsService()`. When launched outside of a Windows Service context (e.g., during development), it must run as a standard console application. No code changes or conditional compilation are required to switch between the two modes. ### REQ-HOST-6: Akka.NET Bootstrap The Host bootstraps the Akka.NET actor system from a **hand-assembled, injection-safe HOCON document** (`AkkaHostedService.BuildHocon`) passed to `ActorSystem.Create` — **not** Akka.Hosting's `AkkaConfigurationBuilder`/`AddAkka()`. Every interpolated value is routed through `QuoteHocon`/`DurationHocon` so a hostname, seed URI or strategy containing a quote, backslash or whitespace cannot corrupt the document, and each timing is rendered in milliseconds so sub-second values survive. The bound `ClusterOptions`/`NodeOptions`/`CommunicationOptions` are startup-validated (`ClusterOptionsValidator`, `StartupValidator`) so a misconfiguration fails fast with a key-naming message. The document configures: - **Remoting**: the node's hostname and port from `NodeOptions`. - **Clustering**: seed nodes and the node's cluster role(s) from configuration. - **Split-Brain Resolver**: the strategy, stable-after and `down-if-alone` from `ClusterOptions`, **with the downing provider named explicitly** (`akka.cluster.downing-provider-class = "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster"`). Akka's default is `NoDowning`, under which the entire SBR section is inert and singletons never migrate on a crash or partition — omitting this one line silently disables failover. This exact omission was arch-review 01's Critical finding; it is now guarded by tests asserting the parsed HOCON key **and** a two-node kill test asserting the behavior (downing + singleton migration). - **Coordinated shutdown**: `run-coordinated-shutdown-when-down = on` plus a cluster-leave phase timeout above the singleton drain budget (see REQ-HOST-4a / the down-if-alone recovery contract in Component-ClusterInfrastructure.md). - **Actor registration**: each component's actors registered conditional on the node's role. > **Bootstrap-mechanism note.** The `Akka.Hosting` / `Akka.Cluster.Hosting` / `Akka.Remote.Hosting` typed-builder packages were referenced but unused and were dropped (arch-review 01 Task 22). The hand-rolled HOCON path is a single hardened, production-shape code path with full test coverage; migrating to Akka.Hosting's typed options is deliberately **not** done here and is tracked as possible future work rather than carried as an unused dependency. Only `Akka.Cluster.Tools` (ClusterSingleton, transitively Akka.Cluster) remains — its ClusterClient/ClusterClientReceptionist surface is no longer used since Phase 4 moved cross-cluster messaging to gRPC. > **Persistence note.** ScadaBridge does not use Akka.Persistence. Durable state > (store-and-forward buffers, site event logs, static attribute writes, > deployment records, configuration) is owned by the individual components and > persisted through component-owned stores — SQLite at sites, MS SQL centrally — > not through an Akka journal/snapshot store. The Host therefore configures no > `akka.persistence` section and references no persistence plugin. There are no > `PersistentActor` subclasses in the system by design. ### REQ-HOST-6a: gRPC command/control services (no ClusterClientReceptionist) As of Phase 4 of the ClusterClient→gRPC migration (2026-07-23), the Host configures **no** `ClusterClientReceptionist` and registers **no** actor with one. Both remaining receptionist registrations were removed: - The central **CentralCommunicationActor** registration is gone. Sites now reach central command/control over gRPC by dialling the central-hosted **`CentralControlService`** (`GrpcCentralTransport`, sticky central-a→central-b failover), not via a ClusterClient locating a receptionist. - The site **SiteCommunicationActor** registration is gone. Central now reaches site command/control over gRPC by dialling the site-hosted **`SiteCommandService`** (`GrpcSiteTransport`, per-site NodeA→NodeB failover). Central hosts `CentralControlService` (site dials in); each site hosts `SiteCommandService` and `SiteStreamGrpcServer` (central dials in). Endpoints are dialled directly from configuration/DB, so there is no cross-cluster actor discovery. **The ManagementActor was never reachable over the receptionist either** (its registration was removed 2026-07-22, ahead of Phase 4). That registration was written for an out-of-cluster CLI that was never built: the shipped CLI speaks HTTP Basic to the central `/management` endpoints, which ask the ManagementActor **in-process** via `ManagementActorHolder` (`ManagementEndpoints.cs`). The actor itself still runs at `/user/management`. > **Migration note.** `Akka.Cluster.Tools` stays a Host dependency for ClusterSingleton; only its ClusterClient/ClusterClientReceptionist surface is now unused. See `docs/plans/2026-07-22-clusterclient-to-grpc-plan.md`. ### REQ-HOST-7: ASP.NET Web Endpoints On central nodes, the Host must use `WebApplication.CreateBuilder` to produce a full ASP.NET Core host with Kestrel, and must map web endpoints for: - Central UI (via `MapCentralUI()` extension method). - Inbound API (via `MapInboundAPI()` extension method). On site nodes, the Host must also use `WebApplication.CreateBuilder` (not `Host.CreateDefaultBuilder`) to host the **SiteStreamGrpcServer** and the **SiteCommandService** (central→site command/control, added in Phase 4) via Kestrel HTTP/2 on the configured `GrpcPort` (default 8083). Kestrel is configured with `HttpProtocols.Http2` on the gRPC port only — no HTTP/1.1 web endpoints are exposed. On central nodes, the Host additionally hosts the **CentralControlService** (site→central command/control) via Kestrel HTTP/2 on the configured `CentralGrpcPort` (default 8083), alongside the HTTP/1.1 Central UI / Inbound API endpoints. The gRPC services are mapped via `MapGrpcService<>()`. **Startup ordering (site nodes)**: 1. Actor system and SiteStreamManager must be initialized before gRPC begins accepting connections. 2. The gRPC server rejects streams with `StatusCode.Unavailable` until the actor system is ready. **Shutdown ordering (site nodes)**: 1. On `CoordinatedShutdown`, stop accepting new gRPC streams first. 2. Cancel all active gRPC streams (triggering client-side reconnect). 3. Tear down actors. 4. Use `IHostApplicationLifetime.ApplicationStopping` to signal the gRPC server. ### REQ-HOST-8: Structured Logging The Host must configure Serilog as the logging provider with: - Configuration-driven sink setup (console and file sinks at minimum). - Automatic enrichment of every log entry with `SiteId`, `NodeHostname`, and `NodeRole` properties sourced from `NodeConfiguration`. - Structured (machine-parseable) output format. ### REQ-HOST-8a: Dead Letter Monitoring The Host must subscribe to the Akka.NET `DeadLetter` event stream and log dead letters at Warning level. Dead letters indicate messages sent to actors that no longer exist — a common symptom of failover timing issues, stale actor references, or race conditions during instance lifecycle transitions. The dead letter count is reported as a health metric (see Health Monitoring). ### REQ-HOST-9: Graceful Shutdown When the Host process receives a stop signal (Windows Service stop, `Ctrl+C`, or SIGTERM), it must trigger Akka.NET CoordinatedShutdown to allow actors to drain in-flight work before the process exits. The Host must not call `Environment.Exit()` or forcibly terminate the actor system without coordinated shutdown. ### REQ-HOST-10: Extension Method Convention Each component library must expose its services to the Host via a consistent set of extension methods: - `IServiceCollection.AddXxx()` — registers the component's DI services. - `AkkaConfigurationBuilder.AddXxxActors()` — registers the component's actors with the Akka.NET actor system (for components that have actors). - `WebApplication.MapXxx()` — maps the component's web endpoints (only for CentralUI and InboundAPI). The Host's `Program.cs` calls these extension methods; the component libraries own the registration logic. This keeps the Host thin and each component self-contained. (The ManagementService component no longer registers the ManagementActor with ClusterClientReceptionist — that registration was removed 2026-07-22, and the whole receptionist surface went in Phase 4.) --- ## Component Registration Matrix | Component | Central | Site | DI (`AddXxx`) | Actors (`AddXxxActors`) | Endpoints (`MapXxx`) | |---|---|---|---|---|---| | ClusterInfrastructure | Yes | Yes | Yes | Yes | No | | Communication | Yes | Yes | Yes | Yes | No | | HealthMonitoring | Yes | Yes | Yes | Yes | No | | ExternalSystemGateway | Yes | Yes | Yes | Yes | No | | AuditLog | Yes | Yes | Yes | Yes | No | | NotificationService | Yes | No | Yes | Yes | No | | NotificationOutbox | Yes | No | Yes | Yes | No | | SiteCallAudit | Yes | No | Yes | Yes | No | | KpiHistory | Yes | No | Yes | Yes | No | | Transport | Yes | No | Yes | No | No | | TemplateEngine | Yes | No | Yes | Yes | No | | DeploymentManager | Yes | No | Yes | Yes | No | | Security | Yes | No | Yes | Yes | No | | CentralUI | Yes | No | Yes | No | Yes | | InboundAPI | Yes | No | Yes | No | Yes | | ManagementService | Yes | No | Yes | Yes | No | | SiteRuntime | No | Yes | Yes | Yes | No | | DataConnectionLayer | No | Yes | Yes | Yes | No | | StoreAndForward | No | Yes | Yes | Yes | No | | SiteEventLogging | No | Yes | Yes | Yes | No | | ConfigurationDatabase | Yes | No | Yes | No | No | --- ## Dependencies - **All 19 component libraries**: The Host references every component project to call their extension methods (excludes CLI, which is a separate executable). Audit Log (#23) ships its central+site code in `ZB.MOM.WW.ScadaBridge.AuditLog`; the Host calls `AddAuditLog()` on both roles, M2+ will add `AddAuditLogActors()`. - **Akka.Cluster.Tools**: ClusterSingleton (singleton manager/proxy); transitively pulls Akka.Cluster (SBR provider) and Akka.Remote. Its ClusterClient/ClusterClientReceptionist surface is no longer used — cross-cluster messaging is gRPC as of Phase 4. The actor system is built from hand-assembled HOCON (REQ-HOST-6), so the `Akka.Hosting`/`Akka.Remote.Hosting`/`Akka.Cluster.Hosting` typed-builder packages are **not** referenced (dropped in arch-review 01 Task 22). No Akka.Persistence plugin — see the Persistence note under REQ-HOST-6. - **Serilog.AspNetCore**: For structured logging integration. - **Microsoft.Extensions.Hosting.WindowsServices**: For Windows Service support. - **ASP.NET Core** (central only): For web endpoint hosting. ## Interactions - **All components**: The Host is the composition root — it wires every component into the DI container and actor system. - **Configuration Database**: The Host registers the DbContext and wires repository implementations to their interfaces. In development, triggers auto-migration; in production, validates schema version. - **ClusterInfrastructure**: The Host configures the underlying Akka.NET cluster that ClusterInfrastructure manages at runtime. - **CentralUI / InboundAPI**: The Host maps their web endpoints into the ASP.NET Core pipeline on central nodes. - **ManagementService**: The Host registers the ManagementActor on central nodes; the CLI reaches it over the HTTP `/management` endpoints (in-process via `ManagementActorHolder`), not via any cluster transport. No ClusterClientReceptionist is configured (removed in Phase 4). - **HealthMonitoring**: The Host's startup validation and logging configuration provide the foundation for health reporting.