Backing Store

Read-Through and Refresh-Ahead

Read-through and refresh-ahead events are responsible for retrieving an object from a backing store (a database, web service, or some other persistent/expensive system of record) and loading it into the ScaleOut service for fast access.

  • Read-through event: An event that is fired when a cache miss occurs, where custom code retrieves the object to be loaded into the ScaleOut cache

  • Refresh-ahead event: Periodically refreshes an object in the ScaleOut cache. An event is scheduled to regularly run custom code that retrieves an object from a system of record and update the ScaleOut cache with the latest version of an object

While these events address different use cases, this topic will cover both types of events because the API usage is nearly identical–in both cases, the event handler is retrieving an object from a backing store.

Registering an Event Handler

The following example illustrates how to register a “load object” event handler using ServiceEvents.setLoadObjectHandler. This handler contains custom code that is responsible for retrieving objects from a data source. In this case, the IEX web service is called to retrieve the stock price associated with a ticker symbol.

package com.scaleout.client.samples.events;

import com.scaleout.client.GridConnection;
import com.scaleout.client.ServiceEvents;
import com.scaleout.client.caching.*;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ReadThroughSample {
        public static void main(String[] args) throws Exception {
                GridConnection connection = GridConnection.connect("bootstrapGateways=server1:721,server2:721;");
                Cache<String, Double> cache = new CacheBuilder<String, Double>(connection, "example", String.class)
                                .build();

                // Inform the ScaleOut service that this client will be processing read-through
                // and/or refresh-ahead events by providing a lambda callback for priceCache:
                ServiceEvents.setLoadObjectHandler(cache, (ticker) -> {
                        System.out.printf("Retrieving price for %s.\n", ticker);
                        try {
                                String requestUri = String.format("https://api.iextrading.com/1.0/stock/%s/price", ticker);
                                HttpClient client = HttpClient.newHttpClient();
                                HttpRequest request = HttpRequest.newBuilder()
                                                .uri(URI.create(requestUri))
                                                .header("accept", "application/json")
                                                .POST(HttpRequest.BodyPublishers.ofString("clear"))
                                                .build();
                                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                                return new ValueFactoryResult<>(Double.parseDouble(response.body()));
                        } catch (Exception e) {
                                return new ValueFactoryResult<>(null);
                        }
                });

                System.out.println("Waiting for events...");
                System.out.println("Press any key to exit.");
                System.in.read();
        }
}

Deployment

As a best practice, applications that register handlers with the ServiceEvents class typically run event-handling code like the sample above in long-running, dedicated processes (for example, a Windows Service or a Linux daemon) that reside locally on each machine hosting the ScaleOut service. See Expiration Events for more information about deployment considerations.

Using Read-Through Events

Once a “load object” event handler has been registered, client applications can use it to perform transparent read-through operations. Clients that want to raise the event when there is a cache miss must configure their cache through the CacheBuilder as follows:

GridConnection connection = GridConnection.connect("bootstrapGateways=server1:721,server2:721;");
Cache<String, Double> cache = new CacheBuilder<String, Double>(connection, "example", String.class)
        .raiseServiceEventForReadThrough()
        .build();

When a cache is configured in this manner, any read operation that would ordinarily cause a cache miss will instead cause the “load object” event to fire. Readers will wait for the object to be loaded by the event handler and will return the result once the handler has completed.

Note

A read-through event handler is allowed to return null to signal that the requested object does not exist in a backing store. However, if repeated attempts are made to read the same missing object, it can be inefficient to continually fire the read-through handler. In this situation, consider having your event handler return a placeholder value (or “tombstone”) to be stored in the cache. The placeholder’s presence will prevent the event from firing, and readers can watch for the tombstone value and treat it the same as a cache miss. Use the tombstone’s lifetime to control how often the backing store is checked for that particular key.

Using server events to perform read-throughs as described here will work well in situations where the clients in your application tier are unable to access the backing store–you can have a dedicated tier of read-through event handlers (often running directly on back-end ScaleOut hosts) that are responsible for populating the cache.

However, in situations where the application tier does have access to the backing store, you may opt for a simpler architecture that does not rely on server events to perform read-through operations. The Cache.readOrAddAsync family of methods accept a valueFactory callback directly in their signatures, allowing for read-through callbacks to execute directly on the clients attempting the cache read.

