Skip to main content
Ben Nadel at RIA Unleashed (Nov. 2009) with: Andy Powell
Ben Nadel at RIA Unleashed (Nov. 2009) with: Andy Powell@umAndy )

Performing A Double-Check Lock Around "Run Once" Code In ColdFusion

By on
Tags:

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.

Double-check lock decision tree shows when cached values are used and when values are initially cached.

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:

  1. Check to see if the setup condition has been reached.

    • If so, skip lock and just consume cached values.
  2. If setup condition has not been reached, acquire exclusive lock.

  3. 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

15,192 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.

15 Comments

@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.

15,192 Comments

@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 🤪

72 Comments

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?

if (arguments.forceRefresh) {
	structDelete(server, "config");
}
15,192 Comments

@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 that service struct and cause an error.

So, to reduce the chances of an issue, my force refresh code looks like this:

// CAUTION: Using param tag so that we don't clobber the data-structure - it may
// be getting actively referenced by another request that is currently loading.
param name="application.services" type="struct" default={};
param name="application.services.blog" type="struct" default={};

application.services.errorService = new component....;
application.services.shortUrlService = new component....;
application.services.blog.commentEditTokenService = new component....;

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).

15,192 Comments

@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 the this.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!

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.