问题
I'm working on a .Net core solution that takes backup of storage files from another microservice and because this process takes too long time, we decided to build this routine under a background task.By following this link: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1 I have implemented the background by using Queued background tasks like the following :
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;
}
}
public class QueuedHostedService : BackgroundService
{
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 cancellationToken)
{
_logger.LogInformation("Queued Hosted Service is starting.");
while (!cancellationToken.IsCancellationRequested)
{
var workItem = await TaskQueue.DequeueAsync(cancellationToken);
try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
$"Error occurred executing {nameof(workItem)}.");
}
}
_logger.LogInformation("Queued Hosted Service is stopping.");
}
}
}
and in the controller action method I did that:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult TakeBackup()
{
// Process #1: update latest backup time in setting table.
var _setting = _settingService.FindByKey("BackupData");
var data = JsonConvert.DeserializeObject<BackUpData>(_setting.Value);
data.LatestBackupTime = DateTime.UtcNow;
_setting.Value = JsonConvert.SerializeObject(data);
_settingService.AddOrUpdate(_setting);
// Process #2: Begin a background service to excaute the backup task.
_queue.QueueBackgroundWorkItem(async token =>
{
// instead of this staff I will replace by the API I want to consume.
var guid = Guid.NewGuid().ToString();
for (int delayLoop = 0; delayLoop < 3; delayLoop++)
{
_logger.LogInformation(
$"Queued Background Task {guid} is running. {delayLoop}/3");
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
_logger.LogInformation(
$"Queued Background Task {guid} is complete. 3/3");
// Here I need to redirect to the index view after the task is finished (my issue) ..
RedirectToAction("Index",new {progress="Done"});
});
return RedirectToAction("Index");
}
}
The logger information displays successfully All what I need is to find away to be able to reload the index controller after the background task is done successfully but for some reason I don't know it can't be redirected.
The Index action method is like that :
public async Task<IActionResult> Index()
{
var links = new List<LinkObject>();
var files = await _storageProvider.GetAllFiles(null, "backup");
foreach (var f in files)
{
var file = f;
if (f.Contains("/devstoreaccount1/"))
{
file = file.Replace("/devstoreaccount1/", "");
}
file = file.TrimStart('/');
links.Add(new LinkObject()
{
Method = "GET",
Href = await _storageProvider.GetSasUrl(file),
Rel = f
});
}
return View(links);
}
Thanks !
回答1:
If you want the current page to interact with a long running task, you don't necessarily need the overhead of BackgroundService. That feature is for cases where there is no page to interact with.
First, the server cannot call a client to tell it to reload. At least not without the use of WebSockets, which would definitely be overkill for this. Instead, you will use Javascript (AJAX) to make background calls to poll for the status of your task. This is a common pattern used by any complex web application.
On the server, you'll create a normal async action method that takes all the time it needs to complete the task.
The web page (after it has loaded) will call this action method using AJAX and will ignore the response. That call will eventually time out, but it's not a concern, you don't need the response and the server will continue processing the action even though the socket connection has terminated.
The web page will subsequently begin polling (using AJAX) a different action method which will tell you whether the task has completed or not. You'll need some shared state on the server, perhaps a database table that gets updated by your background task, etc. This method should always return very quickly - all it needs to do is read the present state of the task and return that status.
The web page will continue polling that method until the response changes (e.g. from RUNNING to COMPLETED.) Once the status changes, then you can reload the page using Javascript or whatever you need to do in response to the task completing.
Note: There are some nuances here, like the cost of holding client connections that you expect to time out. If you care you can optimize these away but in most cases it won't be an issue and it adds complexity.
来源:https://stackoverflow.com/questions/53292738/redirect-to-action-after-finishing-background-task-queue