Skip to content

Nested RPC calls deadlock #58

@eclectocrat

Description

@eclectocrat

When my client calls a server RPC, which then calls a client RPC, I can't await the return value from the client.

eg.:

WebsocketRPCEndpoint.main_loop
  -> await channel handler
    -> call client RPC
    -> await result from client... Oh no, the main_loop is already awaiting! TIMEOUT!

I have "fixed" this for my usecase like so:

class AsyncWebsocketRPCEndpoint(WebsocketRPCEndpoint):
    async def main_loop(self, websocket: WebSocket, client_id: str = None, **kwargs):
        """Override default main loop to handle nested RPC calls."""
        try:
            await self.manager.connect(websocket)
            logger.info(f"Client connected", {"remote_address": websocket.client})
            simple_websocket = self._serializing_socket_cls(
                WebSocketSimplifier(websocket, frame_type=self._frame_type)
            )
            channel = RpcChannel(
                self.methods,
                simple_websocket,
                sync_channel_id=self._rpc_channel_get_remote_id,
                **kwargs,
            )
            # register connect / disconnect handler
            channel.register_connect_handler(self._on_connect)
            channel.register_disconnect_handler(self._on_disconnect)
            await channel.on_connect()

            async def handle_message_safely(data):
                try:
                    await channel.on_message(data)
                except Exception as e:
                    logger.exception(f"🔴 Unhandled error processing message: {e}")

            try:
                while True:
                    data = await simple_websocket.recv()
                    asyncio.create_task(handle_message_safely(data))
            except WebSocketDisconnect:
                logger.info(
                    f"Client disconnected - {websocket.client.port} :: {channel.id}"
                )
                await self.handle_disconnect(websocket, channel)
            except:
                # cover cases like - RuntimeError('Cannot call "send" once a close message has been sent.')
                logger.info(
                    f"Client connection failed - {websocket.client.port} :: {channel.id}"
                )
                await self.handle_disconnect(websocket, channel)
        except:
            logger.exception(f"Failed to serve - {websocket.client.port}")
            self.manager.disconnect(websocket)

I have no idea if this is robust for all cases and am not necessarily suggesting it as a change to the library, I don't have the braincells available to think this through deeply at the moment, but I am leaving this here so that anyone else which naively assumes that RPC's can be nested the way ordinary functions are can see this issue and possibly use this as a workaround for their code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions