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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions