Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BackgroundService on a timer: TimedBackgroundService #36383

Open
sander1095 opened this issue Dec 11, 2018 · 14 comments
Open

BackgroundService on a timer: TimedBackgroundService #36383

sander1095 opened this issue Dec 11, 2018 · 14 comments

Comments

@sander1095
Copy link

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.

@rynowak
Copy link
Member

rynowak commented Dec 11, 2018

/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?

@sander1095
Copy link
Author

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 :)

@analogrelay
Copy link
Contributor

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

@sander1095
Copy link
Author

Any updates?

@MisinformedDNA
Copy link
Contributor

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

@sander1095
Copy link
Author

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/

@sertunc
Copy link

sertunc commented Jan 6, 2020

Any updates?

@analogrelay
Copy link
Contributor

@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.

@analogrelay analogrelay transferred this issue from dotnet/extensions May 13, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-Hosting untriaged New issue has not been triaged by the area owner labels May 13, 2020
@analogrelay analogrelay added this to the Future milestone May 13, 2020
@karimgsaikali2
Copy link

Your solution is here

@ericstj ericstj removed the untriaged New issue has not been triaged by the area owner label Jun 22, 2020
@sander1095
Copy link
Author

@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?

@karimgsaikali2
Copy link

@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?

@sander1095
Copy link
Author

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 :)

@maryamariyan
Copy link
Member

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

@sander1095
Copy link
Author

Sinds the ~2+ years that I posted this I have encountered this issue a few times.

I believe that my previous suggestion is insufficient, this might need more work:

  • If a task executes every 30 seconds but the actual task takes 40 seconds, a 2nd task will already run. You might want to make that configurable; should it throw? Wait?
  • Adding CronJob support could be useful. I found this blog some time ago, but that works with the timer api which won't work if the delay between executions is too big (few weeks). This is why the modern timer api story (API proposal: Modern Timer API #31525) is linked; perhaps that could help?

I sadly don't have the time or knowledge to really help out with this feature, so I'd like someone else to make some proposals. I am also curious what the status of this story is; are there any plans for it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants