Considerations + Alternate Session Facade

Believe it or not, there are other ways to tackle this problem. One common approach is to carry the object's state across as hidden form fields. When the user submits a form step, they are combined with the visible form fields to completely recreate the object. This approach works well for simple beans, but can quickly fall apart as your beans become more complex (e.g. they have structs, arrays, or recordsets as properties).

Another approach is to serialize the bean bean as xml and pass it along in one big hidden form field. You might have to spend some extra time writing toXml() and fromXml() methods for your bean such that it completely express its internal state as an xml string. While I like this approach better than separate form fields, I still believe that you should try in-memory beans first and then investigate these alternatives *only* when you experience an issue.

Wait! What if I set my session timeout to 2 days and I have a very high traffic site? My memory usage is going through the roof with these stupid beans! I only want my bean to last for maybe 20 minutes of inactivity before I reclaim the memory! How do I do this?

First of all, you are about to witness a great reason *why* you should use a Session Facade. While my example app has an extremely rudimentary implementation, we could write a much more "managed" version that would require zero changes elsewhere in your application.

Think about it... the rest of the application knows the facade's "api" as:

  • get(string key)
  • put(string key, any value)
  • delete(string key)

There's nothing that says we can't change *what* the Session Facade does behind the scenes. It's like a fence: your application throws objects over the fence, and expects to get them back later. Sometimes that might not happen, but if the facade says "sorry buddy, you waited too long", then it's up to the calling code to handle that appropriately. What goes on behind the fence is really none of the calling code's business.

So with that said, how would I write a session facade that could store objects for a particular session but wipe them after a certain amount of inactivity that's NOT tied to CF's own session scope?

First, I'd grab a piece of memory in a shared scope like CF's application scope. Remember, if the session facade is living in Mach-ii's property manager, then it is a "singleton", e.g. there's only one instance for the entire application. This means that every call is going through the same instance - there isn't a separate facade for each session.

To start, the new session facade grabs some storage when it is created:

<cffunction name="init" access="public" returntype="multiStep.util.sessionFacade" output="false" hint="I am the constructor for the session facade"> 
   <cfset application.privateStorage = structnew()/>
   <cfreturn this/>
</cffunction>

However, we can still use CF's session scope to tag or remember which session "owns" which object that is stored behind the facade:

<cffunction name="put" access="public" returntype="void" output="false" hint="I put a value into the sessionFacade">
    <cfargument name="key" type="string" required="true" hint="I am the key to store as"/>
    <cfargument name="value" type="any" required="true" hint="I am the value to store"/>
  
    <!--- make sure storage is initialized for this session --->
    <cfif not structKeyExists(application.privateStorage, session.cfid & "_" & session.cftoken)>
       <cfset application.privateStorage[session.cfid & "_" & session.cftoken] = structnew()/>
    </cfif>
  
    <!--- create a place for our value --->
    <cfset application.privateStorage[session.cfid & "_" & session.cftoken][arguments.key] = structnew()/>
  
    <!--- store the value AND it's timestamp so we know how long it has been here --->
    <cfset application.privateStorage[session.cfid & "_" & session.cftoken][arguments.key].timestamp = now()/>
    <cfset application.privateStorage[session.cfid & "_" & session.cftoken][arguments.key].value = arguments.value/>  
  
</cffunction>

Retrieving the object might be a little bit more involved than before:

<cffunction name="get" access="public" returntype="any" output="false" hint="I retrieve a value from the sessionFacade">
   <cfargument name="key" type="string" required="true" hint="I am the key whose value will be retrieved"/>

   <cfif structKeyExists(application.privateStorage, session.cfid & "_" & session.cftoken)
           and structKeyExists(application.privateStorage[session.cfid & "_" & session.cftoken], arguments.key)>
      <cfreturn application.privateStorage[session.cfid & "_" & session.cftoken][arguments.key].value/>
   <cfelse>
      <cfthrow type="sessionFacade.keyNotFound" message="Session Facade could not find value for key: #arguments.key#"/>
   </cfif>
 
</cffunction>

Now we are only left with one problem: how do we cleanup? You have a few options... you could create a scheduled task within coldfusion to cleanup the storage, or you could trigger cleanup whenever someone invoked a method on the facade. Now, you wouldn't want to check the timestamps of EVERY object just because someone asked for theirs, but you could code the facade such that it ran a cleanup routine every five minutes (or until the next time a method is invoked).

First you would need a constant for the time-to-live (TTL) for objects in the facade, another for the minimum amount of time between cleanup, and a variable to store the last point in time the cleanup routine was executed. These should all be configurable from outside the Facade, but I'll leave them hardcoded for now. Within the facade's init:

<cfset variables.objectTTLMinutes = 20 />
<cfset variables.cleanupIntervalMinutes = 5 />
<cfset variables.lastCleanupTime = now() />

Then we add two new private methods:

<cffunction name="cleanup" access="private" returntype="void" output="false" hint="I cleanup the facade's memory">
   <cfset var sessionItem = 0/>
   <cfset var objectItem = 0/>
   <!--- should we cleanup --->
   <cfif shouldCleanup()>
      <!--- loop over each session that has objects --->
      <cfloop collection="#application.privateStorage#" item="sessionItem">
         <!--- loop over each object --->
         <cfloop collection="#application.privateStorage[sessionItem]#" item="objectItem">
            <!--- check the timestamp to see if it's expired --->
            <cfif datediff("n", application.privateStorage[sessionItem][objectItem].timestamp,now()) GT variables.objectTTLMinutes>
               <cfset structDelete(application.privateStorage[sessionItem],objectItem)/>
            </cfif>
         </cfloop>
      </cfloop>   
   </cfif>
</cffunction>

<cffunction name="shouldCleanup" access="private" returntype="boolean" output="false" hint="I determine whether we need to cleanup the facade's memory">
   <!--- return whether the difference in minutes between now and the last time we cleaned up is greater than the interval --->
   <cfreturn dateDiff("n",variables.lastCleanupTime,now()) gt variables.cleanupIntervalMinutes>
</cffunction>

You could then add calls to cleanup() everytime get(), put(), or delete() is called.


06/20/2006 02:32 P - Sami Hoda said...
Very nice.  

08/16/2007 05:03 P - Christopher Harris said...
need appliction for ross stores


Post a comment:

(required, will not be displayed)
 


   You will be sent an email asking you to validate your comment.



Driven by Farcry Open Source CMS. Dressed in Aura.
Powered by ColdFusion MX.