Thursday, May 7, 2015

Enforce Size Constraints in Infinispan/Wildfly using Java Instrumentation API!

Infinispan does not provide size constraints out of the box, just like almost all of the other standard caching frameworks. You may use the Java Instrumentation API as follows to enforce cache size constraints for your product or project that uses Infinispan.

1. Introduce Instrumentation API in your code
a. Develop a class that has the following method in it. The VM will automatically insert an implementation of the Instrumentation API - once it encounters this method.
 public class ServerInstrumentation {  
      private Instrumentation instrumentation = null;   
      private static final Logger LOGGER = Logger.getLogger(ServerInstrumentation.class);  
      private static ServerInstrumentation serverInstrumentation;  

      public static void premain(String args, Instrumentation pInstrumentation) {  
           serverInstrumentation = new ServerInstrumentation();  
           serverInstrumentation.instrumentation=pInstrumentation;  
      }  
      public static Instrumentation getInstance() {  
           return serverInstrumentation.instrumentation;  
      }  
}

b. You may choose to add the following EJB (optionally) to make this object available throughout your JEE application. Re-Implement the methods that you need to use from the Instrumentation API. For example, below we have implemented the sizeOf() which actually internally invokes Instrumentation.getObjectSize().
 @Remote  
 public interface ServerInstrumentationInterface {  
      public long sizeOf(Object toBeSized);  
 }  

 @Singleton  
 @Startup  
 public class ServerInstrumentationEJB implements ServerInstrumentationInterface {  
      private static Instrumentation instrumentation;  
      {  
           ClassLoader classLoader=this.getClass().getClassLoader().getParent();  
           try {  
                Class clazz = classLoader.loadClass(ServerInstrumentation.class.getName());  
                instrumentation=(Instrumentation)clazz.getDeclaredMethod("getInstance").invoke(null);                   
           } catch (Exception e) {  
                System.out.println("FFFFFF");  
           }  
      }  
      public long sizeOf(Object toBeSized) {  
           return instrumentation.getObjectSize(toBeSized);  
      }  
 }  

Package this code as 'agent.jar' with the following contents in MANIFEST.MF
 Premain-Class: ServerInstrumentation  
 Agent-Class: ServerInstrumentation  

c. You should place this in the normal deployment location (standalone\deployments) of Wildfly and also mention it as the -javaagent during server startup.The following contents should be in Wildfly standalone.bat
 "%JAVA%" %JAVA_OPTS% "-javaagent:%JBOSS_HOME%\standalone\deployments\agent.jar" ^  
  "-Dorg.jboss.boot.log.file=%JBOSS_LOG_DIR%\server.log" ^  
  "-Dlogging.configuration=file:%JBOSS_CONFIG_DIR%/logging.properties" ^  
   -jar "%JBOSS_HOME%\jboss-modules.jar" ^  
   -mp "%JBOSS_MODULEPATH%" ^  
    org.jboss.as.standalone ^  
   "-Djboss.home.dir=%JBOSS_HOME%" ^  
    %SERVER_OPTS%  

At the end of this, the Instrumentation methods are available throughout your JEE application for all instrumentation purposes. The EJB will act as a facade over this functionality. Make sure that the following settings exist in your standalone.conf.bat
set "JAVA_OPTS=%JAVA_OPTS% -Djava.net.preferIPv4Stack=true -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Xbootclasspath/p:d:\wildfly\modules\system\layers\base\org\jboss\log4j\logmanager\main\log4j-jboss-logmanager-1.1.0.Final.jar;d:\wildfly\modules\system\layers\base\org\jboss\logmanager\main\jboss-logmanager-1.5.2.Final.jar" 
set "JAVA_OPTS=%JAVA_OPTS% -Djboss.modules.system.pkgs=org.jboss.byteman,org.jboss.logmanager"


2. Use this in your Actual Application (Client)
a. The lookup code is provided below to perform methods using Instrumentation, such as 'Object Size'. This may be used for multiple purposes in your JEE products or solutions, apart form caching constraints.
 // let's do the lookup           
ServerInstrumentationInterface cacheInstrumentationService = (ServerInstrumentationInterface)   
               initialContext.lookup("java:global/agent/" + ServerInstrumentationEJB.class.getSimpleName());  
cacheInstrumentationService.sizeOf(new String("Java and Infinispan Caching Innovation")));  

b. The following method can be implemented in a different class (CacheInstrumentation.java) to perform the following function everytime you add an element to the cache, to check whether the Allocated Size is exceeded. This will have the above EJB Reference/Instance encapsulated in it.
 public boolean isEntityCacheLimitExceeded(Cache cache) {  
           boolean exceeded=false;  
           long currSize = cacheInstrumentationService.sizeOf(cache);  
           long allocSize = 10*1000*1024;  // 10 MB = 10 * 1000 * 1024 bytes
           if(currSize >= allocSize) {  
                exceeded=true;  
           }  
           return exceeded;  
}  


3. Use ConfigurationBuilder to Introduce Constraints
a. The following method will be used to setConfiguration() which will help to notify Infinispan that the configuration has changed, so that it can perform eviction dynamically.
 public class CacheManagerUtilities {  
      public static Configuration setConfiguration(int maxNumberofEntries) {  
           return new ConfigurationBuilder().eviction().maxEntries(maxNumberofEntries)  
                     .strategy(EvictionStrategy.valueOf("LIRS")).expiration()  
                     .lifespan(2000)  
                     .wakeUpInterval(500)  
                     .maxIdle(1000).build();  
      }  
 }  


4. Request/Enforce Eviction using EvictionManager [defineConfiguration()]
a. Everytime you put/add an element in your cache, you can use the sizeOf() from the EJB to check if the size constraints are exceeded. If they are, then dyanmically change the configuration to make the max-entries limit to the currently existing max-entries. We will now rely on Infinispan's Eviction Algorithms to kick-in and do its duty.
 public void put(K key, V value) {            
           while (true) {  
                if (!cacheInstrumentation.isEntityCacheLimitExceeded(cache)) {  
                     cache.put(key, value);  
                     break;  
                } else {  
                     // set the configuration to the infinispan cache programmatically  
                     Configuration configuration = CacheManagerUtilities.setConfiguration(cache.size());  
                     cache.getCacheManager().defineConfiguration(cache.getName(), configuration);  
                    
                     // start eviction manually  
                     EvictionManager evictionManager = cache.getAdvancedCache().getEvictionManager();  
                     evictionManager.processEviction();  
                     
                     // set the configuration to the infinispan cache programmatically - large number such as 9999  
                     configuration = CacheManagerUtilities.setConfiguration(9999);  
                     cache.getCacheManager().defineConfiguration(cache.getName(), configuration);  
                }  
           }  
}

The above usually performs three things:
1. Expire all elements that have reached expiration time (Data Container)
2. Expire all elements that have reached expiration time (Cache Store)
2. Remove all elements as per the eviction policy - Prunes to max-entries

You may also test the above using Arquillian to try out whether the eviction works by creating data within test cases that adhere to LIRS, LRU and Size Constraints.


Note that the above code will also make sure that only 'Serializable' objects are allowed to be Cached. Suitable for all Java/JEE products and solutions that need to Enforce Size as a Constraint mechanism for Caching. Happy Caching!

No comments: