import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.IMap; import com.hazelcast.core.Offloadable; import com.hazelcast.core.ReadOnly; import com.hazelcast.map.AbstractEntryProcessor; import com.hazelcast.map.EntryBackupProcessor; import com.hazelcast.map.EntryProcessor; import java.util.Map; import static org.junit.Assert.assertTrue; /** * This example presents how to use the Offloadable and ReadOnly interfaces together with EntryProcessor. * * The optimization applies to the following methods only: * - executeOnKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor, ExecutionCallback) * * @see IMap#executeOnKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor, com.hazelcast.core.ExecutionCallback) */ public class OffloadableEntryProcessorDemo { public static void main(String[] args) { HazelcastInstance hz = Hazelcast.newHazelcastInstance(); IMap<String, Employee> employees = hz.getMap("employees"); employees.put("John", new Employee(1000)); employees.put("Mark", new Employee(1000)); employees.put("Spencer", new Employee(1000)); // EntryProcessor.process() will run on offloaded thread - a lock will be acquired for the given key String offloadedThread = (String) employees.executeOnKey("John", new OffloadableEntryProcessor()); assertTrue(offloadedThread.contains("cached.thread")); // EntryProcessor.process() will run on offloaded thread - a lock won't be acquired for the given key, since // the EntryProcessor is ReadOnly offloadedThread = (String) employees.executeOnKey("John", new OffloadableReadOnlyEntryProcessor()); assertTrue(offloadedThread.contains("cached.thread")); // EntryProcessor.process() will run on partition thread String partitionThread = (String) employees.executeOnKey("John", new ReadOnlyEntryProcessor()); assertTrue(partitionThread.contains("partition-operation.thread")); // EntryProcessor.process() will run on partition thread partitionThread = (String) employees.executeOnKey("John", new OrdinaryEntryProcessor()); assertTrue(partitionThread.contains("partition-operation.thread")); Hazelcast.shutdownAll(); } /** * By default the EntryProcessor executes on a partition-thread. There is exactly one thread responsible for handling * each partition and each of such threads may handle more than one partition. * * The present design of EntryProcessor assumes users have fast user code execution of the process() method. * In the pathological case where they code that is very heavy and executes in multi-milliseconds, this may create a * bottleneck. * * In order to mitigate the above-mentioned problem an Offloadable interface has been introduced. * If an EntryProcessor implements the Offloadable interface the process() method will be executed in the executor * specified by the getExecutorName() method. * * Offloading will unblock the partition-thread allowing the user to profit from much higher throughput. * The key will be locked for the time-span of the processing in order to not generate a write-conflict. * In this case the threading looks as follows: * 1.) partition-thread (fetch & lock) * 2.) execution-thread (process) * 3.) partition-thread (set & unlock, or just unlock if no changes) * * The optimization applies to the following methods only: * - executeOnKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor, ExecutionCallback) * * @see IMap#executeOnKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor, com.hazelcast.core.ExecutionCallback) */ private static class OffloadableEntryProcessor extends AbstractEntryProcessor<String, Employee> implements Offloadable { @Override public Object process(Map.Entry<String, Employee> entry) { Employee value = entry.getValue(); value.incSalary(10); entry.setValue(value); // returns the name of thread it runs on return Thread.currentThread().getName(); } @Override public String getExecutorName() { return OFFLOADABLE_EXECUTOR; } } /** * The same optimization applies to an EntryProcessor that implements Offloadable and ReadOnly interfaces. * By implementing ReadOnly the developer "promises" to the execute method that they will not modify the entry * within the EntryProcessor. It allows Hazelcast to optimize the execution path in a way that the * execution of the EntryProcessor will not wait until the lock on a specific key has been released. * * If the EntryProcessor implements the Offloadable and ReadOnly interfaces the processing will be offloaded to the * given ExecutorService allowing unblocking the partition-thread. Since the EntryProcessor is not supposed to do * any changes to the Entry the key will NOT be locked for the time-span of the processing. In this case the threading * looks as follows: * 1.) partition-thread (fetch & lock) * 2.) execution-thread (process) * In this case the EntryProcessor.getBackupProcessor() has to return null; otherwise an IllegalArgumentException * exception is thrown. * * The optimization applies to the following methods only: * - executeOnKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor, ExecutionCallback) * * @see IMap#executeOnKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor, com.hazelcast.core.ExecutionCallback) */ private static class OffloadableReadOnlyEntryProcessor implements EntryProcessor<String, Employee>, Offloadable, ReadOnly { @Override public Object process(Map.Entry<String, Employee> entry) { Employee value = entry.getValue(); value.incSalary(10); // returns the name of thread it runs on return Thread.currentThread().getName(); } @Override public EntryBackupProcessor<String, Employee> getBackupProcessor() { // ReadOnly EntryProcessor has to return null, since it's just a read-only operation that will not be // executed on the backup return null; } @Override public String getExecutorName() { return OFFLOADABLE_EXECUTOR; } } /** * If the EntryProcessor implements only ReadOnly without implementing Offloadable the processing unit will not * be offloaded, however, the EntryProcessor will not wait for the lock to be acquired, since the EP will not * do any modifications. * * If the EntryProcessor implements ReadOnly and modifies the entry an UnsupportedOperationException * will be thrown. * * The optimization applies to the following methods only: * - executeOnKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor) * - submitToKey(Object, EntryProcessor, ExecutionCallback) * * @see IMap#executeOnKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor) * @see IMap#submitToKey(Object, EntryProcessor, com.hazelcast.core.ExecutionCallback) */ private static class ReadOnlyEntryProcessor implements EntryProcessor<String, Employee>, ReadOnly { @Override public Object process(Map.Entry<String, Employee> entry) { Employee value = entry.getValue(); value.incSalary(10); // returns the name of thread it runs on return Thread.currentThread().getName(); } @Override public EntryBackupProcessor<String, Employee> getBackupProcessor() { // ReadOnly EntryProcessor has to return null, since it's just a read-only operation that will not be // executed on the backup return null; } } /** * Ordinary EntryProcessor that will run on a partition-thread */ private static class OrdinaryEntryProcessor extends AbstractEntryProcessor<String, Employee> { @Override public Object process(Map.Entry<String, Employee> entry) { Employee value = entry.getValue(); value.incSalary(10); entry.setValue(value); // returns the name of thread it runs on return Thread.currentThread().getName(); } } }