from this sample: here
Using the following queue.
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
public 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;
}
}
Calling it from within an api controller but need to be able to check on its status.
Controller:
var guid = Guid.NewGuid().ToString();
queue.QueueBackgroundWorkItem(async token=>{
await Task.Delay(TimeSpan.FromSeconds(60), token);
});
How do I associate the guid to the task and check on it later?
Related
I am trying to learn to effectively use Minimal API's. How can I reduce duplicate codes, how to create base Endpoint class and do the most job there?
Especially, how to use Mediatr from a static reference and not put in every methods parameter?
My endpoint:
public static class ArticleEndpoints
{
public static void MapArticleEndpoints(this WebApplication app)
{
var articles = app.MapGroup("Articles");
articles.MapPost(nameof(CreateAsync), CreateAsync)
.AddEndpointFilter<ValidationFilter<CreateArticleCommand>>()
.WithName(nameof(CreateAsync))
.Produces(StatusCodes.Status201Created, typeof(Guid))
.ProducesValidationProblem()
.WithOpenApi();
articles.MapPut(nameof(AddTagToArticleAsync), AddTagToArticleAsync)
.AddEndpointFilter<ValidationFilter<AddTagToArticleCommand>>()
.WithName(nameof(AddTagToArticleAsync))
.Produces(StatusCodes.Status200OK, typeof(ArticleGetByIdDto))
.ProducesValidationProblem()
.WithOpenApi();
articles.MapGet(nameof(GetAllAsync), GetAllAsync)
.WithName(nameof(GetAllAsync))
.Produces(StatusCodes.Status200OK, typeof(IReadOnlyCollection<ArticleGetAllDto>))
.WithOpenApi();
articles.MapGet(nameof(GetByIdAsync), GetByIdAsync)
.WithName(nameof(GetByIdAsync))
.Produces(StatusCodes.Status200OK, typeof(TagGetByIdDto))
.WithOpenApi();
}
public static async Task<IResult> CreateAsync([FromBody] CreateArticleCommand command, IMediator mediator, CancellationToken cancellationToken)
{
var id = await mediator.Send(command, cancellationToken);
return Results.Created(nameof(CreateAsync), new { id });
}
public static async Task<IResult> AddTagToArticleAsync([FromBody] AddTagToArticleCommand command, IMediator mediator, CancellationToken cancellationToken)
{
var result = await mediator.Send(command, cancellationToken);
return Results.Ok(result);
}
public static async Task<IResult> GetAllAsync(IMediator mediator, CancellationToken cancellationToken)
{
var result = await mediator.Send(new GetAllArticlesQuery(), cancellationToken);
return Results.Ok(result);
}
public static async Task<IResult> GetByIdAsync([FromBody] GetArticleByIdIncludeTagsQuery query, IMediator mediator, CancellationToken cancellationToken)
{
var result = await mediator.Send(query, cancellationToken);
return Results.Ok(result);
}
}
I saw the implementation of Stephen Cleary's Disposables NuGet package and it seems like it's perfect in my case, even tho, I couldn't find examples on how to inherit from it.
My idea is to make UnsubscribeAsync().GetAwaiter().GetResult(); to await UnsubscribeAsync();, which means it should be wrapped into an IAsyncDisposable. How could I achieve that with a sealed class?
public sealed class LiveTradeManager : ITradeManager, IDisposable
{
private bool _disposed;
private readonly ILogger<LiveTradeManager> _logger;
private readonly TradeOptions _tradeOptions;
private readonly IBotClient _client;
private string _listenKey;
private UpdateSubscription _candleSubscription, _accountUpdateSubscription;
private IDictionary<string, Channel<IBinanceStreamKlineData>> _channels;
public LiveTradeManager(ILogger<LiveTradeManager> logger, IOptions<TradeOptions> tradeOptions, IOptions<ExchangeOptions> exchangeOptions, IBotClientFactory clientFactory)
{
_logger = logger;
_tradeOptions = tradeOptions.Value;
_client = clientFactory.GetBotClient(exchangeOptions.Value.BotClientType);
}
public bool IsPaused { get; set; }
public async Task RunAsync(CancellationToken cancellationToken)
{
try
{
await SubscribeAsync(cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
}
catch (Exception ex) when (Handle(() => _logger.LogError(ex, "Unexpected error.")))
{
}
}
private async Task SubscribeAsync(CancellationToken cancellationToken)
{
// Subscribe to account updates
_listenKey = await _client.GetListenKeyAsync(cancellationToken).ConfigureAwait(false);
void OnOrderUpdate(BinanceStreamOrderUpdate order)
{
// order update logic
}
_accountUpdateSubscription = await _client.SubscribeToUserDataUpdatesAsync(_listenKey, OnOrderUpdate).ConfigureAwait(false);
_ = Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
await _client.KeepAliveListenKeyAsync(_listenKey, cancellationToken).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMinutes(30), cancellationToken).ConfigureAwait(false);
}
}, cancellationToken);
// Subscribe to candle updates
var symbols = _tradeOptions.Symbols.Select(x => x.ToString()).ToList();
_channels = symbols.ToDictionary(x => x, _ =>
Channel.CreateBounded<IBinanceStreamKlineData>(new BoundedChannelOptions(1)
{FullMode = BoundedChannelFullMode.DropOldest}));
async void OnCandleReceived(IBinanceStreamKlineData data)
{
if (IsPaused) return;
try
{
var ohlcv = data.Data.ToCandle();
if (data.Data.Final)
{
_logger.LogInformation(
$"[{data.Symbol}] Finalized candle | Open time: {ohlcv.Timestamp.ToDateTimeFormat()} | Price: {ohlcv.Close}");
_ = Task.Run(async () =>
{
await Task.Delay(10000, cancellationToken).ConfigureAwait(false);
}, cancellationToken);
}
else
{
_logger.LogInformation(
$"[{data.Symbol}] Candle update | Open time: {ohlcv.Timestamp.ToDateTimeFormat()} | Price: {ohlcv.Close}");
await _channels[data.Symbol].Writer.WriteAsync(data, cancellationToken).ConfigureAwait(false);
}
}
catch (TaskCanceledException)
{
}
}
_candleSubscription = await _client
.SubscribeToCandleUpdatesAsync(symbols, KlineInterval.OneMinute, OnCandleReceived)
.ConfigureAwait(false);
var tasks = _channels.Values.Select(async channel =>
{
await foreach (var data in channel.Reader.ReadAllAsync(cancellationToken))
{
// long-running logic...
await Task.Delay(10000, cancellationToken).ConfigureAwait(false);
}
});
// NOTE: this would block further logic
await Task.WhenAll(tasks).ConfigureAwait(false);
}
private async Task UnsubscribeAsync()
{
// Unsubscribe account updates
if (!string.IsNullOrEmpty(_listenKey))
{
await _client.StopListenKeyAsync(_listenKey).ConfigureAwait(false);
}
if (_accountUpdateSubscription != null)
{
await _client.UnsubscribeAsync(_accountUpdateSubscription).ConfigureAwait(false);
}
// Unsubscribe candle updates
if (_candleSubscription != null)
{
await _client.UnsubscribeAsync(_candleSubscription).ConfigureAwait(false);
}
// Channels
if (_channels != null)
{
foreach (var channel in _channels.Values)
{
channel.Writer.Complete();
}
_channels.Clear();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnsubscribeAsync().GetAwaiter().GetResult();
}
_disposed = true;
}
}
public class BotManagerService : BackgroundService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IDiscordClient _discordClient;
private readonly ITradeManager _tradeManager;
public BotManagerService(
IHostApplicationLifetime hostApplicationLifetime,
IOptions<ExchangeOptions> options,
IDiscordClient discordClient,
ITradeManagerFactory tradeManagerFactory)
{
_hostApplicationLifetime = hostApplicationLifetime;
_discordClient = discordClient;
_tradeManager = tradeManagerFactory.GetTradeManager(options.Value.TradeManagerType);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var task1 = _tradeManager.RunAsync(stoppingToken);
var task2 = _discordClient.StartAsync();
await Task.WhenAll(task1, task2).ConfigureAwait(false);
}
finally
{
_hostApplicationLifetime.StopApplication();
}
}
}
I couldn't find examples on how to inherit from it.
Nito.Disposables, like the vast majority of the code I write, is written for composition rather than inheritance.
So, if you have a type that needs to implement IAsyncDisposable, it should contain an IAsyncDisposable implementation and forward its interface methods to that contained object:
public sealed class LiveTradeManager : ITradeManager, IAsyncDisposable
{
private readonly AsyncDisposable _disposable;
...
public LiveTradeManager(...)
{
...
_disposable = new(async () => await UnsubscribeAsync());
}
public ValueTask DisposeAsync() => _disposable.DisposeAsync();
}
I am implementing queues using https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio example.
this is how my code looks:
in startup.cs I am adding my hosted service and background queue
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
then I implement scoped service, hosted service and background queue as following:
namespace Services.Services {
public class QueuedHostedService: BackgroundService {
private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
public QueuedHostedService(IServiceProvider serviceProvider, IBackgroundTaskQueue taskQueue, ILoggerFactory loggerFactory) {
_serviceProvider = serviceProvider;
TaskQueue = taskQueue;
_logger = loggerFactory.CreateLogger < QueuedHostedService > ();
}
public IBackgroundTaskQueue TaskQueue {
get;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try {
await workItem(cancellationToken);
} catch (Exception ex) {
}
}
}
}
}
public interface IBackgroundTaskQueue {
void QueueBackgroundWorkItem(Func < CancellationToken, Task > workItem);
Task < Func < CancellationToken, Task >> DequeueAsync(CancellationToken cancellationToken);
}
namespace Services.Services {
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;
}
}
}
// scoped service
namespace Services.Services {
public class ImportService: BaseService, IImportService {
private readonly IFileProcessingService _scopedProcessingService;
private readonly ConfigurationSettings _configurationSettings;
public IBackgroundTaskQueue Queue {
get;
}
private
const string AZURE_BLOB_CONTAINER = "blobcontainer";
public IServiceProvider Services {
get;
}
public ImportService(IServiceProvider services, IBackgroundTaskQueue queue): base(services) {
Services = services;
_configurationSettings = services.GetService < ConfigurationSettings > ();
_scopedProcessingService = services.GetProcessingService();
Queue = queue;
}
// ---- Main file
public async Task ImportFile(string filePath, long fileSize, int userId, FileFormatType fileFormat, TransactionsDataHeadersMap dataHeadersMap, string delimiter, string dateFormat) {
await _scopedProcessingService.ImportFile(filePath, fileSize, userId, fileFormat, dataHeadersMap, delimiter, dateFormat);
}
public async Task UploadToBlobStorage(IFormFile file, int userId, TransactionalDataFileType type) {
var fileFormat = GetFileFormat(file);
var tempFilePath = await GetTemporaryPath(file);
var fileName = userId.ToString() + "-" + DateTime.Now + "." + fileFormat;
// ....... //
ProcessFile(tempFilePath, fileFormat, file, type, userId);
}
private void ProcessFile(string tempFilePath, FileFormatType fileFormat, IFormFile file, Tyoe type, int userId) {
var delimiter = ",";
Queue.QueueBackgroundWorkItem(async token => {
using(var scope = Services.CreateScope()) {
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService < IFileProcessingService > ();
// do the processing
switch (type) {
case "csv":
await scopedProcessingService.ImportFile(tempFilePath, file.Length, userId, fileFormat, new Headers(), delimiter ? ? ",", "yyyy-MM-dd");
break;
}
}
});
}
}
}
I am adding elemeents to queue on request in controller. Now I want to add another queue for pocessing other requests. Is it possible to use another queue using same Hosted service? I have trouble finding examples how to do that. Should I just add another scoped servide and another background queue?
The first option is the most straightforward - you just create bunch of classes and interfaces QueuedHostedServiceA, QueuedHostedServiceB, IBackgroundTaskQueueA.. (you can use inheritance to reduce code duplication)
Also you can introduce concept of "handler" and make all this stuff generic:
interface IHandler<T> { Task Handle(T msg, CancelationToken ...)}
interface IBackgroundMessageQueue<T> {...} // same impl but with T instead of Func<CancellationToken,Task>
class IBackgroundMessageQueue<T> {...} // same impl but with T instead of Func<CancellationToken,Task>
class QueuedHostedService<T>
{
public QueuedHostedService(..., IBackgroundMessageQueue<T> queue, IHandler<T> h) {... }
protected override async Task ExecuteAsync(CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
T message = await queue.DequeueAsync(cancellationToken);
try {
using(var scp = serviceProvider.CreateScope())
{
var handler = ServiceProvider.GetRequiredService<IHandler<T>>;
await handler.Handle(message, cancellationToken);
}
} catch (Exception ex) {
}
}
}
}
And for each message type you create your own handler:
class ProcessFile(string tempFilePath, FileFormatType fileFormat, IFormFile file, Tyoe type, int userId){}
FileProcessor: IHandler<ProcessFile> {implement your logic from ImportService.ProcessFile}
Then you register everything:
services.AddScoped<IHandler<ProcessFile>, FileProcessor>()
services.AddSingleton<IBackgroundTaskQueue<ProcessFile>, BackgroundTaskQueue<ProcessFile>>();
services.AddHostedService<QueuedHostedService<ProcessFile>>();
and in your ImportService you resolve typed queue:
public ImportService(IBackgroundMessageQueue<ProcessFile> queue)
and enqueue message in it when needed.
For example, I have the following methods:
private async Task<T> Read<T>(string id, string endpoint)
{
//....
}
private async Task<List<T>> List<T>(int start, int count, string endpoint, List<FilterData> filterData = null)
{
//....
}
(and more with different properties)
But all of these method can throw BillComInvalidSessionException
If I called method throws this exception, I want to execute some logic and recall called method.
I.e.:
private async Task<T> ReadWithRetry<T>(string id, string endpoint)
{
try
{
return await Read<T>(id, endpoint);
}
catch (BillComInvalidSessionException)
{
SessionId = new Lazy<string>(() => LoginAsync().Result);
return await ReadWithRetry<T>(id, endpoint);
}
}
private async Task<List<T>> ListWithRetry<T>(int start, int count, string endpoint, List<FilterData> filterData = null)
{
try
{
return await List<T>(start, count, endpoint, filterData);
}
catch (BillComInvalidSessionException)
{
SessionId = new Lazy<string>(() => LoginAsync().Result);
return await ListWithRetry<T>(start, count, endpoint, filterData);
}
}
How to create one common method, which will execute the same logic, but get different methods as parameters?
You can achieve this by using a generic delegate:
private async Task<T> Retry<T>(Func<Task<T>> func)
{
try
{
return await func();
}
catch (BillComInvalidSessionException)
{
SessionId = new Lazy<string>(() => LoginAsync().Result);
return await Retry(func);
}
}
And then your retry methods would turn to:
private async Task<T> ReadWithRetry<T>(string id, string endpoint)
{
return await Retry(async () => await Read<T>(id, endpoint));
}
private async Task<List<T>> ListWithRetry<T>(int start, int count, string endpoint, List<FilterData> filterData = null)
{
return await Retry(async () => await List<T>(start, count, endpoint, filterData));
}
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();
}
}