Skip to content

Commit

Permalink
Add BackgroundService, a base class for long running HostedServices (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
davidfowl committed Sep 18, 2017
1 parent 5006686 commit 712c992
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 23 deletions.
30 changes: 7 additions & 23 deletions samples/GenericHostSample/MyServiceA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,22 @@

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;
}
}
}
}
75 changes: 75 additions & 0 deletions src/Microsoft.Extensions.Hosting.Abstractions/BackgroundService.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}

0 comments on commit 712c992

Please sign in to comment.