Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "OSGi R7 Remote Services between Python and Java"

Line 1: Line 1:
This tutorial shows the use of [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html Remote Services] and [https://travis-ci.org/ECF/Py4j-RemoteServicesProvider Python.Java Distribution Provider] to create services between Python and Java.  By 'create services between Python and Java' what is meant is both service implementations written in Java...with Python consumers, and service implementations in Python with Java consumers.  The code presented below are examples, and everything about the examples (e.g. the service API, impl, and consumer code) can be replaced with one's own micro services.
+
==Introduction==
 +
 
 +
[https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html OSGi Remote Services] have traditionally been declared, implemented, and consumed in Java.  For example [https://wiki.eclipse.org/Using_the_Bndtools_Remote_Services_Project_Templates this tutorial].  ECF's Photon release, however, includes a [https://github.com/ECF/Py4j-RemoteServicesProvider new distribution provider for connecting Java/OSGi and Python runtimes].
 +
 
 +
As well, a Python implementation of OSGi R7 RSA has recently been contributed to the [http://ipopo.readthedocs.io iPopo framework].  [http://ipopo.readthedocs.io iPopo] is a pure Python implementation of the OSGi framework, and iPopo 0.8.0+ includes a pure-Python implementation of RSA.
 +
 
 +
This tutorial shows the use of [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html OSGi R7 Remote Services] and [https://travis-ci.org/ECF/Py4j-RemoteServicesProvider Python.Java Distribution Provider] to expose OSGi remote services between Python and Java.  The tutorial presents a sample remote service, but developers can use the same mechanisms to expose and consume arbitrary services from Java->Python, Python->Java, or both.
  
 
With OSGi R7 and [https://www.eclipse.org/ecf/NewAndNoteworthy_3.14.0.html ECF's Photon/3.14.0 implementation], it's also possible to use [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 Async Remote Services] between Python and Java as shown below.
 
With OSGi R7 and [https://www.eclipse.org/ecf/NewAndNoteworthy_3.14.0.html ECF's Photon/3.14.0 implementation], it's also possible to use [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 Async Remote Services] between Python and Java as shown below.

Revision as of 16:20, 25 July 2018

Introduction

OSGi Remote Services have traditionally been declared, implemented, and consumed in Java. For example this tutorial. ECF's Photon release, however, includes a new distribution provider for connecting Java/OSGi and Python runtimes.

As well, a Python implementation of OSGi R7 RSA has recently been contributed to the iPopo framework. iPopo is a pure Python implementation of the OSGi framework, and iPopo 0.8.0+ includes a pure-Python implementation of RSA.

This tutorial shows the use of OSGi R7 Remote Services and Python.Java Distribution Provider to expose OSGi remote services between Python and Java. The tutorial presents a sample remote service, but developers can use the same mechanisms to expose and consume arbitrary services from Java->Python, Python->Java, or both.

With OSGi R7 and ECF's Photon/3.14.0 implementation, it's also possible to use Async Remote Services between Python and Java as shown below.

Hello Service Interface

Here is a Java Interface that declares the 'IHello' service


import java.util.concurrent.CompletableFuture;
import org.osgi.util.promise.Promise;
 
public interface IHello {
 
    String sayHello(String from, String message);
 
    CompletableFuture<String> sayHelloAsync(String from, String message);
 
    Promise<String> sayHelloPromise(String from, String message);
}

the complete IHello class source with documentation.

This interface defines one synchronous method 'sayHello(String from, String message)' and two async methods: 'sayHelloAsync', and 'sayHelloPromise'. These are asynchronous methods because of the CompletableFuture and Promise return types. As per the Async Remote Services specification, return types are interpreted by the distribution provider as async methods, and so will not block the calling thread even if the underlying communication is blocked or fails.

Here is a simple Java implementation of this service:


@Component(immediate=true,enabled=false,property = { "service.exported.interfaces=*",
"service.exported.configs=ecf.py4j.host",
"osgi.basic.timeout:Long=50000",
"service.intents=osgi.async"}) // osgi.async intent to get the async rs behavior
public class HelloImpl implements IHello {
 
    @Override
    public String sayHello(String from, String message) {
        System.out.println("Java.sayHello called by "+from+" with message: '"+message+"'");
        return "Java says: Hi "+from + ", nice to see you";
    }
    @Override
    public CompletableFuture<String> sayHelloAsync(String from, String message) {
        System.out.println("Java.sayHelloAsync called by "+from+" with message: '"+message+"'");
        CompletableFuture<String> result = new CompletableFuture<String>();
        result.complete("JavaAsync says: Hi "+from + ", nice to see you");
        return result;
    }
    @Override
    public Promise<String> sayHelloPromise(String from, String message) {
        System.out.println("Java.sayHelloPromise called by "+from+" with message: '"+message+"'");
        Deferred<String> deferred = new Deferred<String>();
        deferred.resolve("JavaPromise says: Hi "+from + ", nice to see you");
        return deferred.getPromise();
    }
}

the full HelloImpl source with documentation.

Note the remote service properties specified in the @Component annotation.

 'service.exported.interfaces=*' is an OSGi standard property indicating that this service should be exported as a remote service.  The 'service.exported.configs=ecf.py4j.host' is another standard property indicating that this service should be exported via the Python.Java distribution provider.   

The 'osgi.basic.timeout=Long:50000' indicates that the remote method invocation for all methods in this interface should timeout after 50000ms/50s. This timeout can be set to any desired value and defaults to 30000ms (30s).

The 'service.intents=osgi.async' indicates that the service methods with Promise and CompletableFuture (or Future or CompletionStage) are Async Remote Services and so do not block.

Here is a Python consumer for this service


from pelix.ipopo.decorators import ComponentFactory,Instantiate,Requires,Validate
 
@ComponentFactory("remote-hello-consumer-factory")
# The '(service.imported=*)' filter only allows remote services to be injected
@Requires("_helloservice", "org.eclipse.ecf.examples.hello.IHello",
          False, False, "(service.imported=*)", False)
@Instantiate("remote-hello-consumer")
class RemoteHelloConsumer(object):
 
    def __init__(self):
        self._helloservice = None
        self._name = 'Python'
        self._msg = 'Hello Java'
 
    @Validate
    def _validate(self, bundle_context):
        # call it!
        resp = self._helloservice.sayHello(self._name + 'Sync', self._msg)
        print(
            "{0} IHello service consumer received sync response: {1}".format(
                self._name,
                resp))
 
        # call sayHelloAsync which returns Future and we add lambda to print
        # the result when done
        self._helloservice.sayHelloAsync(
            self._name + 'Async',
            self._msg).add_done_callback(
            lambda f: print(
                'async response: {0}'.format(
                    f.result())))
        print("done with sayHelloAsync method")
 
        # call sayHelloAsync which returns Future and we add lambda to print
        # the result when done
        self._helloservice.sayHelloPromise(
            self._name + 'Promise',
            self._msg).add_done_callback(
            lambda f: print(
                'promise response: {0}'.format(
                    f.result())))
        print("done with sayHelloPromise method")

This example uses ipopo to inject the IHello service proxy into the RemoteHelloConsumer instance...into the self._helloservice . The use of the @ComponentFactory, @Requires, @Instantiate, and @Validate decorators results in this behavior. See the ipopo project version 0.8.0 for more information. The result is that when the _validate method is called, the self._helloservice instance has been discovered and injected and so is ready for calling/use.

Note in _validate that the first call is to the self._helloservice.sayHello method.

        # call it!
        resp = self._helloservice.sayHello(self._name + 'Sync', self._msg)
        print(
            "{0} IHello service consumer received sync response: {1}".format(
                self._name,
                resp))

This is a synchronous remote method call, and so the calling thread will block until a result is returned or the timeout (50000ms) results in exception.

Note that for both the consumer's calling the sayHelloAsync and sayHelloPromise methods

        # call sayHelloAsync which returns Future and we add lambda to print
        # the result when done
        self._helloservice.sayHelloAsync(
            self._name + 'Async',
            self._msg).add_done_callback(
            lambda f: print(
                'async response: {0}'.format(
                    f.result())))
        print("done with sayHelloAsync method")
 
        # call sayHelloAsync which returns Future and we add lambda to print
        # the result when done
        self._helloservice.sayHelloPromise(
            self._name + 'Promise',
            self._msg).add_done_callback(
            lambda f: print(
                'promise response: {0}'.format(
                    f.result())))
        print("done with sayHelloPromise method")

When this consumer calls the sayHelloAsync or sayHelloPromise methods the distribution provider will immediately return a Future instance and proceed with the remote call. The Future will then call the function back provided to it via Future.add_done_callback method. These callbacks will probably (depending upon timing) be called after the _validate method is completed.

The Python.Java distribution provider takes care of the conversion between CompletableFuture and Future, and Promise -> Future in keeping with the contract of the OSGi Async Remote Services.

Running the Hello Example Impl via Bndtools Project Template

With the ECF Photon/3.14.0 release, support has been added for using Bndtools to do remote service development. Before doing the below please use these instructions for setting up a workspace using the ECF/bndtools.workspace template.

1. Create an Empty Bndtools Project in the workspace (of any name).

2. Choose File->New->Run Descriptor File (.bndrun) and select the **Python.Java Hello Example**

Pyjavahello1.png

3. Choose Next> and give some filename for the .bndrun file

4. Open the created filename.bndrun file.

5. Click the **Resolve** button on the lower right of the bndrun editor underneath the Run Requirements list.

6. Click Finish on the resolve window presented.

7. Click the 'Debug OSGi' link in the upper right of the bndrun editor to start the example.

This should result in some console output similar to

osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
INFO: Gateway Server Started
osgi> 17:07:48.611;EXPORT_REGISTRATION;exportedSR={org.eclipse.ecf.examples.hello.IHello}={service.intents=osgi.async, service.exported.configs=ecf.py4j.host, service.id=84, service.bundleid=4, service.scope=bundle, osgi.basic.timeout=50000, component.name=org.eclipse.ecf.examples.hello.javahost.HelloImpl, service.exported.interfaces=*, component.id=6};cID=URIID [uri=py4j://127.0.0.1:25333/java];rsId=1
--Endpoint Description---
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
.. the rest of the endpoint description

Running HelloImpl Example in Apache Karaf

Using Karaf 4.2+ see this page to install ECF Photon into Karaf.

1. Install the Python.Java distribution provider and the Hello example:

karaf@root()>feature:repo-add ecf
karaf@root()>feature:install -v ecf-rs-examples-python.java-hello

This should result in some console output similar to

osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
INFO: Gateway Server Started
osgi> 17:07:48.611;EXPORT_REGISTRATION;exportedSR={org.eclipse.ecf.examples.hello.IHello}={service.intents=osgi.async, service.exported.configs=ecf.py4j.host, service.id=84, service.bundleid=4, service.scope=bundle, osgi.basic.timeout=50000, component.name=org.eclipse.ecf.examples.hello.javahost.HelloImpl, service.exported.interfaces=*, component.id=6};cID=URIID [uri=py4j://127.0.0.1:25333/java];rsId=1
--Endpoint Description---
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
.. the rest of the endpoint description

Running the Python Hello Consumer

TBD with ipopo 0.8.0

Back to the top