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:
parent
500668619f
commit
712c992ca8
|
|
@ -5,38 +5,22 @@ using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace GenericHostSample
|
namespace GenericHostSample
|
||||||
{
|
{
|
||||||
public class MyServiceA : IHostedService
|
public class MyServiceA : BackgroundService
|
||||||
{
|
{
|
||||||
private bool _stopping;
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
private Task _backgroundTask;
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("MyServiceA is starting.");
|
Console.WriteLine("MyServiceA is starting.");
|
||||||
_backgroundTask = BackgroundTask();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task BackgroundTask()
|
stoppingToken.Register(() => Console.WriteLine("MyServiceA is stopping."));
|
||||||
{
|
|
||||||
while (!_stopping)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
|
||||||
Console.WriteLine("MyServiceA is doing background work.");
|
Console.WriteLine("MyServiceA is doing background work.");
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("MyServiceA background task is stopping.");
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue