Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Roy Berris 12 posts 162 karma points
    1 week ago
    Roy Berris
    0

    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?

  • Dennis 10 posts 122 karma points
    1 week ago
    Dennis
    100

    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:

    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!

  • Roy Berris 12 posts 162 karma points
    1 week ago
    Roy Berris
    0

    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 the Dispatch method, this seems to be missing

  • Dennis 10 posts 122 karma points
    1 week ago
    Dennis
    0

    Here's the implementation of ICommandQueue:

    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.

  • Dennis 10 posts 122 karma points
    1 week ago
    Dennis
    0

    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.

Please Sign in or register to post replies

Write your reply to:

Draft