Poor Performance BackgroundService dequeuing from Queue - c#

I have a HostedService which inherits from BackgroundService. It loops and dequeues items from a Queue.
These items are placed on the queue via a Controller from a http request.
If I set Postman runner to fire an item every 500 miliseconds, for up to 60 items. Some early items are dequeued after seconds, but later, it can take up to 10 seconds to dequeue.
I have tried: Queue, ConcurrentQueue and BlockingCollection. All with the same results.
Any ideas? Is this a valid use case for any of the Queue types I mentioned?
Here are the implementation details:
Registration:
services.AddSimpleInjector(_container, options =>
{
options.AddAspNetCore().AddControllerActivation();
options.AddHostedService<QueuedHostedService>();
});
The Background service:
public class QueuedHostedService : BackgroundService
{
private readonly IApiLog _apiLog;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
IApiLog apiLog)
{
TaskQueue = taskQueue;
_apiLog = apiLog;
}
public IBackgroundTaskQueue TaskQueue { get; }
protected override async Task ExecuteAsync(
CancellationToken cancellationToken)
{
_apiLog.Log(new LogEntry("Queued Hosted Service is starting."));
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.Dequeue(cancellationToken);
_apiLog.Log(new LogEntry($"Dequeuing work-item: {nameof(workItem)}"));
try
{
await workItem(cancellationToken);
}
catch (Exception exception)
{
_apiLog.Log(new LogEntry(exception, $"Error occurred executing {nameof(workItem)}."));
}
}
_apiLog.Log(new LogEntry("Queued Hosted Service is stopping."));
}
}
The queue:
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
private readonly IApiLog _apiLog;
private readonly Queue<Func<CancellationToken, Task>> _items = new Queue<Func<CancellationToken, Task>>();
public BackgroundTaskQueue(IApiLog apiLog)
{
_apiLog = apiLog;
}
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem, string eventId, string correlationId)
{
try
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_items.Enqueue(workItem);
_apiLog.Log(new LogEntry($"BackgroundWorkItem has been enqueued for EventId={eventId}, CorrelationId={correlationId}"));
}
catch (Exception exception)
{
_apiLog.Log(new LogEntry(exception, exception.Message));
}
finally
{
_signal.Release();
}
}
public async Task<Func<CancellationToken, Task>> Dequeue(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_items.TryDequeue(out var workItem);
return workItem;
}
}
And in the controller, the method that ultimately places items on the queue:
public void AddRequestToQueue(MyEvent myEvent, string correlationId, string userName)
{
if (string.IsNullOrEmpty(correlationId)) correlationId = Guid.NewGuid().ToString();
_apiLog.Log(new LogEntry($"Adding Update Event Request to Queue. OperationType={OperationType.ToString()}, EventId={myEvent.Id}, CorrelationId={ correlationId }"));
BackGroundTaskQueue.QueueBackgroundWorkItem(async token =>
{
_apiLog.Log(new LogEntry($"Update Event Request Dequeued. OperationType={OperationType.ToString()}, EventId={myEvent.Id}, CorrelationId={ correlationId }"));
await AddSomethingToDatabase(myEvent, correlationId, userName);
var event = _converter.Convert<SomethingElse>(myEvent);
await SendSomethingToRabbit(event, correlationId, OperationType);
}, myEvent.Id.ToString(), correlationId);
}
I am seeing up to 10 seconds between the log lines:
Adding Update Event Request to Queue
and
Update Event Request Dequeued

Related

Task queue without waiting for results

