Skip to content

Websocket _send_heartbeat raises AttributeError #4062

Closed
@pjknkda

Description

Long story short

Websocket heartbeat sender task raises AttributeError when connecting with slow clients.

class WebSocketResponse(StreamResponse):
    ...

    def _reset_heartbeat(self) -> None:
        self._cancel_heartbeat()

        if self._heartbeat is not None:
            self._heartbeat_cb = call_later(
                self._send_heartbeat, self._heartbeat, self._loop)

    def _send_heartbeat(self) -> None:
        if self._heartbeat is not None and not self._closed:
            # fire-and-forget a task is not perfect but maybe ok for
            # sending ping. Otherwise we need a long-living heartbeat
            # task in the class.
            self._loop.create_task(self._writer.ping())  # type: ignore
            ...

    async def prepare(self, request: BaseRequest) -> AbstractStreamWriter:
        ...
        protocol, writer = self._pre_start(request)
        payload_writer = await super().prepare(request)
        assert payload_writer is not None
        self._post_start(request, protocol, writer)
        ...

    def _pre_start(self, request: BaseRequest) -> Tuple[str, WebSocketWriter]:
        self._loop = request._loop

        headers, protocol, compress, notakeover = self._handshake(
            request)

        self._reset_heartbeat()
        ...

    def _post_start(self, request: BaseRequest,
                    protocol: str, writer: WebSocketWriter) -> None:
        self._ws_protocol = protocol
        self._writer = writer
        ...

I guess that the exception is caused by the race condition in the following steps

  1. In WebSocketResponse._pre_start, WebSocketResponse._reset_heartbeat is called and WebSocketResponse._send_heartbeat is scheduled to be executed.
  2. Because await super().prepare can take a long time, even super().prepare task is not finished, WebSocketResponse._send_heartbeat can be executed.
  3. However, self._writer which is used by WebSocketResponse._send_heartbeat is not initialized yet because self._post_start is not called yet.

Expected behaviour

The exception should not be raised.

Actual behaviour

Exception in callback <bound method WebSocketResponse._send_heartbeat of <WebSocketResponse Switching Protocols ...>>
handle: <TimerHandle WebSocketResponse._send_heartbeat>
Traceback (most recent call last):
  File "uvloop/cbhandles.pyx", line 265, in uvloop.loop.TimerHandle._run
  File "/usr/local/lib/python3.7/site-packages/aiohttp/web_ws.py", line 101, in _send_heartbeat
    self._loop.create_task(self._writer.ping())  # type: ignore
AttributeError: 'NoneType' object has no attribute 'ping'

Steps to reproduce

  1. Set a very short websocket heartbeat interval.
  2. Insert a random async delay (which is larger than the heartbeat interval) into StreamResponse.prepare to simulate slow client.
  3. Connect a websocket client to the server.

Your environment

  • aiohttp 3.6.0
  • uvloop 0.13.0
  • python 3.7.1 on linux

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