Chapter 14. Clustered Counters

Clustered counters are distributed and shared across nodes in a Red Hat JBoss Data Grid cluster. Clustered counters allow you to record the count of objects.

Clustered counters are identified by their names and are initialized with a value, which defaults to 0. Clustered counters can also be persisted so that the values are kept after cluster restarts.

There are two types of clustered counter:

  • Strong stores the counter value in a single key for consistency. During updates to the counter, the value is known. Updates to the counter value are performed under the key lock. However, reads of the current value of the counter do not acquire any lock. Strong counters allow the counter value to be bounded and provide atomic operations such as compareAndSet or compareAndSwap.
  • Weak stores the counter value in multiple keys. Each key stores a partial state of the counter value and can be updated concurrently. During updates to the counter, the value is not known. Retrieving the counter value does not always return the current, up to date value.

Both strong and weak clustered counters support updating the counter value, return the current value of a counter, and provide events when a counter value is updated.

14.1. The Counter API

The counter API consists of the following:

  • EmbeddedCounterManagerFactory initializes a counter manager from an embedded cache manager.
  • RemoteCounterManagerFactory initializes a counter manager from a remote cache manager.
  • CounterManager provides methods to create, define, and return counters.
  • StrongCounter implements strong counters. This interface provides atomic updates for a counter. All operations are performed asynchronously and use the CompletableFuture class for completion logic.
  • SyncStrongCounter implements synchronous strong counters.
  • WeakCounter implements weak counters. All operations are performed asynchronously and use the CompletableFuture class for completion logic.
  • SyncWeakCounter implements synchronous weak counters.
  • CounterListener listens for changes to strong counters.
  • CounterEvent returns events when changes to strong counters occur.
  • Handle extends the CounterListener interface.

14.2. Adding Maven Dependencies

To start using clustered counters, add the following dependency to pom.xml:

pom.xml

<dependency>
   <groupId>org.infinispan</groupId>
   <artifactId>infinispan-clustered-counter</artifactId>
   <version>...</version> <!-- 7.2.0 or later -->
</dependency>

14.3. Retrieving the CounterManager Interface

To use clustered counters in Red Hat JBoss Data Grid embedded mode, do the following:

// Create or obtain an EmbeddedCacheManager.
EmbeddedCacheManager manager = ...;

// Retrieve the CounterManager interface.
CounterManager counterManager = EmbeddedCounterManagerFactory.asCounterManager(manager);

To use clustered counters with a Hot Rod client that interacts with a Red Hat JBoss Data Grid remote server, do the following:

// Create or obtain a RemoteCacheManager.
RemoteCacheManager manager = ...;

// Retrieve the CounterManager interface.
CounterManager counterManager = RemoteCounterManagerFactory.asCounterManager(manager);

14.4. Using Clustered Counters

You can define and configure clustered counters in the cache-container XML configuration or programmatically.

14.4.1. XML Configuration for Clustered Counters

The following XML snippet provides an example of a clustered counters configuration:

<?xml version="1.0" encoding="UTF-8"?>
<infinispan>
    <cache-container>
        <!-- cache container configuration goes here -->
        <!-- cache configuration goes here -->
        <counters>
             <strong-counter name="counter-1" initial-value="1">
                 <upper-bound value="10"/>
             </strong-counter>
             <strong-counter name="counter-2" initial-value="2"/>
             <weak-counter name="counter-3" initial-value="3"/>
         </counters>
    </cache-container>
</infinispan>

14.4.1.1. XML Definition

The counters element configures counters for a cluster and has the following attributes:

  • num-owners sets the number of copies of each counter to store across the cluster. A smaller number results in faster update operations but supports a lower number of server crashes. The value must be a positive number. The default value is 2.
  • reliability sets the counter update behavior in a network partition and takes the following values:

    • AVAILABLE all partitions can read and update the value of the counter. This is the default value.
    • CONSISTENT the primary partition can read and update the value of the counter. The remaining partitions can only read the value of the counter.

The strong-counter element creates and defines a strong clustered counter. The weak-counter element creates and defines a weak clustered counter. The following attributes are common to both elements:

  • initial-value sets the initial value of the counter. The default value is 0.
  • storage configures how counter values are stored. This attribute determines if the counter values are saved after the cluster shuts down and restarts. This attribute takes the following values:

    • VOLATILE stores the value of the counter in memory. The value of the counter is discarded when the cluster shuts down. This is the default value.
    • PERSISTENT stores the value of the counter in a private, local persistence store. The value of the counter is saved when the cluster shuts down and restarts.

Attributes specific to the strong-counter element are as follows:

  • lower-bound sets the lower bound of a strong counter. The default value is Long.MIN_VALUE.
  • upper-bound sets the upper bound of a strong counter. The default value is Long.MAX_VALUE.
Note

The value of the initial-value attribute must be between the lower-bound value and the upper-bound value. If you do not specify a lower and upper bound for a strong counter, the counter is not bounded.

Attributes specific to the weak-counter element are as follows:

  • concurrency-level sets the maximum number of concurrent updates to the value of a counter. The value must be a positive number. The default value is 16.

14.4.2. Run-time Configuration of Clustered Counters

You can configure clustered counters on-demand at run-time after the EmbeddedCacheManager is initialized, as in the following example:

CounterManager manager = ...;

// Create three counters.
// The first counter is a strong counter bounded to 10.
manager.defineCounter("counter-1", CounterConfiguration.builder(CounterType.BOUNDED_STRONG).initialValue(1).upperBound(10).build());

// The second counter is an unbounded strong counter.
manager.defineCounter("counter-2", CounterConfiguration.builder(CounterType.UNBOUNDED_STRONG).initialValue(2).build());

// The third counter is a weak counter.
manager.defineCounter("counter-3", CounterConfiguration.builder(CounterType.WEAK).initialValue(3).build());

The defineCounter() method returns true if the counter is defined successfully or false if not. If the counter configuration is not valid, a CounterConfigurationException exception is thrown.

Tip

Use the isDefined() method to determine if a counter is already defined, as in the following example:

CounterManager manager = ...
if (!manager.isDefined("someCounter")) {
    manager.define("someCounter", ...);
}

14.4.3. Programmatic Configuration of Clustered Counters

The following code sample illustrates how to configure clustered counters programmatically with the GlobalConfigurationBuilder:

// Set up a clustered cache manager.
GlobalConfigurationBuilder global = GlobalConfigurationBuilder.defaultClusteredBuilder();

// Create a counter configuration builder.
CounterManagerConfigurationBuilder builder = global.addModule(CounterManagerConfigurationBuilder.class);

// Create three counters.
// The first counter is a strong counter bounded to 10.
builder.addStrongCounter().name("counter-1").upperBound(10).initialValue(1);

// The second counter is an unbounded strong counter.
builder.addStrongCounter().name("counter-2").initialValue(2);

// The third counter is a weak counter.
builder.addWeakCounter().name("counter-3").initialValue(3);

// Initialize a new default cache manager.
DefaultCacheManager cacheManager = new DefaultCacheManager(global.build());

14.4.3.1. Using Clustered Counters

The following code example illustrates how you can use clustered counters that you create and define programmatically:

// Retrieve the CounterManager interface from the cache manager.
CounterManager counterManager = EmbeddedCounterManagerFactory.asCounterManager(cacheManager);

// Strong counters provide greater consistency than weak counters.
// The value of a strong counter is known during an increment or decrement operation.
// The value of a strong counter can also be bounded in cases where a limit is required.
StrongCounter counter1 = counterManager.getStrongCounter("counter-1");

// All methods are asynchronous and return CompletableFuture objects so that you can perform other operations while the counter value is computed.
counter1.getValue().thenAccept(value -> System.out.println("Counter-1 initial value is " + value)).get();

// Attempt to add a value that exceeds the upper-bound value.
counter1.addAndGet(10).handle((value, throwable) -> {
    // Value is null since the counter is bounded to a maximum of 10.
    System.out.println("Counter-1 Exception is " + throwable.getMessage());
    return 0;
}).get();

// Check the counter value. The value should be 10.
counter1.getValue().thenAccept(value -> System.out.println("Counter-1 value is " + value)).get();

//Decrement the counter value. The new value should be 9.
counter1.decrementAndGet().handle((value, throwable) -> {
    // No exception is thrown.
    System.out.println("Counter-1 new value is " + value);
    return value;
}).get();

// The second counter, counter2, is a strong counter that is unbounded. It never throws the CounterOutOfBoundsException.
StrongCounter counter2 = counterManager.getStrongCounter("counter-2");

// All counters allow a listener to be registered.
// The handle interface can remove the listener.
counter2.addListener(event -> System.out
    .println("Counter-2 event: oldValue=" + event.getOldValue() + " newValue=" + event.getNewValue()));

// Adding MAX_VALUE does not throw an exception.
// No increments take effect if the value exceeds the MAX_VALUE.
counter2.addAndGet(Long.MAX_VALUE).thenAccept(aLong -> System.out.println("Counter-2 value is " + aLong)).get();

// Conditional operations are allowed in strong counters.
counter2.compareAndSet(Long.MAX_VALUE, 0)
    .thenAccept(aBoolean -> System.out.println("Counter-2 CAS result is " + aBoolean)).get();
counter2.getValue().thenAccept(value -> System.out.println("Counter-2 value is " + value)).get();

// Reset the value of the second counter to its initial value.
counter2.reset().get();
counter2.getValue().thenAccept(value -> System.out.println("Counter-2 initial value is " + value)).get();

// Retrieve the third counter, counter3.
WeakCounter counter3 = counterManager.getWeakCounter("counter-3");

// The value of weak counters is not available during update operations. As a result these counters can increment faster than strong counters.
// The counter value is computed lazily and stored locally.
counter3.add(5).thenAccept(aVoid -> System.out.println("Adding 5 to counter-3 completed!")).get();

// Check the counter value.
System.out.println("Counter-3 value is " + counter3.getValue());

// Stop the cache manager and release all resources.
cacheManager.stop();