Skip to content

Commit ebe4739

Browse files
committed
handling RestartAsync() being called before StartAsync() in WebJobsScriptHostService
1 parent 395353d commit ebe4739

File tree

3 files changed

+66
-1
lines changed

3 files changed

+66
-1
lines changed

src/WebJobs.Script.WebHost/Diagnostics/Extensions/ScriptHostServiceLoggerExtension.cs

+11
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ public static class ScriptHostServiceLoggerExtension
130130
new EventId(519, nameof(EnteringRestart)),
131131
"Restart requested. Cancelling any active host startup.");
132132

133+
private static readonly Action<ILogger, Exception> _restartBeforeStart =
134+
LoggerMessage.Define(
135+
LogLevel.Debug,
136+
new EventId(520, nameof(RestartBeforeStart)),
137+
"RestartAsync was called before StartAsync. Delaying restart until StartAsync has been called.");
138+
133139
public static void ScriptHostServiceInitCanceledByRuntime(this ILogger logger)
134140
{
135141
_scriptHostServiceInitCanceledByRuntime(logger, null);
@@ -229,5 +235,10 @@ public static void EnteringRestart(this ILogger logger)
229235
{
230236
_enteringRestart(logger, null);
231237
}
238+
239+
public static void RestartBeforeStart(this ILogger logger)
240+
{
241+
_restartBeforeStart(logger, null);
242+
}
232243
}
233244
}

src/WebJobs.Script.WebHost/WebJobsScriptHostService.cs

+20-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class WebJobsScriptHostService : IHostedService, IScriptHostManager, IDis
3232
private readonly SlidingWindow<bool> _healthCheckWindow;
3333
private readonly Timer _hostHealthCheckTimer;
3434
private readonly SemaphoreSlim _hostStartSemaphore = new SemaphoreSlim(1, 1);
35+
private readonly TaskCompletionSource<bool> _hostStartedSource = new TaskCompletionSource<bool>();
36+
private readonly Task _hostStarted;
3537

3638
private IHost _host;
3739
private CancellationTokenSource _startupLoopTokenSource;
@@ -61,6 +63,8 @@ public WebJobsScriptHostService(IOptionsMonitor<ScriptApplicationHostOptions> ap
6163
_healthMonitorOptions = healthMonitorOptions ?? throw new ArgumentNullException(nameof(healthMonitorOptions));
6264
_logger = loggerFactory.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
6365

66+
_hostStarted = _hostStartedSource.Task;
67+
6468
State = ScriptHostState.Default;
6569

6670
if (ShouldMonitorHostHealth)
@@ -140,6 +144,12 @@ private async Task StartHostAsync(CancellationToken cancellationToken, int attem
140144
try
141145
{
142146
await _hostStartSemaphore.WaitAsync();
147+
148+
// Now that we're inside the semaphore, set this task as completed. This prevents
149+
// restarts from being invoked (via the PlaceholderSpecializationMiddleware) before
150+
// the IHostedService has ever started.
151+
_hostStartedSource.TrySetResult(true);
152+
143153
await UnsynchronizedStartHostAsync(cancellationToken, attemptCount, startupMode);
144154
}
145155
finally
@@ -155,11 +165,12 @@ private async Task StartHostAsync(CancellationToken cancellationToken, int attem
155165
/// </summary>
156166
private async Task UnsynchronizedStartHostAsync(CancellationToken cancellationToken, int attemptCount = 0, JobHostStartupMode startupMode = JobHostStartupMode.Normal)
157167
{
158-
cancellationToken.ThrowIfCancellationRequested();
159168
IHost localHost = null;
160169

161170
try
162171
{
172+
cancellationToken.ThrowIfCancellationRequested();
173+
163174
// if we were in an error state retain that,
164175
// otherwise move to default
165176
if (State != ScriptHostState.Error)
@@ -332,6 +343,14 @@ public async Task StopAsync(CancellationToken cancellationToken)
332343

333344
public async Task RestartHostAsync(CancellationToken cancellationToken)
334345
{
346+
// Do not invoke a restart if the host has not yet been started. This can lead
347+
// to invalid state.
348+
if (!_hostStarted.IsCompleted)
349+
{
350+
_logger.RestartBeforeStart();
351+
await _hostStarted;
352+
}
353+
335354
_logger.EnteringRestart();
336355

337356
// If anything is mid-startup, cancel it.

test/WebJobs.Script.Tests/WebJobsScriptHostServiceTests.cs

+35
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,41 @@ public async Task DisposedHost_ServicesNotExposed()
327327
await startTask;
328328
}
329329

330+
[Fact]
331+
public async Task HostRestart_BeforeStart_WaitsForStartToContinue()
332+
{
333+
_host = CreateMockHost();
334+
335+
var hostBuilder = new Mock<IScriptHostBuilder>();
336+
hostBuilder.SetupSequence(b => b.BuildHost(It.IsAny<bool>(), It.IsAny<bool>()))
337+
.Returns(_host.Object)
338+
.Returns(_host.Object);
339+
340+
_webHostLoggerProvider = new TestLoggerProvider();
341+
_loggerFactory = new LoggerFactory();
342+
_loggerFactory.AddProvider(_webHostLoggerProvider);
343+
344+
_hostService = new WebJobsScriptHostService(
345+
_monitor, hostBuilder.Object, _loggerFactory, _mockRootServiceProvider.Object, _mockRootScopeFactory.Object,
346+
_mockScriptWebHostEnvironment.Object, _mockEnvironment.Object, _hostPerformanceManager, _healthMonitorOptions);
347+
348+
// Simulate a call to specialize coming from the PlaceholderSpecializationMiddleware. This
349+
// can happen before we ever start the service, which could create invalid state.
350+
Task restartTask = _hostService.RestartHostAsync(CancellationToken.None);
351+
352+
await _hostService.StartAsync(CancellationToken.None);
353+
await restartTask;
354+
355+
var messages = _webHostLoggerProvider.GetAllLogMessages();
356+
357+
// The CancellationToken is canceled quick enough that we never build the initial host, so the
358+
// only one we start is the restarted/specialized one.
359+
Assert.NotNull(messages.Single(p => p.EventId.Id == 513)); // "Building" message
360+
Assert.NotNull(messages.Single(p => p.EventId.Id == 514)); // "StartupWasCanceled" message
361+
Assert.NotNull(messages.Single(p => p.EventId.Id == 520)); // "RestartBeforeStart" message
362+
_host.Verify(p => p.StartAsync(It.IsAny<CancellationToken>()), Times.Once);
363+
}
364+
330365
public void RestartHost()
331366
{
332367
_hostService.RestartHostAsync(CancellationToken.None).Wait();

0 commit comments

Comments
 (0)