Yesterday, I used to be contemplating the ergonomics of Tags and Objects in ColdFusion. The factor that acquired me began down that path was the will so as to add retry logic to a CFHttp
workflow. And, the truth that extending the bottom performance of sure ColdFusion facets considerably requires transferring to an object-based / method-based interplay. As a fast-follow to that submit, I wished to discover the notion of a “retry coverage” in ColdFusion.
I latest releases of Lucee CFML, you possibly can add “listeners” to sure options of the language. These listeners act considerably like interceptors, permitting you to look at and alter the inputs and outputs of an underlying workflow. For instance, the Lucee CFML Question Listener means that you can present .earlier than()
and .after()
strategies which might be invoked earlier than and after SQL execution, respectively.
Alongside the identical strains, it might be good to supply a “listener” to the CFHttp
tag that drives retry mechanics. To discover this concept, I’ll borrow closely from the Amazon Net Providers (AWS) SDK which encodes the idea of a RetryPolicy
. This retry coverage composes logic that determines which requests could be retried; and, how lengthy the thread ought to wait earlier than trying the retry.
To translate this idea into ColdFusion, I’ll create a ColdFusion part that has two public strategies:
shouldRetry( request, response, depend )
delayBeforeNextRetry( request, response, depend )
These technique names are borrowed straight from the Java SDK. The shouldRetry()
technique returns a Boolean indicating whether or not or not the HTTP request needs to be retried. And, if that’s the case, the delayBeforeNextRetry()
technique returns the variety of milliseconds that the ColdFusion thread ought to pause earlier than making the subsequent HTTP request try.
For example this part API, this is my HttpRetryPolicy.cfc
, which applies some comparatively simplistic logic round retries. Basically, it is simply trying on the underlying HTTP standing code and is asking for a retry when sure standing codes are current:
part
output = false
trace = "I present hooks for managing the retry habits of a ColdFusion HTTP request."
{
/**
* I initialize the retry coverage with the given properties.
*/
public void operate init() {
backoffDurations = [
200, // After 1st attempt.
700, // After 2nd attempt.
1000, // After 3rd attempt.
2000, // After 4th attempt.
4000 // After 5th attempt.
];
maxRetryAttempts = backoffDurations.len();
}
// ---
// PUBLIC METHODS.
// ---
/**
* I decide if the HTTP request needs to be retried.
*/
public boolean operate shouldRetry(
required any httpRequest,
required any httpResponse,
required numeric requestsAttempted
) {
if ( requestsAttempted > maxRetryAttempts ) {
return( false );
}
if ( httpRequest.getAttributes().technique != "get" ) {
return( false );
}
change ( val( httpResponse.statusCode ) ) {
case 408: // Request timeout.
case 429: // Too many requests.
case 500: // Server error.
case 503: // Service unavailable.
case 504: // Gateway timeout.
return( true );
break;
default:
return( false );
break;
}
}
/**
* I decide how lengthy (in milliseconds) the thread ought to sleep earlier than retrying the
* HTTP request.
*/
public numeric operate delayBeforeNextRetry(
required any httpRequest,
required any httpResponse,
required numeric requestsAttempted
) {
return( backoffDurations[ requestsAttempted ] );
}
}
Now, in principle, you would go a ColdFusion part occasion that implements this API to the CFHttp
tag as a “listener”. However, since Lucee would not truly implement a listener for CFHttp
, I’ll faux it through the use of a part that proxies the native Http.cfc
part.
My proxy part will settle for an optionally available occasion of the HttpRetryPolicy.cfc
part after which expose a .ship()
technique with wraps the underlying .ship()
name in retry mechanics. The next ColdFusion part shouldn’t be a whole proxy – it is simply sufficient to reveal the idea:
part
output = false
trace = "PROOF OF CONCEPT: I present a proxy to the native HTTP object that provides retry performance."
{
/**
* I initialize the retryable HTTP request with the given properties.
*/
public void operate init() {
variables.httpRequest = new org.lucee.cfml.Http( argumentCollection = arguments );
variables.retryPolicy = nullValue();
// For demo debugging and illustration.
this.traces = [];
}
// ---
// PUBLIC METHODS.
// ---
/**
* I PROXY the underlying HTTP ship and wrap it with retry mechanics utilizing the given
* retry coverage (if one has been outlined).
*/
public any operate ship() {
if ( isNull( retryPolicy ) ) {
return( httpRequest.ship() );
}
// If the retry coverage is malformed (developer error), we have to stop this
// thread from falling into an infinite loop. As such, we'll solely permit a most
// of 10 retries no matter what the retry coverage offers.
var infiniteLoopCutoff = 10;
var requestsAttempted = 0;
var startedAt = getTickCount();
do {
requestsAttempted++;
// For debugging output within the demo in order that we will see what number of occasions the
// underlying HTTP request was executed.
this.traces.append( "Performing HTTP request (#requestsAttempted# at #( getTickCount() - startedAt )#ms)." );
var httpResponseWrapper = httpRequest.ship();
var httpResponse = httpResponseWrapper.getPrefix();
if ( --infiniteLoopCutoff <= 0 ) {
break;
}
if ( ! retryPolicy.shouldRetry( httpRequest, httpResponse, requestsAttempted ) ) {
break;
}
sleep( retryPolicy.delayBeforeNextRetry( httpRequest, httpResponse, requestsAttempted ) );
} whereas( true );
return( httpResponseWrapper );
}
/**
* For the PROOF OF CONCEPT, I'll require the retry coverage to be set individually
* in order that the `arguments` scope can be utilized to initialize the underlying HTTP request
* with out having to pluck-out the retry coverage.
*/
public any operate setRetryPolicy( required any retryPolicy ) {
variables.retryPolicy = arguments.retryPolicy;
return( this );
}
}
As you possibly can see, the proxy logic is definitely pretty easy:
-
We make the HTTP request utilizing the native
Http.cfc
occasion. -
We ask the retry coverage if the given HTTP response needs to be retried – discover that we do not check the standing code within the proxy; all responses are handed to the retry coverage.
-
If the retry is accepted, we ask the retry coverage how lengthy the thread ought to
sleep()
. -
Repeat.
To check this interaction between the native Http.cfc
and my HttpWithRetry.cfc
, I created a easy ColdFusion web page that may randomly fail 2/3 of the time:
<cfscript>
if ( randRange( 1, 3 ) == 1 ) {
header
statusCode = 200
statusText = "OK"
;
echo( "Success!" );
} else {
header
statusCode = 500
statusText = "ServerError"
;
echo( "Oh no!" );
}
</cfscript>
After which, a easy CFML web page that pulls this all collectively:
<cfscript>
// NOTE: We all know that "goal.cfm" will randomly fail 2/3 of the time.
httpRequest = new HttpWithRetry(
technique = "GET",
url = "http://127.0.0.1:62625/retry-policy/goal.cfm"
);
httpRequest.setRetryPolicy( new HttpRetryPolicy() );
dump( httpRequest.ship().getPrefix() );
dump( httpRequest.traces );
</cfscript>
On this case, the place I am calling the .setRetryPolicy()
technique, you may think that being changed with a listener
attribute on the CFHttp
tag (to observe the present sample that Lucee has established with question and listeners). And, if we run this ColdFusion web page, we get the next output:

As you possibly can see, as a consequence of our randomly failing goal web page, our HttpWithRetry.cfc
ColdFusion part ended up executing 5 consecutive HTTP requests till the HttpRetryPolicy.cfc
rejected the request for an additional retry.
This implementation shouldn’t be supposed to be feature-complete – it is only a proof of idea. However, I do like the concept that the strategy is not making an attempt to be a one-size-fits-all resolution. As a substitute, the HTTP performance could be extensible in a means that totally different API purchasers in user-land may present totally different retry insurance policies.
Need to use code from this submit?
Try the license.