Chapter 4. Architecture

PojoCache uses extensively the JBoss Aop framework to 1) build the interceptor-based architecture to provide behaviors such as locking and POJO transaction commit and rollback, 2) intercept POJO field access (via dynamic interceptor feature). Following explains the concepts and top-level design of PojoCache.

Following figure is an overview of the PojoCache architecture. From the top, we can see that when a call comes in (e.g., attach or detach), it will go through the PojoCache interceptor stack first (configured from pojocache-aop.xml but is mostly read only). After that, it will store the POJO fields into Cache store and then JGroups (for state replication) as configured from a designated cache-service.xml. This architecture provides a decoupled framework that by itself is modularized and pluggable (available in the future).

Please note that you will need to specify a system property jboss.aop.path to set it to the path where pojocache-aop.xml resides or put it in your classpath.

PojoCache architecture overview

Figure 4.1. PojoCache architecture overview

4.1. PojoCache interceptor stack

As mentioned, we use JBoss Aop framework to provide a configurable method-based interceptor stack. The JBoss Aop framework provides a rich set of pointcut language for user to create flexible interceptor stack per method call. For example, in our current implementation, we have defined numerous interceptor element first and then assemble it within the stack element for modularity.

In PojoCache, we have provided a customized behaviors for each method call. E.g., attach, detach, and find would each have their own specific stack for the specific behaviors. Should you need to customize the PojoCache Api behavior, you can implement your own JBoss Aop interceptor and insert it into the respective stack. Following are the stack for each of the API, respectively:

   <!-- 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 mostly self-explanatory. For example, for 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).

Note again that this customized interceptor stack is provided in pojocache-aop.xml where user can also add customized interceptor if needs to.

4.2. Dynamic AOP interception

JBoss Aop provides an API (appendInterceptor) to add an interceptor at runtime. PojoCache uses this feature extensively to provide user transparency. Every "aspectized" POJO class will have an associated org.jboss.aop.InstanceAdvisor instance. During an attach(String id, Object pojo) operation (API explained below), PojoCache will examine to see if there is already an org.jboss.cache.aop.CacheInterceptor attached. (Note that a CacheInterceptor is the entrance of PojoCache to dynamically manage cache contents.) If it has not, one will be added to InstanceAdvisor object. Afterward, any POJO field modification will invoke the corresponding CacheInterceptor instance. Below is a schematic illustration of this process.

JBossAop has the capability to intercept both method level call and field level read write. From the perspective of PojoCache, field level interception is the appropriate mechanism to synchronize with the back-end cache store. Please note that,

  • the field level interception applies to all access qualifiers. That is, regardless whether it is public, protected, or private
  • we skip interception for field with final, and transient qualifiers. As a result, any field with these 3 qualifiers will not be replicated or persisted. Note that since Release 2.0, "static" field modifier will be replicated by default. If user doesn't want to replicate the static field, she can annotate the field with "@Transient" annotation to skip the replication.

The figures shown below illustrate operations to perform field read and write. Once a POJO is managed by PojoCache (i.e., after an attach method has been called), Aop will invoke CacheInterceptor automatically every time there is a field read or write. However, you should see the difference between these figures. While field write operation will go to cache first and, then, invoke the in-memory update, the field read invocation does not involve in-memory reference at all. This is because the value in cache and memory should have been synchronized during write operation. As a result, the field value from the cache is returned.

Dynamic AOP interception for field write

Figure 4.2. Dynamic AOP interception for field write

Dynamic AOP Interception for field read

Figure 4.3. Dynamic AOP Interception for field read

4.3. Logical object mapping by reachability

A complex POJO by definition is an object that may consist of composite object references. Once a complex object is declared "prepared" (e.g., a Person object), during the attach(String id, Object pojo) operation, PojoCache will add a CacheInterceptor instance to the InstanceAdvisor associated with that object, as we have discussed above. In addition, the cache will map recursively the primitive object fields into the corresponding cache nodes.

The mapping rule is as follows:

  • Create a tree node using id (internally translated into Cache fqn, if not yet existed).

  • Go through all the fields (say, with an association java.lang.reflect.Field type field) in POJO,

    • If it is a primitive type, the field value will be stored under id with (key, value) pair of (field.getName(), field.getValue()). The following are primitive types supported now: String, Boolean, Double, Float, Integer, Long, Short, Character.

    • If it is a non-primitive type, creates a child id and then recursively executes another pubObject until it reaches all primitive types.

Following is a code snippet that illustrates this mapping process

