Integration and Interoperability Facilities Framework: Client Libraries Framework

From Gcube Wiki
Revision as of 15:53, 22 May 2012 by Fabio.simeoni (Talk | contribs) (Delegates)

Jump to: navigation, search

gCube includes client libraries for many of its services and defines a general model for their design. The model requires that all libraries offer a common set of capabilities and adopt uniform patterns for the design of their APIs, regardless of service semantics and technology stacks. The model, however, does not indicate how capabilities and patterns should be implemented, nor does it mandate low-level API details.

The client library framework supports the implementation of client libraries which comply with the model. Through code sharing, the framework reduces development costs for client libraries and ensures the consistency and correctness of their implementations.

In this document, we assume familiarity with the design model and illustrate how the framework can be used to develop a model-compliant client library for a hypothetical foo service.

Distribution

The framework is layered across as a set of components, all of which are available in our Maven repositories as artifacts in the org.gcube.core group.

common-clients is the top layer of the framework and comprises classes and interfaces that do not depend on particular technology stacks. In this sense, common-clients is as general as the design model. Lower layers of the framework adapt common-clients to specific stacks. At the time of writing, gCore is the dominant technology stack for gCube services and their clients. common-gcore-clients is thus the only available specialisation of common-clients.

We assume accordingly that foo is a gCore service, i.e. a JAX-RPC Java service that can be deployed in one or more gCore containers on some gCube hosting nodes. We also assume that the client library for foo is developed as a Maven project, in line with system recommendations. To use the framework, the library declares a compile-time dependency on common-gcore-clients in its POM, as follows:

<dependency>
 <groupId>org.gcube.core</groupId>
 <artifactId>common-gcore-clients</artifactId>
 <version>...</version>
 <scope>compile</version>
</dependency>

This dependency brings common-gcore-clients and its transitive dependencies, including common-clients, on the compile-time classpath of the library. The version will vary over time and is 2.0.0 at the time of writing.

The library depends also on the stub library of foo, which we also assume available as a Maven artifact, e.g.:

<dependency>
 <groupId>org.gcube.samples</groupId>
 <artifactId>foo-stubs</artifactId>
 <version>...</version>
 <scope>compile</version>
</dependency>


Framework-dependencies.png

Overview

We consider first the requirements that the model raises against client libraries. This illustrates the challenges faced by client libraries to achieve compliance with the model, hence the likelihood of variations in style and quality across their implementations. We then overview the support offered by the framework towards meeting those challenges in a consistent and cost-effective manner.

Implementation Requirements

The design model for client libraries mandates the use of service proxies. The library represents foo with an interface Foo and its default implementation DefaultFoo. Foo defines methods that correspond to the remote operations of foo endpoints, and DefaultFoo implements the methods against the lower-level API of foo stubs.

For example, if FooPortType is the type of foo stubs and bar() one of their String-valued methods, the proxy pattern maps onto code of this form:

public interface Foo { 
  String bar() throws ...; 
}
 
public class DefaultFoo implements Foo { 
  public String bar() throws ... { 
   ...FooPortType endpoint...
 
   try {
 
     return endpoint.bar();
 
   }
   catch(...) { //fault handling
 
   }
 
}

In itself, the pattern is straightforward. Some complexity may arises from the design requirements of particular Foo methods, including particular types inputs or outputs (e.g. e.g. streams)), faults with diverse semantics (e.g. outages vs. contingencies), and particular invocation semantics (e.g. asynchronous)). The framework offer limited support here, as the design directives provided by the model do not require it on average. Where they do, the model indicates dedicated gCube libraries that provide it (e.g. the streams library, the scope library, or the security library). The framework does include the classes and interfaces upon which its directives are based, however. We discuss these components and their placement within the framework here.

Arguably, the strongest demand that the model makes on the client library concerns how Foo proxies bind to service endpoints. The requirement is for two binding modes:

  • in direct mode, the proxies obtain the address of given service endpoints from clients and execute all their methods against those endpoints;
  • in discovery mode, the proxies identify service endpoints from queries to gCube discovery services;

Implementing direct mode is fairly simple, as clients provide all the binding information. They model addresses as W3CEndpointReferences or - depending on wether foo is a stateless or stateful service - as (host, port) pairs or (host, port, key) triples. As stubs APIs for gCore services model addresses as EndpointReferenceTypes, the library is required to implement address conversion and address validation. Though conceptually simple, the task is error-prone and sufficiently boilerplate to call for reuse through the framework.

Implementing discovery mode is significantly more complicated, as the proxies are responsible for using query results in a fault-tolerant and optimised manner. The model requires that the library implements binding and caching strategies which depend on correct handling of a variety of different fault types. Queries must be value objects that hide the lower-level idioms of query formulation and submission required by the gCube discovery services.

Since the two modes are markedly different, combining them in a single proxy implementation presents its own challenges. In particular, it becomes difficult to implement Foo’s methods uniformly, regardless of the binding mode of proxy instances. Lack of homogeneity extends to proxy configuration and threatens the overall testability of the code. Solutions to these problems are likely to vary in style and quality across client libraries.

Framework Support

We now give a tour of the support offered by the framework towards meeting the implementation challenges discussed above. We expand on the role and use of individual framework components in later sections.

The key contribution of the framework comes in the form of ProxyDelegates, i.e. components that know how to make calls in a given mode on behalf of proxies. The idea is that the library defines explicit Call objects and its proxies pass them to the delegates for execution.


Framework-delegates.png


With this pattern, Foo proxies can be implemented as follows:

public class DefaultFoo implements Foo {
 
  private ProxyDelegate<FooPortType> delegate; 
  public void bar() throws ... {
 
   Call<FooPortType,String> barCall = ... 
   try {
 
     delegate.make(barCall); 
   }
   catch(...) { //fault handling
 
   }  
   ...
}

Call<c/ode>s are anonymous implementations of a simple callback interface:

Call<FooPortType,String> barCall = new Call<FooPortType,String>() {
 @Override
 public String call(FooPortType endpoint) {  return endpoint.bar(); }}

We discuss <code>Calls in more detail here.

Delegates make the callback above, providing the required stub instance fully configured with the address of the target service endpoint, the scope associated with the current thread, and any other call-specific information. Foo proxies need not concern themselves with how the delegate discovers and/or binds to endpoints. They can implement their methods uniformly against the delegate. We discuss ProxyDelegate</code>s in more detail here.

Of course, delegates need to be configured to act on behalf of Foo proxies. The main piece of configuration is a ProxyPlugin object that implements an interface of callback methods. The delegates will consult the plugin to obtain information and services which are specific to theclient library. They will use this information to adapt their binding strategies to foo endpoints.

public class FooPlugin implements Plugin<FooPortType,Foo> {
  ...
}

ProxyPlugins are thus the main point of interface between the framework and the client library. We discuss their callbacks in detail here.


Framework-plugins.png


Besides ProxyPlugins, delegates need configuration specific to the mode in which they are to operate. Delegates that operate in direct mode needs given endpoint addresses, and delegates that operate in discovery mode need queries. Some of the required configuration is provided by Foo clients, other is be provided by the client library.


Framework-config.png


Building and configuring delegates does not need to fall upon the client library either. The library can use builders provided by the framework instead, a StatelessBuilder if foo is stateless and a StatefulBuilder if foo is stateful. The library needs only to create these builders on behalf of its clients, ideally from a static factory method that can be conveniently imported by clients. Assuming a a stateless foo for example, the library can expose builders as follows:

public class FooProxies {
 
  private static final FooPlugin plugin = new FooPlugin(); 
  public static StatelessBuilder<FooPortType,Foo> foo() {    return new StatelessBuilder<FooPortType,Foo>(plugin);  }}

Here, the library creates builders with its ProxyPlugin. The builders will then:

  • gather the required configuration from the clients, using a fluent and statically typed API;
  • create and configure a delegate with the ProxyPlugin<code> and the configuration provided by clients. Different forms of configurations result in delegates that work in direct or in discovery mode;
  • collaborate with the <code>ProxyPlugin to give back to clients Foo proxies configured with the delegate;

For example, library clients may use the StatelessBuilder above as follows:

import static ...FooProxies.*;
...
Foo proxy = foo().at(“acme.org”,8080).build();

Since the client is providing the address of a given endpoint here, the builder creates a Foo proxy that uses a delegate which makes calls in direct mode to that endpoint. On the other hand, if the client uses the DSL as follows:

Foo proxy = foo().build();

the builder creates a Foo proxy that uses a delegate which makes calls in discovery mode. Thus clients are fluently driven towards the proxies they need, and the library can implement its proxies ignoring configuration and binding mode issues.


Framework-builders.png


The builders can also gather additional configuration required by the model (e.g. timeouts), as well as configuration which is specific to the client library. We discuss these possibilities in detail here.

If foo is stateful, the DSL of StatefulBuilders makes room for the configuration of instance queries. Again, the framework provides StatefulQuerys for foo instances and the client library needs only to customise the queries and return them to clients. For example:

public class FooProxies {
  private static final FooPlugin plugin = new FooPlugin();
  ...
  public static StatefulQuery name(String name) {    StatefulQuery query = new StatefulQuery(plugin);    query.addCondition(//Name”,name);    return query;  }}

Here the library exposes queries for service instances that verify a given condition, using XPath to reach within the instance descriptions published within the system. Clients can then embed queries in the DSL of builders, e.g. as follows:

import static ...FooProxies.*;
...
Foo proxy = foo().matching(name(“..”)).build();

We discuss queries in more detail here.

Calls

A call to a foo endpoint is represented in the framework as an object of type Call, where Call is an interface defined in common-clients as follows:

package org.gcube.common.clients;
 
public interface Call<S, R> {
 
	R call(S endpoint) throws Exception;
}

The type parameters describe, respectively, the service stubs and the values returned by the call. Thus a Call to foo endpoints that returns String values is typed as Call<FooPortType,String>.

For its simplicity, Call lends itself to anonymous implementations within proxy classes, e.g.:

public class DefaultFoo implements Foo {
  ...
  @Override
  public void bar() throws ... {
 
   Call<FooPortType,String> barCall = new Call<FooPortType,String>() {      @Override      public String call(FooPortType endpoint) {        return endpoint.bar();    };       ...
 }
}

Alternatively, Calls can be returned from factory methods of a dedicated class (e.g. FooCalls), though the approach is verbose and should be pursued only when implementations are substantial. This should rarely be the case, however, since Calls are expected to do little more than delegate to stub instances. In particular, Calls should not:

  • include code that converts inputs and outputs between the stub API and proxy API. The task may fall within the scope of Calls but it is a better practice to factor conversion code outside Call classes, and in fact proxy classes, where it can also be more easily unit tested. Wether the conversion is performed by static methods of some utility class (e.g. SomeStubType Utils.convert(SomeProxyType)) or in more object-oriented fashion (e.g. SomeStubType SomeProxyType.toStub()), Calls should limit themselves to invoke conversion code before and after delegation;
  • engage in failure handling and let error propagate outside the scope of the call()<code> method, which is intentionally designed to throw any <code>Exception. Failures should be handled instead in proxy classes, as Calls are passed to ProxyDelegates for execution, as we discuss next;
  • set timeouts on stubs instances, or else proxy them in order to set a scope on the outgoing calls. As we shall see, these services are offered transparently by the framework.

Delegates

ProxyDelegates implement a strategy for making Calls to foo endpoints on behalf of proxies. Strategies are encapsulated within delegates and proxies need not concern with their details. The ProxyDelegate interface is thus defined as follows:

package org.gcube.common.clients.delegates;
import ...
 
public interface ProxyDelegate<S> {
	<V> V make(Call<S, V> call) throws Exception;
	ProxyConfig<?,S> config();
}

The type parameter describes the service stubs used in Calls. Thus a ProxyDelegate for Foo proxies is typed as ProxyDelegate<FooPortType>.

The config() method of the interface exposes the configuration of the delegate, an object of type ProxyConfig<code> defined as follows:

package org.gcube.common.clients.config;
import ....;
 
public interface ProxyConfig<A,S> {
 ProxyPlugin<A,S,?> plugin();
 int timeout();
 void addProperty(String name, Object value);
 void addProperty(Property property);
 boolean hasProperty(String property);
 <T> T property(String property, Class<T> clazz)throws IllegalStateException, IllegalArgumentException;
}

Thus delegates expose three pieces of configuration:

  • the <code>ProxyPlugin of the client library. As we discuss in more detail later, plugins provide delegates with the information they require to adapter their strategy to the target services. This piece of configuration is typically of less relevance to proxies, which normally do not need to access library-specific information and, when they do, can always obtain them through direct means (e.g. exposing singleton plugins as constants, or returning non-singleton plugins from factory methods in utility classes);
  • the timeout of calls. As we will see later, timeouts may be either defined by defaults in the framework or the client library, or they may be explicitly configured by clients at proxy creation time. Like ProxyPlugins, timeouts are set directly by ProxyDelegates and remain transparent to proxies;
  • zero or more Propertys, i.e. arbitrary named objects that capture the custom configuration of proxies. When we discuss builders, we show how client libraries can define defaults for such properties as well as accept clients overrides. Accordingly, this is the only piece of configuration that relates directly to proxies rather than ProxyDelegates. The delegates will ignore its meaning and will not use it, but they will make it conveniently available to proxies as part of their configuration. The ProxyConfig interface allows clients to add properties in a couple of different forms, inspect the configuration for the existence of given properties, and access given properties under their specific type.

Notice that, since ProxyDelegates carry the custom configuration of proxies, proxies are fully configured with delegates. This allows proxy classes to resolve initialisation in a minimal manner:

public class DefaultFoo implements Foo {
 
  private final ProxyDelegate<FooPortType> delegate; 
  public DefaultFoo(ProxyDelegate<FooPortType> delegate) {    this.delegate=delegate;  }}


The key method of ProxyDelegate is make(), which proxies invoke to delegate the execution of their Calls. The method is parametric in the output type of Calls, i.e. can be passed any Call that expects a stub instance of the right type.

The method is executed differently by different implementations of the interface. Unsurprisingly, the implementations mirror the binding model required by the design model for client libraries. DirectDelegate makes Calls in direct mode, and DiscoveryDelegate makes Calls in discovery mode. Different delegates require different configurations too. DirectDelegate expects an EndpointConfig with a given endpoint address, while DiscoveryDelegate expects a DiscoveryConfig with a Query and an EndpointCache of endpoint addresses. Caches are used internally by DiscoveryDelegates and client libraries need not be aware of their existence (though we will see later on that they can provide specific cache implementations, if they wish). We discuss queries in detail in a later section.


Framework-classes.png

 All delegates, however, perform make the following callbacks on the ProxyPlugin found in their configuration:

  • given an address of a foo endpoint they ask the plugin to resolve that address in a foo stub instance, which they then pass to Calls;
  • given a failure thrown by Calls, the ask the plugin to convert that fault into an equivalent fault to rethrow to proxies from make()<code>.

We show in later sections how <code>ProxyPlugins implement these callbacks, and the implications of fault conversion for the overall fault handling strategy of client libraries.

Plugins

Builders

Queries

Failures

Additional Support