Skip to content

Commit 93dd5eb

Browse files
authored
Cancel drain/resume calls on client disconnect (#10565)
* Cancel drain calls on client disconnect * Fix test assertion * Cancel on /admin/host/resume calls as well
1 parent 89dc438 commit 93dd5eb

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
@@ -188,7 +188,7 @@ public async Task<IActionResult> GetWorkerProcesses([FromServices] IScriptHostMa
188188
[HttpPost]
189189
[Route("admin/host/drain")]
190190
[Authorize(Policy = PolicyNames.AdminAuthLevelOrInternal)]
191-
public async Task<IActionResult> Drain([FromServices] IScriptHostManager scriptHostManager)
191+
public async Task<IActionResult> Drain([FromServices] IScriptHostManager scriptHostManager, CancellationToken cancellation)
192192
{
193193
_logger.LogDebug("Received request to drain the host");
194194

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

200-
await _drainSemaphore.WaitAsync();
200+
try
201+
{
202+
await _drainSemaphore.WaitAsync(cancellation);
201203

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

217226
[HttpGet]
@@ -251,12 +260,20 @@ public IActionResult DrainStatus([FromServices] IScriptHostManager scriptHostMan
251260
[HttpPost]
252261
[Route("admin/host/resume")]
253262
[Authorize(Policy = PolicyNames.AdminAuthLevelOrInternal)]
254-
public async Task<IActionResult> Resume([FromServices] IScriptHostManager scriptHostManager)
263+
public async Task<IActionResult> Resume([FromServices] IScriptHostManager scriptHostManager, CancellationToken cancellation)
255264
{
256265
try
257266
{
258-
await _resumeSemaphore.WaitAsync();
267+
await _resumeSemaphore.WaitAsync(cancellation);
268+
}
269+
catch (OperationCanceledException)
270+
{
271+
_logger.LogInformation("Resume request was cancelled");
272+
throw;
273+
}
259274

275+
try
276+
{
260277
ScriptHostState currentState = scriptHostManager.State;
261278

262279
_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)