Let’s face it – ArcGIS Server is not fast. Any there are many processes (raster modeling, map production, data extraction etc) that simply can not be completed in the typical web request – response time frame.
In a recent project, I was creating 36 by 36 inch PDF maps, which was taking around 3 minutes (DRG raster layers really slow this down).
My plan was to create an asynchronous web service which we could simply send requests to, and the client would just continue on, and not wait for a response. The web service itself would notify the end user, via email, when the map was ready to be picked up.
Web Method AttributesIn a generic purely .NET situation, this is relatively easy to do – just add the SoapDocumentMethod attribute to the web method…
[WebMethod]
[SoapDocumentMethod(OneWay=true)]
public void CreateMapSheetSynchronous(string emailAddress, string serviceName, string mapSheetId )
{
CreateMap(emailAddress, serviceName, mapSheetId);
}
When this attribute is in place, the web server returns a HTTP 202 response, which indicates that processing has begun. This occurs prior to the web method being invoked. What’s interesting is that it seems that the ASP.NET impersonation does not work in this situation. When it comes to the point of actually connecting to AGS I get an UnauthorizedAccessException. I added in a little code to show me what the current identify was (WindowsIdentity.GetCurrent().Name) and it was my local ASPNET account. If I change back to SoapDocumentMethod(OneWay=false) the impersonation is working correctly, and right before connecting, I could see the current identity was the impersonated identity that I specified in the web.config file.
Ok – so simply setting SoapDocumentMethod(OneWay=true) is not going to cut it. Time to dig into threading…
Asynchronous ThreadsThreading is very cool, and a very hot topic now that multi-core processors are becomming the norm. But, the usual usage is to have some work occur on a background thread, which then notifies the origniating thread that the work is complete. This is not what I want - I want to create a thread, and let it do its work and have it correctly kill itself off. I do not want to have the originating thread wait for it to complete.
Early on in my Googling, I found
Mike Woodring’s sample that shows how to support “Fire-and-Forget” for Asynchronous Delegates without leaking. Nice name! Why is this needed you may ask?
Starting with the 1.1 release of the .NET Framework, the SDK docs now carry a caution that mandates calling EndInvoke on delegates you've called BeginInvoke on in order to avoid potential leaks. This means you cannot simply "fire-and-forget" a call to BeginInvoke without the risk of running the risk of causing problems. (from Mike's site)
Mike’s sample code shows exactly how to do this – very elegant, and best of all – it works. The idea here is that I can call the web service, where I’ve created a delegate to a private method, which actually does the work of creating the map (ok, it calls other classes, but I'm trying to keep this simple!). Mike’s AsyncHelper class will create a thread and call the delegate, and it will correctly call EndInvoke to clean up memory for me. As soon as FireAndForget is called, the WebMethod exits, and the client can continue. Very nice.
Here’s the web method code:
delegate void CreateMapDelegate(string emailAddress, string serviceName, string mapSheetId );
[WebMethod]
[SoapDocumentMethod(OneWay=false)]
public void CreateMapSheet(string emailAddress, string serviceName, string mapSheetId )
{
CreateMapDelegate d = new CreateMapDelegate(CreateMapAsUser);
AsyncHelper.FireAndForget(d, emailAddress, serviceName, mapSheetId);
}
As you can see, the code is pretty simple, so it was quick to implement, but I ran into the same problem – the identity on the new thread was ASPNET, and not the impersonation account. But – this time I was in a better situation. The impersonation was in effect prior to calling AsynchHelper.FireAndForget – thus I could get the identity without having to dig into the Win32 LogOnUser mess. All I needed to do was create a wrapper function for my real CreateMap function which would take an additional argument: the Identity.
So here’s the new Web Method, and the wrapper function…
//Delegate
delegate void CreateMapAsUserDelegate( WindowsIdentity identity,string emailAddress, string serviceName, string mapSheetId );
/// <summary>
/// Create a map sheet using an asynchronous web method
/// </summary>
[WebMethod]
[SoapDocumentMethod(OneWay=false)]
public void CreateMapSheet(string emailAddress, string serviceName, string mapSheetId )
{
// Get the current identity - which is the impersonated
WindowsIdentity impIdent = WindowsIdentity.GetCurrent();
CreateMapAsUserDelegate d = new CreateMapAsUserDelegate(CreateMapAsUser);
AsyncHelper.FireAndForget(d, impIdent, emailAddress, serviceName, mapSheetId);
}
/// <summary>
/// Create the map as a specific windows Identity.
/// This simply wraps the CreateMap function
/// with code that re-sets the impersonation on the new thread
/// </summary>
private void CreateMapAsUser( WindowsIdentity identity, string emailAddress, string serviceName, string mapSheetId )
{
// Get the current identity.
System.Security.Principal.WindowsImpersonationContext wi = identity.Impersonate();
CreateMap(emailAddress, serviceName, mapSheetId);
wi.Undo();
}
The CreateMapAsUser simply impersonates the passed in identity, and then calls the CreateMap function. As long as the impersonation identity has access to ArcGIS Server, all is well.
Summary
If you need to create long-running AGS based web services which can handle their own notification, this is a really good solution. I also tested it’s scalability by firing off requests for ~ 20 maps at a time from my unit tests. And it (slowly) ground through all of them!