2009-03-02

Synchronising downloads and updates

Apparently my boss used to be annoyed with something on his old website. When he wanted to upload an updated binary he often couldn't because his product was so popular that someone was always downloading it at the same time. He'd have to continuously hit Delete in his FTP client until eventually he'd slip in and manage to delete it before updating the new version.

This obviously isn't an ideal solution because:

  1. He constantly had to sit there for around an hour trying to delete it.
  2. Once deleted from the disk any user attempting to download would get a 404 – Not Found error.
  3. During upload any user attempting to download would get an Access Denied error of some kind.

For this website I have decided first of all to only update the files via a HTTP form post. So firstly any long running upload will not deny access to the currently available file, this eliminates problem 3. Additionally, using a form post means I can overwrite the existing file, so this eliminates problem 2.

The potential problems I have now though are:

  1. If he uploads but the file is in use the process will fail, causing him to have to upload again. From his upload use case this is worse than before because it takes much longer to discover your upload hasn't worked.
  2. If a user attempts to access the file during the process of copying from memory to disk they will still get an access error. Although this is now much less likely it could still happen.

My solution was to implement an IFileLockingService. Downloading a file should place a read lock, uploading the file should place a write lock. Multiple read locks may exist at once, but a write lock is exclusive. Any conflicts will result in the lock request waiting until it may proceed.

//Downloading
using (FileLockingService.FileReadLock(Product.Id))
{
    //Write a binary response based on the file
}

//Uploading
using (FileLockingService.FileWriteLock(Product.Id))
{
    //Write the posted file to disk
}

To implement the read/write lock I used a ReaderWriterLockSlim instance per Product.ID. To keep memory usage down I ensured that I used a WeakReference to hold the ReaderWriterLockSlim reference so that it may be collected once no longer referenced.

public class FileLockingService : IFileLockingService
{
    readonly object SyncRoot = new object();
    readonly Dictionary<int, WeakReference> Locks = new Dictionary<int, WeakReference>();

ReaderWriterLockSlim GetLockHandler(int id)
{
    WeakReference resultReference;
    ReaderWriterLockSlim result;
    lock (SyncRoot)
    {
        if (Locks.TryGetValue(id, out resultReference))
        {
            result = (ReaderWriterLockSlim)resultReference.Target;
            if (result != null)
                return result;
        }
        result = new ReaderWriterLockSlim();
        Locks[id] = new WeakReference(result);
    }    
    return result;
}

public IDisposable FileReadLock(int productId)
{
    var lockHandler = GetLockHandler(productId);
    lockHandler.EnterReadLock();
    return new DisposableAction(context => lockHandler.ExitReadLock(), null);
}

public IDisposable FileWriteLock(int productId)
{
    var lockHandler = GetLockHandler(productId);
    lockHandler.EnterWriteLock();
    return new DisposableAction(context => lockHandler.ExitWriteLock(), null);
}
}

DisposableAction is a class I have mentioned here

No comments: