JSR-107 Caching (Part Two!)
Where we learn that JSR-107 isn’t just about annotations.
Overview
This adventure will teach you a little of the JSR-107 API, by walking you through adding a simple item with shared state to a Game On room.
You will come away understanding how to use JSR-107 without the annotations, with additional suggestions for how this could be used further within a room.
Why JSR-107 API?
As mentioned over in Part One JSR-107 is an effort to standardise a Java API for Caching. In part one we looked only at the Annotations part of JSR-107, and here we’ll be covering a quick example of using the Java API directly.
When using the API directly, rather than via the Annotations, some actions become considerably simpler, because you always have direct access to the objects representing the underlying cache, instead of having to abstract your Cache usage via methods that can be appropriately annotated.
We’ll be walking through adding a simple 'toggle' switch to your room, which will be backed by a cache, and have it’s state monitored via a CacheListener.
Prerequisites
This walkthrough builds heavily on the first JSR-107 walkthrough, relying on the previous walkthrough to have;
Walkthrough
Since we’ve already done all the setup within Part One, here we can jump straight into the code =).
Implementing the Toggle
For our simple 'toggle' example, we’ll start by creating an application scoped CDI bean.
We’ll inject that to the RoomImplementation, and add a simple block to the processCommand
switch statement to invoke our toggle.
@ApplicationScoped
public class Toggle {
private Cache<String,String> toggleCache;
public void toggle(){
}
public String getToggleState(){
}
}
That will form the basic framework for our toggle bean.
The first thing to do is instantiate the Cache
we plan to use, this is where we
ideally would like to just do:
@PostConstruct
public void init(){
CacheManager manager = Caching.getCachingProvider().getCacheManager();
MutableConfiguration<String, String> config =
new MutableConfiguration<String,String>().setStoreByValue(true);
toggleCache = manager.createCache("toggle", config);
}
…however if we try that, Redisson will go look for it’s configuration in it’s flat json files. Said json files do not exist, and we’ll end up with an exception to handle etc.
Thankfully, we’ve already written something that can give us a nicely configured CacheManager, and thats our 'default cache manager provider' from part one.
So that allows us to update the boiler plate JSR-107 code just a little to look like:
@PostConstruct
public void init(){
CacheManager manager = (new RedissonCacheManagerProvider())
.getDefaultCacheManager();
MutableConfiguration<String, String> config =
new MutableConfiguration<String,String>().setStoreByValue(true);
toggleCache = manager.createCache("toggle", config);
}
Great! We have our cache instance, now lets look at implementing our toggle
and getToggleState
methods.
We’ll use the cache as a map, with only a single key “toggle”, that we’ll map to the values “on” and “off” depending on the state of the toggle.
Here’s our first attempt at toggle
and getToggleState
;
public void toggle(){
String value = getToggleState();
if("on".equals(value)){
toggleCache.put("toggle","off");
}else{
toggleCache.put("toggle","on");
}
}
public String getToggleState(){
String value = toggleCache.get("toggle");
if(value==null){ value = "on"; }
return value;
}
These look pretty reasonable at first glance, but they hide the fact that the toggle operation should be performed atomically. The test for the toggle state and the update of the state must not allow the state to change between them, otherwise 2 people could attempt to flip the toggle, and instead of the toggle ending back where it started, it will go to it’s alternate state. Not really an issue if you are just testing ideas, but imagine if financial compensation was at stake ;)
One option could be to try to use the replace
method of Cache
which allows
us to only perform the update if the cache has the expected value.
public void toggle(){
String value = getToggleState();
if("on".equals(value)){
toggleCache.replace("toggle","on","off");
}else{
toggleCache.replace("toggle","off","on");
}
}
Problem solved? not so much! We’ve gone from being unaware there’s an issue, to being aware, but ignoring the implications. We should likely test the return for the method, and if we failed our update then we could reattempt the toggle.
public void toggle(){
String value = getToggleState();
if("on".equals(value)){
if(!toggleCache.replace("toggle","on","off")){
toggle();
}
}else{
if(!toggleCache.replace("toggle","off","on")){
toggle();
}
}
}
Awesome, this will pretty much do as we need, except if the system gets really
busy, we risk running out of stack as we recurse deeper and deeper. We could continue
to try to find ways to make replace work, or perhaps look at the JSR-107
EntryProcessor
.
Documented as "An invocable function that allows applications
to perform compound operations on a Cache.Entry
atomically,
according the defined consistency of a Cache", EntryProcessor is typed by the
Key/Value type of the Cache, and the return type of the processor method.
For our toggle, we really don’t need a return type, since all we want to do is
flip the value atomically.
Here’s a simple EntryProcessor that will flip the toggle as we require.
public static class BooleanToggle implements EntryProcessor<String,String,Object>{
@Override
public Object process(MutableEntry<String,String> entry, Object... arguments)
throws EntryProcessorException {
if(entry.getValue().equals("off"))
entry.setValue("on");
else {
entry.setValue("off");
}
return null;
}
}
We use this by updating our toggle
method:
public void toggle(){
toggleCache.invoke("toggle", new BooleanToggle());
}
Now when the toggle is flipped, JSR-107 will use our EntryProcessor to update the value atomically.
We have however, just lost our default 'on' behavior that was provided until now via our 'getToggleState' method.
The easy solution here is to stop making that assumption, and ensure the cache always has a default state before we interact with it.
Doing so is really quite simple, we just add;
toggleCache.putIfAbsent("toggle", "on");
to our init
method. Now if the cache really has no value, and only if it has
no value, we’ll set the value to be 'on'.
Adding the toggle to the room.
Inject the toggle to the RoomImplementation
class by adding the following near
where the MapClient
is injected.
@Inject
protected Toggle toggle;
Find the switch block in the processCommand
method of RoomImplementation
,
add a block like;
case "/toggle":
toggle.toggle();
break;
Awesome, you can now test your toggle. It’s admittedly kinda hard to tell it did anything ;) it’s almost as if I’ve deliberately left out a part so I can have another section in the walkthrough, I’m sensing something titled…
Cache Listeners
Imagine you had a cache that was being modified either by yourself, or another instance of yourself (if you were a room that had been dynamically scaled under load). Imagine further that you wanted to react when the cache changed. Maybe it’s important to you to know when a key has been added or removed. Or just hypothetically, you might want to know when an imaginary toggle has been flipped, so you can send a message to everyone.
Creating our listener.
Before we create our listener, we should understand what type of cache event we want to listen to, as each type has its own listener interface to implement.
For our toggle cache, we’re really only interested in Create and Update events,
so we’ll implement CacheEntryCreatedListener
and CacheEntryUpdatedListener
public class MyCacheEntryListener implements CacheEntryCreatedListener<String, String>,
CacheEntryUpdatedListener<String, String>, Serializable {
private static final long serialVersionUID = -1306798197522730101L;
public MyCacheEntryListener() {
}
@Override
public void onCreated(Iterable<CacheEntryEvent<? extends String, ? extends String>> cacheEntryEvents)
throws CacheEntryListenerException {
for (CacheEntryEvent<? extends String, ? extends String> entryEvent : cacheEntryEvents) {
System.out.println("Toggle initialized to have value "+
entryEvent.getValue());
}
}
@Override
public void onUpdated(Iterable<CacheEntryEvent<? extends String, ? extends String>> cacheEntryEvents)
throws CacheEntryListenerException {
for (CacheEntryEvent<? extends String, ? extends String> entryEvent : cacheEntryEvents) {
System.out.println("Toggle updated to have value "+
entryEvent.getValue());
}
}
}
Wiring the listener up to the Cache
We plug this in within our init
method, using one of JSR-107’s utility
factory creators to add a factory for our listener, that we register
with the Cache.
@PostConstruct
public void init(){
toggleCache = getCache();
MyCacheEntryListener mcel = new MyCacheEntryListener();
CacheEntryListenerConfiguration<String,String> listenConfig =
new MutableCacheEntryListenerConfiguration<String,String>(
FactoryBuilder.factoryOf(mcel),
null,
false,
true);
toggleCache.registerCacheEntryListener(listenConfig);
toggleCache.putIfAbsent("toggle", "on");
}
Now, when you use /toggle
within your room, you’ll see the message
Toggle updated to have value on|off
within the logs for your Room.
Working example repo.
For complete versions of the code discussed so far, check out my Sample JSR-107 Room. It does everything described here, and more, showing usage of both JSR-107 annotations, and direct API usage.
Suggested extensions
- Experiment with the CacheLoader / CacheWriter classes to prepopulate a cache, or write cache updates through to a persistence store.
- Share a cache instance between an annotated method & a non annotated approach.
Conclusion
While the annotated approach for JSR-107 can feel quite restrictive, the API approach offers much more flexibility. The ability to add CacheListeners that respond to cache updates greatly expand the options available to a developer when authoring a microservice that may scale beyond a single instance.
By working through the toggle example, you have built a basic service using a cache, and understood some of the pitfalls you may meet when using the API.
Suggested further adventures.
Why not take a look at the 'Adding Items to a Room' walkthrough next. It’ll teach you ways you can expose your cache understanding within a Room in Game On.