async def process_callbacks(self, message: WebsocketMessage) -> None:
     if self.callbacks is not None:
         tasks = []
         for cb in self.callbacks:
             # If message contains a websocket, then the websocket handle will be passed to the callbacks.
             # This is useful for duplex websockets
             if message.websocket is not None:
                 tasks.append(
                     async_create_task(
                         cb(message.message, message.websocket)))
             else:
                 tasks.append(async_create_task(cb(message.message)))
         await asyncio.gather(*tasks)
    async def start_websockets(self, websocket_start_time_interval_ms: int = 0) -> None:
        if len(self.subscription_sets) < 1:
            raise CryptoXLibException("ERROR: There are no subscriptions to be started.")

        tasks = []
        startup_delay_ms = 0
        for id, subscription_set in self.subscription_sets.items():
            subscription_set.websocket_mgr = self._get_websocket_mgr(subscription_set.subscriptions, startup_delay_ms, self.ssl_context)
            tasks.append(async_create_task(
                subscription_set.websocket_mgr.run())
            )
            startup_delay_ms += websocket_start_time_interval_ms

        done, pending = await asyncio.wait(tasks, return_when = asyncio.FIRST_EXCEPTION)
        for task in done:
            try:
                task.result()
            except Exception as e:
                LOG.error(f"Unrecoverable exception occurred while processing messages: {e}")
                LOG.info(f"Remaining websocket managers scheduled for shutdown.")

                await self.shutdown_websockets()

                if len(pending) > 0:
                    await asyncio.wait(pending, return_when = asyncio.ALL_COMPLETED)

                LOG.info("All websocket managers shut down.")
                raise
    async def run(self) -> None:
        await self.validate_subscriptions()
        await self.initialize_subscriptions()

        try:
            # main loop ensuring proper reconnection if required
            while True:
                LOG.debug(f"Initiating websocket connection.")
                websocket = None
                try:
                    # sleep for the requested period before initiating the connection. This is useful when client
                    # opens many connections at the same time and server cannot handle the load
                    await asyncio.sleep(self.startup_delay_ms / 1000.0)
                    LOG.debug(
                        f"Websocket initiation delayed by {self.startup_delay_ms}ms."
                    )

                    websocket = self.get_websocket()
                    await websocket.connect()

                    done, pending = await asyncio.wait(
                        [
                            async_create_task(self.main_loop(websocket)),
                            async_create_task(self.periodic_loop(websocket))
                        ],
                        return_when=asyncio.FIRST_EXCEPTION)
                    for task in done:
                        try:
                            task.result()
                        except Exception:
                            LOG.debug(
                                "Websocket processing has led to an exception, all pending tasks will be cancelled."
                            )
                            for task in pending:
                                if not task.cancelled():
                                    task.cancel()
                            if len(pending) > 0:
                                try:
                                    await asyncio.wait(
                                        pending,
                                        return_when=asyncio.ALL_COMPLETED)
                                except asyncio.CancelledError:
                                    await asyncio.wait(
                                        pending,
                                        return_when=asyncio.ALL_COMPLETED)
                                    raise
                                finally:
                                    LOG.debug(
                                        "All pending tasks cancelled successfully."
                                    )
                            raise
                except (websockets.ConnectionClosedError,
                        websockets.ConnectionClosedOK,
                        websockets.InvalidStatusCode, ConnectionResetError,
                        WebsocketClosed, WebsocketError,
                        WebsocketReconnectionException) as e:
                    if self.auto_reconnect:
                        LOG.info(
                            "A recoverable exception has occurred, the websocket will be restarted automatically."
                        )
                        self._print_subscriptions()
                        LOG.info(f"Exception: {e}")
                    else:
                        raise
                finally:
                    if websocket is not None:
                        LOG.debug("Closing websocket connection.")
                        await websocket.close()
        except asyncio.CancelledError:
            LOG.warning(f"The websocket was requested to be shutdown.")
        except Exception:
            LOG.error(
                f"An exception occurred. The websocket manager will be closed."
            )
            self._print_subscriptions()
            raise