Situation: there is an asp.net mvc application. There is a button by clicking on which starts the process of generating a certain report. A report can be generated for quite some time, for example, several minutes. After starting the generation, it is also necessary to show the user a modal window with the progress of generation and the possibility of cancellation. I implemented it like this.
private static readonly IList<DownloadTask> DownloadStates = new List<DownloadTask>(); public ActionResult Export() { var task = new DownloadTask(Guid.NewGuid()); DownloadStates.Add(task); string zipFilename = string.Format("{0}.zip", task.Id); GenerateAsync(zipFilename, task); return Json(task.Id, JsonRequestBehavior.AllowGet); } This action is called when you click on the report generation button. This is what happens here. First, a task is created. The code for this class looks like this.
public class DownloadTask { public DownloadTask() { } public DownloadTask(Guid id) { Id = id; State = DownloadState.NotStarted; CancellationTokenSource = new CancellationTokenSource(); } public Guid Id { get; set; } public double CurrentProgress { get; set; } // состояние задачи - запущена, отменена, ошибка, завершена и тд public DownloadState State { get; set; } } The task is placed in the DownloadStates collection.
Here is the code for the asynchronous method GenerateAsync
public async Task<Guid> GenerateAsync(string filename, DownloadTask currentTask) { await Task.Factory.StartNew(() => { try { currentTask.State = DownloadState.Processing; foreach (var page in pages) { // обновление прогресса currentTask.CurrentProgress = // ... // обработка данных } currentTask.State = DownloadState.Completed; } catch { currentTask.State = DownloadState.Faulted; // обработка ошибки } }).ConfigureAwait(false); return currentTask.Id; } That is, clicking on the button we send a request to generate a report. A task is created, a unique Guid is assigned to it, the task is placed in the DownloadStates task collection and after that the asynchronous GenerateAsync method is launched. After starting this method, the server sends the created task to the client in the form of a response. The asynchronous method that is important is started without await on the principle of fire and forget. Further communication with it by the generation process takes place through the DownloadStates collection.
After that, when the server responds with the task ID, the client starts the setInterval function in which a request is sent to the server every 200 ms to find out the progress of the task and display this progress in a modal window. Here is the action code for getting current progress.
public ActionResult Progress(Guid taskId) { var task = DownloadStates.FirstOrDefault(x => x.Id == taskId); string state = ""; bool isFaulted = false; bool isCancelled = false; if (task != null) { switch (task.State) { case DownloadState.Faulted: isFaulted = true; DownloadStates.Remove(task); break; case DownloadState.Processing: state = "processing"; break; // ... прочие ветки } } else isFaulted = true; return Json(new { progress = task != null && task.State != DownloadState.Completed ? task.CurrentProgress * 100 : 100, text = state, isFaulted, isCancelled }, JsonRequestBehavior.AllowGet); } Here the following happens: if the task is executed, then the response returns the current progress to the client (it is stored in the taskbar in the DownloadStates collection and is updated inside the asynchronous method). If an exception occurs during the execution of the asynchronous method, the client returns isFaulted = true and the client receives this value, stops the timer and stops sending requests for progress (the task ended with an error) and displays an error message. If the task has the status Completed, then the progress is set to 100% and the timer on the client also stops and then the request to load the generated report is executed.
In short, it works like this. This scheme really works, but I don’t have much experience with writing such asynchronous code and that’s why I’m tortured with doubts if I did everything right? For example, when calling the GenerateAsync asynchronous method, I don’t call await so as not to wait for it to complete, but immediately return the new task ID to the client. Therefore, I can find out about the error only by setting the isFaulted property of my tasks in the collection stored in the controller and reading this property when receiving the current progress.
I would like to hear the opinion of more experienced programmers about the potential problems of my code (memory leaks, incomplete tasks, unhandled exceptions, maybe you had to put a lock somewhere or could block something or something else?). Are there any weak points here (and I’m sure they are) that it would be worthwhile to improve, especially considering the multi-threaded execution and the potential launch of several tasks at the same time. Thank you in advance!
PS Unfortunately, the use of SignalR turned out to be impossible for reasons beyond my control, so I cannot consider the option to do everything on SignalR
DownloadStates- IgorList<>says: "Any instance members are not guaranteed to be thread safe." - Pavel MayorovConcurrentDictionary<,>solves two problems at once - thread safety and a long list search with a large number of tasks. - Pavel Mayorov