nautechsystems/nautilus_trader · error

Close command should not be drained

Error message

Close command should not be drained

What it means

In the Redis cache's buffer-draining loop (crates/infrastructure/src/redis/cache.rs), `drain_buffer` matches on `DatabaseOperation` variants and panics if it encounters `DatabaseOperation::Close`. Control commands like Close are lifecycle signals that must be handled by the command-processing logic, not persisted/drained as data operations; reaching this branch means an internal invariant was violated — a Close op leaked into the operation buffer.

Source

Thrown at crates/infrastructure/src/redis/cache.rs:869

                } else {
                    log::error!("Null `payload` for `replace_list`");
                }
            }
            DatabaseOperation::Delete => {
                log::debug!(
                    "Processing DELETE for collection: {}, key: {}, payload: {:?}",
                    collection,
                    key,
                    msg.payload.as_ref().map(std::vec::Vec::len)
                );
                // `payload` can be `None` for a delete operation
                if let Err(e) = delete(&mut pipe, collection, &key, msg.payload) {
                    log::error!("{e}");
                } else {
                    has_pending_ops = true;
                }
            }
            DatabaseOperation::Close => panic!("Close command should not be drained"),
            DatabaseOperation::Flush(_) => panic!("Flush command should not be drained"),
        }
    }

    flush_pending_pipeline(conn, &mut pipe, &mut has_pending_ops).await;
}

async fn flush_pending_pipeline(
    conn: &mut ConnectionManager,
    pipe: &mut Pipeline,
    has_pending_ops: &mut bool,
) {
    if !*has_pending_ops {
        return;
    }

    if let Err(e) = pipe.query_async::<()>(conn).await {
        log::error!("{e}");

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Audit how DatabaseOperation::Close enters the buffer; handle it in the control path (e.g. match it in handle_command/process_commands and stop the worker) rather than enqueueing it.
  2. Filter out Close/Flush variants before pushing operations into the buffer.
  3. Reproduce with the exact shutdown sequence, capture a backtrace (RUST_BACKTRACE=1), and file/check for an upstream fix.

Example fix

// before
if let Err(e) = cmd_tx.send(DatabaseOperation::Close).await { /* ... */ } // Close goes into data buffer

// after
// signal shutdown through the dedicated control channel / handle_command match arm,
// not by enqueueing DatabaseOperation::Close into the drained buffer
Defensive patterns

Strategy: validation

Validate before calling

// never enqueue control ops into the data buffer
match op {
    DatabaseOperation::Close | DatabaseOperation::Flush(_) => handle_control(op),
    _ => buffer_tx.send(op).await?,
}

Try / catch

// panics are unrecoverable here; serialize shutdown so Close is handled before flush_buffer runs
shutdown_token.cancel().await; worker.join().await;

Prevention

When it happens

Trigger: A `DatabaseOperation::Close` being enqueued into the database command buffer and then processed by `drain_buffer` (called from `process_commands`, `handle_command`, or `flush_buffer`) instead of being consumed as a shutdown/control signal.

Common situations: Racing a cache shutdown/flush with in-flight writes so a Close command lands in the op queue; custom code or an older version constructing/enqueueing Close into the command channel; bugs in internal command routing during teardown.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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