I am wondering if anyone can help me I am trying to write a class which will create a task queue of tasks which runs without waiting for the result. I have a couple issues the first is I want to remove the completed tasks from the queue so that the task queue does not grow to large and when application shuts down it will clear/finish the task queue first.
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Task? workItem, CancellationToken cancellationToken);
Task<Task?> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly ConcurrentQueue<Task?> _workItems = new();
private readonly ILogger<BackgroundTaskQueue> _logger;
private readonly SemaphoreSlim _signal = new(0);
public BackgroundTaskQueue(ILogger<BackgroundTaskQueue> logger, IHostApplicationLifetime lifetime)
{
_logger = logger;
lifetime.ApplicationStopped.Register(OnStopping);
lifetime.ApplicationStopping.Register(OnStopping);
}
public void QueueBackgroundWorkItem(Task? workItem, CancellationToken cancellationToken)
{
if (workItem is null or { IsCanceled: true })
return;
_workItems.Enqueue(workItem);
_signal.Release();
}
private void OnStopping()
{
try
{
DequeueAsync(CancellationToken.None).GetAwaiter().GetResult();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "An error has occurred whilst attempting to dequeue the background work que.");
}
}
public async Task<Task?> DequeueAsync(CancellationToken cancellationToken)
{
try
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "An error has occurred whilst attempting to dequeue the background work que.");
}
return null;
}
}

Restricting SendAsync calls to 5 messages per second

