NexJ Logo

Lesson - Caching

This lesson discusses the built-in caching capabilities of the framework.

On completing this lesson, participants will:

  • Understand framework caching
  • Cache objects with the Scheme interpreter

Key Concepts

Caching is available for server-side processing. It is generally used to avoid frequent reads of small amounts of data that are rarely updated.

Persistence Mapping Hint

The first caching technique is the simplest and is probably adequate for most needs. On a class' persistence mapping, caching may be set to either instance or class (or none). This configuration is a hint to the framework that frequent simple read queries are to be expected for that class. Simple queries are those that do not involve reading associations and that do not involve filtering based on attributes of associations.

When class caching is configured, the framework will load all instances of the class and, so long as the data remains in cache, will evaluate simple queries against the cached data rather than going to the datasource. The cached data is invalidated (forcefully ejected from cache) if any instance of the class is updated, added or deleted. If there are more than 64 cached instances, unique key queries will use instance caching instead of being evaluated against the class cache.

When instance caching is configured, the framework will cache individual instances of the class as they are read and, for any simple query that identifies at most one row, will search the cached instances before potentially evaluating the query against the datasource. The cached instance is invalidated if it is deleted or updated.

Caching API

If you need more arbitrary data or indexing for cached data, you can use the caching API. This API is implemented on the SysCache class and is also exposed through the UnitOfWork object.
Note: The current UnitOfWork is available in Scheme as ((invocation-context)'unitOfWork).

The cache has the following methods for adding items:

  • (SysCache'putReference key ref): the cache retains a reference (to anything, even a Java object). This function is also exposed as (UnitOfWork'cacheReference key, ref).Important: The object must be immutable (not changeable e.g. a string) or, if the object is not immutable, extra care must be taken that the application logic does not modify it after it has been put in the cache. When retrieving from the cache, a reference to the object is returned. If the object were modified, different invocation contexts could end up with references to the same object which may result in unexpected behavior.

    Never cache Instances with putReference, even if they are not modified by application code. Instances are maintained by the framework, which includes modification of system state associated with them.

  • (SysCache'putCopy key ref): the cache retains a serialized copy of an object (any object serializable in Java i.e. it must implement java.io.Serializable), the object is deserialized each time it is read from cache. This function is also exposed as (UnitOfWork'cacheCopy key ref).
    Note: It is appropriate to cache objects with this method that are changeable and may be referenced in different invocation contexts. Transfer objects, or pairs containing transfer objects are good examples. The object retrieved from the cache will be a full copy of whatever was put in. This is more expensive, but is the best approach in some cases.
  • (SysCache'putInstance key instance attributesPair): the cache retains a transfer object corresponding to a single instance or instance list. The Instance is rebuilt from the transfer object each time it is read. The instance parameter must be an Instance or an InstanceList. The attributesPair parameter determines what parts of the graph of objects to cache in the form (attr1 ... (assoc2 attr21 ... attr2N) ...). This function is also exposed as (UnitOfWork'cacheInstance key instance attributesPair).
    Note: See Object'read for the valid forms of the attributesPair parameter.
  • (UnitOfWork'cacheTransient key ref): the cache retains a reference for the duration of the current InvocationContext.
  • (UnitOfWork'cacheLocal key ref): the cache retains a reference only for the duration of the current UnitOfWork.

Each method takes a key as an argument. The key can be an arbitrary hashable object, but must not change after they have been provided to put* (or cache*) methods. It should be something that retains the same meaning after the completion of the UnitOfWork (e.g. an OID or a string, but not a reference to an Instance or Java object). The key can be used to retrieve the corresponding item from cache, provided that the item still remains in cache.

To retrieve items from the cache, we use (SysCache'get key) or (UnitOfWork'getCached key).

To remove items from the cache, we use (SysCache'remove key) or (UnitOfWork'remove key) .

Two very important things to know about the caching API are:

  1. It is transactional: an item cached through a UnitOfWork is not available through the cache from another transaction until the original UnitOfWork is committed. If the UnitOfWork is rolled back, all cached items are lost.
  2. It is not automatically invalidated. If an item is updated, for instance, it is the responsibility of the developer to call the appropriate uncache method, one of uncache, uncacheLocal or uncacheTransient.

Invalidation

Regardless of which approach is used, caching is done per node in a cluster i.e. per JVM process. When an item is invalidated (automatically for instance/class caching, or manually through the UnitOfWork) it is invalidated on all nodes on which it is present. The framework can choose to eject anything from the cache at any time.

Time to live capabilities may be implemented where the key is the object / set identifier and, in the case of putCopy, the value is a pair of the cached value(s) and a time stamp. When reading from the cache, check the timestamp and if your business rules determine the cached value has expired, invalidate and re-read. Other caching methods will work as well. putInstance expects an Instance or an InstanceList, so a different key is needed for a the timestamp in the time to live scenario.

The cached entries are soft referenced and will be garbage collected in low memory situations. For information on customizing the default garbage collection, see:

  • IBM JVM

    http://www.ibm.com/developerworks/java/jdk/aix/j564/sdkguide.aix64.html

    -Xsoftrefthreshold<number>Sets the number of GCs after which a soft reference is cleared if its referent has not been marked. The default is 32, meaning that on the 32nd GC where the referent is not marked the soft reference is cleared.

  • Sun JVM

    http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html

    Soft references are cleared less aggressively in the server virtual machine than the client. The rate of clearing can be slowed by increasing the parameter SoftRefLRUPolicyMSPerMB with the command line flag -XX:SoftRefLRUPolicyMSPerMB=10000. SoftRefLRUPolicyMSPerMB is a measure of the time that a soft reference survives for a given amount of free space in the heap. The default value is 1000 ms per megabyte. This can be read to mean that a soft reference will survive (after the last strong reference to the object has been collected) for 1 second for each megabyte of free space in the heap. This is very approximate.

Logging

The following JVM setting can be used to see what goes into the cache.

-Dlog4j.logger.nexj.core.runtime.platform.generic.GenericDataCache=ALL 

Caching Examples

Simple Cache and Retrieve Example

; put the immutable object in the cache (SysCache'putReference
    "myKey" "This is my value.") ; get the value from the cache (SysCache'get
    "myKey") ; remove the key from the cache (SysCache'remove "myKey") ; try
    to get it now (SysCache'get "myKey")

Simple Instance Caching Example

In this example, we will write an instance of person to the cache and retrieve it. We need to reset our context before we retrieve from the cache so that we don't just get the local instance from the current unit of work.

; Resets the invocation context keeping the current user
      logged in. (define (reset-context) (define ctx (invocation-context))
      (define env ((ctx'machine)'globalEnvironment)) (env'clearState)
      (ctx'initialize (ctx'principal) env) ) (define p (read-instance Person
      '() '(= lastName "nexjsa") '())) ; put the instance in the cache (define
      p-oid (p':oid)) (SysCache'putInstance p-oid p '(firstName lastName
      (telcoms))) (set! p ()) (commit) (reset-context) ; get the value from
      the cache (SysCache'get p-oid)


Time to Live Copy Caching Example

In this example, we create a function called read-cached-object. It will execute an internal function called getMyObj that returns a new transfer object. Once read-cached-object is called, subsequent calls will read from the cache for fifteen seconds. After that, it will generate the object and re-cache the request. Here we are using the SysCache'putCopy event so we are caching the full transfer object into the cache. This could be used with any serializable object including the results of calls to web services. The example after this one uses putInstance and is better suited to instance caching.

Cut the following example code into a scratch pad and run it in a Server Console.

; Reads copied objects from the cache and refreshes them after a cache timeout. 
; 
; @arg name string The name of the object to copy into the cache 
; @ret object Object copied from the cache that originally was returned by the getMyObj
 function that is internal to read-cached-object.
 (define (read-cached-object name)
   (let 
      ( 
         (getMyObj (lambda (name) (message (: name name) (: object-time (now)) (:
 description "This is any old serializable object returned from some process"))))
         (val (SysCache'get name)) (CACHE_TIMEOUT 15) ; Time in seconds for which to cache the object 
      ) 

      (if 
         (or 
            (null? val) ; nothing in cache 
            (> (now) (date-add-seconds (cdr val) CACHE_TIMEOUT)) ; cache is stale 
         ) 
         ; recache 
         (begin 
            (logger'info "Recaching" name) 
            (let ((p (getMyObj name))) 
               (SysCache'putCopy name (cons p (now))) 
               p 
            ) 
         ) 
         ; read from cache 
         (begin 
            (logger'info 
               "Reading from cache" 
               name 
               "- recaching in" 
               (- CACHE_TIMEOUT 
                  (round 
                     (date-diff-seconds 
                        (now) (cdr val) 
                     ) 
                  ) 
               ) 
               "seconds." 
            ) 
            (car val)
         ) 
      ) 
   ) 
) 

You should see something like...

> (read-cached-object "Joe Test") 
; 23:52:36,157 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Recaching Joe Test 
; #<TO<, @497739>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:36.157000000, 
   description="This is any old serializable object returned from some process" 
)> 

> (read-cached-object "Joe Test") 
; 23:52:36,892 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache Joe Test
 - recaching in 14 seconds. 
; #<TO<, @497739>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:36.157000000, 
   description="This is any old serializable object returned from some process" 
)> 

> (read-cached-object "Joe Test") 
; 23:52:44,391 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache Joe Test
 - recaching in 7 seconds. 
; #<TO<, @497739>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:36.157000000, 
   description="This is any old serializable object returned from some process" 
)> 

> (read-cached-object "Joe Test") 
; 23:52:49,297 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache Joe Test
 - recaching in 2 seconds. 
; #<TO<, @497739>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:36.157000000, 
   description="This is any old serializable object returned from some process" 
)> 

> (read-cached-object "Joe Test") 
; 23:52:51,578 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Recaching Joe Test 
; #<TO<, @10454737>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:51.578000000, 
   description="This is any old serializable object returned from some process" 
)> 

> (read-cached-object "Joe Test") 
; 23:52:52,843 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache Joe Test
 - recaching in 14 seconds. 
; #<TO<, @10454737>( 
   name="Joe Test", 
   object-time=2010-02-10 04:52:51.578000000, 
   description="This is any old serializable object returned from some process" 
)> 

> 


Notice we read from the cache for fifteen seconds then re-cache the result.

Time to Live Instance Caching Example

This example is similar to the previous example, but we create a function called read-cached that uses putInstance rather than putCopy. This function will behave like 'read except that once it is called, subsequent calls will read cached instances from the cache for fifteen seconds. After that, it will re-read and re-cache the request.

The big difference here is that we are working with instances so all of the object's cross unit of work access and lifecycle management are taken care of by the framework. Also notice that we needed to create a sister key to store the cached time because we cannot cache anything but instances.

Cut the following example code into a scratch pad and run it in a Server Console.

; Reads specified copied instances of this class from cache. 
; 
; @arg class metaclass The name of the class to read 
; @arg attributes list The list of attributes to proactively retrieve. 
; @arg where list The where clause 
; @ret instance-collection Collection of instances matching the where clause. 
(define (read-cached class attributes where) 
   (let* 
      ( 
         (where-key (list class where)) ; key is class and where to ensure uniqueness 
         (where-timeout-key (list class where "timeout")) ; this will be the "sister" key to
 maintain the last cached time 
         (cached-time (SysCache'get where-timeout-key)) 
         (CACHE_TIMEOUT 15) ; Time in seconds for which to cache the result set 
      ) 

      (if 
         (or 
            (null? cached-time) ; nothing in cache 
            (> (now) (date-add-seconds cached-time CACHE_TIMEOUT)) ; cache is stale 
         ) 
         ; recache 
         (begin 
            (logger'info "Recaching" where-key) 
            (let ((p (class 'read attributes where '() '() '() #f))) 
               (SysCache'putReference where-timeout-key (now)) 
               (SysCache'putInstance where-key p attributes) 
               p 
            ) 
         ) 
         ; read from cache 
         (begin 
            (logger'info 
               "Reading from cache" 
               where-key 
               "- recaching in" 
               (- CACHE_TIMEOUT 
                  (round 
                     (date-diff-seconds 
                        (now) cached-time 
                     ) 
                  ) 
               ) 
               "seconds." 
            ) 
            (SysCache'get where) 
         ) 
      ) 
   ) 
)

You should see something like...

; #<PCodeFunction:read-cached> 

> 


To run the function, execute the following code multiple times.

(read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa"))


You should see something like...

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:14,161 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Recaching (#<Metaclass
 Person> (= lastName nexjsa)) 
; #<[Instance<UserPerson, OID:1:V32:00000000000010008000BEEF0000000D, CLEAN>(readable=#t,
 locking=281740355, classCode="USR", firstName="Sam", lastName="nexjsa",
 telcoms=Collection(0), user=Instance<InternalUser, OID:1:V32:
00000000000010008000BEEF0000000C, CLEAN>, syncList=(), hasSyncList=#f)]> 

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:14,864 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache
 (#<Metaclass Person> (= lastName nexjsa)) - recaching in 14 seconds. ; #<[Instance
<UserPerson, OID:1:V32:00000000000010008000BEEF0000000D, CLEAN>(readable=#t,
 locking=281740355, classCode="USR", fi rstName="Sam", lastName="nexjsa",
 telcoms=Collection(0), user=Instance<InternalUser, OID:1:V32:
00000000000010008000BEEF0000000C, CLEAN>, syncList=(), hasSyncList=#f)]> 

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:23,034 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache
 (#<Metaclass Person> (= lastName nexjsa)) - recaching in 6 seconds. ; #<[Instance
<UserPerson, OID:1:V32:00000000000010008000BEEF0000000D, CLEAN>(readable=#t,
 locking=281740355, classCode="USR", fi rstName="Sam", lastName="nexjsa", telcoms
=Collection(0), user=Instance<InternalUser, OID:1:V32:
00000000000010008000BEEF0000000C, CLEAN>, syncList=(), hasSyncList=#f)]> 

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:27,939 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from cache
 (#<Metaclass Person> (= lastName nexjsa)) - recaching in 1 seconds. ; #<[Instance
<UserPerson, OID:1:V32:00000000000010008000BEEF0000000D, CLEAN>(readable=#t,
 locking=281740355, classCode="USR", fi rstName="Sam", lastName="nexjsa",
 telcoms=Collection(0), user=Instance<InternalUser, OID:1:V32:
00000000000010008000BEEF0000000C, CLEAN>, syncList=(), hasSyncList=#f)]> 

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:29,580 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Recaching
 (#<Metaclass Person> (= lastName nexjsa)) ; #<[Instance<UserPerson, OID:1:
V32:00000000000010008000BEEF0000000D, CLEAN>(readable=#t, locking=281740355,
 classCode="USR", fi rstName="Sam", lastName="nexjsa", telcoms=Collection(0),
 user=Instance<InternalUser, OID:1:V32:00000000000010008000BEEF0000000C,
 CLEAN>, syncList=(), hasSyncList=#f)]> 

> (read-cached Person '(firstName lastName (telcoms)) '(= lastName "nexjsa")) 
; 23:49:30,533 INFO [GlobalEnvironment] (NexJ-ContainedProcess) Reading from
 cache (#<Metaclass Person> (= lastName nexjsa)) - recaching in 14 seconds. 
; #<[Instance<UserPerson, OID:1:V32:00000000000010008000BEEF0000000D, CLEAN>
(readable=#t, locking=281740355, classCode="USR", fi rstName="Sam",
 lastName="nexjsa", telcoms=Collection(0), user=Instance<InternalUser,
 OID:1:V32:00000000000010008000BEEF0000000C, CLEAN>, syncList=(), hasSyncList=#f)]> 

> 

Notice we read from the cache for fifteen seconds then re-cache the result.