Created
November 7, 2020 14:25
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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