Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sean Corfield
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sean Corfield ( @seancorfield )

Instrumenting Cache Hits And Misses With FusionReactor Metrics In ColdFusion 2021

By on
Tags:

Last month, I mentioned that I cache a lot of data in my ColdFusion blog. And, that FusionReactor gave me peace-of-mind regarding resource consumption in the JVM. For funzies, I thought I would take that one step further and use FusionReactor's custom metrics aggregation to track cache hits and cache misses for incoming requests. To do this, I had to refactor some of my ColdFusion 2021 code to prefer composition of inheritance.

I don't always regret using inheritance in ColdFusion. But, I usually end-up regretting it in most cases. As I've gotten older, I've come to embrace the wisdom of (Wikipedia: Composition over inheritance). With inheritance, the relationship between components is very tightly coupled. But, when you wire ColdFusion components together through composition and the inversion of control (IoC), it gives you an opportunity to swap implementations out as the needs of your application evolve.

Originally, all of the "view partial" / "query" components on my blog extended a base component that provided shared methods for caching. This way, I could Put, Get, and Clear cache entries in each partial without having to rewrite that functionality over-and-over again:

component
	extends = "components.partials.BasePartial"
	accessors = true
	{

	// .... partial-specific code here .... //
}

And, this worked fine for a while. Until I wanted to add instrumentation to the caching. And, suddenly, the tight coupling between the base component and the sub-classing components became frustratingly clear. Since the caching wasn't a "behavior" that I could inject into the partials, every partial would necessarily have to know about the instrumentation. Or, at the very least, accept constructor arguments that it would then have to pass up into a super constructor.

To fix this tight coupling, I ripped out all of the core caching implementation into its own ColdFusion component, PartialCache.cfc:

  • setItem( key, value, expiresWithin )
  • getItem( key )
  • removeItem( key )
  • clearItems()

This ColdFusion component has a simple API that is little more than a glorified Struct; the differentiating feature being that the "setter" takes a "timespan" after which the given key should be expired / expunged from the cache.

