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"

(Running the Hello Example via Bndtools)
(Running the Hello Example via Bndtools)
Line 126: Line 126:
 
Once started, it's necessary to give the following command in the console (to start the Java gateway listener)
 
Once started, it's necessary to give the following command in the console (to start the Java gateway listener)
  
8. **osgi>scr:enable org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider**
+
8. '''osgi>scr:enable org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider'''
  
 
and
 
and
  
9. **scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl**
+
9. '''scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl'''
  
to enable the HelloImpl component and export it via the ecf.py4j.host provider.
+
to enable the HelloImpl component and export it via the ecf.py4j.host provider. See the [https://github.com/ECF/Py4j-RemoteServicesProvider Python.Java provider repo for more info]
 +
 
 +
===Starting/Running the Python Hello Consumer===
 +
 
 +
TBD with ipopo 0.8.0
  
 
==Running Hello Example in Karaf==
 
==Running Hello Example in Karaf==

Revision as of 18:41, 15 June 2018

This tutorial shows the use ofRemote Services and 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.

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

Hello Service Interface

Here is a Java Interface that defines 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 source with documentation is available here.

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

Here is a simple Java implementation of this service:


import java.util.concurrent.CompletableFuture;
import org.eclipse.ecf.examples.hello.IHello;
import org.osgi.service.component.annotations.Component;
import org.osgi.util.promise.Deferred;
import org.osgi.util.promise.Promise;
 
@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 source with documentation is available here.

Note the service properties specified in the @Component annotation in the HelloImpl. The 'service.exported.interfaces=*' indicates that this service should be exported as a remote service. The 'service.exported.configs=ecf.py4j.host' indicates that it should be exported via the ECF 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. The 'service.intents=osgi.async' indicates that the service methods with Promise and CompletableFuture (or Future or CompletionStage) should be considered Async Remote Services upon export. All of these service properties are specified by OSGi R7 Remote Services.

Even though the above implementation of sayHelloAsync and sayHelloPromise complete immediately (no blocking) when remote consumers import an instance of the IHello remote service (with the osgi.async service intent), the distribution provider will return a CompletableFuture (or Promise) immediately without blocking because of communication delays, and complete when the entire remote invocation is complete or fails...because of communication errors or impl errors.

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/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 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. This method is a synchronous method, and so the calling Python thread will block until a result is returned (or the timeout results in exception).

Note that for both the sayHelloAsync and sayHelloPromise methods, that a Python **Future** is returned (i.e. concurrent.futures.Future). In both invocations a lambda is specified via Future.add_done_callback(func) for printing the result when the Future completes.

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 via Bndtools

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.

Once started, it's necessary to give the following command in the console (to start the Java gateway listener)

8. osgi>scr:enable org.eclipse.ecf.examples.hello.provider.ExamplePy4jProvider

and

9. scr:enable org.eclipse.ecf.examples.hello.javahost.HelloImpl

to enable the HelloImpl component and export it via the ecf.py4j.host provider. See the Python.Java provider repo for more info

Starting/Running the Python Hello Consumer

TBD with ipopo 0.8.0

Running Hello Example in Karaf

==Python.Java Services


Once you have created a Workspace using the Remote Services template, it's now possible to use Bndtools project templates to create a new remote service API project, create an implementation of this remote service, create a consumer of the remote service, and run/test the implementation and consumer.

Create a Remote Service API Project

In the new workspace there is a Remote Services API Project template to help create a Remote Service API project. To use this template, select File->New->Bnd OSGi Project

Bndtools.2.png

Select Remote Service API Project->Next

Project Name: org.example.remoteservice.api

Note: this is a suggested name for the example project, when you use this template for creating your own remote service you can specify an appropriate project/bundle/package name.

Choose Next->Finish. This will create a new project in your workspace named org.example.remoteservice.api

Create a Remote Service Impl Project

Select File->Next->Bnd OSGi Project->Remote Service Impl Project template

Project Name: org.example.remoteservice.impl

Select Next. Under Template Parameters for the apipackage parameter specify the name of the API project created above (e.g. org.example.remoteservice.api) and Next->Finish.

Create a Remote Service Consumer Project

Similar to the Impl project above, create a consumer project by selecting the Remote Service Consumer Project template.

Once all three (API, Impl, Consumer) projects are created, the workspace should look something like this

Bndtools.3.png

Create and Use Bnd Run Descriptor for Impl and Consumer

In the Package Explorer select the impl project (e.g. org.example.remoteservice.impl), and then File->New->Run Descriptor File (.bndrun). Select the ECF Generic Distribution Provider template->Next, and give an appropriate File Name...e.g. impl and Finish. Click Resolve under the Run Requirements section and then choose the Run OSGi or Debug OSGi icons in the upper right of the Resolve/Run. This should result in a Console opening and output similar to the following in the console

____________________________
Welcome to Apache Felix Gogo

g! 14:05:38.659;EXPORT_REGISTRATION;exportedSR=[org.example.remoteservice.api.ExampleRemoteService];cID=StringID[ecftcp://slewis-lenovo:54390/server];rsId=1
--Endpoint Description---
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
    <property name="ecf.endpoint.id" value-type="String" value="ecftcp://slewis-lenovo:54390/server"/>
    <property name="ecf.endpoint.id.ns" value-type="String" value="org.eclipse.ecf.core.identity.StringID"/>
    <property name="ecf.endpoint.ts" value-type="Long" value="1524171938628"/>
    <property name="ecf.rsvc.id" value-type="Long" value="1"/>
    <property name="endpoint.framework.uuid" value-type="String" value="45a4a861-3487-4089-a940-be622466a7bc"/>
    <property name="endpoint.id" value-type="String" value="937b3136-0864-4e3b-bf87-0462502eb1a5"/>
    <property name="endpoint.package.version.org.example.remoteservice.api" value-type="String" value="1.0.0"/>
    <property name="endpoint.service.id" value-type="Long" value="64"/>
    <property name="objectClass" value-type="String">
      <array>
        <value>org.example.remoteservice.api.ExampleRemoteService</value>
      </array>
    </property>
    <property name="remote.configs.supported" value-type="String">
      <array>
        <value>ecf.generic.server</value>
      </array>
    </property>
    <property name="remote.intents.supported" value-type="String">
      <array>
        <value>osgi.basic</value>
        <value>osgi.async</value>
        <value>osgi.private</value>
        <value>passByValue</value>
        <value>exactlyOnce</value>
        <value>ordered</value>
      </array>
    </property>
    <property name="service.imported" value-type="String" value="true"/>
    <property name="service.imported.configs" value-type="String">
      <array>
        <value>ecf.generic.server</value>
      </array>
    </property>
    <property name="service.intents" value-type="String" value="osgi.basic"/>
  </endpoint-description>
</endpoint-descriptions>
---End Endpoint Description

This debug output indicates that the ExampleRemoteServiceImpl was exported by the ECF RSA implementation and is ready to be used by a consumer.

Consumer

Similar to the Impl described above, create a Bnd Run Descriptor for the consumer project, then resolve and Run/Debug OSGi.

If the Zeroconf discovery is enabled on your LAN, after a few seconds the following output should appear on the consumer's console

Welcome to Apache Felix Gogo

g! service responds=Hello ExampleRemoteServiceConsumer
14:08:52.185;IMPORT_REGISTRATION;importedSR=[org.example.remoteservice.api.ExampleRemoteService];cID=StringID[ecftcp://slewis-lenovo:54390/server];rsId=1
--Endpoint Description---
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
    <property name="ecf.endpoint.id" value-type="String" value="ecftcp://slewis-lenovo:54390/server"/>
    <property name="ecf.endpoint.id.ns" value-type="String" value="org.eclipse.ecf.core.identity.StringID"/>
    <property name="ecf.endpoint.ts" value-type="Long" value="1524171938628"/>
    <property name="ecf.rsvc.id" value-type="Long" value="1"/>
    <property name="endpoint.framework.uuid" value-type="String" value="45a4a861-3487-4089-a940-be622466a7bc"/>
    <property name="endpoint.id" value-type="String" value="937b3136-0864-4e3b-bf87-0462502eb1a5"/>
    <property name="endpoint.package.version.org.example.remoteservice.api" value-type="String" value="1.0.0"/>
    <property name="endpoint.service.id" value-type="Long" value="64"/>
    <property name="objectClass" value-type="String">
      <array>
        <value>org.example.remoteservice.api.ExampleRemoteService</value>
      </array>
    </property>
    <property name="remote.configs.supported" value-type="String">
      <array>
        <value>ecf.generic.server</value>
      </array>
    </property>
    <property name="remote.intents.supported" value-type="String">
      <array>
        <value>osgi.basic</value>
        <value>osgi.async</value>
        <value>osgi.private</value>
        <value>passByValue</value>
        <value>exactlyOnce</value>
        <value>ordered</value>
      </array>
    </property>
    <property name="service.imported.configs" value-type="String">
      <array>
        <value>ecf.generic.client</value>
      </array>
    </property>
    <property name="service.intents" value-type="String" value="osgi.basic"/>
  </endpoint-description>
</endpoint-descriptions>
---End Endpoint Description

Note the line in the output:

g! service responds=Hello ExampleRemoteServiceConsumer

This indicates that the remote service was discovered, the endpoint description imported, and the proxy injected into the ExampleRemoteServiceConsumer. Then the ExampleRemoteServiceConsumer.activated method was called by DS, which is implemented with a call to the ExampleRemoteService.hello remote service method.

Breakpoints can be set in the Impl, and/or Consumer projects to inspect what's happening at runtime. The example code can also be refactored/renamed/modified.

Back to the top