nautechsystems/nautilus_trader · error

{e}; subscription rollback failed: {close_error}

Error message

{e}; subscription rollback failed: {close_error}

What it means

When subscribe_one re-checks pool openness after a shard was assigned, finding the pool closed triggers a rollback: the token assignment is released and, if the shard became empty, it is closed. If that rollback close itself fails, the original 'pool is closed' error and the rollback close error are chained into this combined message; otherwise the original error is returned alone.

Source

Thrown at crates/adapters/polymarket/src/websocket/pool.rs:529

            out_rx: Mutex::new(None),
            socket_factory: Mutex::new(None),
            closed: AtomicBool::new(false),
        }
    }

    // Callers hold `wire_mutex`.
    async fn subscribe_one(&self, asset_id: String) -> anyhow::Result<()> {
        let token = Ustr::from(asset_id.as_str());

        let Some(handle) = self.assign(token).await? else {
            return Ok(());
        };

        if let Err(e) = self.ensure_open() {
            if let ReleaseOutcome::CloseShard(id, shard) = self.release(token)
                && let Err(close_error) = self.close_shard(id, shard).await
            {
                anyhow::bail!("{e}; subscription rollback failed: {close_error}");
            }
            return Err(e);
        }

        if let Err(e) = handle.subscribe_market(vec![asset_id]).await {
            // Roll back so a failed send leaves no stale assignment or empty shard.
            if let ReleaseOutcome::CloseShard(id, shard) = self.release(token)
                && let Err(close_error) = self.close_shard(id, shard).await
            {
                anyhow::bail!("{e}; subscription rollback failed: {close_error}");
            }
            return Err(e);
        }
        Ok(())
    }

    // Callers hold `wire_mutex`.
    async fn unsubscribe_one(&self, asset_id: String) -> anyhow::Result<()> {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Treat both errors as terminal: the pool is closed — reconnect with a new pool instead of retrying
  2. Avoid racing subscribe against disconnect; cancel subscriptions before initiating shutdown
  3. Log both the original and rollback errors from the combined message for diagnosis
  4. If the rollback failure indicates an already-dead connection, cleanup should tolerate it — retry disconnect/drop the pool
Defensive patterns

Strategy: retry

Validate before calling

if pool.is_closed() { pool = reconnect_pool()?; }

Try / catch

match pool.subscribe_one(asset_id).await {
    Err(e) if e.to_string().contains("pool is closed") => {
        let pool = reconnect_pool()?;
        pool.subscribe_one(asset_id).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: A disconnect/close races with subscribe_one: the pool closes between the initial ensure_open and the post-assignment ensure_open, AND close_shard fails during rollback (e.g., the shard's connection is already dead).

Common situations: Adapter shutdown while subscription requests are in flight; simultaneous disconnect and subscribe from different tasks.

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/57a881746369ce84. Report an issue: GitHub.