Gestire eccezioni nei thread asincroni di WCF

Un servizio WCF fa normalmente uso di I/O threads per gestire una richiesta in ingresso; in particolare, un servizio WCF che gira in IIS gestirà le richieste utilizzando normalmente 2 thread:

  • un worker thread gestito da ASP.NET tramite il CLR thread pool
  • un I/O thread gestito con l’IOThreadScheduler di WCF

Il primo provvede allo “spawning” del secondo mettendosi in attesa del completamente dell’operazione asincrona. Se questo tipo di gestione può sembrare un po’ complicato sotto certi aspetti, sicuramente offre indubbi vantaggi di scalabilità e prestazioni del servizio; tuttavia non voglio aggiungere alteriori dettagli riguardo alla gestione dei thread di WCF, dal momento che con questo post vorrei trattare le possibili opzioni che abbiamo qualora volessimo gestire le eccezioni che si verificano nei thread asincroni di WCF.

Supponiamo che in uno degli I/O thread si verifichi un’eccezione, non necessariamente nel codice scritto da noi: con buona probabilità questa porterà al crash del processo (es. AccessViolationException) o anche semplicemente ad un disservizio (es. ThreadAbortException). Gli strumenti per gestire questo tipo di eccezioni sono 3:

  1. Appdomain.UnhandledException
  2. System.ServiceModel.Dispatcher.ExceptionHandler.AsynchronousThreadExceptionHandler
  3. System.ServiceModel.Dispatcher.ExceptionHandler.TransportExceptionHandler

Appdomain.UnhandledException

L’evento UnhandledException si verifica quando l’applicazione è in presenza di un’eccezione non catchata: è comunque importante dire che la sua gestione non offre alcun modo di impedire la terminazione dell’applicazione. Pur essendo un evento a livello di AppDomain, non è affatto detto che occorra soltanto nell’AppDomain in cui si verifica l’eccezione non gestita: ci sono infatti differenze di comportamento a seconda che il thread in cui si verifica l’eccezione appartenga al default AppDomain oppure no. Per dettagli sul comportamento dell’AppDomain.UnhandledException rimando comunque ad MSDN che offre una descrizione completa e dettagliata.
Pur non riguardando strettamente WCF, le eccezioni che si verificano nei thread asincroni possono essere gestite con questo evento, pertanto l’ho incluso nella trattazione. Ecco un esempio (da MSDN):

 using System;
using System.Security.Permissions;

public class Test {

   [SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.ControlAppDomain)]
   public static void Example()
   {
      AppDomain currentDomain = AppDomain.CurrentDomain;
      currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);

      try {
         throw new Exception("1");
      } catch (Exception e) {
         Console.WriteLine("Catch clause caught : " + e.Message);
      }

      throw new Exception("2");

      // Output:
      //   Catch clause caught : 1
      //   MyHandler caught : 2
   }

   static void MyHandler(object sender, UnhandledExceptionEventArgs args) {
      Exception e = (Exception) args.ExceptionObject;
      Console.WriteLine("MyHandler caught : " + e.Message);
   }

   public static void Main() {
      Example();
   }
}

System.ServiceModel.Dispatcher.ExceptionHandler.AsynchronousThreadExceptionHandler

System.ServiceModel.Dispatcher definisce un punto di estensibilità, che è appunto la classe ExceptionHandler, al fine di permettere la creazione di un exception handler per le eccezioni non gestire che si verificano nel runtime. In particolare è possibile definire un proprio ExceptionHandler ed assegnarlo alla propretà statica AsynchronousThreadExceptionHandler al fine di customizzare la gestione delle eccezioni che si verificano nei thread asincroni di WCF. Attenzione: questa proprietà ha effetto sui thread asincroni di WCF e non su qualunque thread del CLR thread pool utilizzato dal nostro servizio.

L’AsynchronousThreadExceptionHandler viene impostato a livello di AppDomain. By default le eccezioni che si verificano nei thread asincroni possono causare il crash dell’AppDomain: impostando un proprio ExceptionHandler con la proprietà AsynchronousThreadExceptionHandler  è possibile gestire ogni eccezioni e, a differenza dell’AppDomain.UnhandledException, persino impedire il crash dell’AppDomain! E’ importante far presente che spesso la terminazione dell’applicazione è la soluzione migliore, perché l’eccezione potrebbe essere la conseguenza di uno stato corrotto/instabile di essa, quindi gestire l’eccezione senza farla terminare spesso non è la soluzione migliore.

Ecco un esempio (da MSDN):

 static void Main(string[] args)
   {
       // Create an instance of the MyExceptionHander class.
       MyExceptionHandler thisExceptionHandler =
           new MyExceptionHandler();

       // Enable the custom handler by setting 
       //   AsynchronousThreadExceptionHandler property
       //   to this object.
       ExceptionHandler.AsynchronousThreadExceptionHandler = 
           thisExceptionHandler;

       // After the handler is set, write your call to 
       // System.ServiceModel.ICommunication.Open here
   }

La AsynchronousThreadExceptionHandler può essere impostata defininendo per esempio un costruttore statico del servizio WCF, in modo che il custom ExceptionHandler sia assegnato quando la prima istanza del servizio viene creata. Se il servizio WCF gira in IIS con la ASP.NET compatibility, è possibile impostare il nostro ExceptionHandler nel global.asax.

System.ServiceModel.Dispatcher.ExceptionHandler.TransportExceptionHandler

La proprietà TransportExceptionHandler permette di gestire eccezioni che si verificano durante la fase di trasporto dei dati (es. gestione del loop delle connessioni in ingresso) a livelo di AppDomain. By default tali eccezioni vengono ignorate, quindi non causano mai il crash dell’AppDomain. Tali eccezioni sono gestibili anche con l’AsynchronousThreadExceptionHandler, nel senso che le eccezioni gestibili con la TransportExceptionHandler sono un sottoinsieme di quelle gestibili con l’AsynchronousThreadExceptionHandler: è sufficiente impostare TransportExceptionHandler a null per far sì che le eccezioni non vengano più gestite a quel livello e quindi potenzialmente dall’AsynchronousThreadExceptionHandler (se l’abbiamo definito naturalmente).

Per il resto vale quanto detto riguardo all’AsynchronousThreadExceptionHandler.

Andrea Liberatore

Senior Support Engineer

Developer Support Core