component
	output = false
	hint = "I provide a simple key/value cache for partials."
	{

	/**
	* I initialize the partial cache with the given properties.
	*/
	public void function init() {

		items = {};

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I remove all items from the cache.
	*/
	public void function clearItems() {

		items.clear();

	}


	/**
	* I get the item cached at the given key. Or, VOID if undefined or expired.
	*/
	public any function getItem( required string key ) {

		if ( ! items.keyExists( key ) ) {

			return;

		}

		var item = items[ key ];

		if ( item.expiresAt <= getTickCount() ) {

			items.delete( key );
			return;

		}

		return( item.value );

	}


	/**
	* I remove the item cached at the given key.
	*/
	public void function removeItem( required string key ) {

		items.delete( key );

	}


	/**
	* I cache the given item at the given key. The cached item is returned.
	*/
	public any function setItem(
		required string key,
		required any value,
		required numeric expiresWithin
		) {

		items[ key ] = {
			key: key,
			value: value,
			expiresAt: generateExpiresAtTickCount( expiresWithin )
		};

		return( value );

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I generate an expiresAt tick count for the given timespan.
	*/
	private numeric function generateExpiresAtTickCount( required numeric timespan ) {

		return( now().add( "s", fix( 86400 * timespan ) ).getTime() );

	}

}

Once the caching behavior was factored-out into this ColdFusion component, I suddenly had the opportunity to "decorate it". That is, I could now wrap it inside another ColdFusion component that implements the same API and adds additional functionality. In this case, I want to look at the getItem() method; and, record statistics as to how often that method returns void (cache miss) vs. an actual value (cache hit).

This wrapper component receives both a PartialCache.cfc instance and a safe FusionReactor API wrapper through property-based dependency-injection (DI):

component
	accessors = true
	output = false
	hint = "I implement the cache interface and track cache hits and misses."
	{

	// Define properties for dependency-injection.
	property javaAgentApi;
	property partialCache;

	/**
	* I initialize the cache proxy for the given cache.
	*/
	public void function init( required string partialName ) {

		cacheHitMetric = "/partialCache/#partialName#/cacheHit";
		cacheHitCount = 0;

		cacheMissMetric = "/partialCache/#partialName#/cacheMiss";
		cacheMissCount = 0;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I remove all items from the cache.
	*/
	public void function clearItems() {

		return( partialCache.clearItems( argumentCollection = arguments ) );

	}


	/**
	* I get the item cached at the given key. Or, VOID if undefined or expired.
	*/
	public any function getItem( required string key ) {

		var item = partialCache.getItem( argumentCollection = arguments );

		// Record MISS.
		if ( isNull( item ) ) {

			javaAgentApi.metricAdd( cacheMissMetric, ++cacheMissCount );
			return;

		// Record HIT.
		} else {

			javaAgentApi.metricAdd( cacheHitMetric, ++cacheHitCount );
			return( item );

		}

	}


	/**
	* I remove the item cached at the given key.
	*/
	public void function removeItem( required string key ) {

		return( partialCache.removeItem( argumentCollection = arguments ) );

	}


	/**
	* I cache the given item at the given key. The cached item is returned.
	*/
	public any function setItem(
		required string key,
		required any value,
		required numeric expiresWithin
		) {

		return( partialCache.setItem( argumentCollection = arguments ) );

	}

}

As you can see, all of the methods in this ColdFusion component are blind pass-throughs except for the getItem() method. This method examines the response of the wrapped component (PartialCache.cfc); and then, records a cache miss if the value isNull() and a cache hit if the value is defined.

Now, when I'm bootstrapping my ColdFusion blog in the Application.cfc's onApplicationStart() event-handler method, all I have to do is inject the wrapped version of the PartialCache.cfc instead of the plain one. And, since ColdFusion property accessor functions are chainable, we can use a fun little fluent API to wire all the things together:

component {

	public void function onApplicationStart() {

		// .... truncated .... //

		application.partials.blogPost = new components.partials.blogPost.BlogPost()
			.setConfig( config )
			.setJSoupJavaLoader( jSoupJavaLoader )
			.setLogger( logger )
			.setPartialCache(
				new components.partials.InstrumentedPartialCache( "blogPost" )
					.setJavaAgentApi( javaAgentApi )
					.setPartialCache( new components.partials.PartialCache() )
			)
			.setPartialGateway( blogPostPartial )
			.setPartialNormalizer( blogPostNormalizer )
			.setUtilities( utilities )
		;

		// .... truncated .... //

	}

}

As you can see, I'm instantiating a vanilla version of the PartialCache.cfc component and immediately passing it into the wrapped version, InstrumentedPartialCache.cfc. This wrapped version is then passed into the BlogPost.cfc partial, which has no idea that it is receiving anything special. And, as requests come into the system, this wrapped version will start logging the following metrics based on the getItem() outcomes:

  • /partialCache/blogPost/cacheHit
  • /partialCache/blogPost/cacheMiss

In the FusionReactor Cloud dashboard, I can then go into the Metrics section and create a Profile that graphs the cache hits and cache misses for various Partials within my ColdFusion blog:

My blog doesn't get enough traffic to make these graphs particularly interesting. But, even so, you can see how the hits and misses trends tend to cross: as new requests come into the application, the results get cached; and, slowly, the cache misses start to drop as the cache hits begin to rise.

If nothing else, this is just a great reminder of how much more flexible composition makes your ColdFusion application architecture. Today, I'm recording metrics; but, if tomorrow I decided to stop doing this, all I would have to do is "unwrap" the PartialCache.cfc instance that I'm passing into my partial component and literally nothing else has to change. If I were still using inheritance, undoing the instrumentation would be a whole thing.

For completeness, here's the current implementation of my JavaAgentApi.cfc which is a "safe" wrapper for the FusionReactor API (FRAPI) - "safe" in that I can run it in my local development environment where I don't have FusionReactor Java Agent installed.

component
	output = false
	hint = "I provide a SAFE API to the underlying FusionReactor JavaAgent to help instrument the ColdFusion application."
	{

	/**
	* I initialize the java agent API helper.
	*/
	public void function init() {

		// The FusionReactor Agent is not available in all contexts. As such, we have to
		// be careful about trying to load the Java Class; and then, be cautious of its
		// existence when we try to consume it. The TYPE OF THIS VARIABLE will be used
		// when determining whether or not the FusionReactor API should be consumed. This
		// approach allows us to use the same code in the calling context without having
		// to worry if the FusionReactor agent is installed.
		try {

			// NOTE: The FRAPI was on Version 8.x at the time of this writing.
			variables.FRAPIClass = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" );

		} catch ( any error ) {

			variables.FRAPIClass = "";

		}

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I set the application name for the request's MASTER transaction.
	*/
	public void function applicationSetName( required string name ) {

		if ( shouldUseFusionReactorApi() ) {

			getApi().setTransactionApplicationName( javaCast( "string", name ) );

		}

	}


	/**
	* I post the given metric as an AGGREGATE NUMERIC LONG metric; and, optionally,
	* stream the metric to the Cloud dashboard.
	* 
	* NOTE: In the FusionReactor documentation, metrics are all named using slash-
	* notation. As in, "/this/is/my/metric". I recommend you follow this same pattern
	* for consistency.
	*/
	public void function metricAdd(
		required string name,
		required numeric value,
		boolean enableCloudMetric = true
		) {

		if ( shouldUseFusionReactorApi() ) {

			getApi().postNumericAggregateMetric(
				javaCast( "string", name ),
				javaCast( "long", value )
			);

			if ( enableCloudMetric ) {

				getApi().enableCloudMetric( javaCast( "string", name ) );

			}

		}

	}


	/**
	* I post the given metric as an AGGREGATE NUMERIC FLOAT metric; and, optionally,
	* stream the metric to the Cloud dashboard.
	*/
	public void function metricAddFloat(
		required string name,
		required numeric value,
		boolean enableCloudMetric = true
		) {

		if ( shouldUseFusionReactorApi() ) {

			getApi().postNumericAggregateMetric(
				javaCast( "string", name ),
				javaCast( "float", value )
			);

			if ( enableCloudMetric ) {

				getApi().enableCloudMetric( javaCast( "string", name ) );

			}

		}

	}


	/**
	* I end the segment and associate the resultant sub-transaction with the current
	* parent transaction.
	*/
	public void function segmentEnd( required any segment ) {

		if ( shouldUseFusionReactorApi() ) {

			// In the case where the segment is not available (because the FusionReactor
			// agent has not been installed), it will be represented as an empty string.
			// In such cases, just ignore the request.
			if ( isSimpleValue( segment ) ) {

				return;

			}

			segment.close();

		}

	}


	/**
	* I start and return a new Segment to be associated with the current request
	* transaction. The returned Segment should be considered an OPAQUE TOKEN and should
	* not be consumed directly. Instead, it should be passed to the .segmentEnd() method.
	* Segments will show up in the Transaction Breakdown table, as well as in the
	* "Relations" tab in the Standalone dashboard and the "Traces" tab in the Cloud
	* dashboard.
	*/
	public any function segmentStart( required string name ) {

		if ( shouldUseFusionReactorApi() ) {

			return( getApi().createTrackedTransaction( javaCast( "string", name ) ) );

		}

		// If the FusionReactor API feature is not enabled, we still need to return
		// something as the OPAQUE SEGMENT TOKEN so that the calling logic can be handled
		// uniformly within the application code.
		return( "" );

	}


	/**
	* I set the name of the request's MASTER transaction (which is used to separate
	* requests within the FusionReactor dashboard).
	* 
	* CAUTION: Transaction names should container alpha-numeric characters. Including
	* slashes or dots within the name appears to create some unexpected behaviors in the
	* Standalone dashboard.
	*/
	public void function transactionSetName( required string name ) {

		if ( shouldUseFusionReactorApi() ) {

			getApi().setTransactionName( javaCast( "string", name ) );

		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I get the instance of the running FusionReactor API. If FusionReactor is not
	* running, or is not initialized yet, this returns NULL.
	*/
	private any function getApi() {

		return( FRAPIClass.getInstance() );

	}


	/**
	* I check to see if this machine should consume the FusionReactor static API as part
	* of the Java Agent Helper class (this is to allow the methods to exist in the
	* calling context without a lot of conditional consumption logic).
	*/
	private boolean function shouldUseFusionReactorApi() {

		// If we were UNABLE TO LOAD THE FRAPI CLASS, there's no API to consume.
		if ( isSimpleValue( FRAPIClass ) ) {

			return( false );

		}

		// If the underlying FusionReactor instance isn't running yet, the API is null. We
		// have to wait for it to be ready.
		if ( isNull( getApi() ) ) {
		
			return( false );
		
		}

		return( true );

	}

}

As part of this refactoring, I added the metricAdd() and metricAddFloat() methods.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel