Created
July 28, 2017 22:16
-
-
Save bennadel/1a4f9b3da09a505358119e2dfdff30a5 to your computer and use it in GitHub Desktop.
Creating A Generic Proxy For Retry Semantics In ColdFusion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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