Messaging#

When running in emulation mode, ECLYPSE allows services to communicate directly with one another using structured EclypseCommunicationInterface.

Two communication models are currently supported:

  • an MPI-like interface (EclypseMPI), designed for low-latency message passing

  • a REST-style interface (EclypseREST), designed for loosely coupled HTTP interactions

These APIs can be used from within emulated services to exchange information, coordinate behaviour, or simulate service-level protocols.

MPI-style messaging#

The MPI interface provides basic primitives to send and receive messages among services deployed in the same application.

Each Service has access to an mpi attribute (an instance of the internal communication layer), through which it can:

  • send messages to specific services (send())

  • broadcast messages to all neighbours (bcast())

  • receive incoming messages (recv())

All methods are asynchronous and simulate network cost using the placement and topology of the infrastructure. The send and broadcast requests must be awaited explicitly.

Example: Use MPI interface#
""" ... inside the step() method of service-A """

# Send to one neighbour
await self.mpi.send("service-B", {"msg": "ping"})

# Broadcast to all neighbours
await self.mpi.bcast({"msg": "update"})

# Receive next message
message = await self.mpi.recv()

@exchange() decorator#

In addition to direct calls, the EclypseMPI interface provides a decorator (@exchange()) to define communication behaviour declaratively inside service methods.

You can decorate a method to:

  • receive a message before it runs (receive=True)

  • send the return value to a target (send=True)

  • broadcast the return value to all neighbours (broadcast=True)

Sending and broadcasting are mutually exclusive.

Example: Using the exchange() decorator#
from eclypse.remote.service import Service
from eclypse.remote.communication.mpi import exchange

class EchoService(Service):

    @exchange(receive=True, send=True)
    async def step(self, sender_id, message):
        reply = {"msg": f"Echo: {message['msg']}"}
        return sender_id, reply

REST-style Messaging#

ECLYPSE also provides a REST-style communication interface for services. This mode models service interaction using HTTP-like semantics and is better suited for stateless, loosely coupled communication patterns.

To use this style, your service must subclass RESTService.

@register_endpoint() decorator#

You define REST endpoints inside your service by decorating methods with @register_endpoint().

Each endpoint:

  • is associated with a URL pattern and an HTTP method (GET, POST, PUT, DELETE)

  • receives the request data as keyword arguments

  • must return a tuple: (HTTPStatusCode, response_dict)

Example:

from eclypse.remote.communication.rest.codes import HTTPStatusCode
from eclypse.remote.communication.rest.interface import register_endpoint
from eclypse.remote.service import RESTService

class StoreService(RESTService):

    def __init__(self, service_id: str):
        super().__init__(service_id)
        self.store = {}

    @register_endpoint("/data", method="POST")
    async def store_data(self, key: str, value: Any):
        self.store[key] = value
        return HTTPStatusCode.CREATED, {"status": "ok"}

    @register_endpoint("/data", method="GET")
    def get_data(self, key: str):
        value = self.store.get(key, None)
        return HTTPStatusCode.OK, {"value": value}

Making Requests#

From within a service, you can send requests using:

Example: Make a POST request to another service#
""" ... inside the step() method of service-A """

await self.rest.post("StoreService/data", key="item1", value=42)

Each method returns an HTTP request object. When awaited, the request is executed and the object exposes:

  • status_code

  • body

  • response

  • route

For example:

request = await self.rest.post("StoreService/data", key="item1", value=42)
print(request.status_code)
print(request.body)

Endpoint Resolution#

When a REST service starts, it scans all decorated methods using @register_endpoint() and registers them dynamically. Endpoint URLs are scoped per service ID: the final URL is prefixed by the service name.

For example, if a service s1 exposes /data, the full URL is: s1/data.

Routing and delivery are managed by the infrastructure runtime.