TimeSpanCache

Package: MachII.caching.strategies
Inherits from: caching.strategies.AbstractCacheStrategy
A caching strategy which uses a time span eviction policy.

<!--- License: Copyright 2008 GreatBizTools, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Copyright: GreatBizTools, LLC Author: Kurt Wiersma (kurt@mach-ii.com) $Id: TimeSpanCache.cfc 1189 2008-12-02 07:27:46Z peterfarrell $ Created version: 1.6.0 Updated version: 1.6.0 Notes: Configuration parameters Scope - The scope that the cache should be placed in. - The default setting for "scope" is "application". - Valid values are "application", "server" and "session". ScopeKey - The key place the cache in the choosen scope. - Optional and by default the cache will be placed in scope._MachIICache.Hash(appKey & moduleName & cacheName) - Rarely will this need to be used Timespan - Takes a string formatted like ColdFusion's createTimeSpan() function. The list is days, hours, minutes, seconds. - Can also take "forever" for a non-expiring cache. - The default is to cache for 1 hour. CleanupIntervalInMinutes - The interval of time in minutes in which to run the reap() method. Reap will remove expired elements from the cache, but does not "refresh" the data. If an element is not available in the cache and an event-handler requests that data, only that point will the data be "refreshed" and added back into the cache. - The default setting for "cleanupIntervalInMinutes" is "3." - Valid numeric value only. - This attribute will rarely need to be changed. Using all of the default settings will result in caching each element of data for 1 hour in the application scope. Expired cache elements will be cleaned up via reap() which is run every 3 minutes. <property name="Caching" type="MachII.caching.CachingProperty"> <parameters> <!-- Naming a default cache name is not required, but required if you do not want to specify the 'name' attribute in the cache command --> <parameter name="defaultCacheName" value="default" /> <parameter name="default"> <struct> <key name="type" value="MachII.caching.strategies.TimeSpanCache" /> <key name="scope" value="application" /> <key name="timespan" value="0,1,0,0"/><!-- Cache for 1 hour --> <key name="cleanupIntervalInMinutes" value="3" /> </struct> </parameter> </parameters> </property> --->

Method Summary
private any computeCacheUntilTimestamp()

Computes a cache until timestamp in ms.

public void configure()

Configures the strategy.

private any createBigInteger(any value)

Helper method that creates a java.math.BigInteger with the passed value.

public void flush()

Flushes all elements from the cache.

public any get(string key)

Gets en element by key from the cache. Returns 'null' if the key is not in the cache.

public any getCleanupInterval()

Cleanup interval in ms.

public struct getConfigurationData()

Gets pretty configuration data for this caching strategy.

public any getCurrentTickCount()

Gets the current tick count as a big integer. Has logic that is that is used internally for unit testing.

private string getNamedLockName(string actionType)

Gets a named lock name based on choosen scope and other factors

public string getScope()
private string getScopeKey()
private struct getStorage()

Gets a reference to the cache data storage.

private ThreadingAdapter getThreadingAdapter()
public any getTimespan()
public string getTimespanString()
private string hashKey(string key)

Creates a hash from a key name.

public any keyExists(string key)

Checks if an element exists by key in the cache.

public void put(string key, any data)

Puts an element by key into the cache.

public void reap()

Inspects the timestamps of cached elements and throws out the expired ones.

public void remove(string key)

Removes data from the cache by key.

private void removeByHashedKey(string hashedKey)

Removes data from the cache by hashed key.

private void setCleanupInterval(numeric cleanupInterval)

Sets and converts the incoming minutes into ms.

public void setCurrentTickCount(string currentTickCount)

Used internally for unit testing. Set to '' when you want to use the current tick count.

private void setScope(string scope)
private void setScopeKey(string scopeKey)
private void setThreadingAdapter(ThreadingAdapter threadingAdapter)
private void setTimespan(string timespan)

Sets and builds a timespan in ms.

private void setTimespanString(string timespanString)

Sets a timespan string.

private void shouldCleanup()

Cleanups the data storage.

