Using CreateDynamicProxy() To Power Thread-Safe Counters In ColdFusion
Building thread-safe counters in ColdFusion is something that I've looked at several times before. First, as part of my CUID implementation, I looked at using an AtomicInteger
with looping logic; then, at using an AtomicLong
with a modulo operator. In the comments to my first post, BR mentioned that he uses AtomicInteger
's getAndUpdate()
method to accomplish the same thing. The getAndUpdate()
method accepts a java.util.Function
interface. I wanted to see if I could get his approach working using the createDynamicProxy()
function in ColdFusion.
CAUTION: While I love to leverage the underlying Java API in a ColdFusion application, my understanding of Java is actually quite poor. As such, I won't go into much detail here. Consider this more of a code kata than a serious exploration.
The goal here is to create a thread-safe ColdFusion component that will iterate up over a range of values. And, once the component hits the upper-bound on said range, it will loop back to the start of the range and begin counting up once again.
The ColdFusion component that I created acts as both the public API in the ColdFusion context and as the implementation for the createDynamicProxy()
target. This way, I don't have to create a separate ColdFusion component just for the getAndUpdate()
Function. The downside to this approach is that I have to define one of the methods as public
which is not meant to be consumed externally.
Here's my IncrementingAtomicRange.cfc
component. The public method next()
gets the next available range value. And, the "private" (actually public) method applyAsInt()
is the concrete implementation of the IntUnaryOperator
Java interface:
component
output = false
hint = "I provide a thread-safe counter that increments over the given repeating range."
{
/**
* I initialize the atomic range over the given values, inclusively.
*/
public void function init(
required numeric rangeStart,
required numeric rangeEnd,
numeric initialValue = arguments.rangeStart
) {
// Validate range.
if ( rangeStart > rangeEnd ) {
throw(
type = "InvalidArgument",
message = "Range start must be less than or equal to range end."
);
}
// Validate initial value.
if (
( initialValue < rangeStart ) ||
( initialValue > rangeEnd )
) {
throw(
type = "InvalidArgument",
message = "Initial value must be between range start and end inclusive."
);
}
variables.rangeStart = arguments.rangeStart;
variables.ranedEnd = arguments.rangeEnd;
// Internally, our range is going to be implemented using the AtomicIngeger class.
// This allows us to create a thread-safe integer without having to apply locking
// around the value access and mutation.
variables.counter = createObject( "java", "java.util.concurrent.atomic.AtomicInteger" )
.init( javaCast( "int", initialValue ) )
;
// CAUTION: This ColdFusion component acts as BOTH the public API for ColdFusion
// and as the dynamic proxy target for Java. The method, applyAsInt(), is being
// used to implement the given Java interface (IntUnaryOperator), which in turn is
// being consumed by the AtomicInteger class. This way, we don't have to create a
// separate ColdFusion component just for the increment operation.
variables.operator = createDynamicProxy(
this,
[ "java.util.function.IntUnaryOperator" ]
);
}
// ---
// PUBLIC MEHTODS.
// ---
/**
* I get the next value in the range.
*/
public numeric function next() {
return( counter.getAndUpdate( operator ) );
}
// ---
// PRIVATE MEHTODS.
// ---
/**
* INTERNAL USE ONLY: IMPLEMENTATION OF IntUnaryOperator INTERFACE for AtomicInteger.
* This is the function that will be consumed in the Dynamic Proxy that powers the
* ".getAndUpdate()" method call on the AtomicInteger counter.
*
* Note that while this method is marked as PUBLIC, I'm keeping it in the PRIVATE
* methods section of the component as an indication that it should not be consumed
* externally. Note that in Lucee CFML, I could mark this method as PRIVATE and the
* dynamic proxy generation would still work. Adobe ColdFusion complains if the method
* is marked private.
*/
public numeric function applyAsInt( required numeric input ) {
if ( input == ranedEnd ) {
return( rangeStart );
}
return( input + 1 );
}
}
As you can see, the applyAsInt()
"private" (public) method handles the logic for incrementing the range value and then looping back to the start of the range. Only, I don't have to synchronize access to this method because the AtomicInteger
class is already doing that for me. I'm simply passing this method - by way of the createDynamicProxy()
result - into the AtomicInteger
as the operator of the .getAndUpdate()
method.
We can now consume this ColdFusion component to get a thread-safe range counter:
<cfscript>
counter = new IncrementingAtomicRange( 1, 5 );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
writeDump( counter.next() );
</cfscript>
I'm not bothering to invoke this across multiple, parallel threads because previous posts have already confirmed that the Atomic classes in Java are thread-safe (as advertised). That said, when we run this in Lucee CFML 5.3 and Adobe ColdFusion 2021 we get the following output:
As you can see, the range index loops back to the lower-bound after it hits the upper-bound.
This is pretty neat. I've only tried the createDynamicProxy()
function a handful of times over the years. One thing that's especially interesting here is that I think I'm using a "class" as the implementation for a "function reference". From the Java docs:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
If I'm understanding this correctly, it looks like I can use this kind of technique in any situation in which a Java method takes a Function/Lambda expression as its argument. That said, I know very little about Java; so, it's also likely that I'm not fully understanding the ramifications here.
Anyway, just a fun code kata to have in my back pocket now.
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 →