Watching Your Server Processes
Summary: Learn how to create an ASP.NET HTTP Handler to view the life and death of the processes used by a Web site. In addition, learn how to create a configuration section handler. (13 printed pages)
Download ProcessHandlerCS.msi, the Visual C# version.
Download ProcessHandlerVBSample.msi, the Visual Basic version.
Have you ever watched a great barista in action? It is a wondrous ballet of beans, steam and milk as assorted coffee beverages dance their way to eagerly waiting customers. However, even the best barista occasionally has issues. Two orders become intertwined as they are being processed, and you get a soy latte. The arcane coffee hieroglyphics on the cup are wrong, or misinterpreted. Someone orders a "seven pump, 180 degree short chai with a thin cap" and the barista has a buffer overrun trying to interpret the request. When anything like this happens, the process must stop, and begin again. Good servers can shuffle existing requests. Great servers can do it without anyone being the wiser.
Microsoft® ASP.NET has made huge strides towards being a much more reliable system than any of its competitors. However, just as with a great barista, occasionally things go wrong. Fortunately, ASP.NET is a great server. Behind the scenes it can quickly generate a new process, and handle the requests. Often, other than a slightly longer delay when requesting a page, users may not even notice.
Administrators of the ASP.NET system, however, may want to know. Similarly, they may want to know what occurred to bring down the process. Fortunately, this information is available, using the ProcessInfo and ProcessModelInfo classes from the .NET Framework Class Library documentation. In this article, we'll learn how to create an ASP.NET HTTP Handler to use these objects to view the life and death of the processes used by a Web site. In addition, we will create a configuration section handler, enabling us to configure the handler once it is installed.
The ASP.NET process is responsible for compiling and managing all requests to ASP.NET pages. In an ideal world, this process would continue for the life of your server, merrily receiving requests, compiling pages, and returning HTML. However, there are a number of possible events that could impact the process, and bring us back to the real world of Web development. The developer could have failed to properly handle a memory leak or threading issue; the server could have lost connection with the process; or it could even have been configured to happen via the idleTimeout, requestLimit, memoryLimit and similar items in the <processModel> Element section of the Web.Config file. If one of these events occurs, a new ASP.NET worker process is created, and new requests are passed off to his process for handling.
As the ASP.NET process is so vital to the handling of pages, monitoring the process can be equally vital. The ProcessInfo and ProcessModelInfo classes provide a view into the health and lifespan of current and past processes. Figure 1 shows the process list as created in this article.
Figure 1. The process history for a Web server
The ProcessInfo Class stores the data for a given process. You should never create a ProcessInfo object yourself, but instead use the methods of ProcessModelInfo Class to retrieve ProcessInfo objects. Table 1 shows the important properties of the ProcessInfo class.
Table 1. Properties of the ProcessInfo class
|Age||TimeSpan||The total amount of time the process has been running (or had been running). This value could cause a process to restart if the value is higher than the timeout setting in the processModel section of the Web.Config file.|
|PeakMemoryUsed||Integer||The highest amount of memory this process has used (in MB). This value could cause a process to restart if this value is higher than the level set for the memoryLimit in the processModel section of the Web.Config file.|
|ProcessID||Integer||The ID used by the operating system to identify the process. This will be unique for each process (at the time the process is running)|
|RequestCount||Integer||The total number of page requests the process has received. This value could cause a process to restart if it exceeds the level set for the requestLimit in the processModel of the Web.Config file.|
|ShutdownReason||ProcessShutdownReason||This is an enumeration that defines the possible reasons for the process to restart. See Table 2 for possible values|
|StartTime||DateTime||The time the process started.|
|Status||ProcessStatus||An enumeration that defines the current state of the ASP.NET worker process. This is one of Alive, ShuttingDown (process has received the request to shut down), ShutDown (process has completed a normal shut down) or Terminated (the process was forced to shut down)|
Once a process has shutdown, the Shutdown reason will be set to one of the values of the ProcessShutdownReason Enumeration.
Table 2. Possible reasons a process is shut down
|None||The value for a still running process.|
|Timeout||The process restarted as its age went past the value for timeout in the processModel section of the Web.Config file. If this happens too often, you may consider increasing the value. Generally, however, this is an acceptable restart reason.|
|IdleTimeout||The process restarted due to lack of clients. This restart will occur if there have been no client requests for a time period set in the idleTimeout value of the processModel section of the Web.Config file. This is typically an acceptable restart reason.|
|RequestsLimit||The process restarted due to receiving more requests than the value set (requestLimit) in the processModel section of the Web.Config file. This is normally an acceptable restart reason, typically used when you want the process to restart occasionally.|
|MemoryLimitExceeded||The process was restarted due to exceeding the memory limits set with the memoryLimit value in the Web.Config file. This typically represents a problem—perhaps a memory leak—in one of the ASP.NET applications part of the process. If it happens regularly, monitoring memory use of each Web application is in order.|
|RequestQueueLimit||The process restarted as the total number of requests waiting for a response has exceeded the requestQueueLimit value of the Web.Config file. This is usually a sign that something is becoming a delay with the Web server. Either more memory, faster drives or processor or more Web servers may be required.|
|DeadlockSuspected||The process has restarted because it seems to have stopped processing requests. The name of this shutdown reason indicates the most likely reason for it—if two or more threads require another to complete before they can go forward (for example, Thread A requires that Thread B complete writing to a file before going forward, while Thread B requires that Thread A complete a calculation), the process is said to be in a state of Deadlock. If this is suspected, the process will be shut down, with this reason. Normally, you don't want to see this ShutdownReason, if you do, however, look at any thread processing, or resource usage you are using in your applications.|
|PingFailed||As the ASP.NET worker process manages pages, it occasionally receives a ping from the IIS process to determine if it is still needed. Should this fail, the IIS process may shut down the ASP.NET process. This ShutdownReason indicates that there are either definite communication problems on the server receiving the message, or that the ASP.NET worker process stopped working for some reason.|
|Unexpected||You generally don't want to see this message, as it indicates that some other reason stopped the ASP.NET worker process. Beyond monitoring each process; or performing a code review of all running code; there is little that can be done.|
There are two main ways to create HTTP Handlers in ASP.NET. The first is through the creation of a file with an ASHX extension, while the second involves the creation of a class that implements System.Web.IHttpHander (see IHttpHandler Interface). This article will focus on the second form. To create an HTTP Handler, you create an assembly (typically a code library project) with a class that implements System.Web.IHttpHandler. This class is then registered in the Web.Config (or machine.config) file, and it will then be capable of accepting requests. If you look through the machine.config file (in the appropriately named httpHandlers section), you will see a number of currently registered HTTP handlers, including System.Web.UI.PageHandlerFactory, the main handler for ASP.NET pages. When you write an HTTP Handler, you are essentially defining a new way of handling a request.
All HTTP Handlers are created by implementing the System.Web.IHttpHandler Interface. This interface requires that you create a property and a method, as outlined in Table 3.
Table 3. Members of the IHttpHandler Interface
|IsReusable||Property (Boolean)||Determines if an instance of this handler can be re-used. Generally, this should return true, unless your handler requires exclusive access to some resource.|
|ProcessRequest||Method||The "main" method of the HTTP Handler. You will add all processing of the request here. This class is passed the current ASP.NET context. You can retrieve the Request and Response objects from this context.|
The bulk of the work on creating an HTTP handler is in implementing the ProcessRequest for your handler. Generally, you will want to store the request and response objects of the current context, and then use the write method of the response object to create the output. The Microsoft Visual Basic® .NET source for the ProcessRequest for the process viewer handler is shown below.
Public Sub ProcessRequest(ByVal context As HttpContext) _ Implements IHttpHandler.ProcessRequest _context = context _writer = New HtmlTextWriter(context.Response.Output) 'we only want to do this if we're enabled If _config.Enabled Then _writer.WriteLine("<html>") _writer.WriteLine("<head>") _writer.WriteLine(Me.StyleSheet) _writer.WriteLine("</head>") _writer.WriteLine("<body>") _writer.WriteLine("<span class=""content"">") 'write content here 'create table Dim t As New Table() With t .Width = Unit.Percentage(100) .CellPadding = 0 .CellSpacing = 0 End With 'the meat of the routine 'make certain this is a destination machine If (PermittedHost(_context.Request.UserHostAddress)) Then CreateHeader(t) AddProcesses(t) CreateFooter(t) Else CreateErrorReport(t) End If 'write to the stream t.RenderControl(_writer) _writer.WriteLine("</span>\r\n</body>\r\n</html>") End If End Sub
The implementation of ProcessRequest stores the current context and writer. It then creates a new HTML page as output by rendering the opening HTML tags for the page. Next, it creates a table, which will be used to format the output. Finally, if the handler is enabled, and the requesting client is one of the permitted IP addresses, the output is created via the three methods CreateHeader, AddProcesses and CreateFooter. These methods render the appropriate values into cells in the table. This code is fairly repetitive, so for brevity's sake, only the AddProcesses and related methods are shown below.
Private Sub AddProcesses(ByVal table As _ System.Web.UI.WebControls.Table) Dim procs As ProcessInfo() = _ ProcessModelInfo.GetHistory(_config.RequestLimit) Dim row As TableRow _list = New ProcessInfoCollection For Each proc As ProcessInfo In procs row = AddRow(table) _list.Add(proc) AddCell(row, proc.ProcessID.ToString()) AddCell(row, proc.Status.ToString()) AddCell(row, proc.StartTime.ToString("g")) AddCell(row, FormatAge(proc.Age)) AddCell(row, proc.PeakMemoryUsed.ToString("N0") + " MB") AddCell(row, proc.RequestCount.ToString("N0")) AddCell(row, proc.ShutdownReason.ToString()) Next End Sub Private Function AddCell( _ ByVal row As System.Web.UI.WebControls.TableRow, _ ByVal text As String) As System.Web.UI.WebControls.TableCell Dim c As New TableCell() c.Text = text row.Cells.Add(c) Return c End Function
Observant (and technical) readers will likely notice that I could have simplified this code by rendering a DataGrid and binding the ProcessInfoCollection to it, but that wouldn't have been anywhere near as fun to write.
Once you have created your HTTP Handler, it must be installed to make it available. This involves making the class available, and adding the appropriate information in a configuration file to activate the handler.
If you create a simple handler that will only be used by a single vroot, you can make the class available by copying the DLL to the bin directory of that vroot. If you have created an HTTP Handler that will be used by a number of vroots, as our ProcessHandler does, the handler must be installed into the Global Assembly Cache (GAC). In order to be installed in the GAC, a class must have a strong name. In order to have a strong name, it must have an associated strong name key. You must create strong name key files using the sn.exe command-line executable. See the Strong Name Tool (Sn.exe) section of the .NET Framework Tools documentation for details on this program.
Once the handler is available, the next step is to add the configuration to allow it to process requests. This is done by adding an entry in the httpHandlers section of either the Web.Config or machine.config file. The entry identifies the file extensions and actions that will be routed through your handler. The entry for the process viewer handler is as follows.
<add verb="*" path="process.axd" type="Microsoft.Samples.Msdn.Web.ProcessHandler, MsdnProcessHandler, Version=220.127.116.11, Culture=neutral, PublicKeyToken=f5f94c20bb90ce64" />
This entry means that when a request comes in for the "file" process.axd (that does not actually exist) using any HTTP verb, send the request to the class Microsoft.Samples.Msdn.Web.ProcessHandler located in the assembly MsdnProcessHandler. The class should implement IHttpHandler, and it is then responsible for generating the output.
Many ASP.NET applications add custom configuration using the appSettings tag. This is adequate for most applications. However, sometimes the application could use a more targeted solution. When this happens, you can create new sections for your applications.
Creating a new configuration section is a two step process. First, you must create a configuration object. This is an object or structure that has properties representing the configuration data you need. Usually, this object does not have any methods, but it may. The second part of the process is to create a section handler. This section handler is responsible for reading the appropriate information out of your web.config file, and converting it into a configuration object.
The configuration object for the ProcessViewer has four properties. These are described in the table below.
Table 4. Properties of the ProcessViewer configuration object
|Enabled||Boolean||True if the ProcessViewer is available. This allows you to temporarily turn off the handler without removing it from the web.config file.|
|LocalOnly||Boolean||True if the output of the ProcessViewer should only be viewed from the local machine. This is the most secure scenario, and prevents other people from viewing the Process History of your Web applications.|
|RequestLimit||Integer||This is the maximum number of items to display. The ProcessModelInfo.GetHistory returns a maximum of 100 items. This property can be used to reduce this number if needed.|
|PermittedHosts||String array||If LocalOnly is false, any computer could access the Process.axd handler to view the process history for your application. This could be considered a security risk. Therefore, you can assign a list of IP addresses that will be permitted to access the handler. This could be used to restrict access to administrator workstations.|
The second step in creating your own configuration is to create a class that will interpret the XML of the configuration file, and use that information to populate the configuration object. This class must implement the System.Configuration.IConfigurationSectionHandler interface. This interface has only a single method, called Create. The Visual Basic .NET source for the ProcessViewerSectionHandler is shown below (see the download for the C# source).
Public Function Create(ByVal parent As Object, _ ByVal configContext As Object, _ ByVal section As System.Xml.XmlNode) As Object _ Implements Configuration.IConfigurationSectionHandler.Create ' section has the following form: '<processView ' localOnly="true|false" ' requestLimit="<=100" ' enabled="true|false" ' permittedHosts="comma-delimited list of IP addresses" /> Dim result New ProcessViewerConfiguration() Dim config As New ConfigurationHelper(section) Dim max As Integer Dim hosts As String Const delimiter As String = ", " Const MaximumReturnCount As Integer = 100 'confirm settings, and set result.Enabled = config.GetBooleanAttribute("enabled") result.LocalOnly = config.GetBooleanAttribute("localOnly") max = config.GetIntegerAttribute("requestLimit") If max <= MaximumReturnCount Then result.requestLimit = max End If hosts = config.GetStringAttribute("permittedHosts") result.PermittedHosts = hosts.Split(delimiter.ToCharArray()) Return result End Function
The Create method is passed three objects:
- parent—represents the parent configuration section (if available).
- configContext—represents the HttpConfigurationContext object—that is, the remainder of the Web configuration. You can use this to retrieve values from the current web.config file.
- section —the most important parameter, the actual configuration section. You use this to populate the configuration object.
The code above uses a ConfigurationHelper object. This is a simple object used to retrieve specific data types from the section. The code for this class is below.
Friend Class ConfigurationHelper Dim _section As XmlNode Public Sub New(ByVal configSection As XmlNode) _section = configSection End Sub 'Accepts true/false, yes/no Public Function GetBooleanAttribute(ByVal name As String) As Boolean Dim value As String Dim result As Boolean value = GetStringAttribute(name).ToLower() If ((Boolean.TrueString.ToLower() = value) _ OrElse (value = "yes")) Then result = True Else result = False End If Return result End Function Public Function GetIntegerAttribute(ByVal name As String) As Integer Dim value As String Dim result As Integer value = GetStringAttribute(name) result = Int32.Parse(value) Return result End Function Public Function GetStringAttribute(ByVal name As String) As String Dim theAttribute As XmlAttribute Dim result As String theAttribute = _section.Attributes(name) If Not theAttribute Is Nothing Then result = theAttribute.Value End If Return result End Function End Class
In order to use this section handler and configuration object, it must be registered in the appropriate ASP.NET configuration file. As the class may be called from any process, adding it to the machine.config file is most appropriate. Register the section handler in the appropriate location in the configSections section of the machine.config class (I added it to the system.web section)
<sectionGroup name="system.web"> ... other sections <section name="processView" type="Microsoft.Samples.Msdn.Web.ProcessViewerSectionHandler, MsdnProcessHandler, Version=18.104.22.168, Culture=neutral, PublicKeyToken=f5f94c20bb90ce64" /> </sectionGroup>
Once registered, you can add the section to the machine.config file, and your class will be configurable.
Creating an HTTP Handler can provide a powerful mechanism for reaching beyond the capabilities of ASP.NET. They enable the developer to avoid the Page model, and create, modify or extend the content on your Web site. By adding an HTTP Handler for viewing the Process history of ASP.NET, you could diagnose problems in your code (such as memory leaks or unhandled exceptions) or server (such as low memory) that have been leading to customer complaints.
Once the HTTP Handler is created, and in place, you should be much more aware of what is happening in this important process. And you'll have time for a coffee, instead of having to hunt down the reason your Web site process restarted.
- Dino Esposito: The ASP.NET HTTP Runtime
- .NET Framework Class Library documentation: IHttpHandler.ProcessRequest Method sample
About the Author
Kent Sharkey is the Content Strategist for ASP.NET and Microsoft Visual Studio® content for MSDN®.