Methods inherited from caching.strategies.AbstractCacheStrategy:   getStrategyTypeName , getStrategyType , setParameter , init , setParameters , setCacheEnabled , getLog , getParameterNames , getParameter , isCacheEnabled , getParameters , setLog , getCacheStats , isParameterDefined
Method Detail
computeCacheUntilTimestamp

private any computeCacheUntilTimestamp( )

Computes a cache until timestamp in ms.

Parameters:

Code:

	<cffunction name="computeCacheUntilTimestamp" access="private" returntype="any" output="false"
		hint="Computes a cache until timestamp in ms.">

		<cfset var timestamp = getCurrentTickCount() />
		
		
		<cfset timestamp = timestamp.add(getTimespan()) />
			
		<cfreturn timestamp />
	</cffunction> 

configure

public void configure( )

Configures the strategy.

Parameters:

Code:

	<cffunction name="configure" access="public" returntype="void" output="false"
		hint="Configures the strategy.">

		
		<cfif isParameterDefined("timespan")>
			<cfif getParameter("timespan") NEQ "forever" AND ListLen(getParameter("timespan")) NEQ 4>
				<cfthrow type="MachII.caching.strategies.TimeSpanCache"
					message="Invalid timespan of '#getParameter("timespan")#'."
					detail="Timespan must be set to 'forever' or a list of 4 numbers (days, hours, minutes, seconds)." />
			<cfelse>
				<cfset setTimespanString(getParameter("timespan")) />
			</cfif>
		</cfif>
		<cfif isParameterDefined("scope")>
			<cfif NOT ListFindNoCase("application,server,session", getParameter("scope"))>
				<cfthrow type="MachII.caching.strategies.TimeSpanCache"
					message="Invalid Scope of '#getParameter("scope")#'."
					detail="Use 'application', 'server' or 'session'." />
			<cfelse>
				<cfset setScope(getParameter("scope")) />
			</cfif>
		</cfif>
		<cfif isParameterDefined("scopeKey")>
			<cfif NOT Len(getParameter("scopeKey"))>
				<cfthrow type="MachII.caching.strategies.TimeSpanCache"
					message="Invalid ScopeKey of '#getParameter("ScopeKey")#'."
					detail="ScopeKey must have a length greater than 0 and be a valid struct key." />
			<cfelse>
				<cfset setScopeKey(getParameter("scopeKey")) />
			</cfif>
		<cfelseif isParameterDefined("generatedScopeKey")>
			<cfset setScopeKey(getParameter("generatedScopeKey")) />
		<cfelse>
			<cfset setScopeKey(REReplace(CreateUUID(), "[[:punct:]]", "", "ALL")) />
		</cfif>
		<cfif isParameterDefined("cleanupIntervalInMinutes")>
			<cfif NOT isNumeric(getParameter("cleanupIntervalInMinutes")) 
				OR getParameter("cleanupIntervalInMinutes") LTE 0>
				<cfthrow type="MachII.caching.strategies.TimeSpanCache"
					message="Invalid CleanupIntervalInMinutes of '#getParameter("cleanupIntervalInMinutes")#'."
					detail="CleanupIntervalInMinutes must be numeric and greater than 0." />
			<cfelse>
				<cfset setCleanupInterval(getParameter("cleanupIntervalInMinutes")) />
			</cfif>
		</cfif>

		<cfset setThreadingAdapter(variables.utils.createThreadingAdapter()) />
		
		
		<cfset flush() />
	</cffunction> 

createBigInteger

private any createBigInteger( any value )

Helper method that creates a java.math.BigInteger with the passed value.

Parameters:
any value

Code:

	<cffunction name="createBigInteger" access="private" returntype="any" output="false"
		hint="Helper method that creates a java.math.BigInteger with the passed value.">
		<cfargument name="value" type="any" required="true" />
		<cfreturn CreateObject("java", "java.math.BigInteger").init(arguments.value) />
	</cffunction> 

flush

public void flush( )

Flushes all elements from the cache.

Parameters:

