Chapter 3. Architecture

POJO Cache internally uses the JBoss Aop framework to both intercept object field access, and to provide an internal interceptor stack for centralizing common behavior (e.g. locking, transactions).

The following figure is a simple overview of the POJO Cache architecture. From the top, it can be can seen that when a call comes in (e.g., attach or detach), it will go through the POJO Cache interceptor stack first. After that, it will store the object's fields into the underlying Core Cache, which will be replicated (if enabled) using JGroups.

POJO Cache architecture overview

Figure 3.1. POJO Cache architecture overview

3.1. POJO Cache interceptor stack

As mentioned, the JBoss Aop framework is used to provide a configurable interceptor stack. In the current implementation, the main POJO Cache methods have their own independant stack. These are specified in META-INF/pojocache-aop.xml In most cases, this file should be left alone, although advanced users may wish to add their own interceptors. The Following is the default configuration:

   <!-- Check id range validity -->
   <interceptor name="CheckId" class="org.jboss.cache.pojo.interceptors.CheckIdInterceptor"
         scope="PER_INSTANCE"/>

   <!-- Track Tx undo operation -->
   <interceptor name="Undo" class="org.jboss.cache.pojo.interceptors.PojoTxUndoInterceptor"
         scope="PER_INSTANCE"/>

   <!-- Begining of interceptor chain -->
   <interceptor name="Start" class="org.jboss.cache.pojo.interceptors.PojoBeginInterceptor"
         scope="PER_INSTANCE"/>

   <!-- Check if we need a local tx for batch processing -->
   <interceptor name="Tx" class="org.jboss.cache.pojo.interceptors.PojoTxInterceptor"
         scope="PER_INSTANCE"/>

   <!--
      Mockup failed tx for testing. You will need to set PojoFailedTxMockupInterceptor.setRollback(true)
      to activate it.
   -->
   <interceptor name="MockupTx" class="org.jboss.cache.pojo.interceptors.PojoFailedTxMockupInterceptor"
         scope="PER_INSTANCE"/>

   <!-- Perform parent level node locking -->
   <interceptor name="TxLock" class="org.jboss.cache.pojo.interceptors.PojoTxLockInterceptor"
         scope="PER_INSTANCE"/>

   <!-- Interceptor to perform Pojo level rollback -->
   <interceptor name="TxUndo" class="org.jboss.cache.pojo.interceptors.PojoTxUndoSynchronizationInterceptor"
                scope="PER_INSTANCE"/>

   <!-- Interceptor to used to check recursive field interception. -->
   <interceptor name="Reentrant" class="org.jboss.cache.pojo.interceptors.MethodReentrancyStopperInterceptor"
                scope="PER_INSTANCE"/>

   <!-- Whether to allow non-serializable pojo. Default is false. -->
   <interceptor name="MarshallNonSerializable" class="org.jboss.cache.pojo.interceptors.CheckNonSerializableInterceptor"
                scope="PER_INSTANCE">
         <attribute name="marshallNonSerializable">false</attribute>
                </interceptor>

   <stack name="Attach">
      <interceptor-ref name="Start"/>
      <interceptor-ref name="CheckId"/>
      <interceptor-ref name="Tx"/>
      <interceptor-ref name="TxLock"/>
      <interceptor-ref name="TxUndo"/>
   </stack>

   <stack name="Detach">
      <interceptor-ref name="Start"/>
      <interceptor-ref name="CheckId"/>
      <interceptor-ref name="Tx"/>
      <interceptor-ref name="TxLock"/>
      <interceptor-ref name="TxUndo"/>
   </stack>

   <stack name="Find">
      <interceptor-ref name="Start"/>
      <interceptor-ref name="CheckId"/>
   </stack>

