nautechsystems/nautilus_trader · error · anyhow::Error

Polymarket WebSocket handler did not stop after abort

Error message

Polymarket WebSocket handler did not stop after abort

What it means

disconnect() aborts the feed-handler task and waits up to 2 seconds for it to finish. TaskJoinOutcome::Incomplete means the task neither completed, failed, nor acknowledged the abort within that window — it is stuck (e.g. blocked in a non-cancellation-safe await). The disconnect call reports this as an error because a stale handler task may still hold the WebSocket connection and channel receivers.

Source

Thrown at crates/adapters/polymarket/src/websocket/client.rs:459

        log::debug!("Disconnecting Polymarket WebSocket");
        self.signal.store(true, Ordering::Relaxed);

        if let Err(e) = self.cmd_tx.read().await.send(HandlerCommand::Disconnect) {
            log::debug!("Failed to send disconnect (handler may already be shut down): {e}");
        }

        let task_result = match finish_task(
            &mut self.task_handle,
            std::time::Duration::from_secs(2),
            std::time::Duration::from_secs(2),
        )
        .await
        {
            None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => Ok(()),
            Some(TaskJoinOutcome::Failed(error)) => Err(anyhow::anyhow!(
                "Polymarket WebSocket handler failed: {error}"
            )),
            Some(TaskJoinOutcome::Incomplete) => Err(anyhow::anyhow!(
                "Polymarket WebSocket handler did not stop after abort"
            )),
        };
        // Invalidate after the task has stopped so any in-flight auth_tracker.succeed()
        // calls from the handler cannot race with and survive the invalidation.
        self.auth_tracker.invalidate();

        if let Some(control) = &self.socket_control {
            control.deregister();
        }
        log::debug!("Polymarket WebSocket disconnected");
        task_result
    }

    /// Returns `true` if the WebSocket is actively connected.
    #[must_use]
    pub fn is_active(&self) -> bool {
        ConnectionMode::from_atomic(&self.connection_mode).is_active()

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure all connect/read/write operations inside the handler use timeouts (tokio::time::timeout) so abort is honored promptly
  2. Retry disconnect() once — the stuck await may complete and let a second join succeed
  3. Avoid holding locks across .await points in the handler loop
  4. If a stale task remains, drop the whole client so the channel closes and the task unwinds eventually

Example fix

// before
client.disconnect().await?; // may fail: handler stuck >2s after abort
// after
if let Err(e) = client.disconnect().await {
    log::warn!("handler did not stop in time, dropping client: {e}");
    drop(client); // dropping closes channels and lets the task unwind
}
Defensive patterns

Strategy: retry

Try / catch

if let Err(e) = client.disconnect().await {
    if e.to_string().contains("did not stop after abort") {
        tokio::time::sleep(Duration::from_millis(100)).await;
        client.disconnect().await.ok(); // second join usually succeeds
    }
}

Prevention

When it happens

Trigger: Calling disconnect() while the handler task is blocked in an await point that ignores cancellation — e.g. a long blocking read, a lock held across awaits, or a reconnect loop stuck on TCP connect to an unresponsive host without timeouts.

Common situations: Network partition causing a connect attempt with no timeout to hang; handler waiting on a mutex starved by another task; very slow/broken gateway that stalls the read loop past the 2s join window.

Related errors


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