I'm implementing Binance's API.
The documentation says:
WebSocket connections have a limit of 5 incoming messages per second. A message is considered:
A PING frame
A PONG frame
A JSON controlled message (e.g. subscribe, unsubscribe)
For ex. there is a simple web socket wrapper such as the one from the official Binance Connector. According to the limitation above, SendAsync should be restricted 5 messages per second. If a few threads call SendAsync 5 times at the same time (including PING frame which is built-in the ClientWebSocket class), it's going to fail. How can I solve the issue with that limitation gracefully? Using bounded channels is a solution?
public class BinanceWebSocket : IDisposable
{
private IBinanceWebSocketHandler handler;
private List<Func<string, Task>> onMessageReceivedFunctions;
private List<CancellationTokenRegistration> onMessageReceivedCancellationTokenRegistrations;
private CancellationTokenSource loopCancellationTokenSource;
private Uri url;
private int receiveBufferSize;
public BinanceWebSocket(IBinanceWebSocketHandler handler, string url, int receiveBufferSize = 8192)
{
this.handler = handler;
this.url = new Uri(url);
this.receiveBufferSize = receiveBufferSize;
this.onMessageReceivedFunctions = new List<Func<string, Task>>();
this.onMessageReceivedCancellationTokenRegistrations = new List<CancellationTokenRegistration>();
}
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (this.handler.State != WebSocketState.Open)
{
this.loopCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
await this.handler.ConnectAsync(this.url, cancellationToken);
await Task.Factory.StartNew(() => this.ReceiveLoop(loopCancellationTokenSource.Token, this.receiveBufferSize), loopCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
}
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
if (this.loopCancellationTokenSource != null)
{
this.loopCancellationTokenSource.Cancel();
}
if (this.handler.State == WebSocketState.Open)
{
await this.handler.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
await this.handler.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken);
}
}
public void OnMessageReceived(Func<string, Task> onMessageReceived, CancellationToken cancellationToken)
{
this.onMessageReceivedFunctions.Add(onMessageReceived);
if (cancellationToken != CancellationToken.None)
{
var reg = cancellationToken.Register(() =>
this.onMessageReceivedFunctions.Remove(onMessageReceived));
this.onMessageReceivedCancellationTokenRegistrations.Add(reg);
}
}
public async Task SendAsync(string message, CancellationToken cancellationToken)
{
byte[] byteArray = Encoding.ASCII.GetBytes(message);
await this.handler.SendAsync(new ArraySegment<byte>(byteArray), WebSocketMessageType.Text, true, cancellationToken);
}
public void Dispose()
{
this.DisconnectAsync(CancellationToken.None).Wait();
this.handler.Dispose();
this.onMessageReceivedCancellationTokenRegistrations.ForEach(ct => ct.Dispose());
this.loopCancellationTokenSource.Dispose();
}
private async Task ReceiveLoop(CancellationToken cancellationToken, int receiveBufferSize = 8192)
{
WebSocketReceiveResult receiveResult = null;
try
{
while (!cancellationToken.IsCancellationRequested)
{
var buffer = new ArraySegment<byte>(new byte[receiveBufferSize]);
receiveResult = await this.handler.ReceiveAsync(buffer, cancellationToken);
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
break;
}
string content = Encoding.UTF8.GetString(buffer.ToArray());
this.onMessageReceivedFunctions.ForEach(omrf => omrf(content));
}
}
catch (TaskCanceledException)
{
await this.DisconnectAsync(CancellationToken.None);
}
}
}
Second way which I'm not 100% sure it solves it
SendAsync is being called in a loop using Channels. SingleReader is set to true, which means there will be only one consumer at a time. It technically should solve the issue, but I'm not 100% sure because the channel might only be limiting the amount in the buffer.
private readonly Channel<string> _messagesTextToSendQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false
});
public ValueTask SendAsync(string message)
{
Validations.Validations.ValidateInput(message, nameof(message));
return _messagesTextToSendQueue.Writer.WriteAsync(message);
}
public void Send(string message)
{
Validations.Validations.ValidateInput(message, nameof(message));
_messagesTextToSendQueue.Writer.TryWrite(message);
}
private async Task SendTextFromQueue()
{
try
{
while (await _messagesTextToSendQueue.Reader.WaitToReadAsync())
{
while (_messagesTextToSendQueue.Reader.TryRead(out var message))
{
try
{
await SendInternalSynchronized(message).ConfigureAwait(false);
}
catch (Exception e)
{
Logger.Error(e, L($"Failed to send text message: '{message}'. Error: {e.Message}"));
}
}
}
}
catch (TaskCanceledException)
{
// task was canceled, ignore
}
catch (OperationCanceledException)
{
// operation was canceled, ignore
}
catch (Exception e)
{
if (_cancellationTotal.IsCancellationRequested || _disposing)
{
// disposing/canceling, do nothing and exit
return;
}
Logger.Trace(L($"Sending text thread failed, error: {e.Message}. Creating a new sending thread."));
StartBackgroundThreadForSendingText();
}
}
I would try to keep it as simple as possible and use Semaphore Slim to achieve this, I have created a class to perform this task.
public class ThrottlingLimiter
{
private readonly SemaphoreSlim _semaphore;
private readonly TimeSpan _timeUnit;
public ThrottlingLimiter(int maxActionsPerTimeUnit, TimeSpan timeUnit)
{
if (maxActionsPerTimeUnit < 1)
throw new ArgumentOutOfRangeException(nameof(maxActionsPerTimeUnit));
if (timeUnit < TimeSpan.Zero || timeUnit.TotalMilliseconds > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(timeUnit));
_semaphore = new SemaphoreSlim(maxActionsPerTimeUnit, maxActionsPerTimeUnit);
_timeUnit = timeUnit;
}
public async Task WaitAsync(CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
ScheduleSemaphoreRelease();
}
private async void ScheduleSemaphoreRelease()
{
await Task.Delay(_timeUnit).ConfigureAwait(false);
_semaphore.Release();
}
}
Now to Use this class, all you have to do is set your limit and timeSpan
public async Task SendData(List<string> allMessages)
{
// Limiting 5 calls per second
ThrottlingLimiter throttlingLimiter = new ThrottlingLimiter(5, TimeSpan.FromSeconds(1));
await Task.WhenAll(allMessages.Select(async message =>
{
await throttlingLimiter.WaitAsync();
try {
await SendInternalSynchronized(message);
// I am not sure what this SendInternalSynchronized returns but I would return some thing to keep a track if this call is successful or not
}
catch (Exception e)
{
Logger.Error(e, L($"Failed to send text message: {message}'. Error: {e.Message}"));
}
});
}
so basically what will happen here is, no matter how big your list is, the ThrottlingLimiter will only send 5 messages per second and wait for the next second to send the next 5 messages.
so, in your case, get all the data from your call to
await _messagesTextToSendQueue.Reader.WaitToReadAsync();
store that into a list or any collection and pass that to the SendData function.

How to re-enqueue a failed background async task

Good day, I'm trying to create a general purpose task queue service that executes in the background using BackgroundService. I avoided using a Delegate function Func<T1,T2, OuT> as input to EnqueueTask(Task<ResponseHelper> newTask) method because I wanted a generic solution, so I opted to pass a Task<ResponseHelper> instead. But this solution does not allow me to re-enqueue a failed task inside ExecuteAsync(CancellationToken stoppingToken) because the task returns an instance of ResponseHelper which is not a copy of the failed task. Kindly help me correct the code I have to return a dequeued task from DequeueTaskAsync(CancellationToken cancellationToken) function instead of an instance of ResponseHelper.
public interface ITaskQueueHelper
{
void EnqueueTask(Task<ResponseHelper> newTask);
Task<ResponseHelper> DequeueTaskAsync(CancellationToken cancellationToken);
}
public class TaskQueueHelper : ITaskQueueHelper
{
private readonly SemaphoreSlim signal;
private readonly ConcurrentQueue<Task<ResponseHelper>> taskQueue;
public TaskQueueHelper()
{
signal = new SemaphoreSlim(0);
taskQueue = new ConcurrentQueue<Task<ResponseHelper>>();
}
public void EnqueueTask(Task<ResponseHelper> newTask)
{
if (newTask == null)
{
throw new ArgumentNullException(nameof(newTask));
}
taskQueue.Enqueue(newTask);
signal.Release();
}
public async Task<ResponseHelper> DequeueTaskAsync(CancellationToken cancellationToken)
{
await signal.WaitAsync(cancellationToken);
taskQueue.TryDequeue(out var currentTask);
/*I need to return currentTask here, instead of an instance of ResponseHelper*/
return await currentTask;
}
}
public class TaskQueueService : BackgroundService
{
private readonly ITaskQueueHelper taskQueue;
private readonly ILogger<TaskQueueService> logger;
public TaskQueueService(
ITaskQueueHelper _taskQueue,
ILogger<TaskQueueService> _logger)
{
logger = _logger;
taskQueue = _taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
ResponseHelper response = await taskQueue.DequeueTaskAsync(stoppingToken);
try
{
if (!response.Status.Equals(ResultCode.Success))
{
/*I need to re-enqueue a failed task here*/
//taskQueue.EnqueueTask(currentTask);
}
}
catch (Exception e)
{
logger.LogError(e, $"Error occurred executing {nameof(TaskQueueService)}");
}
}
}
}
for retrying you could do:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
ResponseHelper response = await taskQueue.DequeueTaskAsync(stoppingToken);
try
{
if (!response.Status.Equals(ResultCode.Success))
{
// Retry the task.
response = await taskQueue.DequeueTaskAsync(stoppingToken);
}
}
catch (Exception e)
{
logger.LogError(e, $"Error occurred executing {nameof(TaskQueueService)}");
}
}
}