The stack should be self-explanatory. For example, for the Attach stack, we currently have Start, CheckId, Tx, TxLock, and TxUndo interceptors. The stack always starts with a Start interceptor such that initialization can be done properly. CheckId is to ensure the validity of the Id (e.g., it didn't use any internal Id string). Finally, Tx, TxLock, and TxUndo are handling the the proper transaction locking and rollback behavior (if needed).

3.2. Field interception

POJO Cache currently uses JBoss AOP to intercept field operations. If a class has been properly instrumented (by either using the @Replicable annotation, or if the object has already been advised by JBoss AOP), then a cache interceptor is added during an attach() call. Afterward, any field modification will invoke the corresponding CacheFieldInterceptor instance. Below is a schematic illustration of this process.

Only fields, and not methods are intercepted, since this is the most efficient and accurate way to gaurantee the same data is visible on all nodes in the cluster. Further, this allows for objects that do not conform to the JavaBean specficiation to be replicable. There are two important aspects of field interception:

  • All access qualifiers are intercepted. In other words, all private, all protected, all default, and all public fields will be intercepted.
  • Any field with final, static, and/or transient qualifiers, will be skipped. Therefore, they will not be replicated, passivated, or manipulated in any way by POJO Cache.

The figure below illustrates both field read and write operations. Once an POJO is managed by POJO Cache (i.e., after an attach() method has been called), JBoss Aop will invoke the CacheFieldInterceptor every time a class operates on a field. The cache is always consulted, since it is in control of the mapped data (i.e. it gaurantess the state changes made by other nodes in the cluster are visible). Afterwords, the in-memmory copy is updated. This is mainly to allow transaction rollbacks to restore the previous state of the object.

POJO Cache field interception

Figure 3.2. POJO Cache field interception

3.3. Object relationship management

As previously mentioned, unlike a traditional cache system, POJO Cache preserves object identity. This allows for any type of object relationship available in the Java language to be transparently handled.

During the mapping process, all object references are checked to see if they are already stored in the cache. If already stored, instead of duplicating the data, a reference to the original object is written in the cache. All referenced objects are reference counted, so they will be removed once they are no longer referenced.

To look at one example, let's say that multiple Persons ("joe" and "mary") objects can own the same Address (e.g., a household). The following diagram is a graphical representation of the pysical cache data. As can be seen, the "San Jose" address is only stored once.

Schematic illustration of object relationship mapping

Figure 3.3. Schematic illustration of object relationship mapping

In the following code snippet, we show programmatically the object sharing example.

import org.jboss.cache.pojo.PojoCache;
import org.jboss.cache.pojo.PojoCacheFactory;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

String configFile = "META-INF/replSync-service.xml";
PojoCache cache = PojoCacheFactory.createCache(configFile); // This will start PojoCache automatically

Person joe = new Person(); // instantiate a Person object named joe
joe.setName("Joe Black");
joe.setAge(41);

Person mary = new Person(); // instantiate a Person object named mary
mary.setName("Mary White");
mary.setAge(30);

Address addr = new Address(); // instantiate a Address object named addr
addr.setCity("Sunnyvale");
addr.setStreet("123 Albert Ave");
addr.setZip(94086);

joe.setAddress(addr); // set the address reference
mary.setAddress(addr); // set the address reference

cache.attach("pojo/joe", joe); // add aop sanctioned object (and sub-objects) into cache.
cache.attach("pojo/mary", mary); // add aop sanctioned object (and sub-objects) into cache.

Address joeAddr = joe.getAddress();
Address maryAddr = mary.getAddress(); // joeAddr and maryAddr should be the same instance

cache.detach("pojo/joe");
maryAddr = mary.getAddress(); // Should still have the address.

If joe is removed from the cache, mary should still have reference the same Address object in the cache store.

To further illustrate this relationship management, let's examine the Java code under a replicated environment. Imagine two separate cache instances in the cluster now (cache1 and cache2). On the first cache instance, both joe and mary are attached as above. Then, the application fails over to cache2. Here is the code snippet for cache2 (assume the objects were already attached):

/**
 * Code snippet on cache2 during fail-over
 */
import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.pojo.PojoCache;
import org.jboss.test.cache.test.standAloneAop.Person;
import org.jboss.test.cache.test.standAloneAop.Address;

String configFile = "META-INF/replSync-service.xml";
PojoCache cache2 = PojoCacheFactory.createCache(configFile); // This will start PojoCache automatically

Person joe = cache2.find("pojo/joe"); // retrieve the POJO reference.
Person mary = cache2.find("pojo/mary"); // retrieve the POJO reference.

Address joeAddr = joe.getAddress();
Address maryAddr = mary.getAddress(); // joeAddr and maryAddr should be the same instance!!!

maryAddr = mary.getAddress().setZip(95123);
int zip = joeAddr.getAddress().getZip(); // Should be 95123 as well instead of 94086!

3.4. Object Inheritance

POJO Cache preserves the inheritance hierarchy of all attached objects. For example, if a Student extends Person with an additional field year, then once Student is put into the cache, all the class attributes of Person are mapped to the cache as well.

Following is a code snippet that illustrates how the inheritance behavior of a POJO is maintained. Again, no special configuration is needed.

import org.jboss.test.cache.test.standAloneAop.Student;

Student joe = new Student(); // Student extends Person class
joe.setName("Joe Black"); // This is base class attributes
joe.setAge(22); // This is also base class attributes
joe.setYear("Senior"); // This is Student class attribute

cache.attach("pojo/student/joe", joe);

//...

joe = (Student)cache.attach("pojo/student/joe");
Person person = (Person)joe; // it will be correct here
joe.setYear("Junior"); // will be intercepted by the cache
joe.setName("Joe Black II"); // also intercepted by the cache

3.5. Physical object cache mapping model

The previous sections describe the logical object mapping model. In this section, we will explain the physical mapping model, that is, how do we map the POJO into Core Cache for transactional state replication. However, it should be noted that the physical structure of the cache is purely an internal implementation detail, it should not be treated as an API as it may change in future releases. This information is provided solely to aid in better understanding the mapping process in POJO Cache.

When an object is first attached in POJO Cache, the Core Cache node representation is created in a special internal area. The Id fqn that is passed to attach() is used to create an empty node that references the internal node. Future references to the same object will point to the same internal node location, and that node will remain until all such references have been removed (detached).

The example below demonstrates the mapping of the Person object under id "pojo/joe" and "pojo/mary" as metioned in previous sections. It is created from a two node replication group where one node is a Beanshell window and the other node is a Swing Gui window (shown here). For clarity, multiple snapshots were taken to highlight the mapping process.

The first figure illustrates the first step of the mapping approach. From the bottom of the figure, it can be seen that the PojoReference field under pojo/joe is pointing to an internal location, /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2. That is, under the user-specified Id string, we store only an indirect reference to the internal area. Please note that Mary has a similar reference.

Object cache mapping for Joe

Figure 3.4. Object cache mapping for Joe

Object cache mapping for Mary

Figure 3.5. Object cache mapping for Mary

Then by clicking on the referenced internal node (from the following figure), it can seen that the primitive fields for Joe are stored there. E.g., Age is 41 and Name is Joe Black. And similarly for Mary as well.

Object cache mapping for internal node Joe

Figure 3.6. Object cache mapping for internal node Joe

Object cache mapping for internal node Mary

Figure 3.7. Object cache mapping for internal node Mary

Under the /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2, it can be seen that there is an Address node. Clicking on the Address node shows that it references another internal location: /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49ngs-3 as shown in the following figure. Then by the same token, the Address node under /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49na0-4 points to the same address reference. That is, both Joe and Mary share the same Address reference.

Object cache mapping: Joe's internal address

Figure 3.8. Object cache mapping: Joe's internal address

Object cache mapping: Mary's internal address

Figure 3.9. Object cache mapping: Mary's internal address

Finally, the /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49ngs-3 node contains the various various primitive fields of Address, e.g., Street, Zip, and City. This is illustrated in the following figure.

Object cache mapping: Address fields

Figure 3.10. Object cache mapping: Address fields

3.6. Collection Mapping

Due to current Java limitations, Collection classes that implement Set, List, and Map are substituted with a Java proxy. That is, whenever POJO Cache encounters any Collection instance, it will:

  • Create a Collection proxy instance and place it in the cache (instead of the original reference). The mapping of the Collection elements will still be carried out recursively as expected.
  • If the Collection instance is referenced from another object, POJO Cache will swap out the original reference with the new proxy, so that operations performed by the refering object will be picked up by the cache.

The drawback to this approach is that the calling application must re-get any collection references that were attached. Otherwise, the cache will not be aware of future changes. If the collection is referenced from another object, then the calling app can obtain the proxy by using the publishing mechanism provided by the object (e.g. Person.getHobbies()). If, however, the collection is directly attached to the cache, then a subsequent find() call will need to be made to retrieve the proxy.

The following code snippet illustrates obtaining a direct Collection proxy reference:

List list = new ArrayList();
list.add("ONE");
list.add("TWO");

cache.attach("pojo/list", list);
list.add("THREE"); // This won't be intercepted by the cache!

List proxyList = cache.find("pojo/list"; // Note that list is a proxy reference
proxyList.add("FOUR"); // This will be intercepted by the cache

This snippet illustrates obtaining the proxy reference from a refering object:

Person joe = new Person();
joe.setName("Joe Black"); // This is base class attributes
List lang = new ArrayList();
lang.add("English");
lang.add("Mandarin");
joe.setLanguages(lang);
// This will map the languages List automatically and swap it out with the proxy reference.
cache.attach("pojo/student/joe", joe);
lang = joe.getLanguages(); // Note that lang is now a proxy reference
lang.add("French"); // This will be intercepted by the cache

Finally, when a Collection is removed from the cache (e.g., via detach), you still can use the proxy reference. POJO Cache will just redirect the call back to the in-memory copy. See below:

List list = new ArrayList();
list.add("ONE");
list.add("TWO");

cache.attach("pojo/list", list);
List proxyList = cache.find("pojo/list"); // Note that list is a proxy reference
proxyList.add("THREE"); // This will be intercepted by the cache

cache.detach("pojo/list"); // detach from the cache
proxyList.add("FOUR"); // proxyList has 4 elements still.

3.6.1. Limitations

The current implementation has the following limitations with collections:

  • Only List, Set and Map are supported. Also it should be noted that the Java Collection API does not fully describe the behavior of implementations, so the cache versions may differ slightly from the common Java implementations (e.g. handling of NULL)
  • As of PojoCache 2.0, HashMap keys must be serializable. Prior to PojoCache 2.0, HashMap keys were converted to String. This was fixed as you couldn't get the key back in its original form. See issue JBCACHE-399 for more details.