Code:

	<cffunction name="flush" access="public" returntype="void" output="false"
		hint="Flushes all elements from the cache.">
		
		<cfset var dataStorage = getStorage() />
		
		<cfset StructClear(dataStorage) />	
		<cfset getCacheStats().reset() />	
	</cffunction> 

get

public any get( string key )

Gets en element by key from the cache. Returns 'null' if the key is not in the cache.

Parameters:
string key

Code:

	<cffunction name="get" access="public" returntype="any" output="false"
		hint="Gets en element by key from the cache. Returns 'null' if the key is not in the cache.">
		<cfargument name="key" type="string" required="true"
			hint="The key should not be a hashed key." />

		<cfset var dataStorage = getStorage() />
		<cfset var hashedKey = hashKey(arguments.key) />
		<cfset var cacheElement = "" />
		
		<cfset shouldCleanup() />
		
		<cfif keyExists(arguments.key)>
			<cfset cacheElement = dataStorage[hashedKey]>

			<cfif NOT cacheElement.isStale>
				<cfset getCacheStats().incrementCacheHits(1) />
				<cfreturn cacheElement.data />
			<cfelse>
				<cfset getCacheStats().incrementCacheMisses(1) />
			</cfif>
		<cfelse>
			<cfset getCacheStats().incrementCacheMisses(1) />
		</cfif>
	</cffunction> 

getCleanupInterval

public any getCleanupInterval( )

Cleanup interval in ms.

Parameters:

Code:

	<cffunction name="getCleanupInterval" access="public" returntype="any" output="false"
		hint="Cleanup interval in ms.">
		<cfreturn variables.instance.cleanupInterval />
	</cffunction> 

getConfigurationData

public struct getConfigurationData( )

Gets pretty configuration data for this caching strategy.

Parameters:

Code:

	<cffunction name="getConfigurationData" access="public" returntype="struct" output="false"
		hint="Gets pretty configuration data for this caching strategy.">
		
		<cfset var data = StructNew() />
		<cfset var cleanupInterval = getCleanupInterval() />
		
		<cfset data["Scope"] = getScope() />
		<cfset data["Cache Enabled"] = YesNoFormat(isCacheEnabled()) />
		<cfset data["Timespan"] = getTimespanString() />
		<cfset data["Cleanup Interval"] = cleanupInterval.divide(createBigInteger(60000)).toString() & " minutes" />
		
		<cfreturn data />
	</cffunction> 

getCurrentTickCount

public any getCurrentTickCount( )

Gets the current tick count as a big integer. Has logic that is that is used internally for unit testing.

Parameters:

Code:

	<cffunction name="getCurrentTickCount" access="public" returntype="any" output="false"
		hint="Gets the current tick count as a big integer.  Has logic that is that is used internally for unit testing.">
		<cfif Len(variables.currentTickCount)>
			<cfreturn createBigInteger(variables.currentTickCount) />
		<cfelse>
			<cfreturn createBigInteger(getTickCount()) />
		</cfif>
	</cffunction> 

getNamedLockName

private string getNamedLockName( string actionType )

Gets a named lock name based on choosen scope and other factors

Parameters:
string actionType

Code:

	<cffunction name="getNamedLockName" access="private" returntype="string" output="false"
		hint="Gets a named lock name based on choosen scope and other factors">
		<cfargument name="actionType" type="string" required="true" />
		
		<cfset var name = "_MachIITimeSpanCache_" & arguments.actionType & "_" & getScopeKey() />
		
		
		<cfif getScope() EQ "session">
			
			
			<cfif StructKeyExists(StructGet("session"), "sessionId")>
				<cfset name = name & "_" & StructGet("session").sessionId />
			</cfif>
		</cfif>

		<cfreturn name />
	</cffunction> 

getScope

public string getScope( )

Parameters:

Code:

	<cffunction name="getScope" access="public" returntype="string" output="false">
		<cfreturn variables.instance.scope />
	</cffunction> 

getScopeKey

private string getScopeKey( )

Parameters:

Code:

	<cffunction name="getScopeKey" access="private" returntype="string" output="false">
		<cfreturn variables.instance.scopeKey />
	</cffunction> 

getStorage

private struct getStorage( )

Gets a reference to the cache data storage.

Parameters:

Code:

	<cffunction name="getStorage" access="private" returntype="struct" output="false"
		hint="Gets a reference to the cache data storage.">
		<cfreturn StructGet(getScope() & "." & getScopeKey()) />
	</cffunction> 

getThreadingAdapter

private ThreadingAdapter getThreadingAdapter( )

Parameters:

Code:

	<cffunction name="getThreadingAdapter" access="private" returntype="MachII.util.threading.ThreadingAdapter" output="false">
		<cfreturn variables.threadingAdapter />
	</cffunction> 

getTimespan

public any getTimespan( )

Parameters:

Code:

	<cffunction name="getTimespan" access="public" returntype="any" output="false">
		<cfreturn variables.instance.timespan />
	</cffunction> 

getTimespanString

public string getTimespanString( )

Parameters:

Code:

	<cffunction name="getTimespanString" access="public" returntype="string" output="false">
		<cfreturn variables.instance.timespanString />
	</cffunction> 

hashKey

private string hashKey( string key )

Creates a hash from a key name.

Parameters:
string key

Code:

	<cffunction name="hashKey" access="private" returntype="string" output="false"
		hint="Creates a hash from a key name.">
		<cfargument name="key" type="string" required="true" />
		<cfreturn Hash(UCase(Trim(arguments.key))) />
	</cffunction> 

keyExists

public any keyExists( string key )

Checks if an element exists by key in the cache.

Parameters:
string key

Code:

	<cffunction name="keyExists" access="public" returntype="any" output="false"
		hint="Checks if an element exists by key in the cache.">
		<cfargument name="key" type="string" required="true"
			hint="The key should not be a hashed key." />

		<cfset var dataStorage = getStorage() />
		<cfset var hashedKey = hashKey(arguments.key) />
		<cfset var cacheElement = "" />

		<cfif NOT StructKeyExists(dataStorage, hashedKey)>
			<cfreturn false />
		<cfelse>
			<cfset cacheElement = dataStorage[hashedKey] />
			
			<cfif cacheElement.isStale>
				<cfreturn false />
			<cfelseif cacheElement.timestamp.compareTo(getCurrentTickCount()) LT 1>
				<cfset removeByHashedKey(hashedKey) />
				<cfreturn false />
			<cfelse>
				<cfreturn true />
			</cfif>
		</cfif>
	</cffunction> 

put

public void put( string key, any data )

Puts an element by key into the cache.

Parameters:
string key
any data

Code:

	<cffunction name="put" access="public" returntype="void" output="false"
		hint="Puts an element by key into the cache.">
		<cfargument name="key" type="string" required="true"
			hint="The key should not be a hashed key." />
		<cfargument name="data" type="any" required="true" />

		<cfset var dataStorage = getStorage() />
		<cfset var hashedKey = hashKey(arguments.key) />
		<cfset var cacheElement = StructNew() />
		<cfset var cacheUntilTimestamp = computeCacheUntilTimestamp() />
		
		
		<cfif NOT StructKeyExists(dataStorage, hashedKey)>
			<cfset getCacheStats().incrementTotalElements(1) />
			<cfset getCacheStats().incrementActiveElements(1) />
		<cfelse>
			<cfif dataStorage[hashedKey].isStale>
				<cfset getCacheStats().incrementActiveElements(1) />
			</cfif>
		</cfif>
		
		
		<cfset cacheElement.data  = arguments.data />
		<cfset cacheElement.isStale = false />
		<cfset cacheElement.timestamp = cacheUntilTimestamp />
		
		<cfset dataStorage[hashedKey] = cacheElement />
	</cffunction> 

reap

public void reap( )

Inspects the timestamps of cached elements and throws out the expired ones.

Parameters:

