Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 28, 2017 22:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bennadel/1a4f9b3da09a505358119e2dfdff30a5 to your computer and use it in GitHub Desktop.
Save bennadel/1a4f9b3da09a505358119e2dfdff30a5 to your computer and use it in GitHub Desktop.
Creating A Generic Proxy For Retry Semantics In ColdFusion
component
output = false
hint = "I provide automatic retry functionality around the target component."
{
/**
* I initialize the retry proxy with the given target component. Retries will
* only be applied to "transient" errors. And, since the proxy doesn't know which
* errors are transient / retriable, it must check with the isTransientError()
* function.
*
* @target I am the component being proxied.
* @isTransientError I determine if the thrown error is safe to retry (returns a Boolean).
* @retryCount I am the number of retries that will be attempted before throwing an error.
* @includeMethods I am the collection of method names for which to explicitly apply retry semantics.
* @excludeMethods I am the collection of method names for which to explicitly omit retry semantics.
* @output false
*/
public any function init(
required any target,
required function isTransientError,
numeric retryCount = 2,
array includeMethods = [],
array excludeMethods = []
) {
variables.target = arguments.target;
variables.isTransientError = arguments.isTransientError;
variables.retryCount = arguments.retryCount;
generateProxyMethods( includeMethods, excludeMethods );
return( this );
}
// ---
// PUBLIC METHODS.
// ---
// ... proxy methods will be duplicated and injected here ...
// ---
// PRIVATE METHODS.
// ---
/**
* I inspect the target component and create local, public proxy methods that match
* the invocable methods on the target component. All target methods will be proxied;
* however, the proxy will be a RETRY proxy or a BLIND proxy based on the include /
* exclude method name collections.
*
* @includeMethods I am the collection of method names for which to explicitly apply retry semantics.
* @excludeMethods I am the collection of method names for which to explicitly omit retry semantics.
* @output false
*/
private void function generateProxyMethods(
required array includeMethods,
required array excludeMethods
) {
// Look for public methods / closures on the target component and create a
// local proxy method for each invocable property. By explicitly stamping out
// clones of the proxy method, we don't have to rely on the onMissingMethod()
// functionality, which I personally feel makes this a cleaner approach.
for ( var publicKey in structKeyArray( target ) ) {
var publicProperty = target[ publicKey ];
if ( isInvocable( publicProperty ) ) {
// Determine if the given method is being implicitly or explicitly
// excluded from the proxy's retry semantics.
var isIncluded = ( ! arrayLen( includeMethods ) || arrayFindNoCase( includeMethods, publicKey ) );
var isExcluded = arrayFindNoCase( excludeMethods, publicKey );
this[ publicKey ] = ( isIncluded && ! isExcluded )
? proxyRetryTemplate
: proxyBlindTemplate
;
}
}
}
/**
* I return the back-off duration, in milliseconds, that should be waited after
* the given attempt has failed to execute successfully.
*
* @attempt I am the attempt number (starting at zero) that just failed.
* @output false
*/
private numeric function getBackoffDuration( required numeric attempt ) {
return( 1000 * ( attempt + rand() ) );
}
/**
* I determine if the given value is invocable.
*
* @value I am the public property that was plucked from the target component.
* @output false
*/
private boolean function isInvocable( required any value ) {
return( isCustomFunction( value ) || isClosure( value ) );
}
/**
* I provide the template for "blind pass-through" proxy methods. These implement
* no retry logic.
*
* @output false
*/
private any function proxyBlindTemplate( /* ...arguments */ ) {
// Gather the proxy invocation parameters. Since the proxyBlindTemplate() has
// been cloned for each public method on the target, we can get the name of the
// target method by introspecting the name of "this" method.
var methodName = getFunctionCalledName();
var methodArguments = arguments;
return( invoke( target, methodName, methodArguments ) );
}
/**
* I provide the template for "retry" proxy methods.
*
* @output false
*/
private any function proxyRetryTemplate( /* ...arguments */ ) {
// For the purposes of the error message, we'll record the duration of the
// attempted proxy execution.
var startedAt = getTickCount();
// Gather the proxy invocation parameters. Since the proxyRetryTemplate() has
// been cloned for each public method on the target, we can get the name of the
// target method by introspecting the name of "this" method.
var methodName = getFunctionCalledName();
var methodArguments = arguments;
for ( var attempt = 0 ; attempt <= retryCount ; attempt++ ) {
try {
return( invoke( target, methodName, methodArguments ) );
} catch ( any error ) {
// If this is not a retriable error, then rethrow it and let it bubble
// up to the calling context.
if ( ! isTransientError( error ) ) {
rethrow;
}
// If this was our last retry attempt on the target method, throw an
// error and let it bubble up to the calling context.
if ( attempt >= retryCount ) {
throw(
type = "RetryError",
message = "Proxy method failed even after retry.",
detail = "The proxy method [#methodName#] could not be successfully executed after [#( retryCount + 1 )#] attempts taking [#numberFormat( getTickCount() - startedAt )#] ms.",
extendedInfo = serializeJson( duplicate( error ) )
);
}
// Since we're encountering a transient error, let's sleep the thread
// briefly and give the underlying system time to recover.
sleep( getBackoffDuration( attempt ) );
}
}
// CAUTION: Control flow will never get this far since the for-loop will either
// return early or throw an error on the last iteration.
}
}
<cfscript>
// Setup some general error-checking functions (closures work as well). Each of
// these function accepts the Error instance in question and must return a boolean
// indicating that the error is transient (true) or non-retriable (false).
function isMySqlLockTimeoutError( required any error ) {
// Read more: https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock
return(
( error.type == "Database" ) &&
( error.errorCode == "40001" )
);
}
function isSqlServerLockTimeoutError( required any error ) {
// Read more: https://technet.microsoft.com/en-us/library/cc645860(v=sql.105).aspx
return(
( error.type == "Database" ) &&
( error.errorCode == "1222" )
);
}
function isAlwaysTransientError( required any error ) {
return( true );
}
function isNeverTransientError( required any error ) {
return( false );
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Create our retry proxy using the given transient error test.
proxy = new RetryProxy( new TestTarget(), isAlwaysTransientError );
try {
writeDump( proxy.works() );
writeDump( proxy.breaks() );
} catch ( any error ) {
// This should be the "breaks()" method error.
writeDump( error );
}
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment