Managing business object locks on application level
Today I worked out simple application side object locking solution for one server scenario. My motivation came from ASP.NET architecture forum thread How to solve concurrent site issue where one user asks for locking solution that works without changing database. Here is my simple and primitive solution that should help this guy out. I added also sample solution to this posting.
My solution is simple:
- Create class for locked items.
- Create manager class that holds locks and manages them.
- Clear locks when session ends.
- Create page to release all locks.
If you need something more serious then you should keep locks information in database or better than that – use some lock server. Also you may consider developing WCF service.
LockItem class
As a first thing let’s create class that keeps lock information. I call this class as LockItem. This class doesn’t hold references to locked objects – only type and ID as there are many business layers that doesn’t use globally unique identifiers for objects.
public class LockItem
{
public Type ObjectType { get; set; }
public int ObjectId { get; set; }
public string SessionId { get; set; }
public DateTime LockedAt { get; set; }
}
LockItem class also contains property for session because otherwise it is not possible to know what objects session should release. LockedAt property is used to release expired locks.
LockManager
LockManager is the most complex part of this example. It handles all the locking actions and keeps internal locks registry that contains information about locks. Here is the source for LockManager.
public static class LockManager
{
private static List<LockItem> _lockedItems;
static LockManager()
{
_lockedItems = new List<LockItem>();
}
public static bool IsLocked(BusinessBase obj)
{
var locksQuery = from l in _lockedItems
where l.ObjectType == obj.GetType()
&& l.ObjectId == obj.Id
select l;
return (locksQuery.Count() > 0);
}
public static bool Lock(BusinessBase obj, string sessionId)
{
if (IsLocked(obj))
return false;
lock (_lockedItems)
{
var lockItem = new LockItem
{
ObjectType = obj.GetType(),
ObjectId = obj.Id,
LockedAt = DateTime.Now,
SessionId = sessionId
};
_lockedItems.Add(lockItem);
return true;
}
}
public static void ReleaseLock(BusinessBase obj, string sessionId)
{
lock (_lockedItems)
{
var locksQuery = from l in _lockedItems
where obj.GetType() == obj.GetType()
&& obj.Id == l.ObjectId
&& sessionId == l.SessionId
select l;
ReleaseLocks(_lockedItems);
}
}
public static void ReleaseSessionLocks(string sessionId)
{
lock (_lockedItems)
{
var locksQuery = from l in _lockedItems
where sessionId == l.SessionId
select l;
ReleaseLocks(locksQuery);
}
}
public static void ReleaseExpiredLocks()
{
lock (_lockedItems)
{
var locksQuery = from l in _lockedItems
where (DateTime.Now - l.LockedAt)
.TotalMinutes > 20
select l;
ReleaseLocks(locksQuery);
}
}
public static void ReleaseAllLocks()
{
lock (_lockedItems)
{
_lockedItems.Clear();
}
}
private static void ReleaseLocks(IEnumerable<LockItem> lockItems)
{
if (lockItems.Count() == 0)
return;
foreach (var lockItem in lockItems.ToList())
_lockedItems.Remove(lockItem);
}
}
You can write special page to administrator interface that has button for calling ReleaseAllLocks(). It is good idea because at least during development I am sure you are creating many situations where locks are not released due to exceptions.
Release session locks when session ends
When session ends you have to release all locks that this session has. Otherwise those objects will be locked until you restart your web application or call ReleaseLocks method. Although ASP.NET Session_End event is called god knows when and you should not use this method due to this uncertainty I base my example exactly on that event. Copy this method to your Global.asax.ascx file.
protected void Session_End()
{
LockManager.ReleaseSessionLocks(Session.SessionID);
}
Now we have almost everything in place to start using locks.
Testing locks
I created simple view to test locks. You need two browsers to test if locks work okay. One browser locks the object for 20 seconds and the other gives you messages that object is locked. You have to make this second request with another browser during this 20 seconds when object is locked. Here is the ASP.NET MVC controller action for that purpose.
public void Index()
{
Response.Clear();
Response.ContentType = "text/plain";
var product = new Product
{
Id = 1,
Name = "Heineken",
Price = 1.2M
};
if (!LockManager.Lock(product, Session.SessionID))
{
Response.Write("Object has already lock on it!\r\n");
Response.End();
return null;
}
Response.Write("Object successfully locked\r\n");
Response.Flush();
Thread.Sleep(20000);
Response.Write("Releasing lock\r\n");
LockManager.ReleaseLock(product, Session.SessionID);
Response.Write("Lock released\r\n");
Response.End();
}
If you are using old ASP.NET application then paste this code to Page_Load event of your application.
From here …
… you can go on and make your own modifications and customizations to this code. Before going to live with your own locking strategy test it hundred times at least. To make administration easier you can also create page that shows active locks and lets them release one lock at time or by session ID.
I would move the lock( _lockItems) into ReleaseLocks and remove the locks from the methods calling ReleaseLocks.
It is not need to lock the query, since it will be executed in the lock anyway.
And maybe you could use .Any() instead of .Count() since you are not really interested in how many locks there are, just “any” will do ;)
Another thing is that I would let LockManager return a disposable lock object so the dev can use the using keyword to release the lock when done with it.
Something like
using (LockManager.Lock(product, Session.SessionID)){
…
}
or
using (LockManager.TryLock(product, Session.SessionID, out locked))
{
if (locked)
{
…
}
}
// Ryan