Adding Closure-Based Distributed Lock Management To CFRedlock
Yesterday, I release CFRedlock, which is my ColdFusion implementation of the distributed locking algorithm proposed by the Redis group. Unlike the native CFLock tag, however, using CFRedlock requires you to manage the lock life-cycle in your calling code. I really missed the cleanliness of the CFLock tag; so, I tried to recreate it using closures. Now, CFRedlock offers internally synchronized execution of a given closure (or function reference) so that you don't have to explicitly manage your own locks.
Project: See the CFRedlock on my GitHub account.
First, let's look at how the code works if you want to manage the life-cycle yourself. In this demo, I am explicitly obtaining the lock and then explicitly releasing the lock when I am done:
<cfscript>
// Try to acquire a lock that will expire in 20-seconds.
myLock = application.locking.getLock( "my-first-lock", ( 20 * 1000 ) );
// Since we have to be sure to release the lock, we have to wrap our code in
// a try-finally block so that, no matter what, we release the lock when we
// are done using it.
// --
// NOTE: The lock would still expire eventually, but this is good form.
try {
writeOutput( "Locking like a boss!" );
// No matter what happens, release the lock.
} finally {
myLock.releaseLock();
}
</cfscript>
As you can see, a good deal of this code is just there to manage the lock itself, having little to do with your business logic. Using a closure, however, we can push that lock management back into the client so that we don't have to think about it. The DistributedLockClient.cfc has two closure-based methods:
- .executeLock( name, ttl, operator ) - acquire the given lock and execute the operator. Throw error if lock cannot be acquired.
- .executeLockOrSkip( name, ttl, operator ) - acquire the given lock and execute the operator. Skip operator execution but do not throw an error if the lock cannot be acquired.
They both have the same signature. The difference is that the latter one - executeLockOrSkip() - will fail silently rather than throwing an error. Neither of the methods will execute the operator / closure / function if the lock cannot be obtained. But, the executeLockOrSkip() is meant to mimic the throwOnTimeout="false" configuration from the native CFLock tag, which is great for things like scheduled-task execution.
Ok, here's the same workflow as above, but using closure-based lock management:
<cfscript>
// Try to acquire a lock that will expire in 20-seconds. When / if the lock is
// acquired, execute the given closure and return the result.
result = application.locking.executeLock(
"my-first-lock",
( 20 * 1000 ),
function() {
return( "Locking like a boss!" );
}
);
// Output the result of the closure execution.
writeOutput( result );
</cfscript>
Here, you can see that there is no Try/Finally block or any acquired-lock object to manage. You just pass in the closure and the distributed lock client will take care of obtaining the lock, executing your closure, and then releasing the lock. You just have to worry about the business logic and handling any errors that your code may throw.
There is a beautiful simplicity to the native CFLock tag. A simplicity that is lost once you have to start managing locks yourself. But, I think with a little shift in approach, we can encapsulate some of that logic and move back towards some of that original simplicity.
Want to use code from this post? Check out the license.
Reader Comments