Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created June 21, 2022 11:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/bbbeb24a2b153ff2970a2b3e75e5f0f7 to your computer and use it in GitHub Desktop.
Save bennadel/bbbeb24a2b153ff2970a2b3e75e5f0f7 to your computer and use it in GitHub Desktop.
Considering A Stale-While-Revalidate Pattern To Caching In ColdFusion
component
output = false
hint = "I provide access to a flag value which is refreshed asynchronously."
{
/**
* I initialize the flag value.
*/
public void function init() {
variables.currentValue = false;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I get the CURRENT flag value, which may be STALE. Accessing the flag value MAY
* trigger a BACKGROUND FETCH to re-cache the latest value.
*/
public boolean function getValue() {
revalidateInBackground();
// NOTE: For the sake of this demo, we're going to consider this value to be a
// thread-safe value. As such, we're not going to apply any locking around its
// access despite it being cached in-memory and shared across requests.
return( currentValue );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I spawn an asynchronous thread to revalidate the flag in the background.
*/
private void function revalidateInBackground() {
var futureKey = "$$flagAsyncFuture";
// Only run the async validation once per request (an optimization).
// --
// NOTE ON LOCKING: Normally, when I only want to do something once, I would add a
// double-check lock around it. However, in this case, since the scope of the
// FETCH contention is a single request, I'm not going to worry about it. More
// than likely, there will be no race-condition (again, this is scoped to the
// request); and, even if there is contention in this case, the worst-case
// scenario is that we run the asynchronous check more than once, which is fine.
if ( request.keyExists( futureKey ) ) {
return;
}
try {
// CAUTION: We do not want to use the `.error()` method on the runAsync()
// result because doing so will turn it from an ASYNCHRONOUS call into a
// BLOCKING / SYNCHRONOUS call. Instead, we're going to use a try/catch block
// inside the closure so that we can retain the asynchronicity while still
// catching errors internally.
request[ futureKey ] = runAsync(
() => {
try {
systemOutput( "Revalidating in runAsync() closure.", true );
// !! Simulating some sort of background flag change. !!
variables.currentValue = ! variables.currentValue;
} catch ( any error ) {
systemOutput( "ERROR REVALIDATING FLAG!!!!" );
}
}
);
// Catch any errors when spawning the thread that powers the runAsync() function.
// --
// CAUTION: I AM NOT SURE that this is strictly necessary. I don't know how the
// thread-pool exhaustion will manifest in the calling context. That said, I
// usually wrap my thread-spawning code in a try/catch block since I know that the
// CFThread tag will sometimes throw an error when no thread can be spawned.
} catch ( any error ) {
// Swallow thread-pool exhaustion errors for now...
systemOutput( "ERROR: runAsync() could not obtain thread.", true, true );
}
}
}
<cfscript>
systemOutput( "+ + + + + + + + + + + + + + + ", true );
systemOutput( "Starting Demo Request.", true );
systemOutput( "Checking flag...", true );
systemOutput( "--> Flag: [ #application.flag.getValue()# ]", true );
systemOutput( "Checking flag...", true );
systemOutput( "--> Flag: [ #application.flag.getValue()# ]", true );
systemOutput( "Checking flag...", true );
systemOutput( "--> Flag: [ #application.flag.getValue()# ]", true );
systemOutput( "Ending request.", true );
systemOutput( "- - - - - - - - - - - - - - - ", true );
</cfscript>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment