Performing A Double-Check Lock Around "Run Once" Code In ColdFusion
One of the wonderful things about ColdFusion is that it comes with a fully-synchronized application setup workflow thanks to the onApplicationStart()
life-cycle method in the Application.cfc
ColdFusion application framework component. But, not all "setup" code can be run during the bootstrapping of the application. Some setup code needs to be run "on demand" later in the application lifetime. In such scenarios, I almost always reach for a double-check lock pattern of execution. This allows setup code to be synchronized with almost no locking overhead.
The double-check lock is a pattern of deferred synchronization in which the single-threading of code is only applied when a given condition is not yet true. Then, once the condition is true, all subsequent paths through the same code skip the synchronization. That said, this only works if there is a condition that can be checked without locking.
The double-check lock pattern is a 3-step process:
Check to see if the setup condition has been reached.
- If so, skip lock and just consume cached values.
If setup condition has not been reached, acquire exclusive lock.
Once exclusive lock is acquired, check setup condition again to see if it was reached while request was queued-up awaiting lock access.
- If so, skip setup and just consume cached values.
- If not, perform setup logic and cache result.
With this workflow, the vast majority of requests never make it past step-1; the setup code has already been executed by an earlier request and the current request can just read the cached values - no locking required at all. Only the first request(s) actually incur the overhead of blocking-and-waiting for the exclusive lock. And, even if multiple concurrent requests block-and-wait for the lock, only one of the requests ever actually performs the setup logic - the rest of the requests will skip the setup logic due to the second check performed within the lock.
This is a pattern that makes a lot more sense once you see it in action. As such, let's look how I load (and cache) my environment-specific .json
configuration file on every request that runs through my Application.cfc
component. Since loading the configuration requires both a file read and a JSON deserialization, we certainly don't want to be doing this on every request - that would be bad for performance. Instead, we want to load the config file once, cache it, and then consume the cached value on most requests. And, in order to avoid locking, we're going to use the double-check lock pattern.
component
output = false
hint = "I define the application settings and event handlers for the ColdFusion application."
{
// Define application settings.
this.name = "DoubleCheckLockDemo";
this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
this.sessionManagement = false;
this.config = loadConfig();
// Consume the config object on every request in our per-application settings.
// --
// this.datasource = this.config.dsn.source;
// this.smtpServerSettings = this.config.smtp;
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I run once at the top of each page request.
*/
public void function onRequestStart( ) {
dump( this.config );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I return the ENV configuration. If the configuration payloads isn't available yet,
* it is loaded into memory and cached.
*/
private struct function loadConfig() {
// DOUBLE-CHECK LOCK: This technique is used to SINGLE-THREAD an area of the
// application that requires special initialization / setup without having to
// single-thread the entire initialization workflow. This helps minimize the
// impact on performance through "hot areas" of the application.
// STEP 1: Check for the "Condition" to be true without synchronization. The first
// step in the double-check lock is to check the state of the application to see
// if the initialization / setup has already been run. In this case, we'll know
// that the setup has been completed if the "config" object exists in the server
// scope.
// --
// NOTE: Struct access is IMPLICITLY THREAD-SAFE in ColdFusion. As such, we don't
// have to worry about READING from shared-memory here.
if ( server.keyExists( "config" ) ) {
return( server.config );
}
// STEP 2: Lock / Synchronize / Single-Thread the setup logic. If we made it
// this far (ie, the return statement above wasn't executed), we know that the
// configuration data hasn't been cached yet. As such, let's open an EXLUSIVE lock
// to synchronize access to the underlying config file and the caching thereof.
lock
name = "Application.cfc:loadConfig"
type = "exclusive"
timeout = 2
{
// STEP 3: Perform the SECOND CHECK of the "Condition". At this moment, this
// request is the only request that currently has access to this lock.
// However, it's possible that we acquired the lock AFTER another request had
// already acquired it - and had ALREADY PERFORMED THE SETUP. As such, inside
// the lock we have to perform another check to see if the setup logic still
// needs to be performed. We only want to incur the overhead of the setup if
// it is necessary work.
if ( ! server.keyExists( "config" ) ) {
systemOutput( "Loading and caching config object into memory.", true );
server.config = deserializeJson( fileRead( ".env.json" ) );
}
return( server.config );
}
}
}
As you can see, every single request is calling the loadConfig()
method in the Application.cfc
pseudo-constructor. And, almost every single request will pull that config data right out of the server
scope. In this case, our double-check lock "condition" is whether or not the config
key exists in the server
scope. And, since all Struct objects in ColdFusion are implicitly thread-safe, this allows us to read that shared memory without additional locking.
We only dip down into single-threaded, synchronized code if - and only if - the config
object has not yet been cached. In that case, we acquire our lock, check our condition one more time, and then read the config file off of disk and cache the data for future requests.
Hopefully now that you've see it in action, it's more clear how the double-check lock pattern works. Most requests never see the locking - they just consume the cached data. Only the first (few) requests ever deal with locking and the "run once" nature of the setup code. I use this pattern all the time. It works great, especially with anything that needs to be cached in a ColdFusion application.
Want to use code from this post? Check out the license.
Reader Comments
One of my co-workers, Shawn Hartsell, pointed me to a fascinating article that shows some possible short-comings for the double-check lock pattern that are actually taking place at compile time. I am not sure if this affects us in ColdFusion, but it's a very interesting read:
https://www.cs.cornell.edu/courses/cs6120/2019fa/blog/double-checked-locking/
To be clear, this doesn't affect all double-check locks - it has to do with how the code gets compiled and how you are organizing the logic.
@Ben,
I use double checked locking anywhere I'm using caching in C#, especially after being burned a couple of times early on calling expensive database calls much more often that I expected shortly after coming from the single-threaded JavaScript environment in a browser. I've suggested double checked locking in code review multiple times. If you're in a multi-threaded environment and need caching, double checked locking is the way to go, IMO
To my knowledge, I've never run into a case where the possible issues from the linked article have occurred. Not sure if that is due to how I use double checked locking, or if the .NET compiler doesn't try to tackle that type of compilation instruction re-odering, or is smart enough to avoid that type of instruction re-ordering within locks.
@Danilo,
Yeah, I don't think I've ever run into it either. Also, I wonder if caching (which is where I use the pattern most of the time) "fails more gracefully" anyway. But yeah, I'm not going to spend any brain-cycles worrying about it, to be honest 🤪
What approach do you recommend to perform a "forced refresh" of server-scoped variables? Is it safe to just add an additional argument to delete the server key?
@James,
At a high-level, yes - that's usually how I do it. And, sometimes it's no issue at all; and, sometimes it gets tricky. Deleting a struct key becomes challenging when that key points to a struct that also contains other keys. For example, in my blog, I have:
application.services
... where
services
is a Struct that contains a bunch of component instances. The problem with this is that if I just delete and recreate the.services
key during a force refresh of the application state, there's an OK chance that a parallel request is about to reference something inside of thatservice
struct and cause an error.So, to reduce the chances of an issue, my force refresh code looks like this:
This way, if I'm doing a force refresh, I keep the existing structs in place but start overwriting the keys in that struct. This has proven to reduce the chances that I accidentally break another parallel request while force-refreshing the app.
Of course, every situation is different. If I were dealing with a simple "Cache", then yeah, I just delete the key and let the next request "rebuild" the cached value (likely inside a double-check lock).
@All,
Just cross-pollinating here, but I just commented in a newer post on using per-application datasources, that I am using this same double-check lock pattern to load the config data that I need to setup my datasources within the
Application.cfc
pseudo-constructor. In my demo above, I have thethis.datasources
stuff commented-out. But, in practice, here's what I have done:www.bennadel.com/blog/4220-moving-mysql-to-a-per-application-datasource-in-coldfusion-2021.htm
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →