<< Back to home page

Gemfire Server for Test Environments

9 September, 2014

If your application is integrating with a gemfire cache, you may have chosen to stub out this boundary when running behaviour, acceptance or system tests. Intially we were using a fake object (see test doubles) in our test to avoid interaction with a gemfire cache, however we were concerned that the data being returned from the fake object was not visible from our behaviour tests. It went something like this (although I will use a Grocery store as an example instead of our actual domain):

 1 public class FakeGroceryDetailsService implements GroceryDetailsService {
 2 	private Map<String, GroceryDetails> groceryDetails;
 3 
 4 	{
 5 		groceryDetails = new HashMap<String, GroceryDetails>();
 6 		groceryDetails.put("101", new GroceryDetails("Apple", "0.30"));
 7 		groceryDetails.put("102", new GroceryDetails("Banana", "0.15"));
 8 	}
 9 
10 	@Override
11 	public GroceryDetails getGroceryDetailsUsing(String groceryId) {
12 		return groceryDetails.get(groceryId);
13 	}
14 }

There were a few issues with this:

  • Anyone writing new tests needed to have knowledge of the fake object
  • The fake object had to live in the main source directory, and as such is part of your production code
  • A mechanism is required to switch between the fake object and the real implementation

As such, we decided to build our own gemfire cache server.

This can be achieved by creating a cache.xml file and wrapping the regions with a server tag, however I prefer to keep as much as I can in Java code.

Starting Cache Server

Starting up a cache server is simple enough (and the things that are not so obvious are explained below).

 1 public class GroceryStoreCache {
 2 	private static final String DISABLE_MULTICAST = "0";
 3 
 4 	private Cache cache;
 5 
 6 	public GroceryStoreCache(int serverPort, CacheRegion... regions) {
 7 		cache = new CacheFactory().set("mcast-port", DISABLE_MULTICAST).create();
 8 		configureCacheServer(serverPort);
 9 		configureRegions(regions);
10 	}
11 
12 	private void configureCacheServer(int serverPort) {
13 		CacheServer cacheServer = cache.addCacheServer();
14 		cacheServer.setPort(serverPort);
15 	}
16 
17 	private void configureRegions(CacheRegion... regions) {
18 		for (CacheRegion region : regions) {
19 			cache.createRegionFactory(LOCAL).create(region.name());
20 		}
21 	}
22 
23 	public void start() throws IOException {
24 		cache.getCacheServers().get(0).start();
25 	}
26 
27 }

Multicast Port

The multicast port is set to zero to prevent the cache server discovering other gemfire cache instances running on a network. With the gemfire default license, there is a restriction in place that prevents more than three instances of a cache being started using the same multicast port. By setting to zero, the cache will not conflict with any other gemfire cache servers you have running on your build server.

Adding Regions

Regions are simply strings so for this example CacheRegion is just an enum.

1 public enum CacheRegion {
2 	EMPLOYEES,
3 	GROCERIES
4 }

These are added to the cache server using the region factory with LOCAL state. As the cache server will not talk across a network, changes to a region will not need to be reflected in peers.

Populating Regions

With the cache server up and running, the only thing left to do is populate it. There are only two rules that must be followed when populating a region:

  • The object that is added to the cache must implement Serializable
  • The object must be retrievable by an ID

With that in mind, an interface is used to ensure both of those rules are covered.

1 public interface CacheItem extends Serializable {
2 	int getId();
3 }

Then within the cache server code, a populate method can be exposed to allow a class implementing CacheItem to be added to a region.

1 public void populate(CacheRegion region, CacheItem cacheItem) {
2 	cache.getRegion(region.name()).put(cacheItem.getId(), cacheItem);
3 }

For this grocery store cache example, it would make sense to populate the cache with a grocery. The override of toString is to allow the contents of a cache region to be asserted in a test.

 1 public class Grocery implements CacheItem {
 2 	private static final long serialVersionUID = -1145880717859373291L;
 3 
 4 	private int id;
 5 	private String name;
 6 	private String price;
 7 
 8 	public Grocery(int id, String name, String price) {
 9 		this.id = id;
10 		this.name = name;
11 		this.price = price;
12 	}
13 
14 	@Override
15 	public int getId() {
16 		return id;
17 	}
18 
19 	@Override
20 	public String toString() {
21 		return format("id: %d, name: %s, price: %s", id, name, price);
22 	}
23 }

Printing Items in a Region

Due to issues with external systems, we ended up using this cache in our smoke environment (our first integrated environment we deploy to). We found it useful to be able to view the contents of a cache region at any given time. Retrieving the contents of a region on the cache is also straight forward.

1 public String print(CacheRegion region) {
2 	StringBuilder cacheItems = new StringBuilder();
3 	for (Entry<Object, Object> cacheItem : cache.getRegion(region.name()).entrySet()) {
4 		cacheItems.append(cacheItem.getValue() + "\n");
5 	}
6 	return cacheItems.toString();
7 }

Clearing a Region

As the region is essentially a map, clearing the region is performed by retrieving the region and calling clear.

1 public void clear(CacheRegion region) {
2 	cache.getRegion(region.name()).clear();
3 }

Connecting to the Cache Server

There is one subtle difference when connecting to a cache server. Instead of using a locator, a server is used instead. As the majority of gemfire users will be using a client-cache.xml file to define pools and regions, I will include the XML changes required for the client.

Previously your client-cache.xml file may have contained a pool element like this:

1 <pool name="client">
2 	<locator host="localhost" port="10000" />
3 </pool>

This just needs updating to be:

1 <pool name="client">
2 	<server host="localhost" port="10000" />
3 </pool>

Wrapping Up

An example of this cache server can be found at https://github.com/joneland/gemfire-sandbox.

The example includes a runner and exposes some of the cache methods via JMX. By using a JMX client to take care of all the MBean server invocations, it became easy for us to manipulate the cache from our tests. CacheJMXClient is an example of this. We used this client to populate and clear the cache between test scenarios.

1 CacheJMXClient cacheClient = new CacheJMXClient("localhost", "10000");
2 
3 cacheClient.populateGrocery(101, "Apple", "0.30");
4 
5 cacheClient.clearGroceries();

And that is it! Hopefully this post demonstrates how simple it is to get a gemfire cache server up and running for test environments.