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"

 
(30 intermediate revisions by the same user not shown)
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==
  
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.  This is also shown below.
+
[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].
  
==Hello Service Interface==
+
In addition, 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 much of OSGi, and iPOPO 0.8.0+ includes a pure-Python implementation of RSA.
  
Here is a Java Interface that defines the 'IHello' service
+
This tutorial shows the use of [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html OSGi R7 Remote Services] and [https://github.com/ECF/Py4j-RemoteServicesProvider Python.Java Distribution Provider] to expose OSGi remote services both from Java->Python, and Python->Java.  The tutorial presents a sample remote service, but developers can use the same mechanisms to expose and consume arbitrary services between Python and Java.
 +
 
 +
==Java Service Declaration==
 +
 
 +
IHello service declaration:
 
<hr>
 
<hr>
 
<source lang="java">
 
<source lang="java">
Line 21: Line 25:
 
</source>
 
</source>
 
<hr>
 
<hr>
The complete IHello class source with documentation is available [https://github.com/ECF/Py4j-RemoteServicesProvider/blob/master/examples/org.eclipse.ecf.examples.hello/src/org/eclipse/ecf/examples/hello/IHello.java here].
+
[https://github.com/ECF/Py4j-RemoteServicesProvider/blob/master/examples/org.eclipse.ecf.examples.hello/src/org/eclipse/ecf/examples/hello/IHello.java IHello 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 (as defined by the [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 Async Remote Services]) because of the CompletableFuture and Promise return types.  These return types are interpreted by the distribution provider as async methods, and as such will not block the calling thread even if the communication is blocked or fails.
+
This interface defines one synchronous method '''sayHello(String from, String message)''' and two async methods:  '''sayHelloAsync''', and '''sayHelloPromise'''.  These are OSGi R7 asynchronous methods strictly because of the CompletableFuture and Promise return types and the presence of the '''osgi.async''' service intent in the Component properties.  As per the [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 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:
+
==Java Service Implementation==
  
 +
HelloImpl service implementation
 
<hr>
 
<hr>
 
<source lang="java">
 
<source lang="java">
Line 57: Line 62:
 
</source>
 
</source>
 
<hr>
 
<hr>
The full HelloImpl source with documentation is available [https://github.com/ECF/Py4j-RemoteServicesProvider/blob/master/examples/org.eclipse.ecf.examples.hello.javahost/src/org/eclipse/ecf/examples/hello/javahost/HelloImpl.java here].
+
[https://github.com/ECF/Py4j-RemoteServicesProvider/blob/master/examples/org.eclipse.ecf.examples.hello.javahost/src/org/eclipse/ecf/examples/hello/javahost/HelloImpl.java the full HelloImpl source with documentation].
  
Note the service properties specified in the @Component annotation.   
+
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.   
  
'service.exported.interfaces=*' is an OSGi standard property indicating that this service should be exported as a remote serviceThe 'service.exported.configs=ecf.py4j.host' is another standard property indicating that it should be exported via the Python.Java distribution provider. 
+
'''osgi.basic.timeout=Long:50000''' indicates that the remote method invocation for all methods in this interface should timeout after 50000ms/50sThis timeout can be set to any desired value and defaults to 30000ms (30s).
  
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  ([https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 or Future or CompletionStage]) are Async Remote Services and so do not block.
  
The 'service.intents=osgi.async' indicates that the service methods with Promise and CompletableFuture  ([https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html#d0e1407 or Future or CompletionStage]) are Async Remote Services that do not block the calling thread.  This new property is  [https://osgi.org/specification/osgi.cmpn/7.0.0/service.remoteservices.html specified by OSGi R7 Remote Services].
+
==Launching via Bndtools Project Template==
 
+
Here is a Python consumer for this service
+
<hr>
+
<source lang="Python">
+
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")
+
</source>
+
<hr>
+
This example uses ipopo/pelix and pelix.rsa packages to inject the IHello service proxy into the RemoteHelloConsumer instance...into the self._helloservice instance.  The use of the @ComponentFactory, @Requires, @Instantiate, and @Validate decorators results in this behavior.  See the [https://ipopo.readthedocs.io/en/0.7.0/ 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. 
+
 
+
<source lang="Python">
+
        # call it!
+
        resp = self._helloservice.sayHello(self._name + 'Sync', self._msg)
+
        print(
+
            "{0} IHello service consumer received sync response: {1}".format(
+
                self._name,
+
                resp))
+
</source>
+
<hr>
+
This is a synchronous method, and so the calling thread will block until a result is returned or the timeout results in exception.
+
 
+
Note that for both the consumer's calling the sayHelloAsync and sayHelloPromise methods
+
 
+
<source lang="Python">
+
        # 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")
+
</source>
+
<hr>
+
When this consumer calls the sayHelloAsync or sayHelloPromise methods the distribution provider will immediately return a [https://docs.python.org/3/library/concurrent.futures.html#future-objects Future] instance and proceed with the remote call.  The [https://docs.python.org/3/library/concurrent.futures.html#future-objects Future] will then call the function back provided to it via [https://docs.python.org/3/library/concurrent.futures.html#future-objects 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.
+
 
+
==Java:  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 [https://wiki.eclipse.org/Bndtools_Support_for_Remote_Services_Development these instructions for setting up a workspace using the ECF/bndtools.workspace 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 [https://wiki.eclipse.org/Bndtools_Support_for_Remote_Services_Development these instructions for setting up a workspace using the ECF/bndtools.workspace template].
Line 173: Line 89:
  
 
7. Click the 'Debug OSGi' link in the upper right of the bndrun editor to start the example.
 
7. Click the 'Debug OSGi' link in the upper right of the bndrun editor to start the example.
 
Once started, at the gogo console prompt give the following commands to start the Java gateway listener:
 
 
<pre>
 
osgi>scr:enable org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider
 
</pre>
 
  
 
This should result in some console output similar to
 
This should result in some console output similar to
  
 
<pre>
 
<pre>
Component org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider enabled
 
 
osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
 
osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
 
SLF4J: Defaulting to no-operation (NOP) logger implementation
 
SLF4J: Defaulting to no-operation (NOP) logger implementation
Line 189: Line 98:
 
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
 
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
 
INFO: Gateway Server Started
 
INFO: Gateway Server Started
</pre>
 
 
<pre>
 
osgi>scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl
 
</pre>
 
 
to enable the HelloImpl component and export it via the ecf.py4j.host provider. 
 
 
This should result in output to the console the RSA endpoint description xml for the exported endpoint
 
<pre>
 
osgi> scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl
 
Component org.eclipse.ecf.examples.hello.javahost.HelloImpl enabled
 
 
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
 
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 Description---
Line 208: Line 105:
 
</pre>
 
</pre>
  
==Java:  Running HelloImpl Example in Apache Karaf==
+
==Launching via Apache Karaf==
  
 
Using Karaf 4.2+ [https://wiki.eclipse.org/EIG:Install_into_Apache_Karaf see this page to install ECF Photon into Karaf].
 
Using Karaf 4.2+ [https://wiki.eclipse.org/EIG:Install_into_Apache_Karaf see this page to install ECF Photon into Karaf].
Line 216: Line 113:
 
<pre>
 
<pre>
 
karaf@root()>feature:repo-add ecf
 
karaf@root()>feature:repo-add ecf
 +
karaf@root()>feature:repo-refresh ecf
 
karaf@root()>feature:install -v ecf-rs-examples-python.java-hello
 
karaf@root()>feature:install -v ecf-rs-examples-python.java-hello
</pre>
 
 
2.  Enable the Python.Java distribution provider component
 
 
<pre>
 
osgi>scr:enable org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider
 
 
</pre>
 
</pre>
  
Line 228: Line 120:
  
 
<pre>
 
<pre>
Component org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider enabled
 
 
osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
 
osgi> SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
 
SLF4J: Defaulting to no-operation (NOP) logger implementation
 
SLF4J: Defaulting to no-operation (NOP) logger implementation
Line 234: Line 125:
 
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
 
Jul 09, 2018 5:06:56 PM py4j.GatewayServer fireServerStarted
 
INFO: Gateway Server Started
 
INFO: Gateway Server Started
</pre>
 
 
3. Enable the HelloImpl service
 
 
<pre>
 
osgi>scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl
 
</pre>
 
 
This should result in output to the console the RSA endpoint description xml for the exported endpoint
 
<pre>
 
osgi> scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl
 
Component org.eclipse.ecf.examples.hello.javahost.HelloImpl enabled
 
 
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
 
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 Description---
Line 253: Line 132:
 
</pre>
 
</pre>
  
===Running the Python Hello Consumer===
+
This output indicates that the HelloImpl service has been registered and exported for remote access from Python.
 +
 
 +
==Launching Python Framework==
  
TBD with ipopo 0.8.0
+
See [https://ipopo.readthedocs.io/en/1.0.1/tutorials/rsa_pythonjava.html this page from the iPOPO documentation] for starting the Python-side sample code.

Latest revision as of 15:07, 27 January 2021

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.

In addition, a Python implementation of OSGi R7 RSA has recently been contributed to the iPOPO framework. iPOPO is a pure Python implementation of much of OSGi, 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 both from Java->Python, and Python->Java. The tutorial presents a sample remote service, but developers can use the same mechanisms to expose and consume arbitrary services between Python and Java.

Java Service Declaration

IHello service declaration:


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);
}

IHello source with documentation.

This interface defines one synchronous method sayHello(String from, String message) and two async methods: sayHelloAsync, and sayHelloPromise. These are OSGi R7 asynchronous methods strictly because of the CompletableFuture and Promise return types and the presence of the osgi.async service intent in the Component properties. 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.

Java Service Implementation

HelloImpl service implementation


@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.

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.

Launching 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

Launching via 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:repo-refresh 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

This output indicates that the HelloImpl service has been registered and exported for remote access from Python.

Launching Python Framework

See this page from the iPOPO documentation for starting the Python-side sample code.

Back to the top