Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jason Dean

AsyncTaskQueue.cfc - Running Low-Priority Tasks In A Single CFThread

By Ben Nadel on
Tags: ColdFusion

ColdFusion threads (via CFThread) are one of the coolest features of the ColdFusion language. They allow you to run tasks in parallel, which often (but not always) carries some significant performance benefit. With ColdFusion Standard, however, there is a concurrent thread limit of 10; this means that if you spawn more than 10 threads at a time, some of them will start to be queued. This is problematic if you have low-priority tasks causing high-priority tasks to queue up. To try to work within this constraint, I created the AsyncTaskQueue.cfc - a ColdFusion component that processes a queue of tasks inside a single CFThread tag. This way, a set of low-priority tasks will take up, at most, one CFThread.


 
 
 

 
  
 
 
 

Project: See the AsyncTaskQueue.cfc project on my GitHub account.

This is not intended to be a replacement for a real Message Queue system. This is only intended to concentrate multiple instances of related, low-priority CFThread tags. If your system crashes, you lose the queued tasks - just as you would with normal CFThread tags.

The AsyncTaskQueue.cfc exposes one public method for populating the internal task queue:

  • addTask( callback [, callbackArguments ] )

Each task is represented by a callback function and an optional set of arguments. When your task is popped off the queue, it is processed via callback invocation:

callback( argumentCollection = callbackArguments )

Since the callback is invoked without scope, its Variables scope will be bound to the calling context. For this reason, the AsyncTaskQueue.cfc is intended to be extended - or sub-classed - by another component; and, the callback method is intended to be a private method on that sub-class component. This way, when the callback is invoked, the context will be the sub-class component.

To demonstrate, I have created an image-download component that extends the AsyncTaskQueue.cfc. When the user tells the downloader to download a given image, the downloader adds the task the internal queue; the internal queue then executes those downloads in serial, in a single CFThread:

  • <cfscript>
  •  
  • component
  • extends = "lib.AsyncTaskQueue"
  • output = false
  • hint = "I download remote images to a given downloads directory."
  • {
  •  
  • // I initialize the downloader task queue.
  • public any function init( required string downloadsDirectory ) {
  •  
  • super.init();
  •  
  • variables.downloadsDirectory = downloadsDirectory;
  •  
  • return( this );
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I add the download task to the internal, asynchronous queue.
  • public void function download( required string imageUrl ) {
  •  
  • addTask( executeDownload, [ imageUrl ] );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I execute the actual download task (inside a CFThread).
  • private void function executeDownload( required string imageUrl ) {
  •  
  • var downloadRequest = new Http(
  • url = imageUrl,
  • method = "get",
  • getasbinary = "yes",
  • path = downloadsDirectory,
  • file = getFileFromPath( imageUrl )
  • );
  •  
  • downloadRequest.send();
  •  
  • }
  •  
  • }
  •  
  • </cfscript>

As you can see, the public method - download() - adds the task (ie, callback) - executeDownload - to the task queue. When the task item is executed, it is done so in the context of the Downloader.cfc instance, which is why the executeDownload() method has access to the private variable, "downloadsDirectory."

In this example (which is on my GitHub account), I am defining the task using an array of ordered-arguments. You can also define the task using a struct of named-arguments. Internally, ordered arguments are actually converted to named-arguments using index-based keys.

Right now, the AsyncTaskQueue.cfc processes each task inside of a Try-Catch block and simply discards the errors. I'd like to add some way to store and retrieve those errors, which shouldn't be too difficult. I'd also like to provide a way to define a context in which to execute the tasks, in the case where you're not sub-classing the task queue. But, for now, I think this is a decent first version.




Reader Comments

@Dan,

Hmmm, it was my belief that the Standard limitations where:

* 10 CFThread, concurrent.
* 2 CFDocument, concurrent.

... that said, it's definitely possible that I am way off. Locally, I can't check since I'm on the dev edition. I'll try to check on my Standard box (personal VPS) later.

Reply to this Comment

Hi Ben,

Have you used Railo much?
You seem about the only blogger who very rarely mentions it, are you heavily reliant on some adobe cf specific functionality or on there payroll?

joking about the payroll ;)

Reply to this Comment

@Ben:

The funny thing is, I even found a blog post you wrote that indicates the limit is 2 threads:

http://www.bennadel.com/blog/1886-Ask-Ben-Processing-Files-With-CFThread-In-ColdFusion.htm

:)

I could definitely be wrong, but what I seem to recall being the case was that a single request couldn't fire off more than 2 CF threads. So it's possible the 10 thread limit is across multiple threads (so you could have 5 requests, each running 2 threads, but no 1 request could run more than 2 threads.)

My memory's a bit fuzzy on this, so I could be wrong. But that seems to be what I recall from my testing.

Reply to this Comment

@Dan

From the CF9.0.1 Standard Admin > Request Tuning > Tag Limit Settings > Maximum number of threads available for CFTHREAD:

"On Standard Edition, the maximum limit is 10."

I wonder if it started off as 2 and was increased with 9.0.1 perhaps?

Reply to this Comment

@Matt,

The Railo stuff looks really cool; but, all my current production stuff is in Adobe ColdFusion. So, switching to a different provider would be a time cost. I'll look more into it one of these days. I am a big fan of the Railo team!

@Dan,

It's definitely likely that my 2-thread quote was simply something that I had heard; I don't remember ever actually testing. Unfortunately, I didn't get a chance to test over the weekend.

@Julian,

Ah, hopefully that is accurate :D

Reply to this Comment

@Mark,

Very cool stuff! I still haven't gotten into any of the Concurrent stuff in Java. I went to a presentation by Marc Esher re: Concurrent... and did I see one from you too?! I can't remember. It looks like some really cool stuff, though.

I like that you provide a "worker" as the context for the task execution. That is definitely something that I wanted to add. When I was putting mine together, I was thinking about some current components that I have for communicating with external APIs (that I don't care about the return value for). So, I thought it would be nice to be able to simple extend this CFC and deal with it internally.

... but, now that I say that, I think my current components actually already Extends some sort of "BaseService" compoonent... so, extension is really not idea.

One of main objectives was to be able to create a queue that would auto-process. Since I keep my components as application-scoped singletons, I didn't want to have to worry about "running." For example, I have a CFC that communicates with the PusherApp API for WebSockets. And, I'll often have things like this:

realtimeService.pushEvent( "projectCreated", id );
realtimeService.pushEvent( "projectArchived", id );

... etc. And, I pictured the realtimeService component would just add the "event" to the internal queue and flush as necessary.

Creating the "right" locking around the thread and the queue was a pretty fun thing to try and figure out. I *believe* I have it to a point where there can be no unexpected race conditions.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.