Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created November 7, 2020 14:25
Show Gist options
  • Save bennadel/fdf123cbb6721108fc26dcebfc6bbd20 to your computer and use it in GitHub Desktop.
Save bennadel/fdf123cbb6721108fc26dcebfc6bbd20 to your computer and use it in GitHub Desktop.
Using Redis Blocking List Operations To Power Long-Polling In Lucee CFML 5.3.7.47
<cfscript>
param name="url.pollID" type="string";
// Lists in Redis provide both blocking and non-blocking operations. In this case,
// we're going to use BLPOP (Blocking Left-Pop) to block-and-hang the parent page
// request for several seconds (3) while waiting for the given list to be populated.
// If no list-item can be popped in the given timeout, the result will be null.
results = application.redisGateway.withRedis(
( redis ) => {
var blockTimeout = 3; // Time to block in seconds.
var lists = [ "poll:#url.pollID#" ];
var popResult = redis.blpop( blockTimeout, lists );
// The result of the BLPOP operation is 2-tuple where the first index is the
// name of the key (since we can block on multiple keys at the same time);
// and, the second index is the value of the list item that we popped.
if ( ! isNull( popResult ) ) {
return( popResult[ 2 ] );
}
}
);
// If there was no result, the job is still processing. For the sake of simplicity,
// let's just return a 404 Not Found in this case.
if ( isNull( results ) ) {
header
statuscode = 404
statustext = "Not Found"
;
exit;
}
content
type = "application/x-json"
variable = charsetDecode( serializeJson( results ), "utf-8" )
;
</cfscript>
<cfscript>
// In this approach, each polling operation is going to correspond to a unique key
// in our Redis database.
pollID = createUUID();
// Since we don't actually have any background processing in this demo, we're just
// going to simulate some background job with a CFThread tag. The tag will sleep for
// a bit and then "finalize" the job by pushing a value onto the Redis List.
thread
name = "simulatedBackgroundTask"
pollID = pollID
{
// Simulated latency for our processing task.
sleep( 8 * 1000 );
// We're going to denote task completion by pushing an item onto a list in Redis.
// The long-polling operation will be monitoring this list, waiting for an item
// to be poppable.
application.redisGateway.withRedis(
( redis ) => {
var list = "poll:#pollID#";
var listItem = { jobID: 4, status: "done" };
// CAUTION: While we are expecting a polling operation to POP this item
// off the list, we cannot depend on that happening. As such, we want to
// make sure that we use an ATOMIC TRANSACTION to both PUSH the list item
// AND set the list to EXPIRE. This way, if the polling operation never
// happens, this list-key will eventually expire and get expunged from
// our Redis database.
var multi = redis.multi();
multi.rpush( list, [ serializeJson( listItem ) ] );
multi.expire( list, 60 );
multi.exec();
}
);
} // END: Thread.
</cfscript>
<!doctype html>
<html>
<head>
<title>
Using Redis Blocking List Operations To Power Long-Polling In Lucee CFML 5.3.7.47
</title>
</head>
<body>
<h1>
Using Redis Blocking List Operations To Power Long-Polling In Lucee CFML 5.3.7.47
</h1>
<p>
Our polling process will hit a ColdFusion end-point that uses Redis' blocking
list operations to check on the status of a job.
</p>
<script type="text/javascript">
var pollID = "<cfoutput>#encodeForJavaScript( pollID )#</cfoutput>";
// Some CSS for prettier console.log() output.
var cssSuccess = "color: white ; background-color: green ;";
var cssError = "color: white ; background-color: red ;";
// Let's attempt to poll the status of our background job up to 5 times.
longPoll( 5 ).then(
( response ) => {
console.group( "%cLong Poll Succeeded", cssSuccess );
console.log( response );
console.groupEnd();
},
( error ) => {
console.group( "Long Poll Failed" );
console.error( error );
console.groupEnd();
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I poll the job end-point for the given number of attempts. Returns a Promise.
async function longPoll( maxAttempts ) {
for ( var i = 1 ; i <= maxAttempts ; i++ ) {
console.log( `%cLong poll attempt ${ i }.`, cssError );
// NOTE: This Fetch request will HANG for some period of time because the
// server-side logic is going to perform a BLPOP (Blocking Left-Pop)
// operation on a Redis List - the same Redis List that we are pushing an
// item onto at the top of this page (inside the CFThread tag).
var result = await fetch( "./poll-target.cfm?pollID=" + encodeURIComponent( pollID ) );
if ( result.ok ) {
return( result.json() );
}
}
throw( new Error( `Job could not complete in ${ maxAttempts } attempts.` ) );
}
</script>
</body>
</html>
component
output = false
hint = "I provide a super simple wrapper around getting and returning Redis resources."
{
/**
* I initialize the Redis resource wrapper using Jedis 3.3.0.
*/
public void function init() {
variables.jarFiles = [
expandPath( "./lib/jedis-3.3.0/commons-pool2-2.6.2.jar" ),
expandPath( "./lib/jedis-3.3.0/jedis-3.3.0.jar" ),
expandPath( "./lib/jedis-3.3.0/slf4j-api-1.7.30.jar" )
];
var jedisPoolConfig = loadClass( "redis.clients.jedis.JedisPoolConfig" )
.init()
;
variables.redisClient = loadClass( "redis.clients.jedis.JedisPool" )
.init( jedisPoolConfig, "127.0.0.1" )
;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I obtain a Redis resource from the Jedis pool and pass it to the given callback.
* The results of the callback are returned to the calling context. And, the resource
* is safely returned to the Jedis connection pool.
*
* @callback I am the callback that will receive the Redis resource.
*/
public any function withRedis( required function callback ) {
try {
var redis = redisClient.getResource();
return( callback( redis ) );
} finally {
redis?.close();
}
}
// ---
// PRIVATE METHODS.
// ---
/**
* I load the given class using the Jedis JAR files.
*
* @className I am the Java class being loaded.
*/
private any function loadClass( required string className ) {
return( createObject( "java", className, jarFiles ) );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment