Writing your own cache implementation

Play already comes with an easy to scale cache solution included, namely memcached. However, you might not want to use memcached at all. It might not fit your needs, your admin may not want to install another application on a system, or anything else. Instead of not using a cache at all, this recipe shows you how to write your own implementation of a cache. This example will use Hazelcast, a pure Java distributed system as implementation.

You can find the source code of this example in the examples/chapter6/caching-implementation directory.

Getting ready

It requires a little bit of work to get to the point to start programming. We need to get Hazelcast instance up and running and setup an application for development. Hazelcast is basically a distributed data platform for the JVM. Hazelcast has tons of more features that are not needed in this little example—basically only a distributed map is featured here. Hazelcast scales very well, there are live examples of even 100 parallel running instances.

So, first download Hazelcast. Go to http://www.hazelcast.com, click on the Downloads menu and get the latest version, it usually comes in a ZIP file. Unzip it, and run the following commands:

cd hazelcast-${VERSION}
cd bin
sh run.sh

That's it. You should now have a running Hazelcast instance on localhost.

Furthermore, you need to copy two jars into the lib/ directory of your Play application. These are:

hazelcast-${VERSION}.jar
hazelcast-client-${VERSION}.jar

The files are in the lib/ directory of your Hazelcast installation.

As there is already a caching test included in the samples-and-tests directory of every Play framework distribution, we can reuse this test to make sure the hazelcast-based cache implementation works. You should copy the following files to the corresponding directories in your created application:

samples-and-tests/just-test-case/app/models/User.java

samples-and-tests/just-test-case/app/models/MyAddress.java

samples-and-tests/just-test-case/test/CacheTest.java

samples-and-tests/just-test-case/test/users.yml

After copying these files and running the test in your web browser, you should get a working test because no code to change the cache has been built yet. If you are using a Play version other than 1.2, please check the Java files, to see whether you might need to copy other dependant classes as well in order to be able to compile your application.

How to do it...

You should create a new module for your application, for example, via play new-module hazelcast. You should put the following into the conf/dependencies.yml file of your module and call play deps afterwards:

self: play -> hazelcast 0.1

require:
    - play
    - com.hazelcast -> hazelcast-client 1.9.3.1:
        transitive: false
    - com.hazelcast -> hazelcast 1.9.3.1:
        transitive: false

Now create a play.plugins file like this, which references the plugin, which is going to be created:

1000:play.modules.hazelcast.HazelcastPlugin

The next step is to create the plugin itself, which checks whether the Hazelcast cache should be enabled and replaces the cache appropriately:

public class HazelcastPlugin extends PlayPlugin {

  public void onApplicationStart() {
    Boolean isEnabled = new Boolean(Play.configuration.getProperty("hazelcast.enabled"));
    if (isEnabled) {
     Logger.info("Setting cache to hazelcast implementation");
     Cache.forcedCacheImpl = HazelCastCacheImpl.getInstance();
     Cache.init();
    }
  }
}

Now the only missing part is the cache implementation itself. All this code belongs to one class, but to help you understand the code better, some comments follow between some methods. The first step is to create a private constructor and a getInstance() method in order to implement the singleton pattern for this class. The constructor also loads some default parameters from the configuration file. Put this code into your module at src/play/modules/hazelcast/HazelCastCacheImpl.java:

public class HazelCastCacheImpl implements CacheImpl {

   private HazelcastClient client;
   private static HazelCastCacheImpl instance;

   public static HazelCastCacheImpl getInstance() {
      if (instance == null) {
         instance = new HazelCastCacheImpl();
      }
      return instance;
   }

   private HazelCastCacheImpl() {
      String groupName = Play.configuration.getProperty("hazelcast.groupname", "dev");
      String groupPass = Play.configuration.getProperty("hazelcast.grouppass", "dev-pass");
      String[] addresses = Play.configuration.getProperty("hazelcast.addresses", "127.0.0.1:5701").replaceAll(" ", "").split(",");
      client = HazelcastClient.newHazelcastClient(groupName, groupPass, addresses);
   }
   
   private IMap getMap() {
      return client.getMap("default");
   }

The add() and safeAdd() methods only put objects in the cache, if they do not exist in the cache already. On the other hand the set() and safeSet() methods overwrite existing objects. Note that any method with a safe prefix in its name has the contract of executing the action (add, set, replace, or delete) synchronous and must make sure the cache has finished the operation before returning to the caller:

   @Override
   public void add(String key, Object value, int expiration) {
      if (!getMap().containsKey(key)) {
         getMap().put(key, value, expiration, TimeUnit.SECONDS);
      }
   }

   @Override
   public boolean safeAdd(String key, Object value, int expiration) {
      getMap().putIfAbsent(key, value, expiration, TimeUnit.SECONDS);
      return getMap().get(key).equals(value);
   }

   @Override
   public void set(String key, Object value, int expiration) {
      getMap().put(key, value, expiration, TimeUnit.SECONDS);
   }

