Skip to main content
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Aaron Foss and Mark C. Webster
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Aaron Foss Mark C. Webster

Experiment: Wrapping CFThread Execution In A FusionReactor Tracked Transaction In Lucee CFML 5.2.9.40

By
Published in

Now that I have FusionReactor running in our production Lucee CFML apps, I've been spending a lot of time trying to understand how I can best leverage its functionality. And, while it offers excellent insight into the top-level page requests coming into the ColdFusion application, I am struggling to find performance information relating to asynchronous CFThread execution. As such, as an experiment, I wanted to see if I could proxy the spawning of CFThread tags, instrumenting the asynchronous logic using a "tracked transaction" in Lucee CFML 5.2.9.40.

In FusionReactor parlance, a "Tracked Transaction" provides your ColdFusion application with a way to programmatically instrument a given piece of code. Essentially, you start a tracked transaction with a given identifier (the transaction name); you execute your piece of code; and then, you close the tracked transaction. This instrumented segment of the code will then start showing up in the various FusionReactor dashboards:

<cfscript>

	segment = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" )
		.getInstance()
		.createTrackedTransaction( "MyCodeSegment" )
	;

	try {

		// ... the code you are instrumenting ... //

	} finally {

		segment.close();

	}

</cfscript>

At this point, the Transaction, "MyCodeSegment", will start showing up in the Tracing, Relations, and Transactions lists within the FusionReactor dashboard.

To gain more insight into the performance of CFThread tags and their potential correlation with system resource utilization, I want to take the aforementioned instrumentation and use it to proxy the execution of asynchronous logic. And, to do this, I'm going to replace the CFThread tag body with a ColdFusion Closure.

To explore this concept, I've put together a simple demo that simulates the sending of messages in an asynchronous thread. And, instead of calling CFThread directly, I'm calling runSegmentAsync() and passing in a Closure that provides the asynchronous logic. The runSegmentAsync() function take the following arguments:

  • segmentName - This is the name of the Tracked Transaction inside FusionReactor. This should be a single value with no spaces or slashes.

  • callbackArguments - This is the collection of arguments passed to the callback when it is invoked. You can think of these as being akin to the CFThread tag attributes that you would normally use to define an asynchronous context. This can be a Struct or an Array (both work with argumentCollection).

  • callback - I am the ColdFusion closure that provides the logic for the asynchronous execution. It will be invoked with the callbackArguments as its argumentCollection.

  • errorCallback - I am optional ColdFusion closure that will handle any error that is thrown during the invocation of the callback().

And, here's the demo code:

