If you use Mark Mandel's awesome Transfer ORM
in a cluster, you've probably wondered what to do about keeping the cache in sync across servers in the cluster. I've had to solve this problem a couple of times now and I figured I should publish an example of how to do this.First off, what's the problem we're solving?
Transfer maintains a cache of objects so that it doesn't have to hit the database all the time. It can automatically manage a cache that contains as many objects as will fit in memory and discards them when it needs more memory, based on age and usage. Come to Mark Mandel's Transfer ORM Caching Mechanics
session at cf.Objective() to learn more!
The problem is that in a cluster of servers, each server caches objects and doesn't know when another server has updated an object. This causes each server's cache to get out of sync with the database. Not good.
So, what is the solution?
We need to arrange for the other servers in the cluster to be notified when an object is updated, so they can drop the object from cache (and fetch it again when needed).
Transfer has a method call, discardByClassAndKey()
, that drops the specified object from cache, if it is present. Transfer also has an event model that allows us to register listener objects that are invoked whenever an object is updated or deleted (on the local server). Finally, we use JMS - Java Message Service - to send cache update messages from one server to all the others in the cluster.
OMG! How scary! You're going to use event gateways and JMS and complicated stuff I don't understand!
Relax, ColdFusion makes this easy. Trust me.
Players in the solution
- ActiveMQ 4.1.0 - Apache's open source JMS server. Support for ActiveMQ is built into ColdFusion 8 as one of the "example" event gateways.
- CacheSynchronizer.cfc - A new CFC from my Google Project, available under the Apache Source License 2.0. It acts as both the listener object for the Transfer events and as the listener for JMS messages.
- Configuration files for the JMS event gateway. See the comments in the CacheSynchronizer CFC.
Each clustered server will run an instance of ActiveMQ. On Mac OS X or Linux, this is as simple as:
- cd path/to/ActiveMQ-4.1.0
I have no idea what's involved on Windows - you're on your own there - but it shouldn't be too hard.
Each clustered server will be configured to have an instance of the Active MQ event gateway called CacheSync
, using the CacheSynchronizer CFC and an ActiveMQ configuration file. In addition, each server will also have an additional instance for each other server in the cluster with a slightly different configuration file. Again, see the comments in the CacheSynchronizer CFC for more details.
Bottom line: a server notifies its own JMS server about changes and listens to all the other JMS servers to pick up changes on other servers. For most folks, this is going to be perfectly manageable because you'll only have a few event gateways on each of your few servers. For large clusters, this might be more problematic. When I have to address that issue, I'll post an updated solution (but will likely go to a cluster of JMS servers and have each CF server publish / subscribe to that central cluster).
file should define a cacheSynchronizer
bean, using the CacheSynchronizer CFC. The init()
method accepts the transfer
bean and automatically registers itself as the listener for the afterUpdate and afterDelete events in Transfer. You can either declare this lazy-init="false"
to have ColdSpring auto-initialize it or you can explicitly getBean("cacheSynchronizer")
to force initialization when you are loading your bean factory.
Updating or Deleting an Object
When an object is updated (or deleted), Transfer automatically calls the registered listener method. This creates a message structure containing the hostname (just for information), the object's class name (package.object) and the primary key (assuming getId() returns that). The message is sent to the local event gateway (CacheSync
Note: this assumes that all your Transfer objects have a primary key called "id" - if you don't do that, the process would be more complex. You could always add a getId()
method to your Transfer object decorator to retrieve the actual primary key.
Other servers in the cluster receive the message and onIncomingMessage()
is automatically invoked in the CacheSynchronizer CFC. It reads the hostname, class name and ID and then asks Transfer to discard that object from cache.
How simple is that?