Add BackgroundService, a base class for long running HostedServices (#1215)

* Add BackgroundService, a base class for long running HostedServices
- Today the IHostedService pattern is a StartAsync/StopAsync pattern. Neither of these
methods are supposed to return a long running task that represents an execution. If
you wanted to have some logic run on a timer every 5 minutes, it's unnatural to do so
with simple async idioms. This base class implements IHostedService and exposes
a pattern where a long running async Task can be returned.
- The token passed into ExecuteAsync represents the lifetime of the execution.
- StartAsync and StopAsync were made virtual to allow the derived type to
indicate Start failures.
- Added tests
This commit is contained in:
David Fowler 2017-09-18 12:55:54 -07:00 committed by GitHub
parent 500668619f
commit 712c992ca8
3 changed files with 253 additions and 23 deletions

View File

@ -5,38 +5,22 @@ using Microsoft.Extensions.Hosting;
namespace GenericHostSample
{
public class MyServiceA : IHostedService
public class MyServiceA : BackgroundService
{
private bool _stopping;
private Task _backgroundTask;
public Task StartAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("MyServiceA is starting.");
_backgroundTask = BackgroundTask();
return Task.CompletedTask;
}
private async Task BackgroundTask()
{
while (!_stopping)
stoppingToken.Register(() => Console.WriteLine("MyServiceA is stopping."));
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(5));
Console.WriteLine("MyServiceA is doing background work.");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
Console.WriteLine("MyServiceA background task is stopping.");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("MyServiceA is stopping.");
_stopping = true;
if (_backgroundTask != null)
{
// TODO: cancellation
await _backgroundTask;
}
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
/// <summary>
/// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task that represents
/// the lifetime of the long running operation(s) being performed.
/// </summary>
/// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
/// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
}

View File

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.Hosting.Tests
{
public class BackgroundHostedServiceTests
{
[Fact]
public void StartReturnsCompletedTaskIfLongRunningTaskIsIncomplete()
{
var tcs = new TaskCompletionSource<object>();
var service = new MyBackgroundService(tcs.Task);
var task = service.StartAsync(CancellationToken.None);
Assert.True(task.IsCompleted);
Assert.False(tcs.Task.IsCompleted);
// Complete the tsk
tcs.TrySetResult(null);
}
[Fact]
public void StartReturnsCompletedTaskIfCancelled()
{
var tcs = new TaskCompletionSource<object>();
tcs.TrySetCanceled();
var service = new MyBackgroundService(tcs.Task);
var task = service.StartAsync(CancellationToken.None);
Assert.True(task.IsCompleted);
}
[Fact]
public async Task StartReturnsLongRunningTaskIfFailed()
{
var tcs = new TaskCompletionSource<object>();
tcs.TrySetException(new Exception("fail!"));
var service = new MyBackgroundService(tcs.Task);
var exception = await Assert.ThrowsAsync<Exception>(() => service.StartAsync(CancellationToken.None));
Assert.Equal("fail!", exception.Message);
}
[Fact]
public async Task StopAsyncWithoutStartAsyncNoops()
{
var tcs = new TaskCompletionSource<object>();
var service = new MyBackgroundService(tcs.Task);
await service.StopAsync(CancellationToken.None);
}
[Fact]
public async Task StopAsyncStopsBackgroundService()
{
var tcs = new TaskCompletionSource<object>();
var service = new MyBackgroundService(tcs.Task);
await service.StartAsync(CancellationToken.None);
Assert.False(service.ExecuteTask.IsCompleted);
await service.StopAsync(CancellationToken.None);
Assert.True(service.ExecuteTask.IsCompleted);
}
[Fact]
public async Task StopAsyncStopsEvenIfTaskNeverEnds()
{
var service = new IgnoreCancellationService();
await service.StartAsync(CancellationToken.None);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
await service.StopAsync(cts.Token);
}
[Fact]
public async Task StopAsyncThrowsIfCancellationCallbackThrows()
{
var service = new ThrowOnCancellationService();
await service.StartAsync(CancellationToken.None);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
await Assert.ThrowsAsync<AggregateException>(() => service.StopAsync(cts.Token));
Assert.Equal(2, service.TokenCalls);
}
[Fact]
public async Task StartAsyncThenDisposeTriggersCancelledToken()
{
var service = new WaitForCancelledTokenService();
await service.StartAsync(CancellationToken.None);
service.Dispose();
}
private class WaitForCancelledTokenService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Delay(Timeout.Infinite, stoppingToken);
}
}
private class ThrowOnCancellationService : BackgroundService
{
public int TokenCalls { get; set; }
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
stoppingToken.Register(() =>
{
TokenCalls++;
throw new InvalidOperationException();
});
stoppingToken.Register(() =>
{
TokenCalls++;
});
return new TaskCompletionSource<object>().Task;
}
}
private class IgnoreCancellationService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return new TaskCompletionSource<object>().Task;
}
}
private class MyBackgroundService : BackgroundService
{
private readonly Task _task;
public Task ExecuteTask { get; set; }
public MyBackgroundService(Task task)
{
_task = task;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ExecuteTask = ExecuteCore(stoppingToken);
await ExecuteTask;
}
private async Task ExecuteCore(CancellationToken stoppingToken)
{
var task = await Task.WhenAny(_task, Task.Delay(Timeout.Infinite, stoppingToken));
await task;
}
}
}
}