Handling Server-side Methods on a Single Object

The arguments provided by the client-side Cache.singleInvoke caller are delivered to your server-side code in an OperationContext metadata object. This section covers how to host your server-side code and efficiently process these server-side method calls.

Hosting Server-Side Code

Before issuing a singleInvoke call, a JVM running 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.setCacheEntryMethodInvocation() method at process startup to register a callback for invocation requests.

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

  1. Deserialize an argument object from the OperationContext.getParameterObject() property, if one exists.

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

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

  4. Persist the modified state back to ScaleOut service.

  5. 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 singleInvoke caller through InvokeResult.getResult().

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.

// Connect to the cache that stores login times.
GridConnection connection = GridConnection.connect("bootstrapGateways=localhost");
Cache<String, PriceHistory> cache = new CacheBuilder<String, PriceHistory>(connection, "SMISample", String.class)
        .build();

// Assign the cache entry method invocation handler, with a param holder
ServiceEvents.setCacheEntryMethodInvocationHandler(cache, "Historic price query", new CacheEntryMethodInvocationHandler<String, PriceHistory, Long, Double>() {
    @Override
    public Double evaluate(String key, OperationContext<String, PriceHistory, Long> operationContext) {
        long timeToEvaluateMs = operationContext.getParameterObject();
        try {
            CacheResponse<String,PriceHistory> readResponse = operationContext.getCache().read(key);
            if(readResponse.getStatus() == RequestStatus.ObjectRetrieved) {
                PriceHistory historyForKey = readResponse.getValue();
                HashMap<Long,Double> dayToPriceMap = historyForKey.getPriceHistory();
                return dayToPriceMap.getOrDefault(key, 0.0d);
            } else {
                // object not found
                return 0.0d;
            }
        } catch (CacheException e) {
            throw new IllegalStateException("Handler threw an exception", e);
        }
    }

    @Override
    public byte[] serializeResult(Double aDouble) {
        return ByteBuffer.allocate(Double.BYTES).putDouble(aDouble).array();
    }
}, new ParamHolder<Long>() {
    @Override
    public Long getParam(byte[] bytes) {
        return ByteBuffer.wrap(bytes).getLong();
    }
});

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

The application above performs the following actions:

  1. ServiceEvents.setCacheEntryMethodInvocationHandler() is called to register a callback.

  2. cache.read() is called to retrieve the associated stock history from the ScaleOut service.

  3. The object’s price history 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.

Warning

A CacheEntryMethodInvocationHandler 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 (readAndLock, updateAndUnlock, etc.) should be used to protect from race conditions and data loss.