{"record":{"id":"46e374effb17620e","repo":"nautechsystems/nautilus_trader","slug":"timer-event-sender-was-unset-for-rust-callback-sys","errorCode":null,"errorMessage":"timer event sender was unset for Rust callback system","messagePattern":"timer event sender was unset for Rust callback system","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/common/src/live/timer.rs","lineNumber":207,"sourceCode":"    ///\n    /// Starting a timer whose task is still active aborts that task first\n    /// (restart semantics); a previously fired event that is already queued\n    /// still dispatches.\n    ///\n    /// An event whose following schedule would overflow [`UnixNanos`] is the timer's final event.\n    /// The timer then remains expired, and further calls return without starting a task.\n    ///\n    /// # Panics\n    ///\n    /// Panics if using a Rust callback (`Rust` or `RustLocal`) without a `TimeEventSender`.\n    #[allow(unused_variables)]\n    pub fn start(&mut self) {\n        if let OwnerCallback::Senderless(callback) = &self.callback {\n            match callback {\n                #[cfg(feature = \"python\")]\n                TimeEventCallback::Python(_) => {}\n                TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_) => {\n                    panic!(\"timer event sender was unset for Rust callback system\");\n                }\n            }\n        }\n\n        let event_name = self.name;\n        let stop_time_ns = self.stop_time_ns;\n        let interval_ns = DurationNanos::new(self.interval_ns.get());\n\n        let retired_task = self.retire_task();\n\n        if self.exhausted {\n            return;\n        }\n\n        let mut observed_next = retired_task.map_or_else(\n            || self.next_time_ns.load(atomic::Ordering::SeqCst),\n            |retirement| retirement.next_time_ns,\n        );","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/live/timer.rs#L189-L225","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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()`."],"exampleFix":"// before\nlet timer = LiveTimerGenerator::new(name, interval, start, stop, fire_immediately, OwnerCallback::Senderless(TimeEventCallback::Rust(cb)), None /* sender */);\ntimer.start(); // panics\n// after\nlet timer = LiveTimerGenerator::new(name, interval, start, stop, fire_immediately, callback, Some(sender));\ntimer.start();","handlingStrategy":"type-guard","validationCode":"// Guard before start()\nif let OwnerCallback::Senderless(TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_)) = timer.callback() {\n    if timer.sender().is_none() {\n        panic!(\"timer has no TimeEventSender for Rust callback\");\n    }\n}","typeGuard":"fn has_sender_for_rust_callback(cb: &OwnerCallback, sender: &Option<TimeEventSender>) -> bool {\n    !matches!(cb, OwnerCallback::Senderless(TimeEventCallback::Rust(_) | TimeEventCallback::RustLocal(_))) || sender.is_some()\n}","tryCatchPattern":"// Panics are not catchable in Rust; wrap risky setup in a constructor that returns Result\nfn build_timer(...) -> anyhow::Result<LiveTimerGenerator> {\n    let sender = sender.ok_or_else(|| anyhow!(\"sender required for Rust callback\"))?;\n    Ok(LiveTimerGenerator::new(..., Some(sender)))\n}","preventionTips":["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"],"tags":["rust","panic","timer","live-clock","internal-wiring"],"backgroundTag":"internal-invariant-violation","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}