HttpApplication Instanzen…

22. August 2011

Man ist ja irgendwie von WinForms, WPF und Silverlight gewöhnt: In jeder Anwendung hat man eine globale “Application” Klasse die man als Singleton ansprechen kann um darüber an anwendungsglobale Informationen oder Benachrichtigungen zu kommen.

Das Pendant unter ASP.NET ist die Klasse HttpApplication, bzw. die global.asax, mit der man eine Ableitung davon bereitstellen kann. Allerdings: Verglichen mit den anderen Plattformen leidet diese Klasse unter einer Identitätskrise. Und ich stolpere immer wieder über Entwickler, die sich die Gründe dafür nicht ausreichend klar machen…

Der Reihe nach…

Als ASP.NET Entwickler kommt man irgendwann in die Situation, irgendwelche Dinge global erledigen zu wollen, entweder in einem HTTP Modul (Module werden von der HttpApplication Klasse verwaltet), oder eben in der global.asax.

Ein einfaches Beispiel: Ich möchte die Requests zählen und die URLs in eine Log-Datei schrieben, etwa so:

   1: public class Statistics   

   2: {   

   3:     StreamWriter _Writer;   

   4:     int _RequestCount;   

   5:  

   6:     public Statistics(HttpApplication context)   

   7:     {   

   8:         context.BeginRequest += new EventHandler(Context_BeginRequest);   

   9:         string logfile = context.Server.MapPath(@"debugrequests.txt");  

  10:         _Writer = new StreamWriter(logfile);  

  11:     }  

  12:  

  13:     public void Close()  

  14:     {  

  15:         _Writer.Close();  

  16:     }  

  17:  

  18:     void Context_BeginRequest(object sender, EventArgs e)  

  19:     {  

  20:         HttpRequest request = ((HttpApplication)sender).Context.Request;  

  21:         ++_RequestCount;  

  22:         _Writer.WriteLine(_RequestCount + ": " + request.Url);  

  23:     }  

  24: }

