diff --git a/SW.Serverless.UnitTests/ResidentHostShutdownTests.cs b/SW.Serverless.UnitTests/ResidentHostShutdownTests.cs new file mode 100644 index 0000000..2e17c57 --- /dev/null +++ b/SW.Serverless.UnitTests/ResidentHostShutdownTests.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SW.Serverless.Resident; +using System; +using System.Threading.Tasks; + +namespace SW.Serverless.UnitTests +{ + /// + /// An adapter that exits while the host is stopping. The exit is handled on the process's own + /// callback, and the host can stop and dispose its token between that handler checking it is + /// still running and scheduling the restart. The restart then threw ObjectDisposedException on + /// a thread nothing catches, and that took the whole process down. + /// + [TestClass] + public class ResidentHostShutdownTests + { + [TestMethod] + public async Task A_restart_scheduled_after_the_host_stopped_is_dropped() + { + using var loggerFactory = LoggerFactory.Create(_ => { }); + var host = new ResidentAdapterHost(new ResidentOptions(), null, null, null, loggerFactory); + await host.DisposeAsync(); + + var supervised = ResidentAdapterHost.Supervised.For("late-exit", + new AdapterSpec { AdapterId = "late.exit", InstanceKey = "late-exit" }); + + host.ScheduleRestart(supervised, TimeSpan.Zero); + + Assert.IsNull(supervised.PendingRestart, "a stopped host must not schedule a restart"); + } + } +} diff --git a/SW.Serverless/Resident/ResidentAdapterHost.cs b/SW.Serverless/Resident/ResidentAdapterHost.cs index 2c06edd..3f8b3b7 100644 --- a/SW.Serverless/Resident/ResidentAdapterHost.cs +++ b/SW.Serverless/Resident/ResidentAdapterHost.cs @@ -311,9 +311,14 @@ static TimeSpan Backoff(Supervised supervised) => /// stopped or replaced by then. A failed relaunch is retried with backoff rather than logged /// once and abandoned, which left an integration silently dead with nothing to notice it. /// - void ScheduleRestart(Supervised supervised, TimeSpan delay) + internal void ScheduleRestart(Supervised supervised, TimeSpan delay) { - var restart = CancellationTokenSource.CreateLinkedTokenSource(stopping.Token); + // The host can stop, and dispose this token, after an exit was seen but before its + // restart is scheduled. Throwing here, on the process's Exited callback, crashed the app. + CancellationTokenSource restart; + try { restart = CancellationTokenSource.CreateLinkedTokenSource(stopping.Token); } + catch (ObjectDisposedException) { return; } + Interlocked.Exchange(ref supervised.PendingRestart, restart)?.Cancel(); _ = Task.Run(async () =>