Skip to content

Bug: DefaultHasher causes silent TaskId collisions across different task types #21

Description

@ZialeHub

Description

UniqueId::unique_id() hashes only recurrence + self.salt() using std::hash::DefaultHasher. The default implementation of Salt::salt() returns an empty string. This means any two different task types that both
use the default salt and are scheduled with the same Recurrence will produce the same TaskId, causing the second schedule() call to silently return Err(AlreadyScheduled) even though the two tasks are entirely
different.

Additionally, DefaultHasher is explicitly documented by Rust as not guaranteed to be stable across Rust versions, which breaks the serde restart() feature.

Affected file

  • src/task/mod.rs — unique_id() method (lines 69–75)

Reproduction

  #[derive(Task, Salt)] struct TaskA { val: TaskState<u32> }
  impl SyncTaskHandler for TaskA { fn run(&self) {} }

  #[derive(Task, Salt)] struct TaskB { val: TaskState<u32> }
  impl SyncTaskHandler for TaskB { fn run(&self) {} }
  
  let rec = every(5.seconds());
  scheduler.schedule(Box::new(TaskA::new(0)), rec).unwrap(); // Ok
  scheduler.schedule(Box::new(TaskB::new(0)), rec).unwrap(); // Err(AlreadyScheduled) — wrong!

Fix

Mix the concrete type identity into the hash to guarantee uniqueness across types:

  fn unique_id(&self, recurrence: Recurrence) -> TaskId {
      let mut hasher = DefaultHasher::new();
      recurrence.hash(&mut hasher);
      std::any::TypeId::of::<Self>().hash(&mut hasher);
      self.salt().hash(&mut hasher);
      hasher.finish()
  }

Also consider replacing DefaultHasher with a stable, deterministic hasher (e.g. FxHasher or a fixed-seed SipHash) to protect the serde restart feature against cross-version breakage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions