Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Tutorial: Building your first Asynchronous OSGi Remote Service
Contents
Introduction
In a previous tutorial, we defined, implemented and used a simple OSGi Remote Service. Local OSGi services are based upon synchronous invocation of a service method (i.e. the calling thread blocks while the service method is being invoked, and continues execution once the method returns). With Remote Services, call-return semantics means that the calling thread can block, since the call is sent over a network, handled by the remote service, and then the response (method return value) is returned over the network. Because of the I/O required, the thread that makes these remote calls can block. As well, the remote method call may fail...because of a network partition or the remote service host failure.
To support asynchronous non-blocking remote service invocation, ECF's implementation of OSGi Remote Services has created Asynchronous Remote Services. This provides developers of Remote Services the option to expose non-blocking/asynchronous remote method calling to consumers, without any additional programming. This tutorial will step through the process of defining and using an asynchronous remote service, first by using the Java Concurrent API Future class, and then describing the use of Java8 CompletableFuture.
Define the Asynchronous Remote Service
In the previous tutorial a simple time service was defined:
package com.mycorp.examples.timeservice; public interface ITimeService { public Long getCurrentTime(); }
As described above, when the consumer calls/invokes the getCurrentTime() method, without any implementation error it's possible for this method to block for a relatively long time...and/or throw a runtime exception...because the communication failed between the consumer and the remote service host.
ECF uniquely supports the creation of an Asynchronous Remote Service interface...that will automatically be available to clients of Remote Services. For example, here is the asynchronous service interface for the ITimeService given above:
package com.mycorp.examples.timeservice; import java.util.concurrent.Future; public interface ITimeServiceAsync { public Future<Long> getCurrentTimeAsync(); }
Note the following about this interface:
- The name of the asynchronous remote service interface is ITimeServiceAsync, which is the same as ITimeService + Async. In general, this async remote service interface must be named OriginalServiceInterfaceNameAsync.
- The name of the method is getCurrentTimeAsync, which is the same as getCurrentTime and Async. In general the async method must be named originalServiceMethodNameAsync
- The return value is of type Future<Long>. This uses the Java Concurrent API Future class along with the type returned by the getCurrentTime method (Long) as the generic qualifier for the Future class. In general, the pattern here must be Future<OriginalType> or unqualified...e.g. Future. Note that the Void type can be used for methods that return void...e.g. Future<Void>.
- The contract for this asynchronous remote service is that getCurrentTimeAsync will not block, and immediately return a non-null instance of Future<Long>. At the discretion of the caller, the Future<Long> instance may be queried about the availability of the result/completion of the call, and then able to access the underlying result via the methods exposed by the Future class.
This single asynchronous service interface is all that is needed for the remote service host to expose non-blocking access to the getCurrentTime() method to consumers. No actual implementation is necessary, since it's provided automatically by ECF Remote Services implementation when discovered on the consumer.
The source code for the ITimeService and ITimeServiceAsync are available as an example project in the ECF git repo in the com.mycorp.examples.timeservice project.
Registering and Exporting the Remote Service
In the initial tutorial, the host exports the remote service by setting some standardized service properties, and registering the service...here is the remote service registration from the previous tutorial
Dictionary<String, String> props = new Hashtable<String, String>(); props.put("service.exported.interfaces", "*"); // 'ecf.generic.server' is the config used in this example, but others are // possible...for example see https://wiki.eclipse.org/Tutorial:_Creating_a_RESTful_Remote_Service_Provider props.put("service.exported.configs","ecf.generic.server"); bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);
To allow the remote service consumer access to the Asynchronous Remote Service (e.g. ITimeServiceAsync) a new/additional service property must be set:
Dictionary<String, String> props = new Hashtable<String, String>(); props.put("service.exported.interfaces", "*"); // 'ecf.generic.server' is the config used in this example, but others are // possible...for example see https://wiki.eclipse.org/Tutorial:_Creating_a_RESTful_Remote_Service_Provider props.put("service.exported.configs","ecf.generic.server"); // ECF RS property allowing ITimeServiceAsync interface to be automatically // exposed to consumers of the ITimeService by ECF RS implementation props.put("ecf.exported.async.interfaces","*"); bundleContext.registerService(ITimeService.class, new TimeServiceImpl(), props);
The ecf.exported.async.interfaces service property, similar to the service.exported.interfaces standard property, tells the ECF Remote Service implementation to instrument the consumer's proxy for asynchronous access via the asynchronous remote service interface.
The source code for the time service host is available as an example project in the ECF git repo in the com.mycorp.examples.timeservice.host project.
Consumer: Discover and Use the Remote Service
To invoke methods on a remote service, the consumer must first discover the service. Asynchronous Remote Service discovery works exactly the same as normal remote service discovery and so for details of that process see the previous tutorial.
In the previous tutorial, upon discovery, the ITimeService instance is injected in the consumer via the bindTimeService method:
package com.mycorp.examples.timeservice.consumer.ds; import com.mycorp.examples.timeservice.ITimeService; public class TimeServiceComponent { void bindTimeService(ITimeService timeService) { System.out.println("Discovered ITimeService via DS"); // Call the service and print out result! System.out.println("Current time is: " + timeService.getCurrentTime()); } }
All that is necessary to access the ITimeServiceAsync methods is to query the injected timeService instance for the desired asynchronous remote service interface. For example:
void bindTimeService(ITimeService timeService) { if (timeService instanceof ITimeServiceAsync) { ITimeServiceAsync asyncTimeService = (ITimeServiceAsync) timeService; System.out.println("Discovered ITimeServiceAsync via DS"); // Call the asynchronous remote service. Unlike the synchronous getTimeService(), // this method will not block Future<Long> currentTimeFuture = asyncTimeService.getCurrentTimeAsync(); // potentially do other operations here... System.out.println("Current time is: " + currentTimeFuture.get()); } }
With ECF's implementation of remote services, if an ITimeService proxy is created for the consumer, and an asynchronous service interface (i.e. ITimeServiceAsync) is present then the proxy will implement the asynchronous service interface. The actual implementation is provided directly by ECF Remote Services, and neither the host nor the consumer must do anything further for the asynchronous remote service to be usable by consumers. All that's necessary is to declare the asynchronous remote service (ITimeServiceAsync) in the same package as the original (ITimeService), and export a host instance of the remote service as described by the previous tutorial.
The source code for the time service consumer is available as an example project in the ECF git repo in the com.mycorp.examples.timeservice.consumer.ds project.
Using Java8 CompletableFuture
In Java 8, in addition to lambdas, streams and other features, a new type of Future was introduced, called CompletableFuture. Unlike java.util.concurrent.Future, CompletableFuture can be used in a way guaranteed not to block, whereas calling Future.get() can block...if the underlying result does not yet exist.
In ECF 3.8.1/Luna support for CompletableFuture has been added. As shown above, async proxy interfaces can return either java.util.concurrent.Future as above...or java8's java.util.concurrent.CompletableFuture. For example, ITimeServiceAsync can be defined to return CompletableFuture rather than Future:
package com.mycorp.examples.timeservice; import java.util.concurrent.CompletableFuture; public interface ITimeServiceAsync { public CompletableFuture<Long> getCurrentTimeAsync(); }
The source code for Java8 asynchronous time service is available as an example project in the ECF git repo in the com.mycorp.examples.timeservice.async project.
Consumer: Discover and Use the Java8 CompletableFuture
With the remote service host's use of the ecf.exported.async.interfaces service property (as described in Registering the Remote Service above, it's possible on the consumer to have the ITimeServiceAsync, when discovered, to be injected directly into a declarative services component
void bindTimeService(ITimeServiceAsync timeService) { System.out.println("Discovered ITimeServiceAsync via DS"); // Get the CompletableFuture...no blocking here CompletableFuture<Long> cf = timeService.getCurrentTimeAsync(); // print out time when done...no blocking anywhere! cf.thenAccept((time) -> System.out.println("Remote time is: " + time)); }
In this line:
CompletableFuture<Long> cf = timeService.getCurrentTimeAsync();
the asynchronous remote service is being called, and CompletableFuture instance is returned. The implementation of this method is provided by the ECF Remote Service proxy, constructed automatically as part of the Remote Service implementation.
The client may then call CompletableFuture.thenAccept to execute a given block of code when the result of the remote service is available
cf.thenAccept((time) -> System.out.println("Remote time is: " + time));
Note that the given System.out.println("Remote time is: " + time)); is only executed when the underlying remote access is completed, and a time result is available. This means that there is no blocking...i.e. no matter how long the remote service takes to complete, the thread that calls timeService.getCurrentTimeAsync and cf.thenAccept is guaranteed not to block.
The source code for Java8 asynchronous time service consumer is available as an example project in the ECF git repo in the com.mycorp.examples.timeservice.consumer.ds.async project.
Background and Related Articles
Asynchronous Proxies for Remote Services
Getting Started with ECF's OSGi Remote Services Implementation
Static File-based Discovery of Remote Service Endpoints