I've built a page that runs an admin task on a background thread using QueueBackgroundWorkItem
. After queueing the task, the page begins polling a Page Method to check whether the task has completed. But I think my strategy for communicating from the worker thread to the status request threads is flawed.
To communicate accross threads, I'm using an object in session state in StateServer
mode. It seemed to work in all my initial local testing, but that was using InProc session state. Once we got it on the server, it started appearing to hang - polling forever without ever getting a status update. Here is the code:
//Object for communicating across threads
[Serializable]
public class BackgroundTaskStatus
{
public enum BackgroundTaskStatusType
{
None=0,
Pending=1,
Started=2,
Error=3,
Complete=4
}
public BackgroundTaskStatusType Status { get; set; }
public string Message { get; set; }
}
//Class containing a reference to the Session State and
//contains the task for QueueBackgroundWorkItem
public class LocationSiteToolProcessor
{
public static string CopyingStatusKey = "LST_CopyingStatus";
private HttpSessionState _session;
public LocationSiteToolProcessor(HttpSessionState session)
{
_session = session;
}
public void CopyPage(string relativeUrl, bool overwrite, bool subPages, CancellationToken cancellationToken)
{
if(_session[CopyingStatusKey] == null || !(_session[CopyingStatusKey] is BackgroundTaskStatus))
_session[CopyingStatusKey] = new BackgroundTaskStatus();
BackgroundTaskStatus taskStatus = _session[CopyingStatusKey] as BackgroundTaskStatus;
taskStatus.Status = BackgroundTaskStatus.BackgroundTaskStatusType.Started;
try
{
DateTime start = DateTime.Now;
ElevateToWebAdmin();
var pages = LocationSiteRepository.CopyTemplatePage(relativeUrl, overwrite, subPages);
TimeSpan duration = DateTime.Now - start;
taskStatus.Message = (pages != null ? String.Format("Page copied successfully.") : String.Format("No pages were copied.")) +
" Time elapsed: " + duration.ToString("g");
taskStatus.Status = BackgroundTaskStatus.BackgroundTaskStatusType.Complete;
}
catch (Exception ex)
{
taskStatus.Message = ex.ToString();
taskStatus.Status = BackgroundTaskStatus.BackgroundTaskStatusType.Error;
}
}
}
//Code that kicks off the background thread
Session[LocationSiteToolProcessor.CopyingStatusKey] = new BackgroundTaskStatus() { Status = BackgroundTaskStatus.BackgroundTaskStatusType.Pending };
LocationSiteToolProcessor processor = new LocationSiteToolProcessor(Session);
HostingEnvironment.QueueBackgroundWorkItem(c => processor.CopyPage(relativeUrl, overwrite, subPages, c));
//Page Method to support client side status polling
[System.Web.Services.WebMethod(true)]
public static BackgroundTaskStatus GetStatus()
{
//(Modified for brevity)
BackgroundTaskStatus taskStatus = HttpContext.Current.Session[LocationSiteToolProcessor.CopyingStatusKey] as BackgroundTaskStatus;
return taskStatus;
}
I've attached the debugger and what I've observed is the background thread sets the Status
property of the BackgroundTaskStatus
in the session, but when the subsequent status polling requests read that object from session, the property value is unchanged. They seem to be operating on two different copies of the session object.
Now I know that the State Server mode serializes the session and then deserializes the session when it binds it to a new request. So it's possible for GetStatus()
and the background thread to deserialize their own simultaneous copies of the object. But I'm expecting the background thread's change to be serialized back to the same origin and since the GetStatus()
method doesn't write to session, it should eventually read the updated Status
property value after the background thread sets it.
However, it seems like either the session was branched at some point and is storing two different serialized copies of my object, or the Status
set by the background thread is being overwritten, even though GetStatus()
doesn't write to session. Where is it going wrong?
Additionally, is it safe to pass in the HttpSessionState
object like I'm doing or can it be destroyed before the background thread completes (i.e. is it scoped to the initial request)? I was under the impression it was a static object, but now I'm doubtful of that. I want this to be safe to run on a farm but am hoping not to have to get a database involved.
I found some info on this page that is probably relevant:
When a page saves data to Session, the value is loaded into a made-to-measure dictionary class hosted by the HttpSessionState class. The contents of the dictionary is flushed to the state provider when the ongoing request completes.
To me, this sounds like it's saying that my polling request thread does have its entire session serialized back to the state server at the end of its request, even though it hasn't made any changes. Additionally, it would stand to reason, that the session dictionary that my background thread writes to never gets serialized back to the state server after I modify it because its request already ended. Can anyone confirm this?
I found some info on this page that is probably relevant:
When a page saves data to Session, the value is loaded into a made-to-measure dictionary class hosted by the HttpSessionState class. The contents of the dictionary is flushed to the state provider when the ongoing request completes.
To me, this sounds like it's saying that my polling request thread does have its entire session serialized back to the state server at the end of its request, even though it hasn't made any changes. Additionally, it would stand to reason, that the session dictionary that my background thread writes to never gets serialized back to the state server after I modify it because its request already ended.
I took the evidence above as confirmation. I resorted to storing the state in a database instead of session.