Una feature molto importante del framework ASP.NET MVC ha a che vedere con la capacità di gestire operazioni asincrone a livello di controller. Di fatto il movente è lo stesso che si trova dietro le Asynchronous Pages in ASP.NET 2.0, ovvero evitare che si verifichino condizioni di thread starvation nella nostra Web Application per poi vedersi ritornare un simpatico status code 503 (Server too busy). Come sappiamo, quando una richiesta viene ricevuta dal Web Server, il processamento viene affidato ad un thread del threadpool dell’applicazione. In uno scenario sincrono, tale thread rimane in vita fintantoché non vengono effettuate tutte le operazioni previste.
Quindi, potremmo dire a grandi linee che la pipeline asincrona è preferibile in scenari in cui la logica prevede colli di bottiglia che si formano in corrispondenza di operazioni bloccanti, a causa di latenze non legate strettamente al tempo di CPU (es. chiamate ad un WS).
Partendo dal presupposto che una richiesta asincrona possiede lo stesso tempo di esecuzione di una richiesta sincrona, minimizzare il numero di thread in attesa di operazioni bloccanti è una pratica molto apprezzata dal Web Server quando una Web Application viene letteralmente bombardata da centinaia di richieste concorrenti.
Vediamo ora un esempio molto banale di controller asincorno, in cui l’action List è stata creata secondo il pattern asincrono per gestire chiamate “lunghe” ad un Web Service:
public class CustomersController : AsyncController
{
[AsyncTimeout(10000)]
public void ListAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task.Factory.StartNew(() =>
{
try { AsyncManager.Parameters["result"] = new MyServiceClient().GetCustomers(); }
catch (Exception ex) { ... }
finally { AsyncManager.OutstandingOperations.Decrement(); }
);
}
public ActionResult ListCompleted(List<Customer> result)
{
return View("List", result);
}
...
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.Exception is TimeoutException)
{
filterContext.Result = RedirectToAction("TryAgainLater");
filterContext.ExceptionHandled = true;
}
base.OnException(filterContext);
}
}
Alcune importanti osservazioni:
- Un controller asincrono deve ereditare la classe AsyncController. (Gli action methods sincroni sono comunque supportati)
- Il nome del metodo del controller che scatena l’operazione asincrona deve essere seguito dal suffisso “Async”.
- Il nome del metodo del controller che viene invocato dal framework quando l’operazione asincrona viene completata deve essere seguito dal suffisso “Completed”
- Sebbene il pattern asincrono consiste di due metodi, l’invocazione da browser segue la stessa route che si utilizzerebbe per le action sincrone ( …/Customers/List )
- La proprietà AsyncManager.Parameters è un IDictionary<string,object> che permette di definire i parametri che vengono passati al metodo xxxCompleted, evitando allo sviluppatore l’onere di definire delle variabili a livello di classe che siano visibili da entrambi i metodi xxxAsync e xxxCompleted. Occorre inoltre porre attenzione al nome che si assegna alle key dei parametri. Essi devono infatti corrispondere a quelli dichiarati dal metodo xxxCompleted.
- I metodi AsyncManager.OutstandingOperations.Increment(…) e AsyncManager.OutstandingOperations.Decrement() sono necessari rispettivamente per determinare il numero di operazioni asincrone che il controller deve ancora completare e che ha completato. Attenzione a non ometterli ed a gestirli in modo consistente!!!
- L’attributo AsyncTimeout specificato a livello di metodo xxxAsync è utile per definire un timeout per le operazioni a lunga latenza (default: 45 secondi). Se lo si vuole eliminare si può utilizzare il filtro [NoAsyncTimeout] oppure l’equivalente [AsyncTimeout(Timeout.Infinite)]. Se invece lo si vuole gestire per fornire un feedback diverso rispetto al global exception handler di ASP.NET, è possibile svilupparsi un exception filter personalizzato oppure semplicemente fare l’override del metodo OnException(…) del controller, in modo da gestire l’eccezione in modo custom (come in questo caso).
Non è tutto oro ciò che luccica…
- I controller asincroni sono significativamente più complessi rispetto a quelli sincroni, oltre ad essere difficilmente testabili.
- Non ha alcun senso utilizzare controller asincroni se si vogliono semplicemente eseguire dei task paralleli legati alle prestazioni della CPU.
- Occorre assicurarsi che il nostro Web Server sia opportunamente configurato per beneficiare della gestione asincrona delle richieste web: ad esempio, se usiamo IIS7 dobbiamo configurare correttamente il valore della proprietà MaxConcurrentRequestsPerCPU nella sezione <applicationPool> di <system.web>, impostandolo ad un valore sufficientemente alto.