nautechsystems/nautilus_trader · error · anyhow::Error

Architect AX data WebSocket handler did not stop after abort

Error message

Architect AX data WebSocket handler did not stop after abort

What it means

Returned by the Architect AX data WebSocket client's close() when, after issuing an abort to the handler task, the join outcome is TaskJoinOutcome::Incomplete — meaning the task did not finish within the abort/join window.

Source

Thrown at crates/adapters/architect_ax/src/websocket/data/client.rs:1081

        self.signal.store(true, Ordering::Release);

        let outcome = self
            .task_handle
            .finish(Duration::from_secs(2), Duration::from_secs(2))
            .await;

        *self.reconnect_headers.lock() = None;

        if let Some(control) = &self.socket_control {
            control.deregister();
        }

        match outcome {
            None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => Ok(()),
            Some(TaskJoinOutcome::Failed(error)) => Err(anyhow::anyhow!(
                "Architect AX data WebSocket handler failed: {error}"
            )),
            Some(TaskJoinOutcome::Incomplete) => Err(anyhow::anyhow!(
                "Architect AX data WebSocket handler did not stop after abort"
            )),
        }
    }

    async fn send_cmd(&self, cmd: HandlerCommand) -> AxWsResult<()> {
        let guard = self.cmd_tx.read().await;
        guard
            .send(cmd)
            .map_err(|e| AxWsClientError::ChannelError(e.to_string()))
    }
}

impl Drop for AxMdWebSocketClient {
    fn drop(&mut self) {
        if Arc::strong_count(&self.task_handle) == 1 && !self.task_handle.is_empty() {
            self.cancellation_token.load().cancel();
            self.signal.store(true, Ordering::Release);

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check the handler loop for awaits or blocking calls that ignore the abort/cancellation token
  2. Increase the shutdown timeout if it is legitimately slow, or add cooperative cancellation checks in the handler loop
  3. Look for deadlock between handler and command channels (e.g. unbounded queue consumption ordering)
  4. Restart the client/connection; the task may be leaked and should be reported as a bug if reproducible

Example fix

// before
loop {
    let msg = ws.next().await; // ignores cancellation
    ...
}
// after
loop {
    tokio::select! {
        _ = cancel_token.cancelled() => break,
        msg = ws.next() => { ... }
    }
}
Defensive patterns

Strategy: try-catch

Try / catch

// Rust
match client.close().await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("did not stop after abort") => {
        tracing::error!("handler task leaked: {e}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling close() and the handler task neither completes, fails, nor acknowledges the abort in time (e.g. it is stuck in a long await, a blocking loop, or ignoring the abort signal).

Common situations: Handler blocked on a network read without honoring cancellation; a deadlock between the handler and command channel; slow/hung remote server keeping the task alive during shutdown.

Related errors


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