Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dino Bucher
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dino Bucher ( @dinobucher )

Timeouts Only Apply To Last Future Thread, Not The Preceding Future Chain In ColdFusion 2018

By on
Tags:

When I first read the Future documentation in ColdFusion 2018, it was very tempting to think that everything was asynchronous. But, as I demonstrated yesterday, the .then() and .error() Future methods are blocking, synchronous calls in the parent context. That was a bummer of a discovery. And, it directly affects how you can think about Future Timeouts. Initially, I thought that a Future Timeout would apply to the entire preceding Future chain. But, as it turns out, a Timeout only applies to most recently-created Future in the chain.

To demonstrate the application of the Future Timeout, all we have to do is chain several Futures together, each with a sleep() command. Then, attempt to .get() the last one with a small timeout:

<cfscript>

	future = runAsync(
		function() {

			sleep( 1200 );

		}
		// NOTE: The ColdFusion documentation states that there is a DEFAULT TIMEOUT of
		// 1,000ms. This is clearly NOT TRUE.
	).then(
		function() {

			sleep( 500 );

		}
	).then(
		function() {

			sleep( 100 );

		}
	);

	// The Timeout here doesn't apply to the whole "Future Chain", as you might expect;
	// it only applies to the very last Future in the chain.
	future.get( 500 );

	writeOutput( "Done -- no Task timeout error." );

</cfscript>

As you can see, the entire Future chain should take about 1,800ms to resolve. So, if the 500ms timeout in the final .get() call applies to the overall Future chain, this page should throw a "java.util.concurrent.TimeoutException" error. However, when we run this page, we get the following page output:

Done -- no Task timeout error.

This page executes without error because the .get() call at the end only applies to the last-chained .then() callback - not to the entire Future chain. In fact, because we've previously demonstrated that .then() and .error() calls are blocking, synchronous calls in ColdFusion 2018, we know that most of the Future chain has already resolved by the time we get to our .get() call.

In addition to providing a timeout in the .get() method, you can also optionally provide a timeout in each .then() and .error() method. This got me curious about which Future this chained timeout applies to: the contextual Future? Or, the Future that is about to be created?

To test this, we just need to chain a few Futures with insightful sleep() commands at each step:

<cfscript>

	future = runAsync(
		function() {

			sleep( 1000 );

		},
		// This only applies to the sleep( 1000 ) inside the current callback.
		1200
	).then(
		function() {

			sleep( 500 );

		},
		// This only applies to the sleep( 500 ) inside the current callback.
		600
	).then(
		function() {

			sleep( 100 );

		},
		// This only applies to the sleep( 100 ) inside the current callback. This would
		// actually be equivalent to a future.get( 200 ) call below.
		200
	);

	future.get();

	writeOutput( "Done -- no Task timeout error." );

</cfscript>

Now, when we run this code, we get the following output:

Done -- no Task timeout error.

From this, we gain a clear understanding of the .then() and .error() timeouts: they apply to the current callback, not to the preceding Future in the chain. This makes sense in so much as it aligns with the runAsync() method, which accepts a timeout but has no preceding Future to consume.

Based on this new evidence, I wanted to take another stab at writing some pseudo-code for the .then() and error() methods. In my previous attempt, I mistakenly applied the timeout to the preceding Future in the chain. However, it is now clear that the timeout applies only to the Future generated by the provided callback:

<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( callback, thenTimeout ) {

		// !!! BLOCK AND WAIT !!! for the current Future to resolve.
		var result = this.get();

		// Spawn a new Future for the callback.
		var future = runAsync(
			function() {

				return( callback( result ) );

			},
			thenTimeout
		);

		return( future );

	};

	Future.prototype.error = function( callback, errorTimeout ) {

		try {

			// !!! BLOCK AND WAIT !!! for the current Future to resolve.
			var result = this.get();

			// If there is no error, just pass-on the current Future that will now
			// contain the resolved value.
			return( this );

		} catch ( any error ) {

			// Spawn a new Future for the callback.
			var future = runAsync(
				function() {

					return( callback( error ) );

				},
				errorTimeout
			);

			return( future );

		}

	};

</cfscript>

I think this pseudo-code mental model is more on-point.

As an aside, the runAsync() documentation states that there is a 1,000ms default timeout. However, upon further testing (not shown), this does not appear to be the case. If you sleep() a runAsync() callback for more than 1,000ms, no error is thrown.

Futures are definitely an interesting data-type in ColdFusion 2018. But, they're not quite like Promises; which means there's a lot of confusion in my mind that needs to be clarified. Understanding how Timeouts work in a Future chain is just one more step in reaching that clarity.

Want to use code from this post? Check out the license.

Reader Comments

27 Comments

Ben,

Timeout can be specified individually for each of the methods - runasync(), then(), error(). It will apply for the current callback. Timeout for get() will wait on the the result to be available within the specified time.

There is no default timeout. The default timeout value of 1000ms only applies for named parameter invocation. We will get this corrected in the doc.

15,674 Comments

@Vijay,

Sounds good. I think the nesting of runAsync() will also help clarify some of this timeout stuff. Plus, I think nested might make an easier mental model for some of this, rather than "chaining". I'm still sorting it all out in my head :)

20 Comments

Hi Vijay,

Regarding "There is no default timeout. The default timeout value of 1000ms only applies for named parameter invocation."

Are you saying positional parameters RunAsync(myFunction) has no default timeout but named parameters RunAsync(function=myFunction) has a 1000ms timeout!?

Using named parameters should not cause default values to change.

Could you please explain?

Thanks!,
-Aaron

P.S. Awesome observations Ben! Hope you will join PRs ;)

27 Comments

Aaron,

I am talking about the api-

RunAsync(myFunction, timeout)

positional invocation doesn't have any default timeout, but named param has it. This is still an internal default, we have removed that from documentation now. Named param logic requires optional param default values that's why we need to have them.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel