The short version is - how do I (in 2017 having .NET Core 1.1 released) run recurring tasks in .NET Core console app with Microsoft's dependency injection?
The long story and code snippets.
The goal
I have 4 (and will have more) services each of which has say one method that performs certain isolated task and interacts with database. Specifically, I have one service that removes old data from database, one service that "caches" data (run through data in DB and stores aggregated results in other table), one that generates random data and so on... The idea is to run those tasks with different intervals, say every 20 seconds, or once a day.
Current dirty implementation
I have a service that for each task has a while(true)
loop that spawns a service with DI, runs task, Thread.Sleep(interval)
, checks exit condition (class-wide flag) and continue/break
. Most of the methods are async
.
Current problems
First of all, I know that while(true)
and Thread.Sleep(interval)
is a huge hack (technical debt). Second, I have memory leak and I feel like the way I spawn/dispose service has something to do with it. Finally, the code encounters random errors and all exceptions are swallowed. I am not able to catch them in any known to me way.
The solution I want
I want to achieve the goal, but if possible
- avoid large frameworks, like Hangfire and Quartz. I feel like I do not need a fraction of what they can.
- keep app a console app. Although this is a "helper" for ASP project, I want it to be a console app and be able to run it like
dotnet daemons.dll
and it does its job indefinitely until ICtrl+C
it. It also should not grow in memory and CPU/threads consumption. It is OK if it eats up considerable amount of RAM as long as it is a stable number. - use Microsoft DI. That's how all my services are built.
- keep my services
async
. I may reuse them in different scenarios including unit testing.
Code snippets
Example of how I run recurring task:
private async Task RunCacheServiceAsync()
{
// Make sure the service is set to run.
_status[ServiceManagerServices.Cache] = true;
_logger.LogInformation(LoggingEvents.ServiceManager.AsInt(), "Cache service started.");
while (true)
{
var metrics = await _serviceProvider
.GetRequiredService<DataContext>()
.Metrics
.ToListAsync();
// Run the tasks
var tasks = metrics
.Select(
(metric) =>
Task.Factory.StartNew(async () =>
{
await _serviceProvider
.GetRequiredService<ICacheService>()
.CacheMetric(metric);
})
);
// Wait completion of all tasks
await Task.WhenAll(tasks.ToArray());
// Wait
Thread.Sleep(_intervals[ServiceManagerServices.Cache]);
// Check exit condition
if (!_status[ServiceManagerServices.Cache])
{
break;
}
}
}
Example of the task that I need to be periodic:
public async Task CleanDataPointsAsync()
{
var toTimestamp = DateTime.Now - _maxAge;
// remove data points of all types
_context.RemoveRange(
_context.NumericDataPoints.Where(dp => dp.Timestamp < toTimestamp)
);
await _context.SaveChangesAsync();
_logger.LogInformation(LoggingEvents.Clean.AsInt(), "Cleaned old data.");
}
The question
Please, suggest a general approach I can take to achieve the goal considering constraints. Any feedback on posted code and reasoning is appreciated!