How to prepare a console application that pushes and pops into a redis queue?

I need to prepare a console application with 3 buttons, one that adds elements to a Redis queue, one that pops elements out of it and one that displays elements in the queue. I am new to C# and Redis. Can anyone help me with this, or provide me some resources.
I have coded up the connection to the Redis DB and am able to set and get variables.
using System;
using StackExchange.Redis;
namespace RedisConsoleApp1
{
class Program
{
static void Main(string[] args)
{
ConnectionMultiplexer redisCon = ConnectionMultiplexer.Connect("localhost");
IDatabase db = redisCon.GetDatabase();
//db.Lis
db.StringSet("foo", "dog");
string val = db.StringGet("foo");
Console.WriteLine("output is {0}", val);
Console.ReadKey();
}
}
}
Write a channel subscriber like that
public class RedisHostingRunner : HostedService
{
private readonly IServiceProvider _serviceProvider;
IRedisSubscriber _subscriber;
public RedisHostingRunner(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_subscriber = _serviceProvider.GetRequiredService<RedisSubscriber>();
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
//while (!cancellationToken.IsCancellationRequested)
//{
_subscriber.SubScribeChannel();
//await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
//}
}
public Task ShutdownAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
And in your subscriber set a message handler
public void SubScribeChannel()
{
_logger.LogInformation("!SubScribeChannel started!!");
string channelName = _config.ActiveChannelName;
var pubSub = _connectionMultiplexer.GetSubscriber();
try
{
pubSub.Subscribe(channelName, async (channel, message) => await MessageActionAsync(message, channel));
}
catch(Exception ex)
{
_logger.LogInformation(String.Format("!error: {0}", ex.Message));
}
Debug.WriteLine("EOF");
}
In your handler do your job
private async Task MessageActionAsync(RedisValue message, string channel)
{
try
{
Transformer t = new Transformer(_logger);
_logger.LogInformation(String.Format("!SubScribeChannel message received on message!! channel: {0}, message: {1}", channel, message));
string transformedMessage = Transformer.TransformJsonStringData2Message(message);
List<Document> documents = Transformer.Deserialize<List<Document>>(transformedMessage);
await MergeToMongoDb(documents, channel);
_logger.LogInformation("!Merged");
}
catch (Exception ex)
{
_logger.LogInformation(String.Format("!error: {0}", ex.Message));
}
}

.Net Core Queue Background Tasks

Slender answered my original question about what happens to fire and forget, after the HTTP Response is sent, but Now I'm left with the question how to properly queue background tasks
EDIT
As we all know Async void is generally bad, except for in the case when it comes to event handlers, I would like to execute some background logic without have to have the client wait. My original Idea was to use Fire and Forget
Say I have an event:
public event EventHandler LongRunningTask;
And then someone subscribes a fire and forget task:
LongRunningTask += async(s, e) => { await LongNetworkOperation;};
the web api method is call:
[HttpGet]
public async IActionResult GetTask()
{
LongRunningTask?.Invoke(this, EventArgs.Empty);
return Ok();
}
But If I do this my long running task isn't guaranteed to finish, How can I handle running background task without affect the time the time it take to make my request (e.g I don't want to wait for the task to finish first)?
.NET Core 2.1 has an IHostedService, which will safely run tasks in the background. I've found an example in the documentation for QueuedHostedService which I've modified to use the BackgroundService.
public class QueuedHostedService : BackgroundService
{
private Task _backgroundTask;
private readonly ILogger _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILoggerFactory loggerFactory)
{
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger<QueuedHostedService>();
}
public IBackgroundTaskQueue TaskQueue { get; }
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (false == stoppingToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
this._logger.LogError(ex, $"Error occurred executing {nameof(workItem)}.");
}
}
}
}
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
new ConcurrentQueue<Func<CancellationToken, Task>>();
private SemaphoreSlim _signal = new SemaphoreSlim(0);
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
}
public async Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out var workItem);
return workItem;
}
}
Now we can safely queue up tasks in the background without affecting the time it takes to respond to a request.
Just wanted to add some additional notes to #johnny5 answer. Right now you can use https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/ instead of ConcurrentQueue with Semaphore.
The code will be something like this:
public class HostedService: BackgroundService
{
private readonly ILogger _logger;
private readonly ChannelReader<Stream> _channel;
public HostedService(
ILogger logger,
ChannelReader<Stream> channel)
{
_logger = logger;
_channel = channel;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var item in _channel.ReadAllAsync(cancellationToken))
{
try
{
// do your work with data
}
catch (Exception e)
{
_logger.Error(e, "An unhandled exception occured");
}
}
}
}
[ApiController]
[Route("api/data/upload")]
public class UploadController : ControllerBase
{
private readonly ChannelWriter<Stream> _channel;
public UploadController (
ChannelWriter<Stream> channel)
{
_channel = channel;
}
public async Task<IActionResult> Upload([FromForm] FileInfo fileInfo)
{
var ms = new MemoryStream();
await fileInfo.FormFile.CopyToAsync(ms);
await _channel.WriteAsync(ms);
return Ok();
}
}

Categories