Table of Contents

Overview

When objects stored in the ScaleOut distributed data grid are large and complex, a conventional CRUD (Create/Read/Update/Delete) access model can be inefficient when only a small portion of an object's state needs to be accessed. Invoking a method directly on the ScaleOut hosts where the object resides is often faster than pulling the entire object to a client to access its members.

The ScaleOut service provides the SingleObjectInvokeAsync operation, allowing a method to be invoked directly on the server where an object resides. This server-side method can receive an abitrary argument object from the caller and return an arbitrary payload (a "result object") back to the caller.

Conceptually, a SingleObjectInvoke operation is similar to a post event operation, with two important differences:

  1. PostEventAsync operations are handled sequentially for a given key, whereas SingleObjectInvokeAsync operations may execute a hander in parallel to handle multiple simultaneous requests.
  2. PostEventAsync is intended for streaming event scenarios, and the caller will not receive a result value back from the handler. SingleObjectInvokeAsync can return values back to the caller.

Invoking Server-side Methods on a Single Object

Invoking a method on the ScaleOut data grid involves calling the SingleObjectInvokeAsync method (or one of its synchronous variants). The method takes the following parameters:

  • key: The key to the persisted object in ScaleOut service that is to be evaluated by the server-side handler. To minimize network overhead, the event will be sent directly to the ScaleOut host that contains this persisted state object.

  • operationId: An optional, arbitrary string associated with the invocation. This string is typically used to identify the operation to invoke if more than one kind of operation is sent to and handled by the data grid. However, if the operation can be entirely described by a string (say, as a JSON-serialized object) then the operationId string can also serve as the payload of the event.

  • payload: An optional, arbitrary byte array containing the payload of an argument for the handler to use (typically a serialized object).

  • invokeTimeout: The amount of time allowed for the SingleObjectInvoke operation to complete on the server.

Sample Object Type

Consider an application that stores stock histories that can be queried for price values. In this sample, the following PriceHistory type would be defined in a shared class library project so that both client and server-side logic can use it.

[Serializable]
public class PriceHistory
{
    public string TickerSymbol { get; set; }

    public Dictionary<DateTime, decimal> ClosingPrices { get; set; }
}

A client would use a Cache instance to add any number of price history objects to the ScaleOut in-memory data grid. Here we add a single history object:

static async Task AddLargeObjectAsync()
{
    // Create a large object with 10 years of price history.
    int historyDays = 3650;
    var priceHistory = new PriceHistory()
    {
        TickerSymbol = "MSFT",
        ClosingPrices = new Dictionary<DateTime, decimal>(historyDays)
    };
    // Add fake data to ClosingPrices:
    Random rand = new Random();
    for (int i = 0; i < historyDays; i++)
    {
        DateTime historicDate = DateTime.Today - TimeSpan.FromDays(i);
        priceHistory.ClosingPrices.Add(historicDate, 200m * (decimal)rand.NextDouble());
    }

    // Connect to Scaleout service
    var grid = await GridConnection.ConnectAsync("bootstrapGateways=localhost:721");
    var cache = new CacheBuilder<string, PriceHistory>("stock price histories", grid)
                         .SetKeyEncoder(new ShortStringKeyEncoder())
                         .Build();


    // Send this large object to the ScaleOut service:
    var addResponse = await cache.AddOrUpdateAsync(key: priceHistory.TickerSymbol, 
                                                   value: priceHistory);
    // Verify success:
    Debug.Assert(addResponse.Result == ServerResult.Added || addResponse.Result == ServerResult.Updated);
}

Using SingleObjectInvokeAsync

In this sample, the ScaleOut service is storing price history objects, keyed by ticker symbol. Rather than using the traditional method of moving the entire history object over the network to analyze it, we use SingleObjectInvokeAsync to send a method call to the ScaleOut host that holds the stock's history.

static async Task RunMethodOnScaleoutServiceAsync(Cache<string, PriceHistory> cache)
{
    // Invoke a method on the ScaleOut server to find the closing price
    // one year ago.
    DateTime queryDate = DateTime.Today - TimeSpan.FromDays(365);

    // Ticker symbols are the keys to history objects in the cache.
    // Use it as the key for the invoke operation to cause a method to be
    // run on the ScaleOut hosts where the MSFT history resides.
    var response = await cache.SingleObjectInvokeAsync(key: "MSFT",
                                                       operationId: "Historic price query",
                                                       payload: queryDate.GetBytes());

    switch (response.Result)
    {
        case ServerResult.InvokeCompleted:
            Console.WriteLine("Method invoked successfully.");
            decimal price = response.ResultObject.ToDecimal();
            if (price >= 0)
                Console.WriteLine($"{queryDate}: ${price}");
            else
                Console.WriteLine("Market closed or price unavailable.");
            break;
        case ServerResult.UnhandledExceptionInCallback:
            Console.WriteLine("Unhandled exception thrown from handler.");
            Console.WriteLine(Encoding.UTF8.GetString(response.ErrorData));
            break;
        default:
            Console.WriteLine($"Unexpected result: {response.Result}");
            break;
    }
}
Note

The code in the sample above uses custom extension methods to serialize System.DateTime and System.Decimal types. You can find them in Sample Serialization Extension Methods

Unhandled Exceptions

Avoid throwing unhandled exceptions from server-side invocation callbacks whenever possible. In case an exception does escape from your handler, it will be sent back and made available to the SingleObjectInvoke caller through the ErrorData property. The representation and encoding of the error will vary depending on library that is used to handle invoked events; if using this Scaleout.Client library to handle events, the error will be the full "ToString()" text representation of the exception, encoded as UTF-8.