nautechsystems/nautilus_trader · critical

timer event sender was unset for Rust callback system

Error message

timer event sender was unset for Rust callback system

What it means

`LiveTimerGenerator::start` panics when a timer was created with a senderless Rust callback (`TimeEventCallback::Rust` or `RustLocal`) because firing such a timer requires a `TimeEventSender` channel to deliver the time event, and none was set. Python callbacks can run directly on the owner thread, but Rust callbacks are invoked via the event sender, so a missing sender is an internal wiring bug. The panic guards against silently starting a timer whose events would never be delivered.

Source

Thrown at crates/common/src/live/timer.rs:207

    ///
    /// Starting a timer whose task is still active aborts that task first
    /// (restart semantics); a previously fired event that is already queued
    /// still dispatches.
    ///
    /// An event whose following schedule would overflow [`UnixNanos`] is the timer's final event.
    /// The timer then remains expired, and further calls return without starting a task.
    ///
    /// # Panics
    ///
    /// Panics if using a Rust callback (`Rust` or `RustLocal`) without a `TimeEventSender`.
    #[allow(unused_variables)]
    pub fn start(&mut self) {
        if let OwnerCallback::Senderless(callback) = &self.callback {
            match callback {
                #[cfg(feature = "python")]
                TimeEventCallback::Python(_) => {}
                TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => {
                    panic!("timer event sender was unset for Rust callback system");
                }
            }
        }

        let event_name = self.name;
        let stop_time_ns = self.stop_time_ns;
        let interval_ns = DurationNanos::new(self.interval_ns.get());

        let retired_task = self.retire_task();

        if self.exhausted {
            return;
        }

        let mut observed_next = retired_task.map_or_else(
            || self.next_time_ns.load(atomic::Ordering::SeqCst),
            |retirement| retirement.next_time_ns,
        );

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Create the timer through the LiveClock's `set_time_alert_ns`/`set_timer_ns` helpers, which wire the event sender correctly.
  2. Ensure a `TimeEventSender` is supplied when constructing the timer for a Rust callback.
  3. If running on the local thread is intended, use the appropriate registered callback variant rather than a senderless Rust callback.
  4. Check that no code path unsets or never assigns the sender field between construction and `start()`.

Example fix

// before
let timer = LiveTimerGenerator::new(name, interval, start, stop, fire_immediately, OwnerCallback::Senderless(TimeEventCallback::Rust(cb)), None /* sender */);
timer.start(); // panics
// after
let timer = LiveTimerGenerator::new(name, interval, start, stop, fire_immediately, callback, Some(sender));
timer.start();
Defensive patterns

Strategy: type-guard

Validate before calling

// Guard before start()
if let OwnerCallback::Senderless(TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_)) = timer.callback() {
    if timer.sender().is_none() {
        panic!("timer has no TimeEventSender for Rust callback");
    }
}

Type guard

fn has_sender_for_rust_callback(cb: &OwnerCallback, sender: &Option<TimeEventSender>) -> bool {
    !matches!(cb, OwnerCallback::Senderless(TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_))) || sender.is_some()
}

Try / catch

// Panics are not catchable in Rust; wrap risky setup in a constructor that returns Result
fn build_timer(...) -> anyhow::Result<LiveTimerGenerator> {
    let sender = sender.ok_or_else(|| anyhow!("sender required for Rust callback"))?;
    Ok(LiveTimerGenerator::new(..., Some(sender)))
}

Prevention

When it happens

Trigger: Calling `timer.start()` on a `LiveTimerGenerator` constructed via a path that produced `OwnerCallback::Senderless` with a Rust callback and no `TimeEventSender` — e.g. building a timer manually instead of through `set_time_alert_ns`/`set_timer_ns` on the LiveClock, or never assigning the sender field before start.

Common situations: Custom Rust integrations constructing `LiveTimerGenerator` directly rather than via the clock helpers; refactors that change callback registration so the sender field ends up `None`; embeddings (non-Python) that assumed senderless Rust callbacks were supported.

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/46e374effb17620e. Report an issue: GitHub.