Code:

	<cffunction name="reap" access="public" returntype="void" output="false"
		hint="Inspects the timestamps of cached elements and throws out the expired ones.">
			
		<cfset var currentTick = getCurrentTickCount() />
		<cfset var dataStorage = getStorage() />
		<cfset var keyArray = "" />
		<cfset var i = "" />
		<cfset var count = 0 />
		
		<cflock name="#getNamedLockName("cleanup")#" type="exclusive" 
			timeout="1" throwontimeout="false">
			
			
			<cfset variables.lastCleanup = currentTick />
			
			<cfset keyArray = StructKeyArray(dataStorage) />
			
			
			<cfloop from="1" to="#ArrayLen(keyArray)#" index="i">
				<cftry>
					<cfif currentTick.compareTo(dataStorage[keyArray[i]].timestamp) GT 0>
						<cfset removeByHashedKey(keyArray[i]) />
					</cfif>
					<cfcatch type="any">
						
					</cfcatch>
				</cftry>
			</cfloop>
		</cflock>
	</cffunction> 

remove

public void remove( string key )

Removes data from the cache by key.

Parameters:
string key

Code:

	<cffunction name="remove" access="public" returntype="void" output="false"
		hint="Removes data from the cache by key.">
		<cfargument name="key" type="string" required="true"
			hint="The key should not be a hashed key." />
		<cfset removeByHashedKey(hashKey(arguments.key)) />
	</cffunction> 

removeByHashedKey

private void removeByHashedKey( string hashedKey )

Removes data from the cache by hashed key.

Parameters:
string hashedKey

Code:

	<cffunction name="removeByHashedKey" access="private" returntype="void" output="false"
		hint="Removes data from the cache by hashed key.">
		<cfargument name="hashedKey" type="string" required="true"
			hint="The passed key needs to be a hashed key." />

		<cfset var dataStorage = getStorage() />
		<cfset var cacheElement = "" />

		<cfif StructKeyExists(dataStorage, arguments.hashedKey)>
			<cfset cacheElement = dataStorage[arguments.hashedKey] />
			
 			<cfif cacheElement.isStale>
				<cfset StructDelete(dataStorage, arguments.hashedKey, false) />
				<cfset getCacheStats().incrementEvictions(1) />
				<cfset getCacheStats().decrementTotalElements(1) />
			<cfelse>
				<cfset cacheElement.isStale = true />
				<cfset getCacheStats().decrementActiveElements(1) />
			</cfif>
		</cfif>
	</cffunction> 

setCleanupInterval

private void setCleanupInterval( numeric cleanupInterval )

Sets and converts the incoming minutes into ms.

Parameters:
numeric cleanupInterval

Code:

	<cffunction name="setCleanupInterval" access="private" returntype="void" output="false"
		hint="Sets and converts the incoming minutes into ms.">
		<cfargument name="cleanupInterval" type="numeric" required="true"
			hint="Cleanup interval in minutes." />		
		
		<cfset var interval = createBigInteger(arguments.cleanupInterval) />
		
		<cfset interval = interval.multiply(variables.MINUTE) />
		
		<cfset variables.instance.cleanupInterval = interval />
	</cffunction> 

setCurrentTickCount

public void setCurrentTickCount( string currentTickCount )

Used internally for unit testing. Set to '' when you want to use the current tick count.

Parameters:
string currentTickCount

Code:

	<cffunction name="setCurrentTickCount" access="public" returntype="void" output="false" 
		hint="Used internally for unit testing. Set to '' when you want to use the current tick count.">
		<cfargument name="currentTickCount" type="string" required="true" />
		<cfset variables.currentTickCount = arguments.currentTickCount />
	</cffunction> 

setScope

private void setScope( string scope )

Parameters:
string scope

Code:

	<cffunction name="setScope" access="private" returntype="void" output="false">
		<cfargument name="scope" type="string" required="true" />		
		<cfset variables.instance.scope = arguments.scope />
	</cffunction> 

setScopeKey

private void setScopeKey( string scopeKey )

Parameters:
string scopeKey

Code:

	<cffunction name="setScopeKey" access="private" returntype="void" output="false">
		<cfargument name="scopeKey" type="string" required="true" />
		<cfset variables.instance.scopeKey = arguments.scopeKey />
	</cffunction> 

