How do I use the BackgroundTaskRunner to queue a long running task which is dependency injected?
I have a service that has a long running task. (5-10 minutes before completion). Now I need to call this from a external handler, and I want to queue this task from my API controller. It shouldn't repeat on a timer, only when the API controller queue's it.
It's important that it is dependency injected because of the dependencies of my service.
Correct me if I'm wrong;
I can't make the API controller inject the service, because after the task is added to the queue the API controller will respond to the caller. And then Umbraco will dispose the dependencies created for the API controller.
So does anyone have a code example of above problem?
I've recently done something similar. Umbraco has some very nice documentation on the RecurringTaskBase class that shows you how to run background tasks on a timer. This ofcourse is not what you ask, you are asking to trigger something on a signal from an api endpoint. Fortunately this is easily done if you look at the source code of the RecurringTaskBase class in github. You can inherit from the LatchedBackgroundTaskBase class and create your own background task that functions on a signal. Here's an example implementation that I created recently:
Component:
public class PageViewComponent : IComponent
{
private readonly ICommandQueue _commandQueue;
private readonly ILogger _logger;
private readonly BackgroundTaskRunner<IBackgroundTask> _taskRunner;
private PageViewBackgroundWorker _backgroundWorker;
// Dependency injection resolves all your dependencies here
public PageViewComponent(ICommandQueue commandQueue,
ILogger logger)
{
_commandQueue = commandQueue;
_logger = logger;
_taskRunner = new BackgroundTaskRunner<IBackgroundTask>("Page view background errands", _logger);
}
public void Initialize()
{
// pass any dependencies to your new background worker
_backgroundWorker = new PageViewBackgroundWorker(_taskRunner, _commandQueue, _logger);
_taskRunner.TryAdd(_backgroundWorker);
PublishedRequest.Prepared += OnRequestPrepared;
}
public void Terminate()
{
PublishedRequest.Prepared -= OnRequestPrepared;
}
private void OnRequestPrepared(object sender, EventArgs e)
{
if (!(sender is PublishedRequest request))
{
_logger.Warn<PageViewComponent>("Sender is not of type {0}", typeof(PublishedRequest));
return;
}
_commandQueue.Push(new SavePageViewCommand( /* any data that the handler needs to use */ ));
}
}
Background worker:
public class PageViewBackgroundWorker : LatchedBackgroundTaskBase
{
private readonly IBackgroundTaskRunner<LatchedBackgroundTaskBase> _runner;
private readonly ICommandQueue _commandQueue;
private readonly ILogger _logger;
// Add the services that you need to the constructor here.
public PageViewBackgroundWorker(IBackgroundTaskRunner<LatchedBackgroundTaskBase> runner,
ICommandQueue commandQueue,
ILogger logger)
{
_runner = runner;
_commandQueue = commandQueue;
_logger = logger;
_commandQueue.OnCommandAdded += Dispatch;
}
public override async Task RunAsync(CancellationToken token)
{
try
{
while (_commandQueue.TryGet(out var command))
{
// configure await is false so we don't have to wait for the context to become available again.
// NOTE: be aware when creating commands that one command won't necessarily be executed on the same thread as the previous.
// This is where you would perform your logic
await command.ExecuteAsync().ConfigureAwait(false);
}
}
catch (Exception e)
{
_logger.Error<PageViewBackgroundWorker>(e, "An exception occurred while executing a command on the queue.");
}
Repeat();
}
private void Repeat()
{
if (_runner.IsCompleted) return;
Reset();
if (!_runner.TryAdd(this))
{
Dispose();
}
}
public void Dispatch(object sender, EventArgs e)
{
try
{
Release();
}
catch // it's alright if this fails, that means that this task is still running and the message will be consumed
{ }
}
public override bool IsAsync => true;
protected override void DisposeResources()
{
base.DisposeResources();
_commandQueue.OnCommandAdded -= Dispatch;
}
}
In my case, the command queue object is where the signals are sent to. The command queue is a singleton object.
I left a few bits out here and there, but I hope you get the idea. Let me know if you need any additional guidance!
public interface ICommandQueue
{
event EventHandler OnCommandAdded;
void Push(IBackgroundCommand command);
bool TryGet(out IBackgroundCommand command);
}
public class CommandQueue : ICommandQueue
{
private readonly ConcurrentQueue<IBackgroundCommand> _commandQueue;
public CommandQueue()
{
_commandQueue = new ConcurrentQueue<IBackgroundCommand>();
}
public void Push(IBackgroundCommand command)
{
_commandQueue.Enqueue(command);
TriggerOnCommandAdded();
}
public bool TryGet(out IBackgroundCommand command)
{
return _commandQueue.TryDequeue(out command);
}
public event EventHandler OnCommandAdded;
private void TriggerOnCommandAdded()
{
var handler = OnCommandAdded;
handler?.Invoke(this, new EventArgs());
}
}
The Release() method is inherited from LatchedBackgroundTaskBase. Let's see if I can explain this without creating complete chaos 😅
When you call _runner.TryAdd(this), you kinda start a new background task, but not quite. The LatchedBackgroundTaskBase holds on to an instance of TaskCompletionSource and awaits completion of this task before actually executing. When you call Release(), the task is completed and execution starts.
Looking back at this, it may be worth checking what LatchedBackgroundTaskBase actually adds. Perhaps it's also possible to implement IBackgroundTask directly and just throw instances of those on the IBackgroundTaskRunner whenever you want to do background work. If that works, that would make the code much less complicated.
How to use the BackgroundTaskRunner?
Hello,
How do I use the
BackgroundTaskRunner
to queue a long running task which is dependency injected?I have a service that has a long running task. (5-10 minutes before completion). Now I need to call this from a external handler, and I want to queue this task from my API controller. It shouldn't repeat on a timer, only when the API controller queue's it.
It's important that it is dependency injected because of the dependencies of my service.
Correct me if I'm wrong;
I can't make the API controller inject the service, because after the task is added to the queue the API controller will respond to the caller. And then Umbraco will dispose the dependencies created for the API controller.
So does anyone have a code example of above problem?
Hi Roy,
I've recently done something similar. Umbraco has some very nice documentation on the RecurringTaskBase class that shows you how to run background tasks on a timer. This ofcourse is not what you ask, you are asking to trigger something on a signal from an api endpoint. Fortunately this is easily done if you look at the source code of the RecurringTaskBase class in github. You can inherit from the
LatchedBackgroundTaskBase
class and create your own background task that functions on a signal. Here's an example implementation that I created recently:Component:
Background worker:
In my case, the command queue object is where the signals are sent to. The command queue is a singleton object.
I left a few bits out here and there, but I hope you get the idea. Let me know if you need any additional guidance!
Hi, thanks for your response. This helps me!
Just because I'm curious, can you share
ICommandQueue
?And I'm not sure what
Release()
does in theDispatch
method, this seems to be missingHere's the implementation of
ICommandQueue
:The
Release()
method is inherited fromLatchedBackgroundTaskBase
. Let's see if I can explain this without creating complete chaos 😅When you call
_runner.TryAdd(this)
, you kinda start a new background task, but not quite. TheLatchedBackgroundTaskBase
holds on to an instance ofTaskCompletionSource
and awaits completion of this task before actually executing. When you callRelease()
, the task is completed and execution starts.Looking back at this, it may be worth checking what
LatchedBackgroundTaskBase
actually adds. Perhaps it's also possible to implementIBackgroundTask
directly and just throw instances of those on theIBackgroundTaskRunner
whenever you want to do background work. If that works, that would make the code much less complicated.is working on a reply...