Understanding service internals =============================== Registering synchronous services -------------------------------- What happens when a method is decorated with ``@service``? The ``@service`` decorator simply marks the method as a service by setting adding an attribute called ``_morse_service`` of type ``bool`` and with value ``True`` to the method. Before actually registering the service, a mapping between the component and one or several middlewares that will manage incoming requests must be defined (for instance, we may want the :py:class:`morse.actuators.waypoint.Waypoint`'s ``goto`` service to be managed by the YARP middleware for the component ``MainRobot``). These mappings are defined by your builder script (so they are simulation-dependent). At start up, :py:func:`morse.blender.main.init` does the following 1. Reads the configuration; 2. Instantiates classes associated with each component; 3. Registers the mappings (with :py:meth:`morse.core.services.MorseServices.register_request_manager_mapping`); 4. Calls :py:meth:`morse.core.abstractobject.AbstractObject.register_services` on each component instance. The :py:meth:`morse.core.abstractobject.AbstractObject.register_services` method iterates over every method marked as a MORSE service within the class, and calls :py:func:`morse.core.services.do_service_registration` on it. This method finds the middlewares responsible for managing this component's services, and calls :py:meth:`morse.core.request_manager.RequestManager.register_service`. It is up to each middleware to manage the registration of new services. They may have to, for instance, expose the new service into a shared directory (*yellow pages*), etc. Upon incoming request... ------------------------ When a new request comes in, the middleware-specific part receives it, deserializes it calls :py:meth:`morse.core.request_manager.RequestManager.on_incoming_request` with it. This method dispatches the request to the correct component, and executes it (for synchronous services) or starts the execution and returns an internal request ID (for asynchronous services). This internal request ID can be used by the middleware to track the status of a request. On completion, the :py:meth:`morse.core.request_manager.RequestManager.on_service_completion` callback is invoked, and the final result can be sent back to the client. Asynchronous services --------------------- Registration of asynchronous services is almost the same as for synchronous services. The ``@async_service`` decorator simply calls the ``@service`` decorator with the ``asynchronous`` parameter set to ``True``, which results in the original method being wrapped in a new method that takes an extra parameter (a callback) and calls :py:meth:`morse.core.abstractobject.AbstractObject.set_service_callback`. Simplified version of the ``@service`` decorator: .. code-block:: python def service(fn, asynchronous=False): dfn = fn if asynchonous: def decorated_fn(self, callback, *param): self._set_service_callback(callback) fn(self, *param) dfn = decorated_fn dfn.__name__ = fn.__name__ dfn._morse_service = True dfn._morse_service_is_async = async return dfn However, asynchronous services behaviour differs when a request comes in: * The :py:meth:`morse.core.request_manager.RequestManager.on_incoming_request` method creates a new callback function for this service; * It invokes the original method with this callback. * When :py:meth:`morse.core.abstractobject.AbstractObject.completed` is invoked (i.e., when the service is completed), the callback is executed. * This in turn causes the :py:meth:`morse.core.request_manager.RequestManager.on_service_completion` method to be invoked, to notify middleware-specific request managers that the task is complete. .. _manually-registring-services: Manually registering services ----------------------------- While usually unnecessary, it is possible to to manually register services (i.e., without using decorators). This first code snippet shows how to register a synchronous service that uses sockets as the communication interface: .. code-block:: python from morse.middleware.socket_request_manager import SocketRequestManager def add(a, b): return a + b req_manager = SocketRequestManager() req_manager.register_service("test_component", add) while True: # This calls the middleware specific part, responsible for reading # incoming requests and writing back pending results. req_manager.process() # In a real case, you don't want to block on such a loop, and MORSE # calls req_manager.process() on your behalf If you run this sample code, you can test it with a simple Telnet session:: > telnet localhost 4000 Connected to localhost. > req1 test_component add (1,2) req1 OK 3 .. note:: The socket-based protocol is fairly simple: you provide a request id, the name of the component that offers the service, the name of the service, and (optionally) parameters. Parameters must be contained in a valid Python iterable (a tuple, like in the example, or an array). Here, ``req1`` is the custom request id, chosen by the client. For asynchronous services, a callback function is passed to the service. It allows the service to notify when it is complete. This second code snippet shows an example of asynchronous service: .. code-block:: python import types from morse.core import status from morse.middleware.socket_request_manager import SocketRequestManager State = types.SimpleNamespace() State.result_cb = None State.run_computation = False State.value = None # Here is where we start our asynchronous service. # Arbitrary parameters may be passed, but the first one # must be the callback to set the service result upon completion. def start_computation(result_setter, start_value): State.result_cb = result_setter State.value = start_value State.run_computation = True # the service must return true if the task was successfully started return True # This is the actual code called at each simulation step in our component def complex_computation(a): if a == 0: # At the end of the computation, we set the result. # the result can be any valid Python object State.result_cb((status.SUCCESS, "done!")) State.run_computation = False morse.sleep(1) return a - 1 req_manager = SocketRequestManager() # here we explicitely register an asynchronous service. # the optional 'service_name' argument allows to define a custom service # name. req_manager.register_async_service("test_component", start_computation, service_name = "compute") while True: req_manager.process() if State.run_computation: State.value = complex_computation(State.value) print("Value is now %i" % State.value) If you test the code with Telnet:: > telnet localhost 4000 Connected to localhost. > req2 test_component compute (5,) [after 5 seconds] req2 OK done! .. note:: When passing a single parameter, you still need to pass a valid Python iterable, even if it has only one element. Hence the ``(5,)``.