views:

313

answers:

2

I have built a CFC designed to serve as a dynamic, aging cache intended for almost everything worth caching. LDAP queries, function results, arrays, ojects, you name it. Whatever takes time or resources to calculate and is needed more than once. I'd like to be able to do a few things:

  • share the CFC between applications
  • define the scope of the cache (server / application / session / current request only)
  • use different cache instances at the same time, in the same request
  • be independent from CFCs using the cache component
  • generally adhere to common sense (decoupling, encapsulation, orthogonality, locking)

I would of course be using a different cache instance for every distinct task, but I'd like to be able to use the same CFC across applications. The cache itself is (what else) a Struct, private to the cache instance. How would I properly implement caching and locking when the scope itself is subject to change?

For locking, I use named locks ('CacheRead', 'CacheWrite') currently, this is safe but strikes me as odd. Why would I want a server-wide lock for, say, a session-only operation? (Yes, maybe this is academic, but anyway.)

Passing in the APPLICATION scope as a reference when I want application level caching also seems the wrong thing to do. Is there a better way?

+2  A: 

OK - since I misunderstood your question initially I've deleted my previous answer as to not cause any further confusion.

To answer your question about locking:

Named locks should be fine because they don't have to always have the same name. You can name them dynamically depending on what cache you are accessing. When you need to access an element of the private struct you could do something like have the named lock use the key as its name.

This way, the only time a lock would have an effect is if something was trying to access the same cache by name.

JG
Thanks for your effort! :-) I think I will take this approach and generate UUIDs as lock names upon component creation. This way the whole locking thing becomes more self-cointained.
Tomalak
Glad to help. Good luck and happy caching!
JG
+1  A: 

I understand your desire to avoid passing in the actual scope structure that you want to cache to, but your alternatives are limited. The first thing that comes to mind is just passing the name (a string) of the scope you want your cache stored in, and evaluating. By its nature, evaluation is inefficient and should be avoided. That said, I was curious how it might be accomplished. I don't have your code so I just made a dirt-simple "storage" abstraction CFC (skipped caching, as it's irrelevant to what I want to test) here:

cache.cfc:

<cfcomponent>
 <cfset variables.cacheScope = "session" /><!--- default to session --->
 <cfset variables.cache = ""/>

 <cfscript>
 function init(scope){
  variables.cacheScope = arguments.scope;
  return this;
 }

 function cacheWrite(key, value){
  structInsert(evaluate(variables.cacheScope),arguments.key,arguments.value,true);
  return this;
 }

 function cacheRead(key){
  if (not structKeyExists(evaluate(variables.cacheScope), arguments.key)){
   return "";
  }else{
   variables.cache = evaluate(variables.cacheScope);
   return variables.cache[arguments.key];
  }
 } 
 </cfscript>
</cfcomponent>

And a view to test it:

<!--- clear out any existing session vars --->
<cfset structClear(session)/>
<!--- show empty session struct --->
<cfdump var="#session#" label="session vars">
<!--- create storage object --->
<cfset cacher = createObject("component", "cache").init("session")/>
<!--- store a value --->
<cfset cacher.cacheWrite("foo", "bar")/>
<!--- read stored value --->
<cfset rtn = cacher.cacheRead("foo")/>
<!--- show values --->
<cfdump var="#rtn#">
<cfdump var="#session#" label="session vars">

Off topic: I like to write my setter functions to return "this" [as seen above] so that I can chain method calls like jQuery. Part of the view could just as easily been written as:

<cfset rtn = createObject("component", "cache")
    .init("session")
    .cacheWrite("foo", "bar")
    .cacheRead("foo")/>

It's interesting that this is possible, but I probably wouldn't use it in production due to the overhead cost of Evaluate. I'd say that this is valid enough reason to pass in the scope you want to cache into.

If you're still bothered by it (and maybe rightly so?), you could create another CFC that abstracts reading and writing from the desired scope and pass that into your caching CFC as the storage location (a task well-suited for ColdSpring), that way if you ever decide to move the cache into another scope, you don't have to edit 300 pages all using your cache CFC passing in "session" to init, and instead you can edit 1 CFC or your ColdSpring config.

I'm not entirely sure why you would want to have single-request caching though, when you have the request scope. If what you're looking for is a way to cache something for the current request and have it die shortly afterward, request scope may be what you want. Caching is usually more valuable when it spans multiple requests.

Adam Tuttle
Regarding the single-request "use-case": A set of recursive functions in another CFC does transparent function result caching using my cache object. Functions get called the normal way, and before they go to work, they check if they recently calculated a result for the current parameters.
Tomalak
I think the approach I described with a separate scope abstraction cfc that is injected into your cache cfc is the best approach.
Adam Tuttle