Skip to content

Commit 3dc39d4

Browse files
committed
fix(kernel): dedupe cron jobs in saved_crons restore to prevent concurrent activation duplicates
If two registry entries share the same `hand_tag` and `hand_role` (a race between concurrent `activate_hand` calls or a stale cleanup), the loop would collect the same CronJob twice from list_jobs and replay both via add_job — leaving duplicate jobs that fire in lockstep. Track seen CronJobIds per role bucket so each saved job is added at most once.
1 parent e67efec commit 3dc39d4

1 file changed

Lines changed: 12 additions & 4 deletions

File tree

  • crates/librefang-kernel/src/kernel

crates/librefang-kernel/src/kernel/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8610,10 +8610,18 @@ system_prompt = "You are a helpful assistant."
86108610
}
86118611
let taken_crons = self.cron_scheduler.list_jobs(old_id);
86128612
if !taken_crons.is_empty() {
8613-
saved_crons
8614-
.entry(old_role.clone())
8615-
.or_default()
8616-
.extend(taken_crons);
8613+
// Dedupe by job id within this snapshot: if two registry
8614+
// entries somehow tag the same role (concurrent activation
8615+
// racing the `kill_agent` cleanup, or a bug that left two
8616+
// tagged agents alive), the same `CronJob` could be
8617+
// collected twice and re-added twice — yielding duplicate
8618+
// jobs that fire side-by-side. Deterministically keep
8619+
// exactly one per `CronJobId`.
8620+
let bucket: &mut Vec<librefang_types::scheduler::CronJob> =
8621+
saved_crons.entry(old_role.clone()).or_default();
8622+
let seen: std::collections::HashSet<librefang_types::scheduler::CronJobId> =
8623+
bucket.iter().map(|j| j.id).collect();
8624+
bucket.extend(taken_crons.into_iter().filter(|j| !seen.contains(&j.id)));
86178625
}
86188626
if let Err(e) = self.kill_agent(old_id) {
86198627
warn!(agent = %old_id, error = %e, "Failed to kill old hand agent");

0 commit comments

Comments
 (0)