ethereum / lahja

Lahja is a generic multi process event bus implementation written in Python 3.6+ to enable lightweight inter-process communication, based on non-blocking asyncio
MIT License
394 stars 19 forks source link

Proxy object API #159

Closed pipermerriam closed 4 years ago

pipermerriam commented 5 years ago

What was wrong?

Here's a fictional API I've been thinking about.

from lahja import proxy
from my_code import Animal

@dataclass
class Animal:
    name: str
    sound: str

class ProxyAnimal(proxy.Proxy[Animal]):
    name = proxy.Text()
    get_sound = proxy.Method()
    wait_something = proxy.AsyncMethod()

# to serve it:
cat = Animal(name='cat', sound='meow')
with proxy.serve_object(endpoint, ProxyAnimal, cat) as obj_id:
    ...

# to use it from across process boundaries
cat = ProxyAnimal(endpoint, obj_id)

I think this achieves the following goals.

How can it be fixed?

Not sure. One major problem is the need to interact with the event bus from both async and synchronous call sites. Things like object properties and non-async methods will mean that we'll be entering the endpoint APIs from a synchronous context. We can likely pull something off using threads and broadcast_nowait. It might be nice to have a way to interact with EndpointAPI.request as well since everything from the client side will be a request/response.

The Text() and Method() APIs would likely be descriptors which know how to access the endpoint through their parent object.

A meta-class is probably going to be needed to programmatically generate event types but it may be possible to do get by with something simpler like a set of builtin events for the request/response or an internal event.

cburgdorf commented 5 years ago
class ProxyAnimal(proxy.Proxy[Animal]):
    name = proxy.Text()
    get_sound = proxy.Method()
    wait_something = proxy.AsyncMethod()

This :point_up: looks like it would mess with typing, no? How would mypy know the correct types of get_sound or wait_something.

I'm also not entirely sold yet if optimizing for proxy objects is the best way forward. While it is convenient, I'm scared it would lead us back to the RPC style approach to multiprocessing that we replaced with a more explicit, event-based paradigm.

I think that there may be some middle ground where the object on the consumer side is still hand crafted in the sense of it being:

I also think that there isn't evens so much inconvenience on the consumer side today. E.g. this is an RPC module in Trinity fetching the network id via the event bus.

    async def version(self) -> str:
        """
        Returns the current network ID.
        """
        response = await self.event_bus.request(
            NetworkIdRequest(),
            TO_NETWORKING_BROADCAST_CONFIG
        )
        return str(response.network_id)

And this could even still get simpler with https://github.com/ethereum/lahja/issues/90

I think it is more the serving side that needs more of our attention. Currently in Trinity we have the PeerPoolEventServer that receives requests and delegates them somewhere and then wraps the response and sends it back. I think that is the bigger inconvenience we have today. Some of this may be easier if our PeerPool would just natively speak lahja so that there would simply be no need for a PeerPoolEventServer to step in. But it could also be that we could come up with better tools that would just simplify the job of replaying events onto some methods and returning the results (and capturing exceptions).

To recap my thoughts, I'm not saying I'm against proxies, I'm just saying maybe there are other options that we should check before.

pipermerriam commented 4 years ago

I'm no longer convinced this is a good idea.