<cfscript>

	// Instead of just going directly to a CFThread for asynchronous processing, I'm
	// going to see if I can wrap the CFThread interaction inside a FusionReactor
	// "tracked transaction" such that I can see how this CFThread is operating and
	// how it correlates with system resource utilization.
	// --
	// This takes the following arguments:
	// 1. Name of the FusionReactor transaction.
	// 2. Arguments to be passed to closure.
	// 3. Closure to execute asynchronously.
	// 4. [OPTIONAL] Closure to handle errors.
	runSegmentAsync(
		"SendFriendMessagesAsync",
		{
			friends: [ "Sarah", "Max", "Annie", "Timmy" ]
		},
		( friends ) => {

			var messages = friends.map(
				( friend ) => {

					return( formatMessage( friend ) );

				}
			);

			// Simulating some latency to make the graphs more interesting.
			sleep( randRange( 10, 50 ) );

			// Simulate "sending" messages.
			systemOutput( serializeJson( messages ), true, true );

		}
	);

	
	/**
	* I format a message for the given friend.
	* 
	* @name I am the friend's name.
	*/
	public string function formatMessage( required string name ) {
		
		return( "Hello, #name#, how goes it?" );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I run the given callback asynchronously inside a CFThread and wrapped in a
	* FusionReactor tracked transaction with the given name.
	* 
	* @segmentName I am the name of the FusionReactor tracked transaction.
	* @callbackArguments I am the argumentCollection passed to the thread and callback.
	* @callback I am the callback to invoke asynchronously.
	* @errorCallback [OPTIONAL] I am the error handler callback.
	*/
	public void function runSegmentAsync(
		required string segmentName,
		required any callbackArguments,
		required function callback,
		function errorCallback
		) {

		// Since the execution of a CFThread can be a bit of a mystery (from an
		// debugging standpoint), let's include the StackTrace in the FusionReactor
		// transaction description.
		// --
		// NOTE: In a production context, I would probably just rely on "SegmentName" to
		// aid in debugging since I would be afraid that the callstackGet() call would
		// end-up being a performance hit? Possible just unnecessary worry there, though.
		var callstackItems = callstackGet().map(
			( item, i ) => {

				// The first item in the callstack will always be "THIS" line, which
				// doesn't add any insight into the processing. As such, let's replace
				// the first callstack item with a descriptive value.
				if ( i == 1 ) {

					return( "Async segment tracking for CFThread." );

				}

				return( item.template & ":" & item.lineNumber );

			}
		);

		// Inside a CFThread, FusionReactor seems to have trouble figuring out which
		// "app" the spawned thread is supposed to be associated with. As such, let's get
		// the app name from the current transaction and pass it through to the thread
		// where it can be programmatically propagated in the tracked transaction.
		// --
		// CAUTION: In a production setting, you'd have to check to make sure this class
		// is available AND that the .getInstance() call returns a non-null value.
		// However, for the sake of simplicity, I'm keeping this demo naive.
		var transactionAppName = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" )
			.getInstance()
			.getActiveTransaction()
			.getApplicationName()
		;

		thread
			name = "runSegmentAsync-#createUniqueId()#"
			action = "run"
			segmentAppName = transactionAppName
			segmentName = segmentName
			segmentDescription = callstackItems.toList( chr( 10 ) )
			callback = callback
			callbackArguments = callbackArguments
			errorCallback = ( errorCallback ?: nullValue() )
			{

			var segment = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" )
				.getInstance()
				.createTrackedTransaction( segmentName )
			;

			try {

				// Propagate application name and execution context.
				segment.setApplicationName( segmentAppName );
				segment.setDescription( segmentDescription );

				// Invoking the callback that is being wrapped in a tracked transaction.
				callback( argumentCollection = callbackArguments );

			} catch ( any error ) {

				if ( structKeyExists( attributes, "errorCallback" ) ) {

					errorCallback( error );

				} else {

					rethrow;

				}

			} finally {

				segment.close();

			}

		} // END: Thread.

	}

	// -- For debugging the demo --.

	thread action = "join";
	dump( cfthread );

</cfscript>

<script type="text/javascript">

	// Refresh page to generate traffic (the Cloud dashboard seems to have trouble
	// syncing from my local system unless there is a steady stream of traffic).
	setTimeout(
		function() {

			window.location.reload();

		},
		1000
	);

</script>

As you can see, the runSegmentAsync() function is responsible for spawning the CFThread tag. Then, within the CFThread tag, we're creating a new FusionReactor transaction to wrap the execution of the given callback.

Since this Tracked Transaction isn't an incoming "Web Request", it won't show up in the main list of "Request" transactions. However, if you look at the generic list of "Transactions", you will see "SendFriendMessagesAsync" showing up along side Web Requests, HTTP Requests, Database Transactions, and all of the other transactions that are inherently instrumented by FusionReactor's JavaAgent:

As I was experimenting with this technique, one thing I noticed was that the main Web Request Transactions where showing up with the Application Name, "ROOT". But, my CFThread transactions were showing up with the Application Name, "My Application". To keep the two sets of Transactions categorized under the same Application Name, I'm passing the "active transaction" name down into the CFThread. Then, I'm using that Transaction Name to modify the asynchronous CFThread. I am not sure if there is a better way to do this?

CAUTION: None of the FusionReactor Transaction methods are documented in the JavaDocs. As such, I am just piecing this together through trial-and-error. There might be a much more intuitive way to keep the various Transactions categorized under the same application name.

To be clear, this is an experiment. Meaning, I have not tried this in a production context yet. My first step in that direction would be to add this CFThread proxy method to my JavaAgentHelper.cfc which safely wraps the FRAPI class consumption in my ColdFusion code. I'd also want to do some testing to make sure the Closure retains all the necessary access to the lexical context (I have no reason to believe that it wouldn't).

And, if I'm doing something here that the FusionReactor agent already does for me and I'm just not finding it in the dashboards, please let me know!

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

Reader Comments

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