by Ben Wang
11/2005
In-memory caching is a crucial feature in today's large-scale enterprise applications, where scalability and high
performance are required. An in-memory cache can store either application state information (e.g., a
HttpSession
in a web application) or database query results (i.e., entity data). Since many enterprise
applications run in clustered environments, the cache needs to be replicated across the cluster. Furthermore,
if more reliability is needed, the in-memory cache should also be persisted to the hard disk or database.
Most existing in-memory caching solutions fall into the category of what we call "plain" cache system, in which the
direct object references are stored and cached. Since a plain cache deals with the object reference directly, it acts
like an elaborate HashMap
and thus is very intuitive to use. When an object needs to be replicated or
persisted in a plain cache system, the object has to implement the Serializable
interface. However, a
plain cache also has some known limitations regarding to replication or persistency:
Person
instances that share the same
Address
object, upon replication, it will be split into two separate Address
instances
(instead of one).
Figure 1. Plain cache does not preserve object relationship during serialization
Addressing the above problems in plain cache system, there is another new category of cache system: POJO (plain old Java object) cache. A POJO cache is a system that acts as an "object-oriented" and distributed cache. In this system, once a user attaches the POJO to the cache, the caching aspect (e.g., replication and persistence) should be transparent to the user. A user would simply operate on the POJO without worrying about updating the cache content or maintaining the object relationship. There is no explicit API called to manage the cache. In addition, it has three more characteristics:
Serializable
interface for the POJOs.A leading in-memory POJO cache solution is the open-source JBoss Cache . JBoss Cache is the first Java library that supports replicated, persistent, transactional, and fine-grained caching. It can be used both as a POJO cache and a plain cache. Since the JBoss Cache is 100% Java-based, it runs in any Java SE environment, including inside an application server or as a standalone process. JBoss Cache has been used inside the JBoss Application Server for the EJB 3.0 stateful session bean clustering and http session replication, for example.
In this article, we demonstrate how JBoss Cache can be used as a POJO cache (through its JBossCacheAop
component). A use case will also be given to illustrate key features in a distributed setting.
The default plain cache module in JBoss Cache is called TreeCache
. You can configure it either
programmatically or through an external xml file. Here are the features that you can configure:
LRUEvictionPolicy
.The POJO cache module in JBoss Cache is called PojoCache
. In order to use POJO cache, you have to
"prepare" the objects (this process is also known as object instrumentation) before they are cached.
This is needed so the system can
intercept the POJO operations. The object instrumentation process is
performed by the JBoss AOP library. JBoss AOP allows you to specify the
to-be-instrumented classes via an XML file or annotations. Currently, we support only the JDK 1.4 style annotation
(a specific feature of JBoss AOP). Note that JDK 5.0 annotation support is coming in the next release
and it will make the instrumentation process nearly
transparent!
PojoCache
is a sub-class of TreeCache
, so it uses the same XML file for configuration
and provides the same caching functionality as its super class counterpart. The JBoss POJO Cache also has a POJO-based
eviction policy.
In the rest of the article, we will use a "sensor network supervising system" example to illustrate the capability of JBoss POJO Cache in providing instantaneous fine-grained state replication and automatic preservation of state object relationship.
Along a high-speed railway, in the different stations there are thousands of sensors that need to be monitored and supervised. Examples of such instruments include temperature, wind and rain sensors that are critical to the successful operation of a high-speed train. If a particular sensor is malfunctioning, the manager computer in the supervising system should alert the administrator, shutdown the unit, and/or schedule it for maintenance.
Since the operation is mission critical, the supervising system has to be highly available, meaning that whenever one sensor manager computer in the system goes down, the administrator should be able to seamlessly switch over to another one to perform supervision and active management. Hence, all manager computers must replicate the same sensor network state information at real time. Note that the characteristics of this kind of system, i.e the requirement of high-availability and the existence of thousands (or even larger numbers) of elements, are also commonplace elsewhere in modern-day network management. Figure 1 illustrates the overview of such system that includes a clustering capability.
Figure 1. Overview for the sensor supervising system
Because of the hierarchical nature of the sensor network, a complicated domain object graph will typically be required to model the sensor network on the manager side. When the domain object states are not replicated (or persisted), management of object relationships (e.g., adding nodes and traversing the graph nodes) is provided by the JVM itself, and thus is transparent to the end user. However, because the Java serialization process does not recognize the object relationship, this seemingly simplistic object graph relationship will break down when the states are either replicated or persisted. As a result, it renders a simple failover of the manager side components difficult to achieve.
Traditionally, to provide complete failover (or persistence) capability, the system has to be designed to manage the object relationship explicitly, a la modern day object-relation mapping (ORM) solution approach. And in a traditional entity persistence layer-style design you have to do the following:
By using the JBoss POJO Cache to handle your object states, however, none of the problems mentioned above exists! To elaborate, the benefits of using it are as follows:
As we described above, the replication and/or persistence aspects in the POJO cache are totally transparent to the user. Note that for total clustering, there is another aspect of load-balancing or locating the primary and/or secondary managers to which both the client GUI (for administrators) and the sensors should connect. We do not cover such issues here and will focus only on the replication of the sensor network state across the managers.
Figure 2 is the topology for our sensor supervising system example. Basically, in this simplified example, we have two stations
(Tokyo
and Yokohama
), and within each station we will have one wind and one rain sensor,
respectively. For each sensor, there are numerous components that need to be supervised, e.g. the
power supply and the sensor unit itself. Our goal is to supervise the sensors efficiently by 1) monitoring the
individual item status (e.g., a StateItem
) as well as their overall status (e.g., Wind
Summary
), and 2) having the ability to bring up and down individual sensors at runtime without restarting the
clustered manager nodes.
Figure 2. Topology for the sensor supervising system
Figure 3 illustrates this domain model using a class diagram. Essentially, we have a
PropagationManager
at the top level that handles the whole sensor network. It always has one root
Node
. Then the root Node
can have recursively numerous Node
s. Each Node
can have multiple StateItem
s. A StateItem
is the smallest management unit, for example,
representing the unit's power supply.
Figure 3. Class diagram for the sensor supervising system
For the current topology in Figure 2, Japan
represents the root node, while
Tokyo
and Yokohama
are the station nodes, respectively. The different sensors (e.g., rain or
wind) are represented by a Node
object as well. Each sensor then has two
StateItems
, namely Power Supply
and Sensor Unit
.
The relationship between Node
s is bi-directional, e.g., we can navigate from the root node all the way to the
leaf node, or we can navigate backwards through the parent node as well. In addition, there are one WindSensor
Summary
and one RainSensor Summary
StateItem
instances that reference the
respective sensors as well. The purpose of the summary items is to monitor the overall health of the, say, wind and
rain sensor units as a group. As a result, the objects in the object graph for the sensors are multiple referenced
(as shown in Figure 2 and 3).
Before we can use the POJO cache functionality, we will need to use JBoss AOP tools to instrument the POJOs (namely,
PropagationManager
, Node
, and StateItem
classes). We can do it via either
XML declaration or annotation. Here we illustrate the POJO instrumentation via the JBoss AOP JDK 1.4 annotation (JDK 5.0
annotation will be supported in the next JBoss Cache
release.) Below are the code snippets that declare the annotation on the three main interfaces.
/** * @@org.jboss.cache.aop.InstanceOfAopMarker */ public interface PropagationManager { public void setRootNode(String rdn); public void addNode(String parentFdn, String rdn); public void addStateItem(String parentFdn, long itemId, String name, long defaultState); public Node findNode(String fdn); public void stateChange(String fdn, long itemId, long newState); ... }
/** * * @@org.jboss.cache.aop.InstanceOfAopMarker */ public interface Node { public void addChildNode(Node child); public List getChildren(); public void setParentNode(Node parent); public Node getParentNode(); public void addStateItem(StateItem stateItem); public void setSummaryStateItem(StateItem stateItem); public StateItem getSummaryStateItem(); public List getStateItems(); public StateItem findStateItem(long itemId); public void setPropagationRule(PropagationRule rule); ... }
/** * @@org.jboss.cache.aop.InstanceOfAopMarker */ public interface StateItem { public long getItemId(); public boolean setState(long state); public long getState(); public void setName(String name); public String getName(); ... }
Note the annotation inside the JavaDoc -- @@org.jboss.cache.aop.InstanceOfAopMarker
is a JBoss POJO Cache
annotation that essentially declares all instances of this interface will be instrumented (so there is no need to
annotate individual classes). If you want to annotate a specific class (without propagating to its sub-class), you
can also use the @@org.jboss.cache.aop.AopMarker
annotation.
After the interfaces are annotated, we then use a JDK 1.4 style JBoss Aop annotation precompiler, annoc
, and an
aop precompiler, aopc
, to perform compile time instrumentation. Once these steps are done, the
instrumentation process is complete. And we are ready to run the example.
Below is a code snippet that instantiates a PropagationManager
instance and sets the appropriate
relationship between the station and the sensor nodes. Finally, we use the PojoCache
API
putObject()
to put the POJO (in this case, the PropagationManager
instance) under cache
management. After that, any POJO operation will be fine-grain replicated, e.g., a setState()
operation
will only replicate the corresponding state field (of which is an integer).
protected void setUp() throws Exception {
cache1_ = createCache("TestCluster");
cache2_ = createCache("TestCluster");
initPm();
}
protected void tearDown() throws Exception {
cache1_.remove("/");
cache1_.stop();
cache2_.stop();
}
private PojoCache createCache(String name) throws
Exception {
// configure the cache through injection
PropertyConfigurator config = new
PropertyConfigurator();
// read in the replSync xml.
// Here we use synchronous mode replication.
config.configure(tree, "META-INF/replSync-service.xml");
// We can set a different cluster group.
tree.setClusterName(name);
tree.start(); // kick start the cache
return tree;
}
/**
* Populate the propagation tree.
*/
protected void initPm() throws Exception {
pm_ = new PropagationManagerImpl();
pm_.setRootNode("Japan");
pm_.addNode("Japan", "Tokyo"); // Tokyo station
// Wind sensor device
pm_.addNode("Japan.Tokyo", "WindSensor1");
pm_.addStateItem("Japan.Tokyo.WindSensor1", 1000,
"power supply", 1040); // power supply
pm_.addStateItem("Japan.Tokyo.WindSensor1", 1001,
"sensor unit", 1040); // sensor unit
// rain sensor device
pm_.addNode("Japan.Tokyo", "RainSensor1");
pm_.addStateItem("Japan.Tokyo.RainSensor1", 1002,
"power supply", 1040); // power supply
pm_.addStateItem("Japan.Tokyo.RainSensor1", 1003,
"sensor unit", 1040); // sensor unit
pm_.addNode("Japan", "Yokohama"); // Yokohama station
// wind sensor device
pm_.addNode("Japan.Yokohama", "WindSensor2");
pm_.addStateItem("Japan.Yokohama.WindSensor2", 1000,
"power supply", 1040); // power supply
pm_.addStateItem("Japan.Yokohama.WindSensor2", 1001,
"sensor unit", 1040); // sensor unit
// rain sensor device
pm_.addNode("Japan.Yokohama", "RainSensor2");
pm_.addStateItem("Japan.Yokohama.RainSensor2", 1002,
"power supply", 1040); // power supply
pm_.addStateItem("Japan.Yokohama.RainSensor2", 1003,
"sensor unit", 1040); // sensor unit
// summary node for wind sensors in this network
pm_.createNode("WindSummary", "WindSummary");
pm_.setUpperNode("WindSummary",
"Japan.Tokyo.WindSensor1"); // assoication
pm_.setUpperNode("WindSummary",
"Japan.Yokohama.WindSensor2"); // association
// summary node for rain sensor in this network
pm_.createNode("RainSummary", "RainSummary");
pm_.setUpperNode("RainSummary",
"Japan.Tokyo.RainSensor1"); // association
pm_.setUpperNode("RainSummary",
"Japan.Yokohama.RainSensor2"); // association
}
/**
* Main starting point. Called by main.
*/
public void testPropagation() throws Exception {
// Here we ask the pojo cache to manage pm
cache1_.putObject("/monitor", pm_);
// Output
printStatus("Initial state", pm_);
// Retrieve the pojo from the Manager #2
PropagationManager pm2 = (PropagationManager)
cache2_.getObject("monitor");
System.out.println(
"---------------------------------------------");
System.out.println("Modified on Manager #1");
// A state has been changed in one of the item.
// This will be fine-grained replicated.
pm_.stateChange("Japan.Tokyo.RainSensor1", 1003, 1041);
printStatus("Japan.Tokyo.RainSensor1: id: 1003 state:
1040->1041 (retrieved from Manager #2!)", pm2);
System.out.println(
"---------------------------------------------");
System.out.println("Modified on Manager #2");
// A state has been changed in one of the item.
// This will be fine-grained replicated.
pm2.stateChange("Japan.Yokohama.WindSensor2",
1001, 1041); // Modified state on cache #2
printStatus("Japan.Yokohama.WindSensor2: id: 1001 state:
1040->1041 (retrieved from Manager #1!)", pm1);
System.out.println(
"---------------------------------------------");
System.out.println(
"Add a new VibrationSensor on Tokyo station");
// Vibration sensor device
pm_.addNode("Japan.Tokyo", "VibrationSensor1");
pm_.addStateItem("Japan.Tokyo.VibrationSensor1", 1004,
"power supply", 1040); // power supply
pm_.addStateItem("Japan.Tokyo.VibrationSensor1", 1005,
"sensor unit", 1040); // sensor unit
printStatus("Japan.Tokyo.VibrationSensor1:
(retrieved from cache #2)", pm2);
}
public static void main(String[] args) throws Exception {
PropagationReplAopTest pmTest =
new PropagationReplAopTest();
pmTest.setUp();
pmTest.testPropagation();
pmTest.tearDown();
}
In the above example snippet, please note the following key points:
PropagationManager
instance is put into cache management via a putObject()
API call.
After that, only pure POJO operations are needed. For example, we can add additional nodes to
the PropagationManager
instance and it will replicate accordingly.PropagationManager
on cache #2 via a getObject
call.setState()
calls trigger only fine-grained replication to the other cache instance.Finally, when the example is run, the resulting output will printed as follows:
Initial state
---------------------------------------------
Japan (Summary : 2000 [ok])
+ Tokyo (Summary : 2000 [ok])
+ + WindSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1040)
+ Yokohama (Summary : 2000 [ok])
+ + WindSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1040)
---------------------------------------------
Modified on Manager #1
StateItem.setState(): id: 1003 state changed \
from 1040 to 1041
---------------------------------------------
Japan.Tokyo.RainSensor1: id: 1003 state: 1040->1041 \
(retrieved from Manager #2!)
---------------------------------------------
Japan (Summary : 2004)
+ Tokyo (Summary : 2004)
+ + WindSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor1 (Summary : 2004)
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1041)
+ Yokohama (Summary : 2000 [ok])
+ + WindSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1040)
---------------------------------------------
Modified on Manager #2
StateItem.setState(): id: 1001 state changed \
from 1040 to 1041
---------------------------------------------
Japan.Yokohama.WindSensor2: id: 1001 state: 1040->1041 \
(retrieved from Manager #1!)
---------------------------------------------
Japan (Summary : 2004)
+ Tokyo (Summary : 2004)
+ + WindSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor1 (Summary : 2004)
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1041)
+ Yokohama (Summary : 2004)
+ + WindSensor2 (Summary : 2004)
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1041)
+ + RainSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1040)
---------------------------------------------
Add a new VibrationSensor on Tokyo station
StateItem.setState(): id: 1004 state changed from \
2000 to 1040
StateItem.setState(): id: 1005 state changed from \
2000 to 1040
---------------------------------------------
Japan.Tokyo.VibrationSensor1: (retrieved from cache #2)
---------------------------------------------
Japan (Summary : 2004)
+ Tokyo (Summary : 2004)
+ + WindSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1040)
+ + RainSensor1 (Summary : 2004)
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1041)
+ + VibrationSensor1 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1004, state =1040)
+ + | ( name = sensor unit, id = 1005, state =1040)
+ Yokohama (Summary : 2004)
+ + WindSensor2 (Summary : 2004)
+ + | ( name = power supply, id = 1000, state =1040)
+ + | ( name = sensor unit, id = 1001, state =1041)
+ + RainSensor2 (Summary : 2000 [ok])
+ + | ( name = power supply, id = 1002, state =1040)
+ + | ( name = sensor unit, id = 1003, state =1040)
Please note the bold text lines. Basically, we do a POJO operation setState
first on Manager #1
and then print out the propagation tree from the second manager to verify that the state has been updated, and vice
versa. It is worthwhile to repeat that, although not shown in the output for the replication layer traffic, each
setState()
operation will only trigger a fine-grained field level replication. In addition, if the
call is under a transaction context, then the update will be batched, e.g., replicated only when it is ready to commit.
Finally, notice that we have the capability to add a new sensor on the fly into the network. A traditional system would
have required some sort of restart mechanism.
If you are interested to run this example yourself, you can download JBoss Cache release 1.2.4.
Check under the
directory examples/aop/sensor
that contains full source and instruction to run.
In this article we have demonstrated the capability of JBoss Cache acting as a POJO cache by leveraging the
PojoCache
component. By using the POJO cache functionality, it provides seamless failover capability for
POJOs by performing fine-grained replication while perserving object graph relationship.
The author would like to acknowledge Mr. Yusuke Komori of SMG Co. from Japan for kindly contributing the use case in this article.