Update: code examples posted, see below.
Sometimes users require functionality that takes a long time, and as software developers it’s our job to make sure these tasks get done in a user-friendly manner. Client applications (console/windows apps, etc) figured this out a long time ago – they have a bevy of tools like progress bars, rich multithreading options, and the like to make sure the user’s wait time is still interactive and informative. These tools do not help web applications much, however as web apps have evolved with more rich UIs users have incresingly demanded the same UI-friendliness for the web site’s long running tasks. First, though, lets review our current options with ASP.NET right now:
- Postback: Have the long running task be performed in a page’s code-behind. The user will have to wait for a very long page load, and will have zero information about the task’s status, but this is super-simple to implement. If the task runs too long you might get an ASP.NET request timeout.
- AJAX: Perform the task in a web service (or another web application), and use AJAX to call it. This will give the user more than just a browser page-load screen as you can show a friendly wait icon or something to that effect. You really can’t give the user more than “I’m still working,” though, they will have no idea how far along the task is. It is harder to implement than a simple postback, but well worth it in the UI-friendliness. Your AJAX framework may have timeout issues if the task takes too long, though. There will be some things to consider in the UI logic though – what can the user do while this task is running? Do you disable the page’s buttons? etc…
- Client Application: Don’t use ASP.NET at all, just have the user use a client application. This is of course makes this entire post moot, so we’ll go with the assumption that this is not an option.
A Better Option
Now, looking at our current options none of them seem “perfect”, ie they all have some drawback that might affect the user’s experience. Also, they all are single requests, so must be finite in nature. This means that no matter what your timeout values are, there’s a chance the user’s task might take longer than that. Ideally we would want a solution that:
- Gave the user a friendly wait instruction (using text/icons/whatever)
- Gave the user some sort of status of how far along the task is
- Gave the user the ability to cancel the task if they want to stop waiting
- Can go for as long as necessary without a timeout
One solution that meets these criteria is quite simple in theory, in fact you might have already thought of it upon reading the requirements above. Using multiple requests you could do all these things, and for lack of a better term I’ll call this the “Persistent Task Solution” Some pseudo code describing our high-level behavior:
start asynchronous task;
while(task is NOT done)
{
query task status;
}
get task results and display to user;
Persistent Task Solution – Core
Pretty simple to do in a web page, especially with AJAX. Implementing this in ASP.NET, however, presents some real challenges server-side. How can we start an asynchronous task, keep track of its status, and get results for the requests the client will be making? Since we want our processing to be asynchronous a thread makes sense. Let’s see how we might do this in a client application:
public class Job
{
//Fields
private Thread _executingThread;
private object _syncRoot;
//Properties
internal int JobId
{
get
{
if(_executingThread == null)
return -1;
return _executingThread.ManagedThreadId;
}
}
public bool IsFinished
{
get
{
if(_executingThread == null)
return false;
return !_executingThread.IsAlive;
}
public object SyncRoot
{
get { return _syncRoot; }
}
//TODO: Any "status" properties as far as how much has been processed, etc...
//Ctor
public Job()
{
_syncRoot = new object();
}
//Helper Methods
private void Run()
{
//TODO: This is where you would actually perform your task
lock(_syncRoot)
{
JobPool.Stop(JobId); //call the job pool so it get removed from the list, but simply calling this.Stop() would work, too
}
}
//Public Methods
internal void Start()
{
_executingThread = new Thread(new ThreadStart(Run));
_executingThread.Start();
}
internal void Stop()
{
_executingThread.Abort();
}
}
public static class JobPool
{
//Fields
private static Dictionary_jobInternalStorage;
//Static Ctor
static JobPool()
{
_jobInternalStorage = new Dictionary();
}
//Public Methods
public static int StartJob(Job job)
{
job.Start();
_jobInternalStorage.Add(job.JobId, job);
return job.JobId;
}
public static Job GetJob(int jobId)
{
if(!_jobInternalStorage.ContainsKey(jobId))
return null;
return _jobInternalStorage[jobId];
}
public static void Stop(int jobId)
{
if(!_jobInternalStorage.ContainsKey(jobId))
return;
_jobInternalStorage[jobId].Stop();
_jobInternalStorage.Remove(jobId);
}
}
static class Program
{
static void Main(string[] args)
{
Job job = new Job();
JobPool.StartJob(job);
while(!job.IsFinished)
{
//this lock is to make sure the operation is "atomic" - that the processing doesn't change the values mid-way through the code block
lock(job.SyncRoot)
{
//TODO: Display the current status
}
}
}
}
A pretty basic multi-threading example, but the core of it is the Job and JobPool classes. The Job class is responsible for wrapping the thread that does the actual work, and for providing properties/methods for accessing the current status. The JobPool simply provides a way to retrive jobs later on by job ID, which is important in a web environment (we didn’t need it much in this console app, though). This is the crux of our system though – no matter what multithreading implementation we provide the most important thing we need is a way to get back to the Job by some identifier in another request. Not exactly simple, but not very hard given a little time and multithreading know-how.
Persistent Task Solution – Server
Now to get this running on our server so we can make requests to spawn/query jobs. This is where things get difficult fast. You might think “I can just call JobPool from my web service!”, and if you do so you’ll see it work just fine. This is bad. Do not do this. Any threads created in ASP.NET belong to ASP.NET, and it will kill your threads as it sees fit, which odds are is not when it’s convenient for you. It might work on your local dev box, sure, but put it in a production environment with good pool recycling settings and your jobs will be massacred. You don’t want all that blood on your hands, do you? Good, then let’s try to find an alternative to spawning the threads in ASP.NET.
There are several things you could do, of course, but the way I found works best is to make a windows service that your web application can call. With WCF (Windows Communication Foundation) this is actually pretty easy. To do this, think of your web services as a client, and your windows service as a server. They will communicate through named pipes, a windows inter-process communication system. First, you’ll need an interface that both the client and server will use to pass messages, preferably in an assembly that both your applications reference:
[ServiceContract]
public interface IJobService
{
[OperationContract]
int StartJob(/*you would put parameters here needed for the job's processing*/);
[OperationContract]
JobStatus GetStatus(int jobId);
[OperationContract]
void Stop(int jobId);
}
[DataContract]
public class JobStatus
{
[DataMember]
public bool IsFinished { get; set; }
//TODO: Any other status properties you need to pass back to the client
}
The JobStatus class is a container object we can send back-and-forth, and note that anything that “crosses the wire” in this manner will need the [DataContract] and [DataMember] attributes so that is can be serialized/deserialized properly by WCF. Once you have this interface filled in with any application-specific parts you need, the server itself is quite simple. Create a windows service project, and in it make a class that implements your WCF interface:
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
public class JobService : IJobService
{
public int StartJob()
{
Job newJob = new Job();
return JobPool.StartJob(newJob);
}
public JobStatus GetStatus(int jobId)
{
Job job = JobPool.GetJob(jobId);
if(job == null)
return null;
JobStatus status = new JobStatus();
lock(job.SyncRoot)
{
status.IsFinished = job.IsFinished;
//TODO: Any other status filling tasks you might need to do
}
return status;
}
public void Stop(int jobId)
{
JobPool.Stop(jobId);
}
}
This service implementation drives our JobPool, performing job tasks for the client (our web application). The InstanceContextMode attribute setting simply means we want a different JobService instance for each call, this will keep us honest in maintaining a stateless environment (the web app, our client, won’t have a state so why should our server?). The only thing left in the server app is to set up WCF to use our service implementation. First our windows service class:
public partial class ServicesServer : ServiceBase
{
private ServiceHost _serviceHost;
protected override void OnStart(string[] args)
{
_serviceHost = new ServiceHost(typeof(JobService));
_serviceHost.Open();
}
protected override void OnStop()
{
_serviceHost.Close();
}
}
And our App.config to set up the WCF name pipes endpoint:
.serviceModel> contract=" .IJobService" />
Persistent Task Solution – Client
While the server is quite complicated what with having to drive the multi-threaded jobs and all, the client is rather simple. First the usual WCF Web.config shenanigans to set up the named pipes endpoint, but make sure it points at the same address given in the server:
.serviceModel> address="net.pipe://localhost/JobService"
binding="netNamedPipeBinding" bindingConfiguration="MyNamedPipeBinding"
contract=".IJobService" />
Then in our web services, we can call our named pipe service to handle jobs. Note that the web services don’t do much now, the most they might do is handle serializing things back to the web browser in more javascript friendly formats or things like that. No real logic takes place:
[WebService(Namespace = "http://myurl/services")]
public class JobService : WebService
{
private ChannelFactoryJobServiceFactory
{
get
{
if (_jobServiceFactory== null)
_jobServiceFactory= new ChannelFactory("jobService");
return _jobServiceFactory;
}
}
private ChannelFactory_jobServiceFactory;
[WebMethod]
public string GetJobStatus(int jobId)
{
IJobService service = JobServiceFactory.CreateChannel();
JobStatus status = service.GetStatus(jobId);
((IClientChannel)service).Close(); //THIS IS VERY IMPORTANT
return status.ToString(); //TODO: might want to do some better serialization
}
[WebMethod]
public void StopJob(int jobId)
{
IJobService service = JobServiceFactory.CreateChannel();
service.Stop(jobId);
((IClientChannel)service).Close(); //THIS IS VERY IMPORTANT
}
[WebMethod]
public int StartJob()
{
IJobService service = JobServiceFactory.CreateChannel();
int newJobId = service.StartJob();
((IClientChannel)service).Close(); //THIS IS VERY IMPORTANT
return newJobId;
}
}
Like I said, it’s more of a pass-through between the javascript and the windows service, but there are some things to note here. One the ChannelFactory takes a while to set up, so we try to re-use that. Second is make sure you always close your channels after you use them. You can cast the proxy interface object that you are given by the ChannelFactory to the IClientChannel interface to accomplish this. If you don’t, these channels will stay open and you’ll quickly run into a connection limit between here and the server. Other than that, it’s pretty simple, invoke methods on the proxy interface like you would normally.
Persistent Task Solution – The Client’s Client
With our WCF named pipes service now up and running, we can provide a rich UI experience for our task by passing down more information to the browser via our GetJobStatus web method. You can even make a progress bar that works! A progress bar…on the internet! I won’t get into the javascript here, because it’s probably going to be very application specific, but in general you have 3 steps:
- AJAX call to start the job
- Set up a timer that makes an AJAX call asking for the status, updating the UI accordingly
- Do this until you get a status back that denotes the task is finshed, updating the UI accordingly
Conclusion
Phew! This post got longer than I though it would, but mostly because of the code samples I gave. While the “Persistent Task Solution” requires many levels of indirection it fulfills all the requirements listed up near the top. You create a multi-threaded framework for running the tasks, then wrap that in a windows service. The windows service can be called by your web application via WCF using named pipes. You can then use AJAX and web services to allow the browser to invoke the actions it needs to drive the UI. While this solution can be difficult to implement, and would not be suited for every project, it does allow for things that normal web application just don’t support. If you really want to wow your users it can be a good tool to have in your arsenal.
Kantan ni setsume shinakereba gomen na.