und die Anmeldung in der global.asax:

   1: public class Global : System.Web.HttpApplication   

   2: {   

   3:     static Statistics _Statistics;   

   4:  

   5:     public override void Init()   

   6:     {   

   7:         base.Init();   

   8:         _Statistics = new Statistics(this);   

   9:     }  

  10:    

  11:     public override void Dispose()  

  12:     {  

  13:         _Statistics.Close();  

  14:         _Statistics = null;  

  15:         base.Dispose();  

  16:     }

Typische reale Anwendungsfälle wären hier ein Tracing mit log4net, einen Last-Chance-Exception-Handler anmelden, oder SLA-Statistiken aufzusetzen. (Meine Empfehlung wäre, das in einem Modul zu tun, aber das ist für das hier betrachtete Thema irrelevant.)

Der obige Code hat allerdings ein kleines Problem: Er läuft nicht! Warum? Weil eine HttpApplication Instanz eben kein Singleton ist!

Maschinen…

Der Vollständigkeit halber und sicher der trivialste Fall: In Web-Farmen braucht natürlich jeder Rechner seine eigene Instanz. Aber auch wenn der Fakt an sich offensichtlich ist, wird er selten genug berücksichtigt. Typische Probleme treten dann oft im Zusammenhang mit Session oder Caching auf.

Im Beispiel oben wäre das ein echtes Problem wenn das Logfile auf einem gemeinsam genutzten Share zu liegen käme. Aber selbst wenn nicht: Irgendwann werden die Logfiles sicher eingesammelt. Ich würde daher zumindest den Maschinennamen in den Dateinamen aufnehmen bzw. in das Logfile schreiben.

Prozesse und AppDomains…

Im einfachsten Falle hat man auf einer Maschine einen Prozess mit einer AppDomain. Trotzdem muss man sich hier bereits mit “AppDomain Recycling” auseinandersetzen (wahlweise als echtes Recycling der AppDomain innerhalb des Prozesses, oder als Process Recycling; diese Unterscheidung spielt aber aus Entwicklersicht in der Regel keine Rolle).

Beim Recycling wird normalerweise die neue AppDomain erzeugt, bevor die alte heruntergefahren wird. Konsequenz im Beispiel oben: Zwei Prozesse versuchen auf die gleiche Datei schreibend zuzugreifen.

Eigentlich sollte dieses Konzept jedem ASP.NET Entwickler bekannt sein, trotzdem werde ich mit diesem Thema regelmäßig konfrontiert, meist wenn es um InProc Session State geht (bzw. den Verlust der Session Information). Das Problem dabei: Während der Entwicklung wird man nicht mit Recycling konfrontiert.

Man kann sich das Szenario mehrerer paralleler AppDomains bzw. Prozesse aber auch selbst herstellen: Mit einem Web-Garden-Szenario, bei dem von vorne herein mehr wie ein Prozess parallel gestartet wird.

Instanzen…

Selbst innerhalb einer AppDomain ist die Situation nicht ganz so einfach. Auch hier ist man mit mehr wie einer Instanz der HttpApplication Klasse konfrontiert. ASP.NET garantiert, dass eine Instanz zu einem Zeitpunkt genau einen Request bearbeitet:

„During the lifetime of your application, ASP.NET maintains a pool of Global.asax-derived HttpApplication instances. When your application receives an HTTP request, the ASP.NET page framework assigns one of these instances to process that request. That particular HttpApplication instance is responsible for managing the entire lifetime of the request it is assigned to, and the instance can only be reused after the request has been completed.“ (link)

In diesem Pool müssen also mindestens so viele Instanzen vorhanden sein, wie Request parallel bearbeitet werden sollen. Und mit den HttpApplication Instanzen auch jeweils alle Module.

In IIS6 und Cassini werden von vorne herein zwei Instanzen erzeugt, beim IIS Express und IIS7 sind es sogar 4. Zusätzliche Instanzen werden angelegt, wenn man es schafft entsprechende Last aufzubauen. Nach einer gewissen Zeit der Inaktivität werden einzelne Instanzen wieder freigegen, inklusive Aufruf von Dispose(), wie sich das gehört.

Unter diesem Blickwinkel ist die Anforderung im Beispiel oben nicht ganz so simpel umzusetzen. Die Logdatei muss pro AppDomain einen eindeutigen Namen erhalten. Sie darf innerhalb der AppDomain nur einmal geöffnet werden, gleichzeitig müssen die Eventhandler aber für jede Instanz der HttpApplication registriert werden.

HttpApplication unterstützt diese Unterscheidung mit Application_Start()/Application_End(), die einmal pro AppDomain aufgerufen werden, und Init()/Dispose(), die pro Instanz zum Zuge kommen.

Die Crux…

Das beschriebene Verhalten ist zwar dokumentiert, aber als Entwickler kommt man damit eher selten in Berührung. Und wenn man dann nicht weiter darüber nachdenkt und annimmt, HttpApplication würde sich ähnlich verhalten, wie die Application Klassen in WinForms, WPF oder Silverlight, warum sollte man dann Dokumentation lesen?

Also wird man im Init() seine Logdatei öffnen und in Sharing Violations laufen, sie im Dispose() schließen und irgendwann keine Einträge mehr darin finden, und die Eventhandler werden nur im Zusammenhang mit dem Setzen einer statischen Variable registriert, die Events kommen also beim Debuggen noch alle, in Produktion gehen sie aber zumindest teilweise verloren.

Einige Links zum Thema:

  • The ASP.NET HTTP Runtime (link)
  • ASP.NET Application Life Cycle Overview for IIS 5.0 and 6.0 (link)
  • ASP.NET Application Life Cycle Overview for IIS 7.0 (link)
  • Hosting Services (link)
  • ASP.NET Case Study: Lost session variables and appdomain recycles (link)