-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from GeeWee/feature/periodic-tasks
Periodic tasks
- Loading branch information
Showing
9 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
namespace AspNetCoreTestProject | ||
{ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using BetterHostedServices; | ||
|
||
public class PrintingPeriodicTask : IPeriodicTask | ||
{ | ||
public Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
Console.WriteLine("Hello world"); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
|
||
public class CountingPeriodicTask : IPeriodicTask | ||
{ | ||
private SingletonStateHolder singletonStateHolder; | ||
private TransientStateHolder transientStateHolder; | ||
private ScopeStateHolder scopeStateHolder; | ||
|
||
public CountingPeriodicTask(TransientStateHolder transientStateHolder, SingletonStateHolder singletonStateHolder, ScopeStateHolder scopeStateHolder) | ||
{ | ||
this.transientStateHolder = transientStateHolder; | ||
this.singletonStateHolder = singletonStateHolder; | ||
this.scopeStateHolder = scopeStateHolder; | ||
} | ||
|
||
public Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
this.transientStateHolder.Count += 1; | ||
this.singletonStateHolder.Count += 1; | ||
this.scopeStateHolder.Count += 1; | ||
|
||
var random = new Random(); | ||
if (random.Next(0, 10) < 2) | ||
{ | ||
throw new Exception("Oh no something went horribly wrong."); | ||
} | ||
|
||
Console.WriteLine($"Transient state: {this.transientStateHolder.Count}. Scope state: {this.scopeStateHolder.Count}. Singleton state: {this.singletonStateHolder.Count}"); | ||
return Task.CompletedTask; | ||
} | ||
} | ||
|
||
public class SingletonStateHolder | ||
{ | ||
public int Count { get; set; } = 0; | ||
} | ||
|
||
public class ScopeStateHolder | ||
{ | ||
public int Count { get; set; } = 0; | ||
} | ||
|
||
public class TransientStateHolder | ||
{ | ||
public int Count { get; set; } = 0; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
namespace BetterHostedServices | ||
{ | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
/// <summary> | ||
/// A periodic tasks to be executed by a PeriodicTaskRunnerBackgroundService. | ||
/// This task is re-created with a new scope for each invocation to ExecuteAsync. | ||
/// </summary> | ||
public interface IPeriodicTask | ||
{ | ||
/// <summary> | ||
/// This ExecuteAsync is called by the PeriodicTaskRunnerBackgroundService at a given interval. | ||
/// Note that you will get a newly created IPeriodicTask for each invocation. | ||
/// </summary> | ||
/// <param name="stoppingToken">CancellationToken to listen to if you should abandon your work.</param> | ||
/// <returns>Should return a task that will be awaited by the PeriodicTaskRunnerBackgroundService</returns> | ||
public Task ExecuteAsync(CancellationToken stoppingToken); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
namespace BetterHostedServices | ||
{ | ||
/// <summary> | ||
/// | ||
/// </summary> | ||
public enum PeriodicTaskFailureMode | ||
{ | ||
/// <summary> | ||
/// If this failure mode is set and a task throws an uncaught application | ||
/// the task will crash the application. | ||
/// It has the same behaviour as letting the task bubble up from a CriticalBackgroundService. | ||
/// </summary> | ||
CrashApplication = 1, | ||
|
||
/// <summary> | ||
/// If this failure mode is set and a task throws an uncaught application, the error will be logged via | ||
/// the standard ILogger implementation, and nothing further will be done. | ||
/// The next periodic task will continue as planned. | ||
/// </summary> | ||
RetryLater = 5 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
namespace BetterHostedServices | ||
{ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <typeparam name="TPeriodicTask"></typeparam> | ||
public class PeriodicTaskRunnerBackgroundService<TPeriodicTask> : CriticalBackgroundService | ||
where TPeriodicTask : IPeriodicTask | ||
{ | ||
private readonly ILogger<PeriodicTaskRunnerBackgroundService<TPeriodicTask>> logger; | ||
private readonly IServiceProvider serviceProvider; | ||
|
||
private readonly PeriodicTaskFailureMode periodicTaskFailureMode; | ||
private readonly TimeSpan timeBetweenTasks; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="applicationEnder"></param> | ||
/// <param name="logger"></param> | ||
/// <param name="serviceProvider"></param> | ||
/// <param name="periodicTaskFailureMode"></param> | ||
/// <param name="timeBetweenTasks"></param> | ||
public PeriodicTaskRunnerBackgroundService( | ||
IApplicationEnder applicationEnder, | ||
ILogger<PeriodicTaskRunnerBackgroundService<TPeriodicTask>> logger, | ||
IServiceProvider serviceProvider, | ||
PeriodicTaskFailureMode periodicTaskFailureMode, | ||
TimeSpan timeBetweenTasks) : base(applicationEnder) | ||
{ | ||
this.logger = logger; | ||
this.serviceProvider = serviceProvider; | ||
this.periodicTaskFailureMode = periodicTaskFailureMode; | ||
this.timeBetweenTasks = timeBetweenTasks; | ||
} | ||
|
||
/// <summary> | ||
/// Executes the given TPeriodicTask in a new scope each time. | ||
/// </summary> | ||
/// <param name="stoppingToken"></param> | ||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
// Ensure that we can crate the service. Do this synchronously so that we'll fail-fast no matter the failure mode | ||
// if the task can't run at all. | ||
using (var scope = this.serviceProvider.CreateScope()) | ||
{ | ||
_ = scope.ServiceProvider.GetRequiredService<TPeriodicTask>(); | ||
} | ||
|
||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
try | ||
{ | ||
using var scope = this.serviceProvider.CreateScope(); | ||
var periodicTask = scope.ServiceProvider.GetRequiredService<TPeriodicTask>(); | ||
await periodicTask.ExecuteAsync(stoppingToken); | ||
} | ||
catch (Exception e) | ||
{ | ||
this.logger.LogError(e, $"Exception while processing message in {typeof(TPeriodicTask)}"); | ||
// If failure mode is set to end application, go through the normal OnError flow that crashes the application. | ||
if (this.periodicTaskFailureMode == PeriodicTaskFailureMode.CrashApplication) | ||
{ | ||
this.OnError(e); | ||
} | ||
|
||
if (this.periodicTaskFailureMode == PeriodicTaskFailureMode.RetryLater) | ||
{ | ||
this.logger.LogWarning(e, $"Exception while processing message in {typeof(TPeriodicTask)}. Retrying in {this.timeBetweenTasks}"); | ||
} | ||
} | ||
|
||
await Task.Delay(this.timeBetweenTasks, stoppingToken); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
namespace BetterHostedServices.Test.IntegrationUtils | ||
{ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
public class CrashingPeriodicTask: IPeriodicTask | ||
{ | ||
public async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
await Task.Delay(50, stoppingToken); // Just to yield control | ||
throw new Exception("oh no"); | ||
} | ||
} | ||
|
||
public class IncrementingThenCrashingPeriodicTask: IPeriodicTask | ||
{ | ||
private readonly SingletonStateHolder singletonStateHolder; | ||
|
||
public IncrementingThenCrashingPeriodicTask(SingletonStateHolder singletonStateHolder) => this.singletonStateHolder = singletonStateHolder; | ||
|
||
public async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
this.singletonStateHolder.Count += 1; | ||
throw new Exception("oh no"); | ||
} | ||
} | ||
|
||
public class SingletonStateHolder | ||
{ | ||
public int Count { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
namespace BetterHostedServices.Test | ||
{ | ||
using System; | ||
using System.Threading.Tasks; | ||
using FluentAssertions; | ||
using IntegrationUtils; | ||
using Microsoft.AspNetCore.TestHost; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Xunit; | ||
|
||
public class PeriodicTasksTest | ||
{ | ||
private readonly CustomWebApplicationFactory<DummyStartup> _factory; | ||
|
||
public PeriodicTasksTest() | ||
{ | ||
_factory = new CustomWebApplicationFactory<DummyStartup>(); | ||
} | ||
|
||
[Fact] | ||
public async Task PeriodicTask_ShouldEndApplication_IfFailureModeIsSetToCrash() | ||
{ | ||
// Arrange | ||
var applicationEnder = new ApplicationEnderMock(); | ||
|
||
var factory = this._factory.WithWebHostBuilder(builder => | ||
{ | ||
builder.ConfigureTestServices(services => | ||
{ | ||
services.AddTransient<IApplicationEnder>(s => applicationEnder); | ||
services.AddPeriodicTask<CrashingPeriodicTask>(PeriodicTaskFailureMode.CrashApplication, TimeSpan.FromSeconds(1)); | ||
}); | ||
}); | ||
|
||
var client = factory.CreateClient(); | ||
// Act & assert - we should crash here at some point | ||
|
||
// Task is hella flaky because it depends on the internals of the IHostedService - try yielding a bunch of times | ||
// to hope that it's done requesting application shutdown at this point | ||
for (int i = 0; i < 10; i++) | ||
{ | ||
await Task.Delay(50); | ||
await Task.Yield(); | ||
} | ||
|
||
// due to https://github.com/dotnet/aspnetcore/issues/25857 we can't test if the process is closed directly | ||
applicationEnder.ShutDownRequested.Should().BeTrue(); | ||
} | ||
|
||
[Fact] | ||
public async Task PeriodicTask_ShouldContinueRunningTasks_IfFailureModeIsSetToRetry() | ||
{ | ||
// Arrange | ||
var applicationEnder = new ApplicationEnderMock(); | ||
var stateHolder = new SingletonStateHolder(); | ||
|
||
var factory = this._factory.WithWebHostBuilder(builder => | ||
{ | ||
builder.ConfigureTestServices(services => | ||
{ | ||
services.AddTransient<IApplicationEnder>(s => applicationEnder); | ||
services.AddPeriodicTask<IncrementingThenCrashingPeriodicTask>(PeriodicTaskFailureMode.RetryLater, TimeSpan.FromMilliseconds(50)); | ||
services.AddSingleton<SingletonStateHolder>(s => stateHolder); | ||
}); | ||
}); | ||
|
||
var client = factory.CreateClient(); | ||
// Act & assert - we crash here after each invocation, but we truck on. The stateHolder should keep being incremented | ||
|
||
// Task is hella flaky because it depends on the internals of the IHostedService - try yielding a bunch of times | ||
// to hope that it's done requesting application shutdown at this point | ||
for (int i = 0; i < 10; i++) | ||
{ | ||
await Task.Delay(100); // 1s ms all in all | ||
await Task.Yield(); | ||
} | ||
|
||
stateHolder.Count.Should().BeGreaterThan(5); | ||
|
||
// due to https://github.com/dotnet/aspnetcore/issues/25857 we can't test if the process is closed directly | ||
applicationEnder.ShutDownRequested.Should().BeFalse(); | ||
} | ||
} | ||
} |