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
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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