Skip to content

Commit 4583c05

Browse files
authored
Cancel drain/resume calls on client disconnect (#10565) (#10591)
* Cancel drain calls on client disconnect * Fix test assertion * Cancel on /admin/host/resume calls as well
1 parent 0e87849 commit 4583c05

File tree

2 files changed

+76
-20
lines changed

2 files changed

+76
-20
lines changed

src/WebJobs.Script.WebHost/Controllers/HostController.cs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public async Task<IActionResult> GetWorkerProcesses([FromServices] IScriptHostMa
187187
[HttpPost]
188188
[Route("admin/host/drain")]
189189
[Authorize(Policy = PolicyNames.AdminAuthLevelOrInternal)]
190-
public async Task<IActionResult> Drain([FromServices] IScriptHostManager scriptHostManager)
190+
public async Task<IActionResult> Drain([FromServices] IScriptHostManager scriptHostManager, CancellationToken cancellation)
191191
{
192192
_logger.LogDebug("Received request to drain the host");
193193

@@ -196,21 +196,30 @@ public async Task<IActionResult> Drain([FromServices] IScriptHostManager scriptH
196196
return StatusCode(StatusCodes.Status503ServiceUnavailable);
197197
}
198198

199-
await _drainSemaphore.WaitAsync();
199+
try
200+
{
201+
await _drainSemaphore.WaitAsync(cancellation);
200202

201-
// Stop call to some listeners gets stuck, not waiting for the stop call to complete
202-
_ = drainModeManager.EnableDrainModeAsync(CancellationToken.None)
203-
.ContinueWith(
204-
antecedent =>
205-
{
206-
if (antecedent.Status == TaskStatus.Faulted)
203+
// Stop call to some listeners gets stuck, not waiting for the stop call to complete
204+
// We do not pass in this APIs cancellation token as we do not want drain to be aborted once we have the semaphore.
205+
_ = drainModeManager.EnableDrainModeAsync(CancellationToken.None)
206+
.ContinueWith(
207+
antecedent =>
207208
{
208-
_logger.LogError(antecedent.Exception, "Something went wrong invoking drain mode");
209-
}
210-
211-
_drainSemaphore.Release();
212-
});
213-
return Accepted();
209+
if (antecedent.Status == TaskStatus.Faulted)
210+
{
211+
_logger.LogError(antecedent.Exception, "Something went wrong invoking drain mode");
212+
}
213+
214+
_drainSemaphore.Release();
215+
});
216+
return Accepted();
217+
}
218+
catch (OperationCanceledException)
219+
{
220+
_logger.LogInformation("Drain request was cancelled");
221+
throw;
222+
}
214223
}
215224

216225
[HttpGet]
@@ -250,12 +259,20 @@ public IActionResult DrainStatus([FromServices] IScriptHostManager scriptHostMan
250259
[HttpPost]
251260
[Route("admin/host/resume")]
252261
[Authorize(Policy = PolicyNames.AdminAuthLevelOrInternal)]
253-
public async Task<IActionResult> Resume([FromServices] IScriptHostManager scriptHostManager)
262+
public async Task<IActionResult> Resume([FromServices] IScriptHostManager scriptHostManager, CancellationToken cancellation)
254263
{
255264
try
256265
{
257-
await _resumeSemaphore.WaitAsync();
266+
await _resumeSemaphore.WaitAsync(cancellation);
267+
}
268+
catch (OperationCanceledException)
269+
{
270+
_logger.LogInformation("Resume request was cancelled");
271+
throw;
272+
}
258273

274+
try
275+
{
259276
ScriptHostState currentState = scriptHostManager.State;
260277

261278
_logger.LogDebug($"Received request to resume a draining host - host status: {currentState.ToString()}");

test/WebJobs.Script.Tests/Controllers/Admin/HostControllerTests.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,41 @@ public void GetDrainStatus_HostRunning_ReturnsExpected(int outstandingRetries, i
275275
Assert.Equal(outstandingInvocations, resultStatus.OutstandingInvocations);
276276
}
277277

278+
[Fact]
279+
public async Task GetDrain_HostRunning_BeginsDrain()
280+
{
281+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
282+
var drainModeManager = new Mock<IDrainModeManager>(MockBehavior.Strict);
283+
drainModeManager.Setup(m => m.EnableDrainModeAsync(default)).Returns(Task.CompletedTask);
284+
var serviceProviderMock = scriptHostManagerMock.As<IServiceProvider>();
285+
serviceProviderMock.Setup(x => x.GetService(typeof(IDrainModeManager))).Returns(drainModeManager.Object);
286+
287+
var result = (AcceptedResult)await _hostController.Drain(scriptHostManagerMock.Object, default);
288+
Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode);
289+
drainModeManager.Verify(x => x.EnableDrainModeAsync(default), Times.Once());
290+
}
291+
292+
[Fact]
293+
public async Task GetDrain_HostRunning_ClientDisconnect_DoesNotDrain()
294+
{
295+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
296+
var drainModeManager = new Mock<IDrainModeManager>(MockBehavior.Strict);
297+
var serviceProviderMock = scriptHostManagerMock.As<IServiceProvider>();
298+
serviceProviderMock.Setup(x => x.GetService(typeof(IDrainModeManager))).Returns(drainModeManager.Object);
299+
300+
using var cts = new CancellationTokenSource();
301+
cts.Cancel();
302+
303+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => _hostController.Drain(scriptHostManagerMock.Object, cts.Token));
304+
drainModeManager.Verify(x => x.EnableDrainModeAsync(default), Times.Never);
305+
}
306+
278307
[Fact]
279308
public async Task GetDrain_HostNotRunning_ReturnsServiceUnavailable()
280309
{
281310
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
282311

283-
var result = (StatusCodeResult)await _hostController.Drain(scriptHostManagerMock.Object);
312+
var result = (StatusCodeResult)await _hostController.Drain(scriptHostManagerMock.Object, default);
284313
Assert.Equal(StatusCodes.Status503ServiceUnavailable, result.StatusCode);
285314
}
286315

@@ -318,7 +347,7 @@ public async Task ResumeHost_HostNotRunning_ReturnsExpected(ScriptHostState host
318347
scriptHostManagerMock.SetupGet(p => p.State).Returns(hostStatus);
319348
drainModeManager.Setup(x => x.IsDrainModeEnabled).Returns(drainModeEnabled);
320349

321-
var result = (StatusCodeResult)await _hostController.Resume(scriptHostManagerMock.Object);
350+
var result = (StatusCodeResult)await _hostController.Resume(scriptHostManagerMock.Object, default);
322351
Assert.Equal(expectedCode, result.StatusCode);
323352
scriptHostManagerMock.Verify(p => p.RestartHostAsync(It.IsAny<CancellationToken>()), Times.Never());
324353
}
@@ -336,7 +365,7 @@ public async Task ResumeHost_HostRunning_DrainModeEnabled_StartNewHostSuccessful
336365
drainModeManager.Setup(x => x.IsDrainModeEnabled).Returns(true);
337366

338367
var expectedBody = new ResumeStatus { State = ScriptHostState.Running };
339-
var result = (OkObjectResult)await _hostController.Resume(scriptHostManagerMock.Object);
368+
var result = (OkObjectResult)await _hostController.Resume(scriptHostManagerMock.Object, default);
340369

341370
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
342371
Assert.Equal(expectedBody.State, (result.Value as ResumeStatus).State);
@@ -355,11 +384,21 @@ public async Task ResumeHost_HostRunning_DrainModeNotEnabled_DoesNotStartNewHost
355384
drainModeManager.Setup(x => x.IsDrainModeEnabled).Returns(false);
356385

357386
var expectedBody = new ResumeStatus { State = ScriptHostState.Running };
358-
var result = (OkObjectResult)await _hostController.Resume(scriptHostManagerMock.Object);
387+
var result = (OkObjectResult)await _hostController.Resume(scriptHostManagerMock.Object, default);
359388

360389
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
361390
Assert.Equal(expectedBody.State, (result.Value as ResumeStatus).State);
362391
scriptHostManagerMock.Verify(p => p.RestartHostAsync(It.IsAny<CancellationToken>()), Times.Never());
363392
}
393+
394+
[Fact]
395+
public async Task ResumeHost_HostRunning_ClientDisconnect_DoesNotStartNewHost()
396+
{
397+
var scriptHostManagerMock = new Mock<IScriptHostManager>(MockBehavior.Strict);
398+
using var cts = new CancellationTokenSource();
399+
cts.Cancel();
400+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => _hostController.Resume(scriptHostManagerMock.Object, cts.Token));
401+
scriptHostManagerMock.Verify(p => p.RestartHostAsync(It.IsAny<CancellationToken>()), Times.Never());
402+
}
364403
}
365404
}

0 commit comments

Comments
 (0)