Read only EntryProcessors with Hazelcast

Posted at — Sep 30, 2015

This post is a follow-up on my post EntryProcessors and EntryBackupProcessors with Hazelcast. In the comments, Peter Veentjer from Hazelcast gave me the idea to use an EntryProcessor to read part of the data. I will show you below how to best do this.

We start with a cache that has 10 User objects in it and we want to retrieve the user names of all users in the cache.

Without an entry processor

We can get the list of names without using an entry processor. For example:

public class ReadOnlyEntryProcessorTest0 {

    public static void main(String[] args) throws InterruptedException {

        Config config = new Config();
        config.setProperty("hazelcast.initial.min.cluster.size", "2");

        HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

        // Take a lock so only 1 of the 2 nodes will execute the entry processor
        ILock doItLock = hazelcastInstance.getLock("doItLock");

        doItLock.lock();

        IMap<Long, User> testMap;
        try {
            testMap = hazelcastInstance.getMap("testMap");
            if (!testMap.containsKey(1L)) {
                IntStream.rangeClosed(1, 10)
                         .mapToObj(ReadOnlyEntryProcessorTest0::createUser)
                         .forEach(user -> testMap.put(user.getId(), user));

                testMap.values().stream().map(User::getName).forEach(name -> System.out.println("name = " + name));
            }
        } finally {
            doItLock.unlock();
        }
    }

    private static User createUser(int i) {
        User user = new User();
        user.setId(i);
        user.setName("Wim" + i);
        return user;
    }
}

So the example first adds 10 User objects in the cache and then retrieves them again, getting the "name" of each User object and printing this. This means Hazelcast has to fetch (and serialize) the full User object from the other nodes running in the cluster.

Since we are only interested in a small part of the object, we can use an EntryProcessor to retrieve just that.

Using an EntryProcessor

This the same example, but this time using the AbstractEntryProcessor class:

public class ReadOnlyEntryProcessorTest1 {

    public static void main(String[] args) throws InterruptedException {

        Config config = new Config();
        config.setProperty("hazelcast.initial.min.cluster.size", "2");

        HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

        // Take a lock so only 1 of the 2 nodes will execute the entry processor
        ILock doItLock = hazelcastInstance.getLock("doItLock");

        doItLock.lock();

        IMap<Long, User> testMap;
        try {
            testMap = hazelcastInstance.getMap("testMap");
            if (!testMap.containsKey(1L)) {
                IntStream.rangeClosed(1, 10)
                         .mapToObj(ReadOnlyEntryProcessorTest1::createUser)
                         .forEach(user -> testMap.put(user.getId(), user));

                System.out.println("Calling the entry processor");

                Map<Long, Object> userNames = testMap.executeOnEntries(new ReadUserNamesEntryProcessor());
                userNames.values().forEach(name -> System.out.println("name = " + name));
            }
        } finally {
            doItLock.unlock();
        }
    }

    private static User createUser(int i) {
        User user = new User();
        user.setId(i);
        user.setName("Wim" + i);
        return user;
    }

    private static class ReadUserNamesEntryProcessor extends AbstractEntryProcessor<Long, User> {

        public ReadUserNamesEntryProcessor() {
        }

        @Override
        public Object process(Map.Entry<Long, User> entry) {
            String name = entry.getValue().getName();
            System.out.println("Returning name from primary entry: " + name);
            return name;
        }
    }
}

The printing of the names has now changed from this (not using an entry processor):

testMap.values().stream()
.map( User::getName )
.forEach( name -> System.out.println( "name = " + name ) );

to this (using an entry processor):

Map<Long, Object> userNames = testMap.executeOnEntries( new ReadUserNamesEntryProcessor() );
userNames.values().forEach( name -> System.out.println( "name = " + name ) );

The advantage here is that Hazelcast only has to serialize (and send over the network) the "name" String instead of the full User object.

If you would run the example, you will notice that the text "Returning name from primary entry" will be printed 20 times instead of the expected 10 times (There are 10 User objects in the cache). This is because AbstractEntryProcessor by default, also runs the processor on the backup entries.

In a read-only use case, this has no use at all (For starters, a EntryBackupProcessor cannot return a value anyway). So to have the best performance, we need call the super constructor with false to avoid that a backup processor is used.

This is now the code for our optimal EntryProcessor:

private static class ReadUserNamesEntryProcessor extends AbstractEntryProcessor<Long, User> {

    public ReadUserNamesEntryProcessor() {
        super(false);
    }

    @Override
    public Object process(Map.Entry<Long, User> entry) {

        String name = entry.getValue().getName();
        System.out.println("Returning name from primary entry: " + name);
        return name;
    }
}

An alternative would be to implement the interface and return null yourself:

private static class ReadUserNamesEntryProcessor implements EntryProcessor<Long, User> {

    public ReadUserNamesEntryProcessor() {
    }

    @Override
    public Object process(Map.Entry<Long, User> entry) {
        String name = entry.getValue().getName();
        System.out.println("Returning name from primary entry: " + name);
        return name;
    }

    @Override
    public EntryBackupProcessor<Long, User> getBackupProcessor() {
        return null;
    }
}

It would be nice if Hazelcast provided a ReadOnlyEntryProcessor abstract class. It would be more explicit that remembering having to call the super with 'false'. Maybe it could even throw an Exception if you would try to call 'entry.setValue' from such an EntryProcessor.

If you want to be notified in the future about new articles, as well as other interesting things I'm working on, join my mailing list!
I send emails quite infrequently, and will never share your email address with anyone else.