Using Refresh-Ahead Events

A cache can be configured to use the “load object” event handler to periodically refresh its contents from a backing store:

GridConnection connection = GridConnection.connect("bootstrapGateways=server1:721,server2:721;");
Cache<String, PlayerStats> cache = new CacheBuilder<String, PlayerStats>(connection, "example", String.class)
        .backingStoreEventInterval(Duration.ofSeconds(5))
        .backingStoreMode(BackingStoreMode.WriteBehind)
        .build();

As an alternative to a cache-wide configuration, an individual object can be configured for periodic refresh by supplying the appropriate cache policy when the object is added to the cache:

PlayerStats playerStats = new PlayerStats();
CreatePolicy policy = new CreatePolicyBuilder()
        .setBackingStoreEventInterval(Duration.ofMinutes(1))
        .setBackingStoreMode(BackingStoreMode.RefreshAhead)
        .build();
cache.add("MSFT", 111.00d, policy);

Write Behind Events

When an object’s state needs to be persisted to a backing store such as a database or web service, write-behind events can be used to write an object’s changes on a scheduled basis. This feature allows you to decrease database load by consolidating writes to your database instead of making a round trip to it every time a cached object is modified.

Use the ServiceEvents.setStoreObjectHandler method to register a “store object” handler for write-behind events.

The ScaleOut service will only fire a write-behind event for an object if it has been modified since the last write-behind event occurred. This further reduces load on your database by eliminating unnecessary writes.

In addition to persisting modifications to an object, the service will fire an erase-behind event when an object is removed from the ScaleOut cache. Register a handler using ServiceEvents.setEraseObjectHandler to be notified of removals.

package com.scaleout.client.samples.events;

import com.scaleout.client.GridConnection;
import com.scaleout.client.ServiceEvents;
import com.scaleout.client.caching.*;

public class WriteBehindSample {
        static class PlayerStats{
                public int wins;
                public int losses;
        }
        public static void main(String[] args) throws Exception {
                GridConnection connection = GridConnection.connect("bootstrapGateways=server1:721,server2:721;");
                Cache<String, PlayerStats> cache = new CacheBuilder<String, PlayerStats>(connection, "example", String.class)
                                .build();

                // Inform the ScaleOut service that this client will be processing write-behind
                // events by providing a lambda callback for the cache:
                ServiceEvents.setStoreObjectHandler(cache, (playerId, playerStats) -> {
                        int playerWins = playerStats.wins;
                        int playerLosses = playerStats.losses;
                        // (...remainder of DB code elided.)

                        System.out.println("Write behind.");
                });

                // Also register a handler for erase-behind events to mark a player as 'offline'
                // in the database upon removal from the ScaleOut cache:
                ServiceEvents.setEraseObjectHandler(cache, (playerId) -> {
                        // UPDATE players SET status = "offline" where player_id = ...
                        System.out.println("Erase behind.");
                });

                System.out.println("Waiting for events...");
                System.out.println("Press any key to exit.");
                System.in.read();
        }
}

Deployment

As a best practice, applications that register handlers with the ServiceEvents class typically run event-handling code like the sample above in long-running, dedicated processes (for example, a Windows Service or a Linux daemon) that reside locally on each machine hosting the ScaleOut service.

Configuring Objects for Write-Behind/Erase-Behind

GridConnection connection = GridConnection.connect("bootstrapGateways=server1:721,server2:721;");
Cache<String, PlayerStats> cache = new CacheBuilder<String, PlayerStats>(connection, "example", String.class)
        .backingStoreEventInterval(Duration.ofSeconds(5))
        .backingStoreMode(BackingStoreMode.WriteBehind)
        .build();

As an alternative to a cache-wide configuration, an individual object can be configured for periodic write-behind/erase-behind by supplying the appropriate cache policy when the object is added to the cache:

PlayerStats playerStats = new PlayerStats();
CreatePolicy policy = new CreatePolicyBuilder()
        .setBackingStoreEventInterval(Duration.ofSeconds(5))
        .setBackingStoreMode(BackingStoreMode.WriteBehind)
        .build();
cache.add("player_id", playerStats, policy);