Runtime: BackgroundService on a timer: TimedBackgroundService

Created on 11 Dec 2018  路  13Comments  路  Source: dotnet/runtime

Is your feature request related to a problem? Please describe.

I discovered ASP.NET Core 2.1 has a BackgroundService that can be easily used for long running async tasks. It sadly does not have the option to run it on a timer which could be very handy since we are creating an abstraction for IHostedService anyway.

I _could_ remember the last time the method ran and then, if x time passed, run the method again (thus creating my own timer), but that would have to run in ExecuteAsync() which feels odd because even though ExecuteAsync is called, the method can't guarantee the _core_ functionality is actually executed. I also do not know the pattern of ExecuteAsync calls which could result in the timer not being useful because it would depend on ExecuteAsync being called consistently to make sure there is as little delay as possible when _actually_ executing the task, but this could also be because of my new knowledge of this class.

Describe the solution you'd like

I'd like to have a new abstraction for IHostedService or BackgroundService called TimedBackgroundService. This could have a constructor argument/configuration argument that has a cron expression to make it run on a timer. Maybe even an attribute?

Describe alternatives you've considered

It could be possible to add a cronjob expression to services.AddHostedService() to decide there when a backgroundservice will run.

Additional context

None.

area-Extensions-Hosting feature request

Most helpful comment

We'll look at this in 3.0. No guarantees, but it seems like a useful thing to add.

All 13 comments

/cc @glennc @davidfowl

This is something we've been thinking about for the future. If we provided a simple base class that's a singleton service that was called on an interval you choose, is that what you want? I think it would be possible for you to test-drive something like this through a sample until we have it built in.

We'd probably also do things like include logging and error handling by default. Are there other features you'd expect this to have?

When you mention logging and error handling, do you mean that the sample will include these or the TimedBackgroundService? I ask for clarification because BackgroundService does not have logging or error handling by default and it seems a bit silly to me to suddenly introduce these 2 functionalities in a new TimedBackgroundService class.

Assumptions: (Please bear in mind I haven't read much about hosted services yet)

  • If I would have a reference to the Service (if this is possible) and would call ExecuteAsync even though it is not its time yet, it would still execute since I called it manually. If this is bad design, you could have a ForceRun() method that would force it to run. I'm just throwing ideas out there now :)

A couple new features I can come up with, are:

  • public DateTimeOffset LastTimeExecuted {get; }

    • Can be used to get the last time a method ran.

    • You could also make it a public ExecutionResult LastExecutionResult which could be a class/struct with the following properties:



      • public bool Succeeded


      • True when the Task was completed successfully or when there were no uncaught exceptions;


      • public DateTimeOffset LastTimeExecuted {get; } (same as above)



  • public Timespan TimeUntilNextExecution {get; }

    • Can be used to get the time until the next execution, possibly in combination with calling it manually as I mentioned above.

Thanks for your quick answer! It's exciting to see you guys already looked into this :)

We'll look at this in 3.0. No guarantees, but it seems like a useful thing to add.

Any updates?

I'm looking for a solution as well. It should be as easy as setting a TimerTrigger in Azure Functions.

You could also look into a WebJob for now if you host it in Azure, if you feel more comfortable with that.

Otherwise, look at my own solution for this issue in this StackOverflow issue; please keep the comments in mind that bring good feedback; it's possible that the solution can be optimized or that it might not meet requirements.

https://stackoverflow.com/questions/53727850/how-to-run-backgroundservice-on-a-timer-in-asp-net-core-2-1/

Any updates?

@sertunc not at this time. We're doing planning for 5.0 and will consider this during that process, but there's no guarantee as it will be prioritized against all the other work on our queue.

Your solution is here

@karimgsaikali2

Someone pointed out in my stackoverflow post that using Timer and BackgroundService might not execute the tasks if your API is not getting requests. I can't confirm that. But your link basically uses the same code as my stackoverflow post, so I don't see how this is a 100% better solution?

Your link does have a good point though! In that person's post they lock the jobs so it waits until the current one is finished. That would be useful/mandatory as well if it is included as an extension in the net core repo.

@anurse Any updates? I expect it not to launch with .NET 5, but I'm curious to know what you think of adding a monitor/lock system and to know if this will ever be added?

@sander1095
I wonder if the lock used in the post mentioned post is correct?

Indeed using Monotor.TryEnter will prevent other threads to enter the sub DoWork(),
but allow the same thread to re-enter the sub DoWork()!

Maybe instead of using the void DoWork() we may use async void DoWork() and use SemaphoreSlim,
Indeed SemaphoreSlim will prevent any threads to enter the void, it will wait and execute one after another.

Therefore instead of :
private void DoWork(object state)
{
_logger.LogDebug($"Try to execute next iteration {_counter + 1} of DoWork ");
if (Monitor.TryEnter(_lock))
{
try
{
_logger.LogDebug($"Running DoWork iteration {_counter}");
_myService.DoWorkAsync().Wait();
_logger.LogDebug($"DoWork {_counter} finished, will start iteration {_counter + 1}");
}
finally
{
_counter++;
Monitor.Exit(_lock);
}
}
}

We will have:

SemaphoreSlim _sync = new SemaphoreSlim(1);

private async void DoWork(object state)
{
await _sync.WaitAsync();
try
{
_logger.LogDebug($"Try to execute next iteration {_counter + 1} of DoWork ");
_logger.LogDebug($"Running DoWork iteration {_counter}");
await _myService.DoWorkAsync();
_logger.LogDebug($"DoWork {_counter} finished, will start iteration {_counter + 1}");
}
finally
{
_counter++;
_sync.Release();
}
}

What do you think?

I am not that experienced with Semaphore or Monitor, sadly.

Also, your code is not formatted and rather hard to read. I would ask one of the dot net experts to look more into this :)

The next step for this would be to prepare APIs and samples from guideline: https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md

Was this page helpful?
0 / 5 - 0 ratings