Skip to content

A rescheduled idle-cache-clean timer survives Session::close() #147

Open

Description

@soarkit
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions