RSA Remote Services using XmlRpc transport
Introduction
This tutorial shows how to create and run a simple remote service using the XmlRpc provider. The XmlRpc distribution provider is one of several supported by the RSA Remote Services (OSGi R7-compliant) implementation.
Requirements
This tutorial sample requires Python 3.4+ or Python 2.7, and version 0.8.0+ of iPOPO.
Defining the Remote Service with a Python Class
We’ll start by defining a Python hello service that can to be exported by RSA for remote access.
In the sample.rsa package is the helloimpl_xmlrpc module, containing
the XmlRpcHelloImpl class
@ComponentFactory("helloimpl-xmlrpc-factory")
@Provides(
"org.eclipse.ecf.examples.hello.IHello"
)
@Instantiate(
"helloimpl-xmlrpc",
{
# uncomment to automatically export upon creation
# "service.exported.interfaces":"*",
"osgi.basic.timeout": 60000,
},
)
class XmlRpcHelloImpl(HelloImpl):
pass
The XmlRpcHelloImpl class has no body/implementation as it inherits
its implementation from the HelloImpl class, which we will discuss in
a moment.
The important parts of this class declaration for remote services are
the @Provides class decorator and the commented-out
service.exported.interfaces and osgi.basic.timeout properties in
the @Instantiate decorator.
The @Provides class decorator gives the name of the service
specification provided by this instance. This is the name that both
local and remote consumers use to lookup this service, even if it’s
local-only (i.e. not a remote service). In this case, since the
original IHello interface is a java interface class, the
fully-qualified name of the
interface class is used.
For an example of Java↔Python remote services see
this tutorial.
For Python-only remote services it’s not really necessary for this service specification be the name of a Java class, any unique String could have been used.
The osgi.basic.timeout is an optional property that gives a maximum
time (in milliseconds) that the consumer will wait for a response before
timing out.
The service.exported.interfaces property is a
required property for remote service export.
If one wants to have a remote service exported immediately upon
instantiation and registration as an iPOPO service, this property can be
set to value * which means to export all service interfaces.
The service.exported.interfaces property is commented out so that it
is not exported immediately upon instantiation and registration.
Instead, for this tutorial the export is performed via iPOPO console
commands. If these comments were to be removed, the RSA impl will export
this service as soon as it is instantiated and registered, making it
unnecessary to explicitly export the service as shown in the
Exporting the XmlRpcHelloImpl as a Remote Servicesection below.
The HelloImpl Implementation
The XmlRpcHelloImpl class delegates all the actual implementation to
the HelloImpl class, which has the code for the methods defined for
the “org.eclipse.ecf.examples.hello.IHello” service specification
name, with the main method sayHello:
class HelloImpl:
def sayHello(self, name='Not given', message='nothing'):
print(
"Python.sayHello called by: {0} with message: '{1}'".format(
name, message))
return "PythonSync says: Howdy {0} that's a nice runtime you got there".format(
name)
The sayHello method is invoked via a remote service consumer once the
service has been exporting.
Exporting the XmlRpcHelloImpl as a Remote Service
Go to the pelix home directory and start the run_rsa_xmlrpc.py main
program
bash$ python -m samples.run_rsa_xmlrpc
** Pelix Shell prompt **
$
To load the module and instantiate and register an XmlRpcHelloImpl
instance type
$ start samples.rsa.helloimpl_xmlrpc
Bundle ID: 18
Starting bundle 18 (samples.rsa.helloimpl_xmlrpc)...
In your environment, bundle number might not be 18… that is fine.
If you list services using the sl console command you should see an
instance of IHello service
$ sl org.eclipse.ecf.examples.hello.IHello
+----+-------------------------------------------+--------------------------------------------------+---------+
| ID | Specifications | Bundle | Ranking |
+====+===========================================+==================================================+=========+
| 20 | ['org.eclipse.ecf.examples.hello.IHello'] | Bundle(ID=18, Name=samples.rsa.helloimpl_xmlrpc) | 0 |
+----+-------------------------------------------+--------------------------------------------------+---------+
1 services registered
The service ID (20 in this case) may not be the same in your environment… again that is ok… but make a note of what the service ID is.
To export this service instance as remote service and make it available
for remote access, use the exportservice command in the pelix console,
giving the number (20 from above) of the service to export:
$ exportservice 20 # use the service id for the org.eclipse.ecf.examples.hello.IHello service if not 20
Service=ServiceReference(ID=20, Bundle=18, Specs=['org.eclipse.ecf.examples.hello.IHello']) exported by 1 providers. EDEF written to file=edef.xml
$
This means that the service has been successfully exported. To see this
use the listexports console command:
$ listexports
+--------------------------------------+-------------------------------+------------+
| Endpoint ID | Container ID | Service ID |
+======================================+===============================+============+
| b96927ad-1d00-45ad-848a-716d6cde8443 | http://127.0.0.1:8181/xml-rpc | 20 |
+--------------------------------------+-------------------------------+------------+
$ listexports b96927ad-1d00-45ad-848a-716d6cde8443
Endpoint description for endpoint.id=b96927ad-1d00-45ad-848a-716d6cde8443:
<?xml version='1.0' encoding='cp1252'?>
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
<endpoint-description>
<property name="objectClass" value-type="String">
<array>
<value>org.eclipse.ecf.examples.hello.IHello</value>
</array>
</property>
<property name="remote.configs.supported" value-type="String">
<array>
<value>ecf.xmlrpc.server</value>
</array>
</property>
<property name="service.imported.configs" value-type="String">
<array>
<value>ecf.xmlrpc.server</value>
</array>
</property>
<property name="remote.intents.supported" value-type="String">
<array>
<value>osgi.basic</value>
<value>osgi.async</value>
</array>
</property>
<property name="service.intents" value-type="String">
<array>
<value>osgi.async</value>
</array>
</property>
<property name="endpoint.service.id" value="20" value-type="Long">
</property>
<property name="service.id" value="20" value-type="Long">
</property>
<property name="endpoint.framework.uuid" value="4d541077-ee2a-4d68-85f5-be529f89bec0" value-type="String">
</property>
<property name="endpoint.id" value="b96927ad-1d00-45ad-848a-716d6cde8443" value-type="String">
</property>
<property name="service.imported" value="true" value-type="String">
</property>
<property name="ecf.endpoint.id" value="http://127.0.0.1:8181/xml-rpc" value-type="String">
</property>
<property name="ecf.endpoint.id.ns" value="ecf.namespace.xmlrpc" value-type="String">
</property>
<property name="ecf.rsvc.id" value="3" value-type="Long">
</property>
<property name="ecf.endpoint.ts" value="1534119904514" value-type="Long">
</property>
<property name="osgi.basic.timeout" value="60000" value-type="Long">
</property>
</endpoint-description>
</endpoint-descriptions>
$
Note that listexports produced a small table with Endpoint ID,
Container ID, and Service ID columns. As shown above, if the
Endpoint ID is copied and used in listexports, it will then print out
the endpoint description (XML) for the newly-created endpoint.
Also as indicated in the exportservice command output, a file
edef.xml has also been written to the filesystem containing the
endpoint description XML known as EDEF.
EDEF is a standardized XML format
that gives all of the remote service meta-data required for a consumer
to import an endpoint. The edef.xml file will contain the same XML
printed to the console via the
listexports b96927ad-1d00-45ad-848a-716d6cde8443 console command.
Importing the XmlRpcHelloImpl Remote Service
For a consumer to use this remote service, another python process should be started using the same command:
bash$ python -m samples.run_rsa_xmlrpc
** Pelix Shell prompt **
$
If you have started this second python process from the same location,
all that’s necessary to trigger the import of the remote service, and
have a consumer sample start to call it’s methods is to use the
importservice console command:
$ importservice
Imported 1 endpoints from EDEF file=edef.xml
Python IHello service consumer received sync response: PythonSync says: Howdy PythonSync that's a nice runtime you got there
done with sayHelloAsync method
done with sayHelloPromise method
Proxy service=ServiceReference(ID=21, Bundle=7, Specs=['org.eclipse.ecf.examples.hello.IHello']) imported. rsid=http://127.0.0.1:8181/xml-rpc:3
$ async response: PythonAsync says: Howdy PythonAsync that's a nice runtime you got there
promise response: PythonPromise says: Howdy PythonPromise that's a nice runtime you got there
This indicates that the remote service was imported, and the methods on the remote service were called by the consumer.
Here is the code for the consumer (also in samples/rsa/helloconsumer_xmlrpc.py)
from pelix.ipopo.decorators import ComponentFactory, Instantiate, Requires, Validate
from concurrent.futures import ThreadPoolExecutor
@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:
def __init__(self):
self._helloservice = None
self._name = 'Python'
self._msg = 'Hello Java'
self._executor = ThreadPoolExecutor()
@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._executor.submit(
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._executor.submit(
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")
For having this remote service injected, the important part of things is
the @Requires decorator
@Requires("_helloservice", "org.eclipse.ecf.examples.hello.IHello",
False, False, "(service.imported=*)", False)
This gives the specification name required org.eclipse.ecf.examples.hello.IHello, and it also gives an OSGi filter
"(service.imported=*)"
As per the Remote Service spec
this requires that the IHello service is a remote service, as all
proxies must have the service.imported property set, indicating that
it was imported.
When importservice is executed the RSA implementation does the
following:
Reads the edef.xml from filesystem (i.e. ‘discovers the service’)
Create a local proxy for the remote service using the edef.xml file
The proxy is injected by iPOPO into the
RemoteHelloConsumer._helloservicememberThe
_activatedmethod is called by iPOPO, which uses theself._helloserviceproxy to send the method calls to the remote service, using HTTP and XML-RPC to serialize thesayHellomethod arguments, send the request via HTTP, get the return value back, and print the return value to the consumer’s console.
Note that with Export, rather than using the console’s exportservice
command, it may be invoked programmatically, or automatically by the
topology manager (for example upon service registration). For Import,
the importservice command may also be invoked automatically, or via
remote service discovery (e.g. etcd, zookeeper, zeroconf, custom, etc).
The use of the console commands in this example was to demonstrate the
dynamics and flexibility provided by the OSGi R7-compliant RSA
implementation.
Exporting Automatically upon Service Registration
To export automatically upon service registration, all that need be done
is to un-comment the setting the service.exported.interfaces
property in the Instantiate decorator:
@ComponentFactory("helloimpl-xmlrpc-factory")
@Provides(
"org.eclipse.ecf.examples.hello.IHello"
)
@Instantiate(
"helloimpl-xmlrpc",
{
"service.exported.interfaces": "*",
"osgi.basic.timeout": 60000,
},
)
class XmlRpcHelloImpl(HelloImpl):
pass
Unlike in the example above, when this service is instantiated and
registered, it will also be automatically exported, making unnecessary
to use the exportservice command.