setThreadingAdapter

private void setThreadingAdapter( ThreadingAdapter threadingAdapter )

Parameters:
ThreadingAdapter threadingAdapter

Code:

	<cffunction name="setThreadingAdapter" access="private" returntype="void" output="false">
		<cfargument name="threadingAdapter" type="MachII.util.threading.ThreadingAdapter" required="true" />
		<cfset variables.threadingAdapter = arguments.threadingAdapter />
	</cffunction> 

setTimespan

private void setTimespan( string timespan )

Sets and builds a timespan in ms.

Parameters:
string timespan

Code:

	<cffunction name="setTimespan" access="private" returntype="void" output="false"
		hint="Sets and builds a timespan in ms.">
		<cfargument name="timespan" type="string" required="true"
			hint="Must be in format of 0,0,0,0 (days,hours,minutes,seconds) or 'forever'." />
		
		<cfset var offset = "" />
		<cfset var value = "" />
		
		<cfif arguments.timespan EQ "forever">
			<cfset offset = createBigInteger("1228000000000") />
		<cfelse>
			<cfset offset = createBigInteger("0") />
			
			
			
			<cfif ListGetAt(timespan, 4) NEQ 0>
				<cfset value = createBigInteger(ListGetAt(arguments.timespan, 4)) />	
				<cfset value = value.multiply(variables.SECOND) />
				<cfset offset = offset.add(value) />
			</cfif>
			
			
			<cfif ListGetAt(timespan, 3) NEQ 0>
				<cfset value = createBigInteger(ListGetAt(arguments.timespan, 3)) />
				<cfset value = value.multiply(variables.MINUTE) />
				<cfset offset = offset.add(value) />
			</cfif>
			
			
			<cfif ListGetAt(timespan, 2) NEQ 0>
				<cfset value = createBigInteger(ListGetAt(arguments.timespan, 2)) />
				<cfset value = value.multiply(variables.HOUR) />
				<cfset offset = offset.add(value) />
			</cfif>
			
			
			<cfif ListGetAt(timespan, 1) NEQ 0>
				<cfset value = createBigInteger(ListGetAt(arguments.timespan, 1)) />
				<cfset value = value.multiply(variables.DAY) />
				<cfset offset = offset.add(value) />
			</cfif>
		</cfif>
				
		<cfset variables.instance.timespan = offset />
	</cffunction> 

setTimespanString

private void setTimespanString( string timespanString )

Sets a timespan string.

Parameters:
string timespanString

Code:

	<cffunction name="setTimespanString" access="private" returntype="void" output="false"
		hint="Sets a timespan string.">
		<cfargument name="timespanString" type="string" required="true"
			hint="Must be in format of 0,0,0,0 (days,hours,minutes,seconds) or 'forever'." />
		<cfset variables.instance.timespanString = arguments.timeSpanString />
		<cfset setTimespan(arguments.timespanString) />
	</cffunction> 

shouldCleanup

private void shouldCleanup( )

Cleanups the data storage.

Parameters:

Code:

	<cffunction name="shouldCleanup" access="private" returntype="void" output="false"
		hint="Cleanups the data storage.">
		
		<cfset var diffTimestamp = getCurrentTickCount() />
		
		<cfset diffTimestamp = diffTimestamp.subtract(getCleanupInterval()) />
		
		<cfif diffTimestamp.compareTo(variables.lastCleanup) GT 0>
			
			<cflock name="#getNamedLockName("cleanup")#" type="exclusive" 
				timeout="1" throwontimeout="false">
				<cfif diffTimestamp.compareTo(variables.lastCleanup) GT 0>
					<cfif getThreadingAdapter().allowThreading()>
						
						<cfset variables.lastCleanup = getCurrentTickCount() />
						<cfset getThreadingAdapter().run(this, "reap") />
					<cfelse>
						<cfset reap() />
					</cfif>
				</cfif>
			</cflock>
		</cfif>
	</cffunction>