Table of Contents

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.

Note

Arbitrary-length key strings are not currently supported by the ScaleOut service when read-through events are used. Short key strings (26 bytes or less) can be used instead--use the ShortStringKeyEncoder as illustrated below if string keys are needed.

(About the sample: Data provided for free by IEX. View IEX's Terms of Use.)

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using Scaleout.Client;
using Scaleout.Client.KeyEncoding;

class LoadObjectEventHandler
{
    static readonly string connString = "bootstrapGateways=localhost:721";

    static async Task Main(string[] args)
    {
        var conn = await GridConnection.ConnectAsync(connString);
        // Create a cache of stock prices, keyed by ticker symbol (a short string value):
        var priceCache = new CacheBuilder<string, decimal>("Stock Prices", conn)
                                .SetKeyEncoder(new ShortStringKeyEncoder())
                                .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(priceCache, async (ticker) =>
        {
            Console.WriteLine($"Retrieving price for {ticker}.");
            var requestUri = $"https://api.iextrading.com/1.0/stock/{ticker}/price";
            using (var httpClient = new HttpClient())
            {
                var response = await httpClient.GetAsync(requestUri);
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();

                decimal price = decimal.Parse(responseBody);
                return new ValueFactoryResult<decimal>(price);
            }
        });

        Console.WriteLine("Waiting for events to fire.");
        Thread.Sleep(Timeout.Infinite);
    }
}

When the callback is invoked by the ScaleOut service, it is supplied with the key of the object (here, a ticker symbol) whose value needs to be loaded. The callback must return a ValueFactoryResult<TValue> instance that contains the object to be stored in the ScaleOut cache (a stock price).

The ValueFactoryResult<TValue> constructor can also accept a CreatePolicy argument and tag metadata to be associated with the object being stored. Supply a policy to the ValueFactoryResult constructor if you need to fine-tune how the object is stored in the ScaleOut cache--otherwise, the cache's default policies are used.

Note

The ServiceEvents.SetLoadObjectHandler method is overloaded to accept either a synchronous or async backing store callback method.

If the callback returns null or if an unhandled exception is thrown, the API interprets this as a signal that the object does not exist in the backing store. The current value for the key will be deleted and any clients trying to read the value will receive a result of NotFound.

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:

var conn = await GridConnection.ConnectAsync("bootstrapGateways=localhost:721");

var priceCache = new CacheBuilder<string, decimal>("Stock Prices", conn)
                        .SetKeyEncoder(new ShortStringKeyEncoder())
                        .RaiseServiceEventForReadThrough()
                        .Build();

// Triggers event to load object if there's a cache miss.
var result = await priceCache.ReadAsync("AAPL");

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.

Tip

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:

var conn = await GridConnection.ConnectAsync("bootstrapGateways=localhost:721");

var priceCache = new CacheBuilder<string, decimal>("Stock Prices", conn)
                        .SetKeyEncoder(new ShortStringKeyEncoder())
                        .SetBackingStoreEvent(TimeSpan.FromMinutes(1),
                                              BackingStoreMode.RefreshAhead)
                        .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:

var policyBuilder = priceCache.GetPolicyBuilder();
policyBuilder.BackingStoreMode = BackingStoreMode.RefreshAhead;
policyBuilder.BackingStoreEventInterval = TimeSpan.FromMinutes(1);

await priceCache.AddAsync("MSFT", 111.00m, policyBuilder.Build());