Table of Contents

Overview

The arguments provided by the client-side SingleObjectInvokeAsync caller are delivered to your server-side code in a SingleObjectInvokeArgs object. This section covers how to host your server-side code and efficiently process these server-side method calls.

Handling Invocation Calls

Hosting Server-Side Code

Before issuing a SingleObjectInvoke call, an assembly containing your server-side code must be launched on the system(s) running the ScaleOut service.

The ScaleOut service load balances the delivery of method invocations to all hosts in your cluster, so the server-side process must be started and running on all ScaleOut hosts before invocations are issued. Typically, these worker processes are launched and managed using ScaleOut's Invocation Grid feature. They can also be manually deployed as a long-running Windows Service or Linux daemon.

Registering an Invocation Handler

Server-side event handling code uses the ServiceEvents.SetSingleObjectInvokeHandler method at process startup to register a callback invocation requests.

This callback is invoked repeatedly as invocations arrive, and typically consists of the following steps:

  1. If more than one kind of invocation is being accepted, route the call to different handler logic as needed, typically using the SingleObjectInvokeArgs's OperationId property to discriminate between different types of operations.

  2. Deserialize an argument object in the SingleObjectInvokeArgs's Bytes property, if one exists.

  3. Retrieve the object associated with the invocation from the local ScaleOut service.

  4. Perform any analysis and, optionally, modify the state of the associated object.

  5. Persist the modified state back to ScaleOut service.

  6. Serialize any result from the handler to a byte array and return it from the method. This result object will then be made available to the original SingleObjectInvoke caller through the ResultObject property.

In the sample below, we continue with the stock price history example from the prior topic. It illustrates registration of a basic server-side handler that deserializes an invocation argument (a date), retrieves the associated stock history object from the local ScaleOut service, and queries the object's large dictionary to find the price on the date in question.

static async Task Main(string[] args)
{
    var grid = await GridConnection.ConnectAsync("bootstrapGateways=localhost:721");
    var cache = new CacheBuilder<string, PriceHistory>("stock price histories", grid)
                      .SetKeyEncoder(new ShortStringKeyEncoder())
                      .Build();

    // Provide a callback to inform the ScaleOut service that this local client will
    // handle SingleObjectInvoke calls:
    ServiceEvents.SetSingleObjectInvokeAsyncHandler(cache, async (key, invokeArgs) =>
    {
        if (invokeArgs.OperationId != "Historic price query")
        {
            // Unhandled exceptions like this are returned to the SingleObjectInvoke
            // caller through the returned InvokeResponse.ErrorData property:
            throw new ArgumentException("Unexpected operation requested.");
        }

        DateTime queryDate = invokeArgs.Bytes.ToDateTime();
        decimal historicPrice = -1m;

        // Retrieve the price history from the local ScaleOut service:
        var readResponse = await cache.ReadAsync(key);
        switch (readResponse.Result)
        {
            case ServerResult.Retrieved:
                // Found a history object for this ticker symbol. Find the
                // price for the date in question.
                PriceHistory priceHistory = readResponse.Value;
                priceHistory.ClosingPrices.TryGetValue(queryDate, out historicPrice);
                break;
            case ServerResult.NotFound:
                // No history object for this ticker symbol. Returning -1.
                break;
            default:
                throw new Exception($"Unexpected read response: {readResponse.Result}");
        }

        // An invocation handler can return a byte array, which is returned back to
        // the SingleObjectInvoke caller through the InvokeResponse.ResultObject property.
        return historicPrice.GetBytes();
    });

    // Wait indefinitely for events to arrive.
    Console.ReadLine();
}

The application above performs the following actions:

  1. ServiceEvents.SetSingleObjectInvokeAsyncHandler is called to register a lambda as a callback.

  2. The incoming event payload (assumed to be a DateTime value) is deserialized.

  3. Cache.ReadAsync is called to retrieve the associated stock history from the ScaleOut service.

  4. The object's ClosingPrices dictionary is checked for the price on the specified date. If found, the price is serialized to a byte array and returned to the caller.

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

Important

Your SingleObjectInvoke callback may be executed in parallel for a given key if multiple simultaneous invocations are made. The example above does not modify the PriceHistory object being accessed, so no synchronization is needed. If the object did need to be modified, however, then locking methods on the Cache class (ReadExclusiveAsync, UpdateAndReleaseExclusive, etc.) should be used to protect from race conditions and data loss.