Repository navigation
Description
- Repository:
pingdotgg/ts-rust - Target commit:
39a7f142b64b - File:
crates/ts_goport/src/gostd/local.rs:565
Summary
Using the public session API: schedule the idle-cache-clean timer (A), let its 30 s delay expire so A's callback is queued but not yet run, schedule it again (B, which replaces the handle), run the dispatch queue so A's stale callback executes, then close_idle_cache_clean()/Session::close().
Affected code
crates/ts_goport/src/gostd/local.rs:
557 inner.arm(when(d));
558 LOCAL.with(|l| {
559 l.timers.borrow_mut().insert(id, inner.clone());
560 });
561 LocalTimer { inner }
562 }
563
564 impl LocalTimer {
565 /// Go `t.Stop()` of an AfterFunc timer: true if the call stops the
566 /// timer, false if the timer has already expired (its function is queued
567 /// or has run) or been stopped. Stop does not remove a queued run.
568 pub fn stop(&self) -> bool {
569 // The timer thread is not woken: at the old `when` it finds this
570 // timer gone and waits for the next one.
571 let pending = lock(&self.inner.shared.timers).disarm(self.inner.id);
572 self.inner.forget_if_idle();
573 pending
574 }
575
576 /// Go `t.Reset(d)` of an AfterFunc timer: true if the timer had been
577 /// active; false if it had expired or been stopped, in which case the
Expected behavior
After Close, no idle-cache-clean timer may remain armed or run; a replaced timer's stale queued callback must not clear the newer timer handle.
Actual behavior
Using the public session API: schedule the idle-cache-clean timer (A), let its 30 s delay expire so A's callback is queued but not yet run, schedule it again (B, which replaces the handle), run the dispatch queue so A's stale callback executes, then close_idle_cache_clean()/Session::close(). The newer timer B is still armed and its callback runs after close; the cancel function captured for B fires although close supposedly cancelled it.
How to reproduce
Drive the public session/timer APIs with a shortened delay: schedule A, wait for it to queue, reschedule B, run pending callbacks, close, and observe Bs callback still executing.
Root cause
schedule_idle_cache_clean stores the timer handle in a session field (src/project/session.rs:1140). The timer thread runs the previous callback after stop() only prevents future runs, not an already-queued run. When As queued callback executes after B was stored, it assigns None to the slot (treating it as its own), erasing Bs handle; close then has nothing to cancel. gostd::local (src/gostd/local.rs:565) has no way to remove a queued run.
Suggested fix
Tag each scheduled run with a generation/handle id and have the callback clear the slot only when it still owns it; make close cancel by generation rather than by the stored handle. Add a test that queues A, reschedules B, runs As stale callback, closes, and asserts B never fires.
Activity