A look at runAsync, futures and callbacks in ColdFusion 2018 Update 2.
One of the enhancements in ColdFusion 2018 is the addition of the runAsync
method which allows for asynchronous programming.
In ColdFusion the RunAsync
function returns a Future
object. A Future is like a promise where the result of an operation can be provided sometime in the future. You would return a Future where you want to attach callbacks, instead of passing callbacks into a function.
Having not used ColdFusion’s Futures before I thought I try it out to see how it works. I’m testing against ColdFusion 2018 Update 2.
To test how runAsync works, I’ve created the following simple FeedReader.cfc
.
component { function fetchNewsReports() { sleep(4000); return SerializeJSON({ "message": "fetchNewsReports - delay 4000" }); } function fetchWinningLotteryNumbers() { sleep(3000); return SerializeJSON({ "message": "fetchWinningLotteryNumbers - delay 3000" }); } function fetchWeatherForecast() { sleep(2000); return SerializeJSON({ "message": "fetchWeatherForecast - delay 2000" }); } function fetchFootballScores() { sleep(1000); return SerializeJSON({ "message": "fetchFootballScores - delay 1000" }); } }
Nothing very exciting to see here, each method has a delay built into it of between 1 and 4 seconds to simulate a slow process. Each one returns a JSON string as if we where calling an API.
One of the advantages of using a Future is that you can register multiple callbacks by chaining then()
methods together. The result (if any) from each then()
is passed into the subsequent then()
Here’s example of chaining hooked up to my FeedReader.cfc
function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); // call runAsync and chain the callbacks to log the output var a = runAsync(FeedReader.fetchNewsReports) .then(function(r) { print(r); }) .then(function() { return FeedReader.fetchWinningLotteryNumbers(); }) .then(function(r) { print(r); }) .then(function() { return FeedReader.fetchWeatherForecast(); }) .then(function(r) { print(r); }) .then(function() { return FeedReader.fetchFootballScores(); }) .then(function(r) { print(r); }); print("before .get() - elapsed time: #getTickCount()-start#"); // ensure futures are done a.get(); print("completed - total time: #getTickCount()-start#"); } // place to track what is happening log = []; // function to use as callback function print(message) { log.append(message); } // kick the whole thing off main(); // show what happened writeDump(log);
In the above code I wrap the FeedReader.fetchNewsReports
method call in runAsync()
. The result from that call gets passed into the following then()
. The next then()
follows that etc, etc.
I’ve deliberately chained the FeedReader
methods in order of slowest first.
Running the above code produces:
1 started 2 before .get() - elapsed time: 7 3 {"message":"fetchNewsReports - delay 4000"} 4 {"message":"fetchWinningLotteryNumbers - delay 3000"} 5 {"message":"fetchWeatherForecast - delay 2000"} 6 {"message":"fetchFootballScores - delay 1000"} 7 completed - total time: 10003
From this we can see that the whole operation took 10 seconds overall and ran each method in FeedReader
in sequence, waiting for it to complete before calling the next then()
.
We can also see that the async code did not execute until a.get()
was called as we didn’t need the result until that point.
—–
Before we go any further, there is a lot of unnecessary code that I included to make it clear what the callbacks functions are receiving and returning, but they make it quite hard to read. Let’s clean that up and do something much more readable.
function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); // call runAsync and chain the callbacks to log the output var a = runAsync(FeedReader.fetchNewsReports) .then(print) .then(FeedReader.fetchWinningLotteryNumbers) .then(print) .then(FeedReader.fetchWeatherForecast) .then(print) .then(FeedReader.fetchFootballScores) .then(print); print("before .get() - elapsed time: #getTickCount()-start#"); // ensure futures are done a.get(); print("completed - total time: #getTickCount()-start#"); } log = []; function print(message) { log.append(message); } main(); writeDump(log);
This code does exactly the same as it did before, but we’ve got rid of all those anonymous functions as they didn’t actually do anything. This makes the code much shorter and easier to read. You can clearly see that it is going to do x then do y then do z etc.
Just to prove I haven’t broken it – running the above code produces:
1 started 2 before .get() - elapsed time: 2 3 {"message":"fetchNewsReports - delay 4000"} 4 {"message":"fetchWinningLotteryNumbers - delay 3000"} 5 {"message":"fetchWeatherForecast - delay 2000"} 6 {"message":"fetchFootballScores - delay 1000"} 7 completed - total time: 10002
—–
Each of the methods in the FeedReader
is returning a JSON string. Really we want to parse that and grab the message from it to print. As the then()
calls can be chained we can add a callback to do the deserialisation and pass the message onto the next then()
in the chain.
function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); var a = runAsync(FeedReader.fetchNewsReports) .then(parseFeed) .then(print) .then(FeedReader.fetchWinningLotteryNumbers) .then(parseFeed) .then(print) .then(FeedReader.fetchWeatherForecast) .then(parseFeed) .then(print) .then(FeedReader.fetchFootballScores) .then(parseFeed) .then(print); print("before .get() - elapsed time: #getTickCount()-start#"); // ensure futures are done a.get(); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function print(message) { log.append(message); } main(); writeDump(log);
Running the above now outputs just the message from the JSON string:
1 started 2 before .get() - elapsed time: 4 3 fetchNewsReports - delay 4000 4 fetchWinningLotteryNumbers - delay 3000 5 fetchWeatherForecast - delay 2000 6 fetchFootballScores - delay 1000 7 completed - total time: 10004
—–
What I’d like to be able to do is call each of the FeedReader
methods but for them not to be blocking – in other words, don’t wait for it to complete before calling the next one.
To do this I made each FeedReader method call a separate Future.
function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); var a = runAsync(FeedReader.fetchNewsReports) .then(parseFeed) .then(print); var b = runAsync(FeedReader.fetchWinningLotteryNumbers) .then(parseFeed) .then(print); var c = runAsync(FeedReader.fetchWeatherForecast) .then(parseFeed) .then(print); var d = runAsync(FeedReader.fetchFootballScores) .then(parseFeed) .then(print); print("before .get() - elapsed time: #getTickCount()-start#"); // make sure all futures are done a.get(); b.get(); c.get(); d.get(); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function print(message) { log.append(message); } main(); writeDump(log);
Running the above code produces:
1 started 2 before .get() - elapsed time: 6 3 fetchFootballScores - delay 1000 4 fetchWeatherForecast - delay 2000 5 fetchWinningLotteryNumbers - delay 3000 6 fetchNewsReports - delay 4000 7 completed - total time: 4002
A couple things to note here:-
- the script took 4 seconds to run, which is the same time as the slowest process (fetchNewsReports).
- the order of the calls has changed to be in the order they return.
—–
Having to do a get()
call on each Future is a bit tedious. It would be great if I could have a function that took all the futures and did that for us. To do that I’ve created a await
function that accepts an array of futures.
function await(futures=[]) { futures.each(function(future) { future.get(); }); } function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); var a = runAsync(FeedReader.fetchNewsReports) .then(parseFeed) .then(print); var b = runAsync(FeedReader.fetchWinningLotteryNumbers) .then(parseFeed) .then(print); var c = runAsync(FeedReader.fetchWeatherForecast) .then(parseFeed) .then(print); var d = runAsync(FeedReader.fetchFootballScores) .then(parseFeed) .then(print); print("before await() - elapsed time: #getTickCount()-start#"); await([a,b,c,d]); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function print(message) { log.append(message); } main(); writeDump(log);
The code now is a bit neater. The output from running it is:
1 started 2 before await() - elapsed time: 0 3 fetchFootballScores - delay 1000 4 fetchWeatherForecast - delay 2000 5 fetchWinningLotteryNumbers - delay 3000 6 fetchNewsReports - delay 4000 7 completed - total time: 4000
—–
I’d like to do a bit more refactoring and get rid of all those then()
calls. I could change await to return the result of each future. A bit like how Javascript’s Promise.all
behaves. I’ll also do the JSON parsing so that just the message is returned.
function await(futures=[]) { return futures.map(function(future) { return future.then(parseFeed).get(); }); } function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("start"); var a = runAsync(FeedReader.fetchNewsReports); var b = runAsync(FeedReader.fetchWinningLotteryNumbers); var c = runAsync(FeedReader.fetchWeatherForecast); var d = runAsync(FeedReader.fetchFootballScores); print("before await() - elapsed time: #getTickCount()-start#"); var resolvedFutures = await([a,b,c,d]); print("after await() - elapsed time: #getTickCount()-start#"); resolvedFutures.each(print); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function print(message) { log.append(message); } main(); writeDump(log);
That’s a bit cleaner and script execution is still 4 seconds.
—-
So far, we’ve only looked at the ‘happy path’ but what it one of the methods threw an error? A future has an error()
method. Before we can test that, we need to fake an error. I’m going to do that by adding this method to my FeedReader.cfc
.
function fetchGoBoom() { throw(type="FeedReader.fetchGoBoom"); }
I’ll update the code to call fetchGoBoom
instead of fetchFootballScores
and use the error()
method to handle the exception, adding a simple parseError
function.
function await(futures=[]) { return futures.map(function(future) { return future .then(parseFeed) .error(parseError) .get(); }); } function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); var a = runAsync(FeedReader.fetchNewsReports); var b = runAsync(FeedReader.fetchWinningLotteryNumbers); var c = runAsync(FeedReader.fetchWeatherForecast); // change to throw error var d = runAsync(FeedReader.fetchGoBoom); print("before await() - elapsed time: #getTickCount()-start#"); var resolved = await([a,b,c,d]); print("after await() - elapsed time: #getTickCount()-start#"); resolved.each(print); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function parseError(error) { return "==> ERROR: " & error.cause.type; } function print(message) { log.append(message); } main(); writeDump(log);
The output from running the above is:
1 started 2 before await() - elapsed time: 0 3 after await() - elapsed time: 4003 4 fetchNewsReports - delay 4000 5 fetchWinningLotteryNumbers - delay 3000 6 fetchWeatherForecast - delay 2000 7 ==> ERROR: FeedReader.fetchGoBoom 8 completed - total time: 4003
The error in fetchGoBoom
method call has been caught and passed into the error()
callback, parsed and a string returned.
—–
We’re handling an error in the FeedReader
, but the error may happen in the then()
callback. Let’s see what happens if we introduce an error at that point.
function throwError() { throw(type="await.then"); } function postError(message) { return "executed after error callback : " & message; } function await(futures=[]) { return futures.map(function(future) { return future .then(throwError) .error(parseError) .then(postError) .get(); }); } function main() { var FeedReader = new FeedReader(); var start = getTickCount(); print("started"); var a = runAsync(FeedReader.fetchNewsReports); var b = runAsync(FeedReader.fetchWinningLotteryNumbers); var c = runAsync(FeedReader.fetchWeatherForecast); // change to throw error var d = runAsync(FeedReader.fetchGoBoom); print("before await() - elapsed time: #getTickCount()-start#"); var resolved = await([a,b,c,d]); print("after await() - elapsed time: #getTickCount()-start#"); resolved.each(print); print("completed - total time: #getTickCount()-start#"); } log = []; function parseFeed(json) { var data = DeserializeJSON(json); return data.message; } function parseError(error) { return "==> ERROR: " & error.cause.type; } function print(message) { log.append(message); } main(); writeDump(log);
The output from running the above is:
1 started 2 before await() - elapsed time: 1 3 after await() - elapsed time: 4008 4 executed after error of type: ==> ERROR: await.then 5 executed after error of type: ==> ERROR: await.then 6 executed after error of type: ==> ERROR: await.then 7 executed after error of type: ==> ERROR: FeedReader.fetchGoBoom 8 completed - total time: 4008
So an error that happens in then()
is also caught and passed into the error()
callback, we can also continue after the error if we chain a subsequent then()
.
There we have it. This seems like a useful addition to ColdFusion. I hope my exploration of runAsync will be useful for others.
You must be logged in to post a comment.