for (Iterator i = type.getFields().iterator(); i.hasNext();) {
   Field field = (Field) i.next();
   Object value = field.get(obj);
   CachedType fieldType = getCachedType(field.getType());
   if (fieldType.isImmediate()) {
      immediates.put(field.getName(), value);
   } else {
      _attach(new Fqn(fqn, field.getName()), value);
   }
   ...
}

Let's take an example POJO class definition from the Appendix section where we have a Person object that has composite non-primitive types (e.g., List and Address).

Person joe = new Person();
joe.setAddress(new Address());

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

The PojoCache APIs will be explained in fuller details later. But notice the illustration of object mapping by reachability. The String Id pojo/joe is associated with the POJO joe. Then under that fqn, there are three children nodes: addr, skills, and languages. If you look at the Person class declaration, you will find that addr is an Address class, skills is a Set , and languages is a List type. Since they are non-primitive, they are recursively inserted under the parent object (joe) until all primitive types are reached. In this way, we have broken down the object graph into a tree view which fit into our internal structure. Also note that all the primitive types will be stored inside the respective node's HashMap (e.g., addr will have Zip , Street , etc. stored there).

Here is a code snippet to demonstrate the object mapping by reachability feature that we just explained. Notice how a Person object (e.g., joe) that has complex object references will be mapped into the underlying cache store as explained above.

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";
boolean toStart = false; // Deault is true.
PojoCache cache = PojoCacheFactory.createCache(configFile, toStart);

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

Address addr = new Address(); // instantiate an Address object named addr
addr.setCity("Sunnyvale");
addr.setStreet("123 Albert Ave");
addr.setZip(94086);
joe.setAddress(addr); // set the address reference

cache.start(); // kick start cache cache
cache.attach("pojo/joe", joe); // add aop sanctioned object (and sub-objects) into cache.
// since it is aspectized, use of plain get/set methods will take care of cache contents automatically.
joe.setAge(41);

Note that a typical PojoCache usage involves instantiating the PojoCache from the PojoCacheFactory by passing in the underlying Cache configuration. Then, a user creates the aspectized POJO that will be put into PojoCache using attach() API.

In addition, PojoCache also supports get/set with parameter type of some Collection classes (i.e., List , Map , and Set ) automatically. For example, the following code snippet in addition to the above example will trigger PojoCache to manage the states for the Languages list as well. Details of Collection class support will be given later.

ArrayList lang = new ArrayList();
lang.add("Ensligh");
lang.add("Mandarin");
joe.setLanguages(lang);

4.4. Object relationship management

Like we have mentioned, traditional cache system does not support object relationship management during serialization (be it to the persistent data store or replicated to the other in-memory nodes.) Examples of object relationship are like an address object is shared by members of the household, and a child-parent relationship. All these relationship will be lost once the objects are replicated or persisted. As a result, explicit mapping will be needed outside of the cache system to express the object relationship. PojoCache, in contrast, can manage object relationship transparently for users.

During the mapping process, we will check whether any of its associated object is multiple or circular referenced. A reference counting mechanism has been implemented associating with the CacheInterceptor. If a new object created in the cache referenced to another POJO, a referenced fqn will be stored there to redirect any query and update to the original node.

To look at one example, let's say that multiple Persons ("joe" and "mary") objects can own the same Address (e.g., a household). Graphically, here is what it will look like in the tree nodes. Like we have covered in the previous section on the mapping by reachability, the POJO will map recursively into the cache. However, when we detect a multiple reference (in this case, the Address), we will keep track of the reference counting for the sub-object addr.

Schematic illustration of object relationship mapping

Figure 4.4. 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.

Notice that after we remove joe instance 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 that we have two separate cache instances in the cluster now (cache1 and cache2). Let's say, on the first cache instance, we put both joe and mary under cache management as above. Then, we failover to cache2. Here is the code snippet:

