@@ -27,6 +27,10 @@ internal sealed class LifetimeManager
2727 private static LifetimeManager ? _instance ;
2828 private readonly ConcurrentQueue < object > _shutdownHooks = new ( ) ;
2929
30+ // Signaled when RunShutdownTasks finishes. Subsequent callers wait on this
31+ // instead of returning early, preventing the runtime from tearing down the process prematurely.
32+ private readonly ManualResetEventSlim _shutdownComplete = new ( false ) ;
33+
3034 // We can be triggered by multiple shutdown paths (ProcessExit, CancelKeyPress, signal handlers, etc).
3135 // This flag ensures shutdown hooks run at most once.
3236 private int _shutdownStarted ;
@@ -136,35 +140,18 @@ public void RunShutdownTasks(Exception? exception = null)
136140 // Ensure shutdown runs once even if multiple events fire.
137141 if ( Interlocked . Exchange ( ref _shutdownStarted , 1 ) != 0 )
138142 {
143+ // Shutdown already started — wait for it to finish instead of returning immediately.
144+ // This prevents the runtime from tearing down the process (e.g. after ProcessExit returns)
145+ // before hooks have completed.
146+ _shutdownComplete . Wait ( ) ;
139147 return ;
140148 }
141149
142- #if NET6_0_OR_GREATER
143- // Unregister our termination handlers once shutdown begins (best-effort).
144- // This avoids re-entrancy and keeps the intent clear: after shutdown starts, we don't want to
145- // initiate additional termination paths.
146- try
147- {
148- _sigtermRegistration ? . Dispose ( ) ;
149- _sigtermRegistration = null ;
150- }
151- catch ( Exception ex )
152- {
153- // Best-effort: logging during shutdown should never prevent shutdown from continuing.
154- Log . Warning ( ex , "Failed to dispose SIGTERM termination signal handler registration." ) ;
155- }
156-
157- try
158- {
159- _sighupRegistration ? . Dispose ( ) ;
160- _sighupRegistration = null ;
161- }
162- catch ( Exception ex )
163- {
164- // Best-effort: logging during shutdown should never prevent shutdown from continuing.
165- Log . Warning ( ex , "Failed to dispose SIGHUP termination signal handler registration." ) ;
166- }
167- #endif
150+ // Note: we intentionally do NOT dispose signal registrations here.
151+ // They must stay alive so that duplicate signals arriving during shutdown
152+ // are still handled (and canceled) by our handler, preventing the OS from
153+ // killing the process before hooks finish. They'll be cleaned up by the
154+ // GC/finalizer when the process exits.
168155
169156 try
170157 {
@@ -210,6 +197,10 @@ public void RunShutdownTasks(Exception? exception = null)
210197 {
211198 // Swallow as there's nothing we can with it anyway
212199 }
200+ finally
201+ {
202+ _shutdownComplete . Set ( ) ;
203+ }
213204
214205 static void SetSynchronizationContext ( SynchronizationContext ? context )
215206 {
@@ -263,49 +254,27 @@ private void TryRegisterTerminationSignalHandlers()
263254
264255 private void TerminationSignalHandler ( PosixSignalContext context )
265256 {
266- // Ensure this handler initiates termination at most once.
267257 if ( Interlocked . Exchange ( ref _terminationExitInitiated , 1 ) != 0 )
268258 {
269- // Another signal already initiated termination; do nothing.
259+ // Duplicate signal while shutdown is in progress.
260+ // Wait for the first handler to finish running shutdown tasks.
261+ _shutdownComplete . Wait ( ) ;
270262 return ;
271263 }
272264
265+ // Calling Environment.Exit(0); caused an issue in Microsoft Orleans (look https://github.com/DataDog/dd-trace-dotnet/issues/8165)
266+ // The Posix signals registration mechanism doesn't use a normal MulticastDelegate kind of list; it's using a HashSet<Token> internally.
267+ // meaning that the call order is not deterministic, creating a flaky behavior between all the handlers.
268+ // The fact that there's no way to guarantee that we are the last handler means that we cannot force the exit of the process to raise
269+ // the finalization events calls because that means other handlers will not be called, for that reason we will just proceed with a manual
270+ // cleanup of our tasks without forcing the exit so other handlers can be executed as well.
271+
272+ // First signal: run shutdown tasks synchronously before the handler returns.
273+ // We intentionally do NOT set context.Cancel here — after the handler returns,
274+ // the runtime/OS will perform the default action (terminate the process with
275+ // exit code 143), which is the desired behavior once hooks have completed.
273276 try
274277 {
275- // On Unix, Cancel prevents the OS default handler from immediately terminating the process.
276- // (On Windows, SIGTERM/SIGHUP can't be canceled.)
277- if ( ! OperatingSystem . IsWindows ( ) )
278- {
279- // See PosixSignalRegistration.Create remarks:
280- // https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration.create
281- context . Cancel = true ; // Keep the process alive long enough to take the managed shutdown path.
282- }
283- }
284- catch ( Exception ex )
285- {
286- // Best-effort. If we can't cancel default handling, still attempt a managed exit.
287- Log . Warning ( ex , "Failed to cancel default termination signal handling. Graceful shutdown may not run." ) ;
288- }
289-
290- // Intentionally do NOT call RunShutdownTasks() directly here.
291- //
292- // Reason: pre-.NET 10 behavior was "termination signal => graceful managed exit => ProcessExit event".
293- // Our existing shutdown flow is attached to AppDomain.CurrentDomain.ProcessExit (CurrentDomain_ProcessExit),
294- // so we initiate a managed shutdown and let ProcessExit invoke RunShutdownTasks just like before.
295- //
296- // Supporting runtime source references:
297- // - Environment.Exit is an internal runtime call:
298- // https://github.com/dotnet/runtime/blob/main/src/coreclr/System.Private.CoreLib/src/System/Environment.CoreCLR.cs
299- // - ProcessExit is raised by the runtime via AppDomain.OnProcessExit():
300- // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/AppDomain.cs
301- try
302- {
303- // Calling Environment.Exit(0); caused an issue in Microsoft Orleans (look https://github.com/DataDog/dd-trace-dotnet/issues/8165)
304- // The Posix signals registration mechanism doesn't use a normal MulticastDelegate kind of list; it's using a HashSet<Token> internally.
305- // meaning that the call order is not deterministic, creating a flaky behavior between all the handlers.
306- // The fact that there's no way to guarantee that we are the last handler means that we cannot force the exit of the process to raise
307- // the finialization events calls because that means other handlers will not be called, for that reason we will just proceed with a manual
308- // cleanup of our tasks without forcing the exit so other handlers can be executed as well.
309278 RunShutdownTasks ( ) ;
310279 }
311280 catch ( Exception ex )
0 commit comments