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
- Create the timer through the LiveClock's `set_time_alert_ns`/`set_timer_ns` helpers, which wire the event sender correctly.
- Ensure a `TimeEventSender` is supplied when constructing the timer for a Rust callback.
- If running on the local thread is intended, use the appropriate registered callback variant rather than a senderless Rust callback.
- 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
- Always construct live timers via LiveClock::set_time_alert_ns / set_timer_ns
- Never hand-construct LiveTimerGenerator with a senderless Rust callback
- Add an assertion test that timer construction carries a sender for Rust callbacks
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
- Callback should exist
- Failed to set spread quote timer
- Failed to set spread quote vega pricing timeout
- Event '{}' should have associated handler
- timer queue peeked Some but pop returned None
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/46e374effb17620e.
Report an issue: GitHub.