Skip to content

Commit

Permalink
periodic task implementation seems to work
Browse files Browse the repository at this point in the history
  • Loading branch information
GeeWee committed Sep 13, 2021
1 parent 74d6d80 commit 248dede
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 0 deletions.
62 changes: 62 additions & 0 deletions AspNetCoreTestProject/PeriodicTasks.cs
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;
}

}
8 changes: 8 additions & 0 deletions AspNetCoreTestProject/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public void ConfigureServices(IServiceCollection services)
// services.AddHostedService<YieldingAndThenCrashingBackgroundService>(); // This will not crash the application
// services.AddHostedService<ImmediatelyCrashingCriticalBackgroundService>(); // Crash
// services.AddHostedService<YieldingAndThenCrashingCriticalBackgroundService>(); // Crash

services.AddTransient<TransientStateHolder>();
services.AddSingleton<SingletonStateHolder>();
services.AddScoped<ScopeStateHolder>();
// TODO ensure that service is registered

// services.AddPeriodicTask<PrintingPeriodicTask>(PeriodicTaskFailureMode.CRASH_APPLICATION, TimeSpan.FromSeconds(1));
services.AddPeriodicTask<CountingPeriodicTask>(PeriodicTaskFailureMode.CrashApplication, TimeSpan.FromSeconds(5));
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,11 @@ method on the `IServiceCollection`
services.AddHostedServiceAsSingleton<ISomeBackgroundService, SomeBackgroundService>();
```
After that, you can inject them via the DI container just like any ordinary singleton.

## RunPeriodicTasks
If you simply want your BackgroundService to run a periodic tasks, there's some boilerplate you generally have to deal with.
Best-practices for using BackgroundServices to run periodic tasks are [documented here](https://www.gustavwengel.dk/testing-and-scope-management-aspnetcore-backgroundservices) - but you can also use this library.

```csharp
services.
```
20 changes: 20 additions & 0 deletions src/IPeriodicTask.cs
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);
}
}
28 changes: 28 additions & 0 deletions src/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace BetterHostedServices
{
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

/// <summary>
/// Extensions on IServiceCollection
Expand Down Expand Up @@ -49,6 +51,32 @@ public static void AddHostedServiceAsSingleton<TService>(this IServiceCollection
services.AddHostedService<HostedServiceWrapper<TService>>();
}

/// <summary>
/// Add a periodic task
/// TODO
/// </summary>
/// <param name="services"></param>
/// <param name="failureMode"></param>
/// <param name="timeBetweenTasks"></param>
/// <typeparam name="TPeriodicTask"></typeparam>
public static void AddPeriodicTask<TPeriodicTask>(this IServiceCollection services, PeriodicTaskFailureMode failureMode, TimeSpan timeBetweenTasks)
where TPeriodicTask : class, IPeriodicTask
{
services.AddTransient<TPeriodicTask>();

services.AddHostedService<PeriodicTaskRunnerBackgroundService<TPeriodicTask>>((services) =>
{
return new PeriodicTaskRunnerBackgroundService<TPeriodicTask>(
applicationEnder: services.GetRequiredService<IApplicationEnder>(),
logger: services.GetRequiredService<ILogger<PeriodicTaskRunnerBackgroundService<TPeriodicTask>>>(),
serviceProvider: services.GetRequiredService<IServiceProvider>(),
periodicTaskFailureMode: failureMode,
timeBetweenTasks: timeBetweenTasks
);
});
}



}

Expand Down
22 changes: 22 additions & 0 deletions src/PeriodicTaskFailureMode.cs
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
}
}
83 changes: 83 additions & 0 deletions src/PeriodicTaskRunnerBackgroundService.cs
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);
}
}
}
}

0 comments on commit 248dede

Please sign in to comment.