   @Override
   public boolean safeSet(String key, Object value, int expiration) {
      try {
          set(key, value, expiration);
          return true;
      } catch (Exception e) {}
      return false;
   }

The replace() and safeReplace() methods only place objects in the cache if other objects already exist under the referenced key. The get() method returns a not casted object from the cache, which also might be null. The get() method taking the keys parameter as varargs allows the developer to get several values at once. In case you are in need of easily incrementing or decrementing numbers, you also should implement the incr() and decr() methods:

   @Override
   public void replace(String key, Object value, int expiration) {
      if (getMap().containsKey(key)) {
         getMap().replace(key, value);
      }
   }

   @Override
   
public boolean safeReplace(String key, Object value, int expiration) {
      if (getMap().containsKey(key)) {
         getMap().replace(key, value);
         return true;
      }
      return false;
   }

   @Override
   public Object get(String key) {
      return getMap().get(key);
   }

   @Override
   public Map<String, Object> get(String[] keys) {
      Map<String, Object> map = new HashMap(keys.length);
      for (String key : keys) {
         map.put(key, getMap().get(key));
      }
      return map;
   }

   @Override
   public long incr(String key, int by) {
      if (getMap().containsKey(key)) {
         getMap().lock(key);
         Object obj = getMap().get(key);
         if (obj instanceof Long) {
            Long number = (Long) obj;
            number += by;
            getMap().put(key, number);
         }
         getMap().unlock(key);
         return (Long) getMap().get(key);
      }
      return 0;
   }

   @Override
   public long decr(String key, int by) {
      return incr(key, -by);
   }

The last methods are also quickly implemented. The clear() method should implement a complete cache clear. The delete() and deleteSafe() methods remove a single element from the cache. An important method to implement is the stop() method, which allows the implementation to correctly shutdown before it stops working. This is executed if your Play application is stopped, for example:

   @Override
   public void clear() {
      getMap().clear();
   }

   @Override
   public void delete(String key) {
      getMap().remove(key);
   }

   @Override
   public boolean safeDelete(String key) {
      if (getMap().containsKey(key)) {
         getMap().remove(key);
         return true;
      }
      return false;
   }

   @Override
   public void stop() {
      client.shutdown();
   }
}

You can now build the module via play build-module and then switch to the application, which includes the CacheTest class.

There is only one thing left to do to get the new cache implementation working in your application. Enable your newly built module in your application configuration. You need to edit conf/application.conf and add Hazelcast-specific configuration parameters:

hazelcast.enabled=true
hazelcast.groupname=dev
hazelcast.grouppass=dev-pass
hazelcast.addresses=127.0.0.1:5701

Then start the application and you should be able to run the preceding CacheTest that is referenced.

How it works...

The configuration is pretty self explanatory. Every Hazelcast client needs a username, a password, and an IP to connect to. The HazelcastPlugin sets the field forcedCacheImpl of the Cache class to the hazelcast implementation and reruns the init() method. From now on the Hazelcast cache is used.

I will not explain every method of the cache, as most of the methods are pretty straightforward. The getMap() method fetches the default map of the Hazelcast cluster which basically works like the map interface, but has a few more features such as the expiration.

Primarily, the class implements the singleton pattern by not providing a public constructor, but a getInstance() method in order to make sure that always the same instance is used.

There is also a client variable of type HazelcastClient, which does the communication with the cluster. The private constructor parses the Hazelcast specific setup variables and tries to connect to the cluster then.

The getMap() method returns a Hazelcast-specific IMAP, which basically is a map, but also has support for expiry. In case you are wondering where the client.getMap("default") is coming from, it is the default name of the distributed map, configured in Hazelcast. There is a hazelcast.xml file in the same directory, where the run.sh file resides.

The IMap structure gives us basically everything needed to implement the add, set, replace, and delete and their safe companion methods used in the cache. As opposed to other cache implementations, often there is no difference between the regular and the safe implementation due to the behavior of Hazelcast.

A little trick had to be done for the incr() method. First, the CacheTest enforces that incrementing in the cache only works if the key has already been created before. Second, it is the only code where a lock is put on the key of the map (not on the whole map), though no increments are actually missing out. If another process is incrementing, the method blocks until the key is unlocked again. Third, we only add increment counters if the values are of type Long and thus can be incremented.

It is also useful to implement the stop() method of the cache interface, which allows a graceful shutdown of the client.

There's more...

When you start the Hazelcast server, you get into a console where you can issue some commands. For example, you can issue m.size() there to get the current size of the distributed map. This could help you to debug some issues you might face.

More about Hazelcast

You should read more about Hazelcast as it is a really interesting technology. Go to http://www.hazelcast.com/ or http://code.google.com/p/hazelcast/ for more information.

Try building a Redis cache

As you have seen, it is pretty simple to build your own cache implementation. You could try to build a Redis cache as the next step. There are pretty simple libraries out there for Redis, like JRedis, which would make such an implementation quite easy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41