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

OSGi R7 Remote Services between Python and Java

Revision as of 17:16, 25 July 2018 by Slewis.composent.com (Talk | contribs) (Running Hello Example in Apache Karaf)

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.

Hello Service

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 Implementation of Hello 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.

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 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 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: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