async def keepalive_ping(self) -> None:
        """
        Send a Ping frame and wait for a Pong frame at regular intervals.

        This coroutine exits when the connection terminates and one of the
        following happens:
        - :meth:`ping` raises :exc:`ConnectionClosed`, or
        - :meth:`close_connection` cancels :attr:`keepalive_ping_task`.

        """
        if self.ping_interval is None:
            return

        try:
            while True:
                await asyncio.sleep(self.ping_interval, loop=self.loop)

                # ping() cannot raise ConnectionClosed, only CancelledError:
                # - If the connection is CLOSING, keepalive_ping_task will be
                #   canceled by close_connection() before ping() returns.
                # - If the connection is CLOSED, keepalive_ping_task must be
                #   canceled already.
                ping_waiter = await self.ping()

                if self.ping_timeout is not None:
                    try:
                        await asyncio.wait_for(
                            ping_waiter, self.ping_timeout, loop=self.loop
                        )
                    except asyncio.TimeoutError:
                        logger.debug("%s ! timed out waiting for pong", self.side)
                        self.fail_connection(1011)
                        break

        except asyncio.CancelledError:
            raise

        except Exception:
            logger.warning("Unexpected exception in keepalive ping task", exc_info=True)
Send a Ping frame and wait for a Pong frame at regular intervals.

        This coroutine exits when the connection terminates and one of the
        following happens:
        - :meth:`ping` raises :exc:`ConnectionClosed`, or
        - :meth:`close_connection` cancels :attr:`keepalive_ping_task`.