7.7. Remote Event Listeners (Hot Rod)

Event listeners allow Red Hat JBoss Data Grid Hot Rod servers to be able to notify remote clients of events such as CacheEntryCreated, CacheEntryModified, and CacheEntryRemoved. Clients can choose whether or not to listen to these events to avoid flooding connected clients. This assumes that clients maintain persistent connections to the servers.
Client listeners for remote events can be added similarly to clustered listeners in library mode. The following example demonstrates a remote client listener that prints out each event it receives.

Example 7.7. Event Print Listener

import org.infinispan.client.hotrod.annotation.*;
import org.infinispan.client.hotrod.event.*;

@ClientListener
public class EventLogListener {

   @ClientCacheEntryCreated
   public void handleCreatedEvent(ClientCacheEntryCreatedEvent e) {
      System.out.println(e);
   }

   @ClientCacheEntryModified
   public void handleModifiedEvent(ClientCacheEntryModifiedEvent e) {
      System.out.println(e);
   }

   @ClientCacheEntryRemoved
   public void handleRemovedEvent(ClientCacheEntryRemovedEvent e) {
      System.out.println(e);
   }

}
  • ClientCacheEntryCreatedEvent and ClientCacheEntryModifiedEvent instances provide information on the key and version of the entry. This version can be used to invoke conditional operations on the server, such a replaceWithVersion or removeWithVersion.
  • ClientCacheEntryRemovedEvent events are only sent when the remove operation succeeds. If a remove operation is invoked and no entry is found or there are no entries to remove, no event is generated. If users require remove events regardless of whether or not they are successful, a customized event logic can be created.
  • All client cache entry created, modified, and removed events provide a boolean isCommandRetried() method that will return true if the write command that caused it has to be retried due to a topology change. This indicates that the event has been duplicated or that another event was dropped and replaced, such as where a Modified event replaced a Created event.

Important

Remote event listeners are available for the Hot Rod Java client only.

Warning

Remote event listeners is a Technology Preview feature and is therefore not supported in JBoss Data Grid 6.4.

7.7.1. Adding and Removing Event Listeners

Registering and Event Listener with the Server

The following example registers the Event Print Listener with the server. See Example 7.7, “Event Print Listener”.

Example 7.8. Adding an Event Listener

RemoteCache<Integer, String> cache = rcm.getCache();
cache.addClientListener(new EventLogListener();
Removing a Client Event Listener

A client event listener can be removed as follows

Example 7.9. Removing an Event Listener

EventLogListener listener = ...
cache.removeClientListener(listener);

7.7.2. Remote Event Client Listener Example

The following procedure demonstrates the steps required to configure a remote client listener to interact with the remote cache via Hot Rod.

Procedure 7.2. Configuring Remote Event Listeners

  1. Download the Red Hat JBoss Data Grid Server distribution from the Red Hat Customer Portal

    The latest Red Hat JBoss Data Grid distribution includes the Hot Rod server with which the client will communicate.
  2. Start the server

    Start the JBoss Data Grid server by using the following command from the root of the server.
    $ ./bin/standalone.sh
  3. Write an application to interact with the Hot Rod server

    • Maven users

      Create an application with the following dependency, changing the version to 6.4.0-Final-redhat-1 or better.
      <dependency>
        <groupId>org.infinispan</groupId>
        <artifactId>infinispan-remote</artifactId>
        <version>${infinispan.version}</version>
      </dependency>
      
    • Non-Maven users, adjust according to your chosen build tool or download the distribution containing all JBoss Data Grid jars.
  4. Write the client application

    The following demonstrates a simple remote event listener that logs all events received.
    import org.infinispan.client.hotrod.annotation.*;
    import org.infinispan.client.hotrod.event.*;
    
    
    @ClientListener
    public class EventLogListener {
    
    
     @ClientCacheEntryCreated
     @ClientCacheEntryModified
     @ClientCacheEntryRemoved
     public void handleRemoteEvent(ClientEvent event) {
       System.out.println(event);
      }
    
    
    }
    
  5. Use the remote event listener to execute operations against the remote cache

    The following example demonstrates a simple main java class, which adds the remote event listener and executes some operations against the remote cache.
    import org.infinispan.client.hotrod.*;
    
    
    RemoteCacheManager rcm = new RemoteCacheManager();
    RemoteCache<Integer, String> cache = rcm.getCache();
    EventLogListener listener = new EventLogListener();
    try {
     cache.addClientListener(listener);
     cache.put(1, "one");
     cache.put(1, "new-one");
     cache.remove(1);
    } finally {
     cache.removeClientListener(listener);  
    }
    
Result

Once executed, the console output should appear similar to the following:

ClientCacheEntryCreatedEvent(key=1,dataVersion=1)
ClientCacheEntryModifiedEvent(key=1,dataVersion=2)
ClientCacheEntryRemovedEvent(key=1)
The output indicates that by default, events come with the key and the internal data version associated with current value. The actual value is not sent back to the client for performance reasons. Receiving remote events has a performance impact, which is increased with cache size, as more operations are executed. To avoid inundating Hot Rod clients, filter remote events on the server side, or customize the event contents.

7.7.3. Filtering Remote Events

To prevent clients being inundated with events, Red Hat JBoss Data Grid Hot Rod remote events can be filtered by providing key/value filter factories that create instances that filter which events are sent to clients, and how these filters can act on client provided information.
Sending events to remote clients has a performance cost, which increases with the number of clients with registered remote listeners. The performance impact also increases with the number of modifications that are executed against the cache.
The performance cost can be reduced by filtering the events being sent on the server side. Custom code can be used to exclude certain events from being broadcast to the remote clients to improve performance.
Filtering can be based on either key or value information, or based on cache entry metadata. To enable filtering, a cache event filter factory that produces filter instances must be created. The following is a sample implementation that filters key “2” out of the events sent to clients.

Example 7.10. KeyValueFilter

package sample;
 
import java.io.Serializable;
import org.infinispan.filter.*;
import org.infinispan.metadata.*;
 
@NamedFactory(name = "basic-filter-factory")
public class BasicKeyValueFilterFactory implements KeyValueFilterFactory {
  @Override public KeyValueFilter<Integer, String> getKeyValueFilter(final Object[] params) {
    return new BasicKeyValueFilter();
  }
 
    static class BasicKeyValueFilter implements KeyValueFilter<Integer, String>, Serializable {
      @Override public boolean accept(Integer key, String value, Metadata metadata) {
        return !"2".equals(key);
      }
    }
}
In order to register a listener with this key value filter factory, the factory must be given a unique name, and the Hot Rod server must be plugged with the name and the cache event filter factory instance.

7.7.3.1. Custom Filters for Remote Events

Custom filters can improve performance by excluding certain event information from being broadcast to the remote clients.

Note

Deployment of custom listeners on JDG Server is available as Technology Preview in JDG 6.4.
To plug the JBoss Data Grid Server with a custom filter use the following procedure:

Procedure 7.3. Using a Custom Filter

  1. Create a JAR file with the filter implementation within it. Each factory must have a name assigned to it via the org.infinispan.filter.NamedFactory annotation. The example uses a KeyValueFilterFactory.
  2. Create a META-INF/services/org.infinispan.notifications.filter.KeyValueFilterFactory file within the JAR file, and within it write the fully qualified class name of the filter class implementation.
  3. Deploy the JAR file in the JBoss Data Grid Server.
Once the server is plugged with the filter, add a remote client listener that will use the filter. The following example extends the EventLogListener implementation provided in Remote Event Client Listener Example (See Section 7.7.2, “Remote Event Client Listener Example”), and overrides the @ClientListener annotation to indicate the filter factory to use with the listener.

Example 7.11. Add Filter Factory to the Listener

@org.infinispan.client.hotrod.annotation.ClientListener(filterFactoryName = "basic-filter-factory")
public class BasicFilteredEventLogListener extends EventLogListener {}
The listener can now be added via the RemoteCacheAPI. The following example demonstrates this, and executes some operations against the remote cache.

Example 7.12. Register the Listener with the Server

import org.infinispan.client.hotrod.*;

RemoteCacheManager rcm = new RemoteCacheManager();
RemoteCache<Integer, String> cache = rcm.getCache();
BasicFilteredEventLogListener listener = new BasicFilteredEventLogListener();
try {
  cache.addClientListener(listener);
  cache.putIfAbsent(1, "one");
  cache.replace(1, "new-one");
  cache.putIfAbsent(2, "two");
  cache.replace(2, "new-two");
  cache.putIfAbsent(3, "three");
  cache.replace(3, "new-three");
  cache.remove(1);
  cache.remove(2);
  cache.remove(3);
} finally {
  cache.removeClientListener(listener);
}
The system output shows that the client receives events for all keys except those that have been filtered.
Result

The following demonstrates the resulting system output from the provided example.

ClientCacheEntryCreatedEvent(key=1,dataVersion=1)
ClientCacheEntryModifiedEvent(key=1,dataVersion=2)
ClientCacheEntryCreatedEvent(key=3,dataVersion=5)
ClientCacheEntryModifiedEvent(key=3,dataVersion=6)
ClientCacheEntryRemovedEvent(key=1)
ClientCacheEntryRemovedEvent(key=3)

Important

Filter instances must be marshallable when they are deployed in a cluster in order for filtering to occur where the event is generated, even if the event is generated in a different node to where the listener is registered. To make them marshallable, either make them extend Serializable, Externalizable, or provide a custom Externalizer.

7.7.3.2. Enhanced Filter Factories

When adding client listeners, users can provide parameters to the filter factory in order to generate different filter instances with different behaviors from a single filter factory based on client-side information.
The following configuration demonstrates how to enhance the filter factory so that it can filter dynamically based on the key provided when adding the listener, rather than filtering on a statically given key.

Example 7.13. Configuring an Enhanced Filter Factory

package sample;

import java.io.Serializable;
import org.infinispan.filter.*;
import org.infinispan.metadata.*;


@NamedFactory(name = "basic-filter-factory")
public class BasicKeyValueFilterFactory implements KeyValueFilterFactory {
  @Override public KeyValueFilter<Integer, String> getKeyValueFilter(final Object[] params) {
    return new BasicKeyValueFilter(params);
}

  static class BasicKeyValueFilter implements KeyValueFilter<Integer, String>, Serializable {
    private final Object[] params;
    public BasicKeyValueFilter(Object[] params) { this.params = params; }
    @Override public boolean accept(Integer key, String value, Metadata metadata) {
      return !params[0].equals(key);
    }
  }
}
The filter can now filter by “3” instead of “2”:

Example 7.14. Running an Enhanced Filter Factory

import org.infinispan.client.hotrod.*;

RemoteCacheManager rcm = new RemoteCacheManager();
RemoteCache<Integer, String> cache = rcm.getCache();
BasicFilteredEventLogListener listener = new BasicFilteredEventLogListener();
try {
  cache.addClientListener(listener, new Object[]{3}, null); // <- Filter parameter passed
  cache.putIfAbsent(1, "one");
  cache.replace(1, "new-one");
  cache.putIfAbsent(2, "two");
  cache.replace(2, "new-two");
  cache.putIfAbsent(3, "three");
  cache.replace(3, "new-three");
  cache.remove(1);
  cache.remove(2);
  cache.remove(3);
} finally {
  cache.removeClientListener(listener);
}
Result

The provided example results in the following output:

ClientCacheEntryCreatedEvent(key=1,dataVersion=1)
ClientCacheEntryModifiedEvent(key=1,dataVersion=2)
ClientCacheEntryCreatedEvent(key=2,dataVersion=3)
ClientCacheEntryModifiedEvent(key=2,dataVersion=4)
ClientCacheEntryRemovedEvent(key=1)
ClientCacheEntryRemovedEvent(key=2)
The amount of information sent to clients can be further reduced or increased by customizing remote events.

7.7.4. Customizing Remote Events

In Red Hat JBoss Data Grid, Hot Rod remote events can be customized to contain the information required to be sent to a client. By default, events contain only a basic set of information, such as a key and type of event, in order to avoid overloading the client, and to reduce the cost of sending them.

Note

Deployment of custom listeners on JDG Server is available as Technology Preview in JDG 6.4.
The information included in these events can be customized to contain more information, such as values, or contain even less information. Customization is done via CacheEventConverter instances, which are created by implementing a CacheEventConverterFactory class. Each factory must have a name associated to it via the @NamedFactory annotation.
To plug the JBoss Data Grid Server with an event converter use the following procedure:

Procedure 7.4. Using a Converter

  1. Create a JAR file with the converter implementation within it. Each factory must have a name assigned to it via the org.infinispan.filter.NamedFactory annotation.
  2. Create a META-INF/services/org.infinispan.notifications.filter.CacheEventConverterFactory file within the JAR file and within it, write the fully qualified class name of the converter class implementation.
  3. Deploy the JAR file in the JBoss Data Grid Server.
Converters can also act on client provided information, allowing converter instances to customize events based on the information provided when the listener was added. The API allows converter parameters to be passed in when the listener is added.

7.7.4.1. Adding a Converter

When a listener is added, the name of a converter factory can be provided to use with the listener. When the listener is added, the server looks up the factory and invokes the getConverter method to get a org.infinispan.filter.Converter class instance to customize events server side.
The following example demonstrates sending custom events containing value information to remote clients for a cache of Integers and Strings. The converter generates a new custom event, which includes the value as well as the key in the event. The custom event has a bigger event payload compared with default events, however if combined with filtering, it can reduce bandwidth cost.

Example 7.15. Sending Custom Events

import org.infinispan.filter.*; 
 
@NamedFactory(name = "value-added-converter-factory")
class ValueAddedConverterFactory implements ConverterFactory {
  public Converter<Integer, String, ValueAddedEvent> getConverter(final Object[] params) {
    return new ValueAddedConverter();
  }
 
  static class ValueAddedConverter implements Converter<Integer, String, ValueAddedEvent> {
    public ValueAddedEvent convert(Integer key, String value, Metadata metadata) {
      return new ValueAddedEvent(key, value);
    }
  }
}
 
// Must be Serializable or Externalizable.
class ValueAddedEvent implements Serializable {
    final Integer key;
    final String value;
    ValueAddedEvent(Integer key, String value) {
      this.key = key;
      this.value = value;
    }
}

7.7.4.2. Lightweight Events

Other converter implementations are able to send back events that contain no key or event type information, resulting in extremely lightweight events at the expense of having rich information provided by the event.
In order to plug the server with this converter, deploy the converter factory and associated converter class within a JAR file including a service definition inside the META-INF/services/org.infinispan.filter.ConverterFactory file as follows:
sample.ValueAddedConverterFactor
The client listener must then be linked with the converter factory by adding the factory name to the @ClientListener annotation.
@ClientListener(converterFactoryName = "value-added-converter-factory")
public class CustomEventLogListener { ... }

7.7.4.3. Dynamic Converter Instances

Dynamic converter instances convert based on parameters provided when the listener is registered. Converters use the parameters received by the converter factories to enable this option. For example:

Example 7.16. Dynamic Converter

import org.infinispan.notifications.cachelistener.filter.CacheEventConverterFactory;
import org.infinispan.notifications.cachelistener.filter.CacheEventConverter;

class DynamicCacheEventConverterFactory implements CacheEventConverterFactory {
   public CacheEventConverter<Integer, String, CustomEvent> getConverter(final Object[] params) {
      return new DynamicCacheEventConverter(params);
   }
}

// Serializable, Externalizable or marshallable with Infinispan Externalizers needed when running in a cluster
class DynamicCacheEventConverter implements CacheEventConverter<Integer, String, CustomEvent>, Serializable {
   final Object[] params;

   DynamicCacheEventConverter(Object[] params) {
      this.params = params;
   }

   public CustomEvent convert(Integer key, String value, Metadata metadata, String prevValue, Metadata prevMetadata, EventType eventType) {
      // If the key matches a key given via parameter, only send the key information
      if (params[0].equals(key))
         return new ValueAddedEvent(key, null);

      return new ValueAddedEvent(key, value);
   }
}
The dynamic parameters required to do the conversion are provided when the listener is registered:
RemoteCache<Integer, String> cache = rcm.getCache();
cache.addClientListener(new EventLogListener(), null, new Object[]{1});

7.7.4.4. Adding a Remote Client Listener for Custom Events

Implementing a listener for custom events is slightly different to other remote events, as they involve non-default events. The same annotations are used as in other remote client listener implementations, but the callbacks receive instances of ClientCacheEntryCustomEvent<T>, where T is the type of custom event we are sending from the server. For example:

Example 7.17. Custom Event Listener Implementation

import org.infinispan.client.hotrod.annotation.*;
import org.infinispan.client.hotrod.event.*;

@ClientListener(converterFactoryName = "value-added-converter-factory")
public class CustomEventLogListener {

    @ClientCacheEntryCreated
    @ClientCacheEntryModified
    @ClientCacheEntryRemoved
    public void handleRemoteEvent(ClientCacheEntryCustomEvent<ValueAddedEvent> {
        System.out.println(event);
    }
}
To use the remote event listener to execute operations against the remote cache, write a simple main Java class, which adds the remote event listener and executes some operations against the remote cache. For example:

Example 7.18. Execute Operations against the Remote Cache

import org.infinispan.client.hotrod.*;

RemoteCacheManager rcm = new RemoteCacheManager();
RemoteCache<Integer, String> cache = rcm.getCache();
CustomEventLogListener listener = new CustomEventLogListener();
try {
  cache.addClientListener(listener);
  cache.put(1, "one");
  cache.put(1, "new-one");
  cache.remove(1);
} finally {
  cache.removeClientListener(listener);
}
Result

Once executed, the console output should appear similar to the following:

	ClientCacheEntryCustomEvent(eventData=ValueAddedEvent{key=1, value='one'}, eventType=CLIENT_CACHE_ENTRY_CREATED)
ClientCacheEntryCustomEvent(eventData=ValueAddedEvent{key=1, value='ne-wone'}, eventType=CLIENT_CACHE_ENTRY_MODIFIED)
ClientCacheEntryCustomEvent(eventData=ValueAddedEvent{key=1, value='null'}, eventType=CLIENT_CACHE_ENTRY_REMOVED

Important

Converter instances must be marshallable when they are deployed in a cluster in order for conversion to occur where the event is generated, even if the event is generated in a different node to where the listener is registered. To make them marshallable, either make them extend Serializable, Externalizable, or provide a custom Externalizer for them. Both client and server need to be aware of any custom event type and be able to marshall it in order to facilitate both server and client writing against type safe APIs. On the client side, this is done by an optional marshaller configurable via the RemoteCacheManager. On the server side, this is done by a marshaller added to the Hot Rod server configuration.

7.7.5. Event Marshalling

When filtering or customizing events, the KeyValueFilter and Converter instances must be marshallable. As the client listener is installed in a cluster, the filter and/or converter instances are sent to other nodes in the cluster in order for filtering and conversion to occur where the event originates, improving efficiency. These classes can be made marshallable by having them extend Serializable or by providing and registering a custom Externalizer.

Note

Deployment of custom listeners on JDG Server is available as Technology Preview in JDG 6.4.
To deploy a Marshaller instance server-side, use a similar method to that used for filtering and customized events.

Procedure 7.5. Deploying a Marshaller

  1. Create a JAR file with the converter implementation within it. Each factory must have a name assigned to it via the org.infinispan.filter.NamedFactory annotation.
  2. Create a META-INF/services/org.infinispan.commons.marshall.Marshaller file within the JAR file and within it, write the fully qualified class name of the marshaller class implementation
  3. Deploy the JAR file in the Red Hat JBoss Data Grid Server
The Marshaller can be deployed either in a separate jar, or in the same jar as the CacheEventConverter, and/or CacheEventFilter instances.

Note

Only the deployment of a single Marshaller instance is supported. If multiple marshaller instances are deployed, warning messages will be displayed as a reminder indicating which marshaller instance will be used.

7.7.6. Remote Event Clustering and Failover

When a client adds a remote listener, it is installed in a single node in the cluster, which is in charge of sending events back to the client for all affected operations that occur cluster-wide.
In a clustered environment, when a node containing the listener goes down, the Hot Rod client implementation transparently fails over the client listener registration to a different node. This may result in a gap in event consumption, which can be solved using one of the following solutions.
State Delivery

The @ClientListener annotation has an optional includeCurrentState parameter, which when enabled, has the server send CacheEntryCreatedEvent event instances for all existing cache entries to the client. This allows clients to recompute their state or computation in the event the Hot Rod client transparently fails over registered listeners. The performance of the includeCurrentState parameter is impacted by the cache size, and therefore it is disabled by default.

@ClientCacheFailover

Rather than relying on receiving state, users can define a method with the @ClientCacheFailover annotation, which receives ClientCacheFailoverEvent parameter inside the client listener implementation. If the node where a Hot Rod client has registered a client listener fails, the Hot Rod client detects it transparently, and fails over all listeners registered in the node that failed to another node.

During this failover, the client may miss some events. To avoid this, the includeCurrentState parameter can be set to true. Alternatively, Hot Rod clients can be made aware of failover events by adding a callback handler. This callback method is an efficient solution to handling cluster topology changes affecting client listeners.

Example 7.19. @ClientCacheFailove

import org.infinispan.client.hotrod.annotation.*;
import org.infinispan.client.hotrod.event.*;
 
@ClientListener
public class EventLogListener {
// ...

    @ClientCacheFailover
    public void handleFailover(ClientCacheFailoverEvent e) {
      // Deal with client failover, e.g. clear a near cache.
    }
}