/**
 * 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 cache = PojoCacheFactory.createCache(configFile); // This will start PojoCache automatically

Person joe = cache.find("pojo/joe"); // retrieve the POJO reference.
Person mary = cache.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!

4.5. Object inheritance hierarchy

PojoCache preserves the POJO object inheritance hierarchy automatically. For example, if a Student extends Person with an additional field year (see POJO definition in the Appendix section), then once Student is put into the cache, all the class attributes of Person can be managed 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

4.6. 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 internal core Cache for transactional state replication.

Starting in release 2.0, we have adopted a so-called "flat space" mapping approach. That is, whenever a POJO attachment is called with a specified String id, we would create an instance of PojoReference first and stored under the String id region. PojoRefernce in turns contains a reference to an internal location where the real POJO mapping is held. If this POJO is of complex type, e.g., it has sub-types, this "flat space" mapping process will be executed recursively.

For example, let's take the mapping of the Person POJO under id "pojo/joe" and "pojo/mary" illustrated in the previous sections. Keep in mind that a "Person" object would have sub-objects (such as Address) that we need to recursively map. This example 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, we have created couple snapshots to highlight the mapping process.

The following figure illustrates the first step of the mapping approach. From the bottom of the figure, we can see that the PojoReference field under pojo/joe is pointing to an internal location, /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2. That is, under user-specified Id string, we store only an indirect reference to the internal area. Please note that Mary has a similar reference. (Note that the _lock_ is used to control POJO level transaction locking and is considered internal.)

Object cache mapping for Joe

Figure 4.5. Object cache mapping for Joe

Object cache mapping for Mary

Figure 4.6. Object cache mapping for Mary

Then by clicking on the referenced internal node (from the following figure), we can see 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 4.7. Object cache mapping for internal node Joe

Object cache mapping for internal node Mary

Figure 4.8. Object cache mapping for internal node Mary

Under the /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49n5o-2, we can see that there is an Address node, since Address is by itself a POJO. 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, when your click Address under /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49na0-4, it 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 4.9. Object cache mapping: Joe's internal address

Object cache mapping: Mary's internal address

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

Finally, clicking on the /__JBossInternal__/5c4o12-lpaf5g-esl49n5e-1-esl49ngs-3 node again, we can see the primitive fields are stored there, e.g., Street, Zip, and City. This is illustrated in the following figure.

Object cache mapping: Address fields

Figure 4.11. Object cache mapping: Address fields

The above example give you a flavor of the "flat space" mapping approach. The biggest advantage for this approach is when a sub-POJO is multiple referenced. In this case, the reference is so-called canonical in that all references have a logical PojoReference that points to the real one. Of course, the downside is if we don't have any objection relationship then the "flat space" mapping will incur the overhead of additional mapping into the PojoCache internal region.

4.7. Collection class proxy

The POJO classes that inherit from Set , List , and Map are treated as "aspectized" automatically. That is, users need not declare them "prepared" in the xml configuration file or via annotation. Since we are not allowed to instrument the Java system library, we will use a proxy approach instead. That is, when we encounter any Collection instance, we 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 a sub-object, e.g., inside another POJO, we will swap out the original reference with the new proxy one to promote transparent usage.

To obtain the proxy reference, users can then use another find to retrieve this proxy reference and use this reference to perform POJO operations.

Here is a code snippet that illustrates the usage of a 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

Here is another snippet to illustrate the dynamic swapping of the Collection reference when it is embedded inside another object:

Person joe = new Person();
joe.setName("Joe Black"); // This is base class attributes
ArrayList 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);
ArrayList lang = joe.getLanguages(); // Note that lang is a proxy reference
lang.add("French"); // This will be intercepted by the cache

As you can see, getLanguages simply returns the field reference that has been swapped out for the proxy reference counterpart.

Finally, when you remove a Collection reference from the cache (e.g., via detach), you still can use the proxy reference since we will update the in-memory copy of that reference during detachment. Below is a code snippet illustrating this:

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.

4.7.1. Limitation

Use of Collection class in PojoCache helps you to track fine-grained changes in your collection fields automatically. However, current implementation has the follow limitation that we plan to address soon.

Currently, we only support a limited implementation of Collection classes. That is, we support APIs in List, Set, and Map. However, since the APIs do not stipulate of constraints like NULL key or value, it makes mapping of user instance to our proxy tricky. For example, ArrayList would allow NULL value and some other implementation would not. The Set interface maps to java.util.HashSet implementation. The List interface maps to java.util.ArrayList implementation. The Map interface maps to java.util.HashMap implementation.

Another related issue is the expected performance. For example, the current implementation is ordered, so that makes insert/delete from the Collection slow. Performance between Set, Map and List collections also vary. Adding items to a Set is slower than a List or Map, since Set does not allow duplicate entries.

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.

4.8. POJO requirement

The POJO requirement for PojoCache are:

  • It does not require to implement Serializable, but it does require there is a no-arg constructor declared (can be private). This is needed such that during failover, it has a way to reconstruct the POJO.
  • There are specific default behaviors for different modifiers as mentioned ( transient and final). However, the user can override the transient field to make it replicatable through the JBoss Aop:
       <annotation-introduction expr="field(* POJO->aTransientField)">
           @org.jboss.cache.pojo.annotation.Serializable
       </annotation-introduction>
    
    where you are annotating an original transient field aTransientField with a NonTransient that causes that transient field to be replicated.