The Future Methods .then() And .error() Are Blocking In ColdFusion 2018
The other day, I read that a new version of ColdFusion has been release: ColdFusion 2018. And, while perusing the "What's New" list, I happened to see that asynchronous programming with "Futures" was one of the newly-introduced features. I've never used Futures in Java before; though, I have used Promises in JavaScript quite extensively. I have no idea how similar the two data-types are. So, I wanted to start digging into the ColdFusion implementation of Futures in order to see how the two approaches diverge. One unexpected behavior that I ran into immediately is that the .then() and .error() methods, exposed on the Future object, appear to be blocking.
Coming from the world of Promises, I expect everything except the Promise constructor to run asynchronously to the parent request. This includes methods like .then() and .catch(). So, naturally, I attempted to map this mental model onto the world of Futures. But, with Futures - at least with those in ColdFusion 2018 - the Promise-like methods of .then() and .error() appear to block the parent request before spawning a subsequent asynchronous thread.
To see this in action, all we need to do is write to the ColdFusion log in and around calls to runAsync():
<cfscript>
writeLog( "Outer A" );
runAsync(
function() {
sleep( 1000 );
writeLog( "Inner A" );
}
);
writeLog( "Outer B" );
</cfscript>
Here, we're writing to the logs before, after, and in the middle of the runAsync() call. And, when we run this code, we get the following [truncated]log output:
[INFO ] [XNIO-1 task-22] - Outer A
[INFO ] [XNIO-1 task-22] - Outer B
[INFO ] [AsynchExecutorThreadFactory-Thread_7] - Inner A
With the Promise-based mental model, this is exactly what we would expect. The writeLog() call that comes after the runAsync() method executes before the writeLog() call in the runAsync() method. This is because the runAsync() method is spawned in a parallel thread and is put to sleep for 1-second.
So far Promises and Futures look the same. But, if we add either a .then() or a .error() method (the choice doesn't matter) to the runAsync() response, behaviors start to diverge:
<cfscript>
writeLog( "Outer A" );
runAsync(
function() {
sleep( 1000 );
writeLog( "Inner A" );
}
).then(
function() {
writeLog( "Inner B" );
}
);
writeLog( "Outer B" );
</cfscript>
As you can see, we've done nothing here but chain a .then() call to the runAsync() Future. And, if we run this ColdFusion code, we get the following [truncated] log output:
[INFO ] [XNIO-1 task-16] - Outer A
[INFO ] [AsynchExecutorThreadFactory-Thread_8] - Inner A
[INFO ] [XNIO-1 task-16] - Outer B
[INFO ] [AsynchExecutorThreadFactory-Thread_16] - Inner B
This time, the first writeLog() within the runAsync() method executes before the writeLog() that comes afterward (in the parent context). By attaching a .then() call, the previously-asynchronous runAsync() callback has become - for all intents and purposes - a blocking call on the parent request.
This is clearly a different flow of control than what is presented in the world of Promises.
What's really interesting is that if look at the thread-name in the logs, we can see that the runAsync() callback and the .then() callback actually run in two completely different threads (Thread_8 and Thread_16, respectively). From this, I can gather that the .then() and .error() methods are probably just thin wrappers around the blocking, synchronous .get() call on the Future object.
Perhaps the .then() and .error() methods might be implemented with the following pseudo-code:
<cfscript>
// ------------------------------------------------------------------------------- //
// THIS IS JUST PSEUDO-CODE. I KNOW THAT COLDFUSION DOES NOT USE PROTOTYPES. THIS
// IS JUST A MENTAL GESTURE HERE, NOT AN ACTUAL IMPLEMENTATION. I'M JUST MAKING
// THIS STUFF UP AS I GO (HELPING TO BUILD MY MENTAL MODEL).
// ------------------------------------------------------------------------------- //
Future.prototype.then = function( thenCallback, thenTimeout ) {
// !!! BLOCK AND WAIT !!! for the current Future to resolve.
var result = this.get( thenTimeout );
// Spawn a new Future for the callback.
var newFuture = runAsync(
function() {
return( thenCallback( result ) );
}
);
return( newFuture );
};
Future.prototype.error = function( errorCallback, errorTimeout ) {
try {
// !!! BLOCK AND WAIT !!! for the current Future to resolve.
this.get( errorTimeout );
// If there is no error, just pass-on the current Future.
return( this );
} catch ( any error ) {
// Spawn a new Future for the callback.
var newFuture = runAsync(
function() {
return( errorCallback( error ) );
}
);
return( newFuture );
}
};
</cfscript>
As you can see, in my guesstimate of what is going on behind the scenes, the .then() and .error() methods are calling the .get() method on the current Future. This will block the current thread and wait for the Future to resolve. This would explain why, in a chained Future, all but the last callback appear to execute synchronously.
Futures seem like a really exciting data-type in ColdFusion 2018. And, as someone that has been using Promises in JavaScript for years, I'm very eager to see what kind of non-blocking workflows I can start to leverage in ColdFusion - a language that is, by default, synchronous and blocking. The tricky parts will be figuring out where the mental model of Promises and Futures diverge. And how to use Futures to really drive concurrent processing.
Want to use code from this post? Check out the license.
Reader Comments
@All,
As a relevant follow-up, I wanted to gain some clarity around the "timeout" values in a Future chain. At first, I thought that a timeout would apply to the previous Future chain; however, testing reveals that timeouts only apply to the Future created by the given .then() or .error() callback:
www.bennadel.com/blog/3491-timeouts-only-apply-to-last-future-thread-not-the-preceding-future-chain-in-coldfusion-2018.htm
This makes some sense in that is mirros the
runAsync( callback, timeout )
method, which takes a timeout value, but has no preceding Future to consume.Ben,
Please try below code snippet -
Below is the output-
Outer A
Outer B
Inner A
Inner B
First level then and error calls block main thread, but if you have nested runasync and have then and error attached then it won't block main thread as you can see in the output above.
@Vijay,
Yeah, it seems like the nested
runAsync()
is going to become a pattern that I explore more deeply. As you can see, it has come up in the comments in like 4 different posts already :P I've got some fun ideas marinating in the back of my mind.Awesome Ben, It would be great to see you building amazing stuffs with our async capability :)
@All,
I just tested this
runAsync().then()
in Lucee 5.3, and it seems to exhibit the same behavior; the initialrunAsync()
call is async, but when you add the.then()
, the execution of the first closure starts to block on the page and log-output is not in the order you would expect.