In one of my recent projects, I have a situation where I need to generate two files on the server based on user input. Both files can be quite big (in terms of mobile devices), and take a long time to generate by the server and to download by the user. The two files must be generated sequentially, as the second file depends on the first. One way to implement this would be to generate both files and then return to the user the download URLs in one request-response. The problem with this approach is that the user has to wait for a very long time before the server finishes the job. An obvious improvement is to return immediately after the first file is generated, and at the same time to spawn a new thread to generate the second file. This way, upon the request responded, the user can start to download the first file while the server is working on the generation of the second file. One problem with this approach is that the user will not know when the second file is ready for download. My solution is to create another page that allows the user to check the status of the second file. If it is ready, the response will include the download URL so the user can start to download it. If it is not ready, the user can either wait or do something else and check the status later. This works fine in my case, as the downloading time for the first file is almost always longer than the time needed to generate the second file.
Another problem with this approach is that my ASP.NET application is running in a web farm environment in which I have a shared file store for the generated files and the newly spawned thread does not have proper permissions to write to the shared file store. The following diagram shows my simple web farm of two web servers, WS1 and WS2, and the file store. The file store is just a shared folder on one of the two web servers.
I use the steps below to configure the web servers so they can access to the shared folder. These steps apply to Windows 2000 (Advanced) Server OS. Yes! I'm still using Windows 2000 servers. :)
- Create a Windows user account, say "
SharedUsr
", on both WS1 and WS2.
- Add
<identity impersonate="true" userName="SharedUsr" password="xxxxxx"/>
to web.config on both servers.
- Run these commands on both servers to grant "
SharedUsr
" access to the shared folder used by my ASP.NET application:
C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS1\SharedUsr
C:\WINNT\Microsoft.NET\Framework\v2.0.50727>aspnet_regiis -ga WS2\SharedUsr
- Make sure "
SharedUsr
" has write permission on the shared folder
This, however, does not work if the current thread spawns a new thread to write to the shared folder, because the new thread does not automatically impersonate "SharedUsr
". I have to write code to force the impersonation. The following is the code. I have left out some details for clarity.
......
using System.IO;
using System.Security.Principal;
using System.Threading;
public partial class GenerateFiles : System.Web.UI.Page
{
private WindowsIdentity wid = null;
protected void Page_Load(object sender, EventArgs e)
{
wid = WindowsIdentity.GetCurrent();
try
{
if (ThreadPool.QueueUserWorkItem(new WaitCallback(File2Callback),
new File2Generator(file2Location)))
{
Response.Write(file1URL);
}
else
{
Response.Write("Server too busy. The work item could not be queued.");
}
}
catch (Exception ex)
{
Response.Write(ex.Message);
}
}
private void File2Callback(object o)
{
WindowsPrincipal principal = new WindowsPrincipal(wid);
Thread.CurrentPrincipal = principal;
WindowsImpersonationContext wic = wid.Impersonate();
File2Generator f2gen = (File2Generator)o;
f2gen.Generate();
wic.Undo();
}
}
In the beginning of Page_Load
, we assume file1
has been generated. Then, we use a worker thread from the threadpool to queue the generation of file2
for execution. The QueueUserWorkItem
method has two parameters. The first parameter specifies the function to be called when the worker thread has its turn. The second parameter is the data to be passed to the function. In our case, it's an instance of File2Generator
class, which encompasses necessary data and functionality to generate file2
. The implementation of File2Generator
is not listed here because the detail is not important for making the point.
The callback function, File2Callback
, will be executed on the worker thread that has the Windows identity "ASPNET
", not "SharedUsr
". So in it, we need to change the worker thread's impersonation before calling the Generate
method of File2Generator
.