fix(host): unhandled boot exception now kills the process instead of wedging the container (#34)
Root cause: dotnet runs as container PID 1 and Linux ignores default-action signals sent to PID 1, so the runtime's unhandled-exception path (banner, then abort() -> SIGABRT) could never terminate the process — it printed the trace and spun the main thread at 100% CPU with the container `running`, so `restart: unless-stopped` never fired. Reproduced deterministically: same StartupValidator throw exits 134 under an init process and wedges without one. Two layers, each covering the other's gap: - Program.cs registers an AppDomain.UnhandledException handler before the first statement that can throw: prints the trace, best-effort flushes Serilog, Environment.Exit(134) — exit() is a syscall PID 1 CAN perform, 134 preserves the 128+SIGABRT crash code, and it covers every thread, not just the boot window. It cannot fire under WebApplicationFactory (the test host catches entry-point exceptions), so the designed boot-refusal exceptions still propagate to tests unchanged. - docker-compose: init: true on all 8 nodes for the crash paths that bypass the managed event (Environment.FailFast, runtime-internal aborts). The CoordinatedShutdown no-Environment.Exit guard gains a precise carve-out (exactly one call, only inside the handler); Environment.Exit still fires the CLR shutdown hook Akka binds via run-by-clr-shutdown-hook = on, so the crash path skips nothing abort() kept. New pin test keeps the handler ahead of the configuration build. Live-verified on the rig image: crash now yields Exited (134) + RestartCount climbing under `unless-stopped`, trace intact, with and without init; full 8-node rig redeployed healthy with docker-init as PID 1. Closes #34. Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
@@ -20,6 +20,29 @@ public class CoordinatedShutdownTests
|
||||
foreach (var file in sourceFiles)
|
||||
{
|
||||
var content = File.ReadAllText(file);
|
||||
|
||||
// Sole permitted call site: Program.cs's AppDomain.UnhandledException handler
|
||||
// (ScadaBridge#34). That is the crash path — the alternative there was never a
|
||||
// CoordinatedShutdown but the runtime's abort(), which cannot terminate PID 1
|
||||
// and wedged the container; Environment.Exit still fires the CLR shutdown hook
|
||||
// Akka binds via run-by-clr-shutdown-hook = on, so it skips nothing abort kept.
|
||||
// Everywhere else the original rule stands: no code path may bypass
|
||||
// CoordinatedShutdown by exiting directly.
|
||||
if (Path.GetFileName(file) == "Program.cs")
|
||||
{
|
||||
var occurrences = CountOccurrences(content, "Environment.Exit(");
|
||||
Assert.Equal(1, occurrences);
|
||||
|
||||
var handlerAt = content.IndexOf(
|
||||
"AppDomain.CurrentDomain.UnhandledException +=", StringComparison.Ordinal);
|
||||
Assert.True(handlerAt >= 0, "Program.cs must register the UnhandledException handler");
|
||||
var handlerEnd = content.IndexOf("};", handlerAt, StringComparison.Ordinal);
|
||||
var exitAt = content.IndexOf("Environment.Exit(", StringComparison.Ordinal);
|
||||
Assert.True(exitAt > handlerAt && exitAt < handlerEnd,
|
||||
"Environment.Exit in Program.cs is only permitted inside the UnhandledException handler");
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.DoesNotContain("Environment.Exit", content,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user