According to this article
The Begin Event Handler is Always Invoked
The second impliciation of AsyncTimeout that I had not really internalized until recently is that the begin event handler for a registered async task is always invoked, even if the page has timed out before ASP.NET gets around to starting that task.
In this scenario, the remainder of the tasks are basically doomed. The page has timed out already, but ASP.NET is still going to go through the motions of calling the begin event handler for all registered tasks, followed immediately by a call to their corresponding timeout event handlers.
AsyncTimeout Does Not Imply Asynchronous Task Cancellation
where possible I should be mindful to not kick off additional tasks once the whole page timeout has already fired and I should cancel remaining running tasks once the timeout occurs. Is this recommended/safe? How would I go about cleanly canceling any HttpWebRequest.BeginGetResponse calls given there are multiple places the async task could be? For instance if I'm inside the BeginGetResponse call, what should I return for IAsyncResult to stop processing in its tracks safely? Would the same guidelines apply to SqlCommand.BeginExecuteReader?
I've put together a sample code from a few examples and my own additions:
<%@ Page Language="C#" Async="true" AsyncTimeout="30" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Threading" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected class RequestState
{
public HttpWebRequest Request;
public string RequestID; // simple identifier of the request
public string Url;
public DateTime RequestStartTime;
public RequestState(string requestID, string url)
{
this.RequestID = requestID;
this.Url = url;
}
public void CreateRequest()
{
Request = (HttpWebRequest)WebRequest.Create(Url + pageDest);
Request.Method = "GET";
Request.Proxy = WebRequest.DefaultWebProxy;
Request.Timeout = 1000 * 3; // Connection timeout
Request.ReadWriteTimeout = 1000 * 15; // Read response timeout
RequestStartTime = DateTime.Now; // Technically the request didn't start yet, but this is close enough
}
}
protected class ResponseDetails
{
public string RequestID;
public string Url;
public string ServerID;
public double Duration;
public string ResponseString;
public string FormattedDuration
{
get
{
return String.Format("{0:N0}ms", Duration);
}
}
public bool IsServerOk
{
get
{
// trivial check, better check intended for real implementation
return ResponseString.Contains("<body");
}
}
public ResponseDetails(RequestState reqState, string serverID, string responseString)
{
this.RequestID = reqState.RequestID;
this.ServerID = serverID;
this.Url = reqState.Url;
this.ResponseString = responseString;
this.Duration = (DateTime.Now - reqState.RequestStartTime).TotalMilliseconds;
}
}
public const string pageDest = "/";
Dictionary<string, string> serverRequests = new Dictionary<string, string>()
{
{ "dev1", "http://www.yahoo.com" },
{ "dev2", "http://www.msn.com" },
{ "dev3", "http://www.google.com" }
};
Dictionary<string, ResponseDetails> serverResponses = new Dictionary<string, ResponseDetails>();
object dictLock = new object();
int maxIOThreads = 0;
int maxWorkerThreads = 0;
protected bool ShowThreadingInfo = false;
protected bool ShowRequestTime = true;
protected bool ExecuteInParallel = true;
private string GetThreadingCounts()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<b>EndIOCPUpDate {0}</b><br />", DateTime.Now);
/*
sb.AppendFormat("CompletedSynchronously: {0}<br/><br/>" + AR.CompletedSynchronously + "<br /><br />");
sb.AppendFormat("isThreadPoolThread: " + System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br />";
sb.AppendFormat("ManagedThreadId : " + System.Threading.Thread.CurrentThread.ManagedThreadId + "<br />";
sb.AppendFormat("GetCurrentThreadId : " + AppDomain.GetCurrentThreadId() + "<br />";
sb.AppendFormat("Thread.CurrentContext : " + System.Threading.Thread.CurrentContext.ToString() + "<br />";
*/
int availWorker = 0;
int maxWorker = 0;
int availCPT = 0;
int maxCPT = 0;
ThreadPool.GetAvailableThreads(out availWorker, out availCPT);
ThreadPool.GetMaxThreads(out maxWorker, out maxCPT);
if (maxIOThreads < (maxCPT - availCPT))
maxIOThreads = (maxCPT - availCPT);
if (maxWorkerThreads < (maxWorker - availWorker))
maxWorkerThreads = (maxWorker - availWorker);
sb.AppendFormat("--Available Worker Threads: {0}<br/>", availWorker);
sb.AppendFormat("--Maximum Worker Threads: {0}<br/>", maxWorker);
sb.AppendFormat("--Available Completion Port Threads: {0}<br/>", availCPT);
sb.AppendFormat("--Maximum Completion Port Threads: {0}<br/>", maxCPT);
sb.AppendFormat("===========================<br /><br />");
return sb.ToString();
}
protected void Page_Load(object sender, EventArgs e)
{
foreach (KeyValuePair<string, string> kvp in serverRequests)
{
RequestState reqState = new RequestState(kvp.Key, kvp.Value);
Page.RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(this.BeginGetStatusPage),
new EndEventHandler(this.EndGetStatusPage), new EndEventHandler(this.TimeoutHandler), reqState, ExecuteInParallel));
}
}
protected override void OnPreRenderComplete(EventArgs e)
{
base.OnPreRenderComplete(e);
// write the result messages to the Label.
MessageOut();
}
private IAsyncResult BeginGetStatusPage(Object sender, EventArgs e, AsyncCallback cb, object state)
{
RequestState reqState = (RequestState)state;
AddTraceMessage("Begin " + reqState.Url);
reqState.CreateRequest();
return reqState.Request.BeginGetResponse(cb, reqState);
}
void EndGetStatusPage(IAsyncResult asyncResult)
{
AddTraceMessage("EndAsync");
if (asyncResult != null)
{
RequestState reqState = asyncResult.AsyncState as RequestState;
AddTraceMessage("End " + reqState.Url);
string serverKey = null;
string retString = null;
StringBuilder sb = new StringBuilder();
try
{
using (WebResponse response1 = (WebResponse)reqState.Request.EndGetResponse(asyncResult))
{
AddTraceMessage("End Response " + reqState.Url);
// grab a custom header later, not useful yet
serverKey = response1.Headers["Server"];
// we will read data via the response stream
using (Stream resStream = response1.GetResponseStream())
{
using (StreamReader rdr = new StreamReader(resStream))
{
sb.Append(rdr.ReadToEnd());
rdr.Close();
}
resStream.Close();
}
response1.Close();
}
}
catch (WebException ex)
{
sb.AppendLine(ex.Status.ToString() + ": " + ex.Message);
}
retString = sb.ToString();
AddTraceMessage("End Response2 " + reqState.Url);
ResponseDetails rd = new ResponseDetails(reqState, serverKey, retString);
UpdateServerResponses(rd);
}
}
void UpdateServerResponses(ResponseDetails details)
{
lock (dictLock)
{
serverResponses.Add(details.RequestID, details);
}
}
// This doesn't actually cancel the task I don't think
void TimeoutHandler(IAsyncResult asyncResult)
{
AddTraceMessage("Request Timed Out");
// Aborts one request running during page timeout. What about the rest??
RequestState reqState = asyncResult.AsyncState as RequestState;
reqState.Request.Abort();
ShowErrorDetails("<span style='color:red;font-weight:bold'>Timed out:</span> " + reqState.RequestID + "<br/><br/>");
}
private void ShowErrorDetails(ResponseDetails rd)
{
PanelErrors.Visible = true;
LabelErrors.Text += String.Format("<span style='color:blue;font-weight:bold'>{0}</span><br/>{1}<br/><br/>", rd.RequestID, rd.ResponseString);
}
private void ShowErrorDetails(string message)
{
PanelErrors.Visible = true;
LabelErrors.Text += message;
}
private void MessageOut()
{
PanelThreading.Visible = ShowThreadingInfo;
Page.Trace.Write(_trace.ToString());
foreach (KeyValuePair<string, string> kvp in serverRequests)
{
string respStr = "<font color='red'>No Reply</font>";
if (serverResponses.ContainsKey(kvp.Key))
{
ResponseDetails rd = serverResponses[kvp.Key];
if (rd.IsServerOk)
{
respStr = "OK";
respStr += " (" + rd.ServerID + ")";
}
else
{
ShowErrorDetails(rd);
respStr = "<font color='red'>ERROR</font>";
}
//In parallel task mode it seems to return the time of the single longest individual request. Really weird.
if (ShowRequestTime)
respStr += " [" + rd.FormattedDuration + "]";
}
this.Label1.Text += String.Format("{0}: {1}<br/>", kvp.Key, respStr);
}
this.Label1.Text += String.Format("<br/><b>MaxWorkerThreads:</b> {0}<br/>", maxWorkerThreads);
this.Label1.Text += String.Format("<b>MaxIOThreads:</b> {0}<br/>", maxIOThreads);
}
StringBuilder _trace = new StringBuilder();
DateTime _pageStartTime = DateTime.Now;
public void AddTraceMessage(string message)
{
double t = (DateTime.Now - _pageStartTime).TotalSeconds;
lock (_trace)
{
_trace.AppendFormat("Thread:[{0:000}] {1:00.000} -- {2}\r\n",
System.Threading.Thread.CurrentThread.GetHashCode(), t, message);
LabelThreading.Text += GetThreadingCounts();
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label runat="server" ID="Label1"></asp:Label>
<asp:Panel runat="server" ID="PanelErrors" Visible="false">
<br />
<h2>
Error Details:</h2>
<br />
<asp:Label runat="server" ID="LabelErrors"></asp:Label>
</asp:Panel>
<asp:Panel runat="server" ID="PanelThreading" Visible="true">
<br />
<h2>
Threading Details:</h2>
<br />
<asp:Label runat="server" ID="LabelThreading"></asp:Label>
</asp:Panel>
</div>
</form>
</body>
</html>
Update: Is there any other information I should provide?