Skip to content

Annotation Support for Pivotal GemFire Functions

John Blum edited this page Sep 10, 2018 · 1 revision

Spring Data GemFire 1.3.0 introduces annotation support to simplify working with GemFire Function Execution. GemFire provides classes to implement and register Functions deployed to cache servers that may be invoked remotely by member applications, typically cache clients. Functions may execute in parallel, distributed among multiple servers, combining results in a map-reduce pattern, or may be targeted to a single server. A Function execution may be also be targeted to a specific region.

GemFire's native function execution framework provides APIs to support remote execution within various defined scopes (region, member groups, servers, etc.), the ability to aggregate results, along with runtime options. The implementation and execution of remote functions, as with any RPC protocol, requires some boilerplate code. Spring Data GemFire, true to Spring's core value proposition, aims to hide the mechanics of remote function execution and allow developers to focus on POJO programming and business logic. To this end, Spring Data GemFire introduces annotations to declaratively register methods as functions, and invoke them remotely via annotated interfaces.

Implementation vs Execution

There are two primary concerns to address. First is the function implementation (server) which must interact with the FunctionContext to obtain the invocation arguments, the ResultsSender and so on. The function typically accesses the Cache and or Region and is typically registered with the FunctionService under a unique Id.

The application invoking a function (the client) does not depend on the implementation. To invoke a function remotely, the application instantiates an Execution providing the function ID, invocation arguments, the function target or scope (region, server, servers, member, members). If the function produces a result, the invoker uses a ResultCollector to aggregate and acquire the execution results. In certain scenarios, a custom ResultCollector implementation is required and may be registered with the Execution.

NOTE: 'Client' and 'Server' are used here in the context of function execution which may have a different meaning then client and server in a client-server cache topology. While it is common for a member with a Client Cache to invoke a function on one or more Cache Server members it is also possible to execute functions in a peer-to-peer configuration

Server Side Requirements

GemFire provides a Function interface to implement a function which is executed via

   void execute(FunctionContext functionContext)  

The FunctionContext provides runtime context including the client's calling arguments and a ResultSender interface to send results back to the client. Additionally, if the function is executed on a Region, the FunctionContext is an instance of RegionFunctionContext which provides additional context such as the target Region and any Filter (set of specific keys) associated with the Execution. If the Region is a Partition Region, the function should use the PartitonRegionHelper to extract only the local data.

In keeping with Spring's core values, it should be possible for a developer to write a simple POJO and have the Spring container bind one or more of it's methods to a Function. The signature for a POJO method targeted for a function must generally conform to the the client's execution arguments. However, in the case of a region execution, the region data must also be provided (presumably to be the local data if the region is a partition region). Additionally the method may require the filter that was applied, if any.

This suggests that the client and server may share a contract for the calling parameters but that the method signature may contain additional information provided by the FunctionContext. One possibility is that the client and server share a common interface, but this is not required. The only constraint is that the method signature includes the same sequence of calling arguments once the additional parameters are resolved. For example, suppose the client provides a String and int as the calling arguments:

  Object[] args = new Object[]{"hello", 123}

Then the Spring container should be able to bind to any method signature similar to the following. Let's ignore the return type for the moment:

    public Object method1(String s1, int i2) {...}
    public Object method2(Map<?,?> data, String s1, int i2) {...}
    public Object method3(String s1, Map<?,?>data, int i2) {...}
    public Object method4(String s1, Map<?,?> data, Set<?> filter, int i2) {...}
    public void method4(String s1, Set<?> filter, int i2, Region<?,?> data) {...}

The general rule is that once any additional arguments, i.e., region data and filter, are resolved the remaining arguments must correspond exactly to the expected calling parameters.

The method's return type must be void or a type that may be serialized (either java.io.Serializable, DataSerializable, or using PDX). The latter is also true of the calling arguments. The Region data should normally specified as a Map but may also be of type Region if necessary.

Server Side Annotations

A POJO targeted for functions uses annotations as illustrated by the following example:

 @Component
 public class MyFunctions {

        @GemfireFunction
        public String function1(String s1, @RegionData Map<?,?> data, int i2) {
             ...
        }

        @GemfireFunction("myFunction", HA=true, optimizedForWrite=true)
        public void function2(String s1, @RegionData Map<?,?> data, int i2, @Filter Set<?> keys) {
            ...
        }

        @GemfireFunction
        public List<String> functionWithContext(FunctionContext functionContext) {
            ...
        }
 } 

