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. } }