fix(host): fatal-log, flush and exit non-zero when the host fails to start (#618)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:52:54 -04:00
parent 3baa332d6b
commit 413bc5c04f
5 changed files with 94 additions and 1 deletions
@@ -262,7 +262,10 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl
_logger.LogWarning(
"OtOpcUaServerHostedService: node ACL enforcement is ENABLED — EVERY data-plane operation "
+ "(read, browse, subscribe, write, alarm commands, HistoryRead) now requires a matching "
+ "NodeAcl grant. A principal with no grant can connect and see nothing.");
+ "NodeAcl grant. A principal with no grant can still connect and read the standard OPC UA "
+ "server nodes (ns=0: ServerStatus, NamespaceArray, ServiceLevel, ...) — those are served by "
+ "the SDK core node manager and are deliberately ungated because clients need them for "
+ "session setup — but it sees no plant data (raw/UNS namespaces).");
}
// Propagate the configured tie-cluster over-fetch bound onto the node manager so HistoryRead-Raw
@@ -57,6 +57,14 @@ using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet;
using ZB.MOM.WW.Secrets.Sqlite;
// #618 — the composition root runs inside one try/catch (not re-indented, to keep the diff reviewable)
// so ANY failure to start — an unknown role, a ValidateOnStart rejection, a hosted service that
// throws from StartAsync — is logged Fatal, flushed, and ends the process with a NON-ZERO exit code.
// Left as an unhandled exception the host could stay up as a zombie under a supervisor/container
// (partially-started Akka threads), reporting "running" while serving nothing. See the catch at the
// bottom of this file.
try
{
var builder = WebApplication.CreateBuilder(args);
// The SINGLE role authority: Cluster:Roles first, OTOPCUA_ROLES as the fallback (archreview
@@ -740,6 +748,23 @@ if (LogPipelineDiagnostics.CheckFileSinkGrowth(app.Configuration,
}
await app.RunAsync();
}
catch (Exception ex)
{
// Log.Logger is still the silent bootstrap logger when the failure happened before AddZbSerilog
// ran (e.g. EffectiveRoles.Resolve), so ALSO write to stderr — the one channel that always exists.
Log.Fatal(ex, "OtOpcUa.Host terminated during startup/run");
Console.Error.WriteLine($"FATAL: OtOpcUa.Host terminated: {ex}");
Log.CloseAndFlush();
// Exit explicitly: non-background threads left by a partially-started host (Akka, Kestrel) would
// otherwise keep the process alive after the failure. Failure path only — a clean shutdown returns
// from RunAsync normally and never reaches here, so CoordinatedShutdown is not skipped.
Environment.Exit(1);
}
finally
{
Log.CloseAndFlush();
}
namespace ZB.MOM.WW.OtOpcUa.Host
{