These annotations enable the Spring container to create a PojoFunctonWrapper for each method annotated with @GemfireFunction. If the POJO declares multiple functions, each wrapper references the same instance along with the method name which is invoked via reflection. The container must create the instance and inject it into each wrapper instance.

  • NOTE: The POJO must itself be a Spring Bean, indicated in the above example with the @Component class annotation. This has an added advantage that, since it shares an application context with GemFire components, it is possible to inject the Cache or existing Regions as members that may be accessed by the function.*

Once the wrapper class is created, it is registered with the FunctionService. The name of the function, by convention is the same as the method name, if no value is provided by @GemfireFunction. Note that this annotation also provides configuration attributes, 'HA' and 'optimizedForWrite' which are set as properties in the function wrapper. If the method's return type is void, then the 'hasResult' property is automatically set to false. Otherwise it is true.

The PojoFunctionWrapper implements Function and binds the method parameters and invokes the target method in its execute() method. It also sends the method's return value using the ResultSender. If the return type is a Collection or Array, the ResultSender.sendResult() will be invoked for each item. If you need to directly control the ResultSender, provide the FunctionContext to the method.

In accordance with Spring standards, server side annotations must be explicitly enabled. This may be done in XML:

   <gfe:annotation-driven/>

Or in Java Config:

   @EnableGemfireFunctions

Client Side Requirements

A process invoking a remote function needs to specify calling arguments, a function Id, the target (onRegion, onServers, onServer, onMember, onMembers) and optionally a Filter set. In theory, all that is required is an interface definition supported by annotations.

Using Spring's ProxyFactory, the container can create a dynamic proxy for any defined interface which will use the FunctionService to create an Execution, invoke the execution and coerce the results to a defined return type, if necessary.

This technique follows Spring's classic remoting capabilities, such as RmiProxyFactoryBean or HttpInvokerProxyFactoryBean. In this case a single interface definition may map to multiple function executions - one corresponding to each method.

Client Side Function Execution

Using annotated interfaces to generate a dynamic proxy is very similar to the way Spring Data Repositories work. In fact, this follows the same approach. Annotated interfaces are discovered using Spring's classpath scanning mechanism. Any types that match the scanner's criteria (e.g., package name, etc) are automatically proxied using the GemfireFunctionProxyFactoryBean. Instances of this class are injected with an implementation of GemfireFunctionOperations. There are specialized implementations for "onRegion" functions to handle Filters, etc. but the strategy is essentially the same. GemFireFunctionOperations are realized by specialized template classes, such as GemfireOnRegionFunctionTemplate that each extend from AbstractFunctionTemplate where the actual invocation is done.

NOTE: These template classes may be used directly without annotations or proxy interfaces and do not require the Function to be registered.

Client Side Annotations

To support client side function execution, the following annotations are provided: @OnRegion, @OnServer, @OnServers, @OnMember, @OnMembers. These correspond to the different template types and also to the Execution implementations provided GemFire's FunctionService and each exposes the appropriate attributes. Also each annotation provides an optional 'resultCollector' attribute which is the name of a Spring bean implementing ResultCollector to use for the execution.

*NOTE: The proxied interface binds all declared methods to the same execution configuration. Although it is expected that single method interfaces will be common, all methods in the interface are backed by the same proxy instance and therefore the same template instance.

Here are some examples:

   @OnRegion(region="someRegion", resultCollector="myCollector")
   public interface FunctionExecution {
        @FunctionId("function1")
        public String doIt(String s1, int i2);
        public String getString(Object arg1, @Filter Set<Object> keys) ;
   }

Note the @FunctionId annotation is optional. By convention the method name will be the function id. Also the @Filter annotation may be provided to invoke the function with a filter.

  @OnServer(id="testClientOnServerFunction",pool="gemfirePool",resultCollector="myResultCollector")
  public interface ServerFunctions {
       ...
  }

If an 'id' is specified, it will be the id of the Proxy. Normally, it is a generated name. Other object references are the bean ids. In the above example 'myResultCollector' is a registered Spring bean.

Again, these annotations must be explicitly enabled in XML:

 <gfe-data:function-executions base-package="org.example.myapp.functions"/>

Or in Java:

  @EnableGemfireFunctionExecutions(basePackages = "org.example.myapp.functions")

The interface is injected into an application class:

 @Component
 public class MyApp {
     
    @Autowired FunctionExecution functionExecution;
    
    public void doSomething() {
         functionExecution.doIt("hello", 123);
    }

 }