nautechsystems/nautilus_trader · error · anyhow::Error

Failed to start WebSocket handler task generation: {e}

Error message

Failed to start WebSocket handler task generation: {e}

What it means

After shutting down previous OKX WebSocket handler tasks, `connect` starts a new task generation in the handler task group. If `start_generation()` returns an error (task group in a bad state, e.g. still draining or already shutting down), connect aborts with this message.

Source

Thrown at crates/adapters/okx/src/websocket/client.rs:610

    /// # Errors
    ///
    /// Returns an error if the connection process fails.
    pub async fn connect(&mut self) -> anyhow::Result<()> {
        let connect_lock = Arc::clone(&self.connect_lock);
        let _connect_guard = connect_lock.lock().await;

        if !self.handler_tasks.is_empty() && !self.handler_tasks.all_finished() {
            anyhow::bail!("Cannot connect while previous WebSocket handler task is still running");
        }

        if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
            self.handler_tasks.begin_shutdown();
            self.handler_tasks
                .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
                .await
                .map_err(|e| anyhow::anyhow!("Previous WebSocket handler failed: {e}"))?;
            self.handler_tasks.start_generation().map_err(|e| {
                anyhow::anyhow!("Failed to start WebSocket handler task generation: {e}")
            })?;
        }
        let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
            anyhow::anyhow!("Failed to acquire WebSocket handler task spawner: {e}")
        })?;
        let handler_abort = CancellationToken::new();
        *self.handler_abort.lock() = handler_abort.clone();
        let mut rollback = ConnectRollback {
            handler_tasks: Arc::clone(&self.handler_tasks),
            signal: Arc::clone(&self.signal),
            handler_abort: handler_abort.clone(),
            socket_control: self.socket_control.clone(),
            armed: true,
        };

        // Reset signal so is_active()/is_closed() work after a previous close()
        self.signal.store(false, Ordering::Release);

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Serialize connection lifecycle: guard connect/disconnect behind a mutex or ensure only one reconnect driver exists.
  2. Retry connect after the previous shutdown completes (backoff and re-attempt).
  3. Inspect the inner `{e}` to identify whether the task group is draining/shutdown and wait for that state to clear.
Defensive patterns

Strategy: retry

Try / catch

// rust
for attempt in 0..3 {
    match client.connect().await {
        Ok(()) => break,
        Err(e) if e.to_string().contains("handler task generation") => {
            tokio::time::sleep(Duration::from_millis(500 * (attempt + 1))).await;
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: connect is called while the handler task group cannot begin a new generation — typically immediately after a failed shutdown of the prior generation or concurrent connect calls on the same client.

Common situations: Concurrent `connect()` calls from multiple threads/tasks sharing one client; reconnect logic racing with an in-progress disconnect; calling connect during client shutdown.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/a131dd89c1e177ec. Report an issue: GitHub.