views:

144

answers:

2

Simplest explanation I can produce:

In my .NET1.1 web app I create a file on disc, in the Render method, and add an item to the Cache to expire within, say, a minute. I also have a callback method, to be called when the cache item expires, which deletes the file created by Render. In the Page_Init method I try to access the file which the Render method wrote to disc. Both these methods have a lock statement, locking a private static Object.

Intention:

To create a page which essentially writes a copy of itself to disc, which gets deleted before it gets too old (or out of date, content-wise), while serving the file if it exists on disc.

Problem observed:

This is really two issues, I think. Requesting the page does what I expect, it renders the page to disc and serves it immediately, while adding the expiry item to the cache. For testing the expiry time is 1 minute.

I then expect that the callback method will get called after 60 seconds and delete the file. It doesn't.

After another minute (for the sake of argument) I refresh the page in the browser. Then I can see the callback method get called and place a lock on the lock object. The Page_Init also gets called and places a lock on the same object. However, both methods appear to enter their lock code block and proceed with execution.

This results in: Render checks file is there, callback method deletes file, render method tries to serve now-deleted-file.

Horribly simplified code extract:

public class MyPage : Page
{
  private static Object lockObject = new Obect();

  protected void Page_Init(...)
  {
    if (File.Exists(...))
    {
      lock (lockObject)
      {
        if (File.Exists(...))
        {
          Server.Transfer(...);
        }
      }
    }
  }

  protected override void Render(...)
  {
    If (!File.Exists(...))
    {
      // write file out and serve initial copy from memory
      Cache.Add(..., new CacheItemRemovedCallback(DoCacheItemRemovedCallback));
    }
  }

  private static void DoCacheItemRemovedCallback(...)
  {
    lock (lockObject)
    {
      If (File.Exists(...))
        File.Delete(...);
    }
  }
}

Can anyone explain this, please? I understand that the callback method is, essentially, lazy and therefore only calls back once I make a request, but surely the threading in .NET1.1 is good enough not to let two lock() blocks enter simultaneously?

Thanks,

Matt.

A: 

First thing I would do is open the Threads window and observe which thread is the Page_Init is running on and which thread the Call Back is running on. The only way I know that two methods can place a lock on the same object is if they are running in the same thread.

Edit

The real issue here is how Server.Transfer actually works. Server.Transfer simply configures some ASP.NET internal details indicating that the request is about to be transfer to a different URL on the server. It then calls Response.End which in turn throws a ThreadAbortException. No actual data has been read or sent to the client at that time.

Now when the exception occurs code execution leaves the block of code protect by the lock. At this time the Call back function can acquire the lock and delete the file.

Now somewhere deep inside ASP.NET the ThreadAbortException is handled in some way and the request for the new URL is processed. At this time it finds the file has gone missing.

AnthonyWJones
Ok, thanks for the advice - I didn't actually realise there was a threads window!Anyway, having done that I see you are right and that there are two threads each able to enter their own lock statement blocks while having a lock on the one static object.This, as I understand it however, goes against what Microsoft say it should do. Unless I'm getting confused with (perhaps) disparate uses of the term 'thread': http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.71).aspx
Matt W
You're not confused. Two different threads cannot lock the same object at the same time. Period. Something else is happening. Add `readonly` to the lockObject so that it can't be replaced. How do manage to observe this behaviour exactly?
AnthonyWJones
I'm just stepping through the code.The outward observation is that the page which is visibly cached on disc gives an exception when I try to Server.Transfer to it. It should be there because just before I transfer to it, I check for it's existence. However, when the transfer takes place, it's gone, causing the exception.The inward view of the issue shows that both lock statements are executing simultaneously. One gets ahead of the other when one requests an object from the cache. I would assume this is where the other thread can get ahead because its not making a slow framework call.
Matt W
A: 

Not sure why your solution doesn't work, but that might be a good thing, considering the consequences...

I would suggest a completely different route. Separate the process of managing the file from the process of requesting the file.

Requests should just go to the cache, get the full path of the file, and send it to the client.

Another process (not bound to requests) is responsible for creating and updating the file. It simply creates the file on first use/access and stores the full path in the cache (set to never expire). At regular/appropriate intervals, it re-creates the file with a different, random name, sets this new path in the cache, and then deletes the old file (being careful that it isn't locked by another request).

You can spawn this file managing process on application startup using a thread or the ThreadPool. Linking your file management and requests will always cause you problems as your process will be run concurrently, requiring you to do some thread synchronization which is always best to avoid.

Will
What I ended up doing was not deleting the files on a cache expiry, but took the cache out of the equation entirely - by simply checking the LastWrite and LastModified stamps. This requires that I re-stamp them when I overwrite the files with newer versions, because I did not observe any changes in their time stamps in Explorer, or when checking back programmatically for a file which had been overwritten moments before.
Matt W