Reputation: 1136
I have method A (a Post request using System.Web.Mvc.Controller ) that calls (and awaits) the (long running) asynchronous method B (which creates PDF files).
If, in the mean time (while method A and B are still running) method C (another Post request) is called, method C should wait until method B has finished, because it needs the results (the PDF files) from that method.
I could call method B again from method C, but that would be redundant, since it is already running...
Which concepts exist to achieve this?
Edit
Some simplified sample code to show what I have now:
public class OrderController : Controller
{
private MyContext _context; //Set by Dependency Injection
[HttpPost]
public async Task<IActionResult> SaveOrder(Order order) {
_context.Entry(order).Status = Modified;
_context.SaveChanges();
_ = GenerateAndSavePDF(order).ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
// Ideally, if the SaveOrder method is called multiple times in a short period (when the PDF generation is already running from the previous save), the already running tasks should be stopped and only the latest one should be run (in order to get a PDF with the latest order information)
return View(order);
}
public async Task GenerateAndSavePDF(Order order) {
var pdf = ...... // code to generate pdf
order.pdf = pdf;
_context.SaveChanges();
}
[HttpPost]
public async Task SendEmail(int orderId) {
Order = context.Orders.FirstOrDefault(order => order.Id = orderId);
var mail = ...... // code to generate mail;
mail.attachment = order.pdf; // <==== This is where I need to wait for method GenerateAndSavePdf to be finished before I can send the mail
mail.Send();
}
}
So if a user saves an order, the PDF gets generated. But if in the mean time, the users also chooses to send the email, the SendEmail method has to wait until the PDF is available...
Upvotes: 2
Views: 1114
Reputation: 101150
Definitions:
First of all, I would just have let Method A wait until B is ready and then return the result for it. That would work well and is the easiest solution.
If that's not what you want, I would see Method A as a job initiator which just returns a queue ticket which can be used to check the result.
To enable that, the class which contains method B must keep track of tickets and which Task
they correspond to.
So you would have something like this in your controller:
public Task<string> MethodA(MyViewModel model)
{
var businessEntity = Convert(model);
return _pdfService.GenerateBuild(businessEnitty);
}
public Task<PdfContent> MethodC(string ticket)
{
if (_pdfService.TryGet(ticket, out var pdf)
return StreamContent(pdf.Stream);
return HttpCode(425); //Too early
}
The return types/methods are incorrect, but I think that you can find the correct ones.
Then in the PDF service where method B is, you need to manage a queue. Also note that this class must be registered as a single instance in the IoC container, as it should survive between http requests (a new controller instance is created for every request).
Here is a very simple example:
public class PdfService
{
Dictionary<string, Task> _workMap = new Dictionary<string, Task>();
public string StartGeneration(PdfModel model)
{
var pdfBuilder = new PdfBuilder();
var task = pdfBuildr.BuildAsync(model);
var ticket = Guid.NewGuid().ToString("N");
_workMap[ticket] = task;
return ticket;
}
public bool TryGet(string ticket, out PdfDocument doc)
{
var task = _workMap[ticket);
if (task.IsCompleted)
{
doc = task.Result;
return true;
}
return false;
}
}
Something like that. Do note that the code does not compile. It's here to illustrate a path that you can take.
Upvotes: 0
Reputation: 1761
Oversimplified example of what you need:
public class ExampleTaskTracker
{
// the task we're awaiting if there one
Task<object> task;
// initiates the process
public void Start()
{
if (task == null || task.IsCompleted)
task = Task.Delay(5000).ContinueWith((o) => new object());
}
// waits the process to finish if there's one
public async Task<object> End()
{
if (task == null)
return null;
return await task;
}
}
Also don't forget to use CancellationToken if you want to cancel the task at a certain point
Upvotes: 1
Reputation: 4119
async/await
is what you need.
Just make sure each of described metods returns either Task
or Task<T>
(and never void
) and var pdf = await GeneratePDF()
await it throughtout a whole callstack. That's it, as simple as that.
Keep in mind that you have few embarrassingly simple combinators in the TPL
: Task.WhenAny(...)
and .WhenAll()
to introduce more complicated behaviours. For example, .WhenAny(Task.Delay(timeout), ...)
is perfect for timeout-guarding long-running operations.
Another important options is CancellationToken
which can be passed over to make your async operations cancellable. Say, timeout fired and you'd like to (at least, try to) stop background operation rather then let it burn your CPU uselessly; without CancellationToken
it wouldn't be an easy goal, but luckily it becomes dead simple when CancellationToken
presents.
Upvotes: 0