Synchronising downloads and updates (2)
There was a potential deadlock in the last piece of code. When you obtain any kind of lock you will mostly use a try..finally block to ensure the lock is released when you have finished with it. The using() command expands to a try..finally block so the original code might look okay, however if you mentally expand the code you will see the problem.
using (FileLockingService.FileReadLock(product1.ID))
{
//Some code here
}
This becomes
var disposable = FileLockingService.FileReadLock(product1.ID);
try
{
//Some code here
}
finally
{
disposable.Dispose();
}
So why is this a problem? In a single threaded application it isn't, but this is a website so there are multiple threads running at the same time. Now take into account the fact that a web request has a timeout, what if the thread is aborted after the lock is obtained? Ordinarily this isn't a problem because the acquisition is followed immediately by a "try", but if we expand this code further it is evident that this isn't the case for the code I originally wrote. The code below is indented per method rather than the normal logical indendation
- var disposable =
- //Code inside FileLockingService
- var lockHandler = GetLockHandler(int id);
- lockHandler.EnterWriteLock();
//The next command must be a try block - var enterWriteLockResult = new DisposableAction(..............); //Oops!
- if (action == null) throw new ArgumentNullException("Action");
- this.Action = action;
- this.Context = context;
- if (action == null) throw new ArgumentNullException("Action");
- return enterWriteLockResult;
- //Code inside FileLockingService
- try
{
//Some code here
}
finally
{
disposable.Dispose();
}
So here you see that rather than there being a TRY block immediately after the EnterWriteLock the code above would create a DisposableAction, check a parameter, assign two instance members, and then return a result. If anything went wrong at any of these points (OutOfMemoryException, ThreadAbortException, etc) then we'd end up with a lock that would not get removed. The code has been changed as follows.
public class FileLockingService : IFileLockingService
{
readonly object SyncRoot = new object();
readonly Dictionary<int, WeakReference> Locks = new Dictionary<int, WeakReference>();
public ReaderWriterLockSlim GetLockHandler(int productId)
{
WeakReference resultReference;
ReaderWriterLockSlim result;
lock (SyncRoot)
{
if (Locks.TryGetValue(productId, out resultReference))
{
result = (ReaderWriterLockSlim)resultReference.Target;
if (result != null)
return result;
}
result = new ReaderWriterLockSlim();
Locks[productId] = new WeakReference(result);
}
return result;
}
}
and when I use this service:
FileLockingService.GetLockHandler(product.ID).EnterReadLock();
//.NET wont allow any exceptions until flow passes into the try block
try
{
//This is the earliest point that an exception can occur after acquiring the lock
using (var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Response.ClearHeaders();
Response.AddHeader("Content-Type", "Application/Binary");
Response.AddHeader("Content-Disposition", "attachment; filename=" + product.ClientFileName);
using (var resultStream = DownloadService.Process(currentUser, new List<string>(), product.ClientFileName, sourceStream))
{
int bytesRead;
byte[] data = new byte[1024];
while ((bytesRead = resultStream.Read(data, 0, data.Length)) > 0)
Response.OutputStream.Write(data, 0, bytesRead);
Response.End();
}//using
}//filestream
}
finally
{
FileLockingService.GetLockHandler(product.ID).ExitReadLock();
}
Lesson learned: When it comes to thread synchronisation don't do anything fancy!
Comments