1use std::collections::BTreeMap;
10use std::sync::Arc;
11use std::time::Duration;
12
13use bhtune_cli::cancel::CtrlCHandle;
14use tokio::sync::Mutex;
15use tokio::task::JoinHandle;
16
17struct ActiveTask {
18 cancel: CtrlCHandle,
19 handle: JoinHandle<()>,
20}
21
22#[derive(Default)]
23struct ActiveRunState {
24 tasks: BTreeMap<i64, ActiveTask>,
25 exclusive: Option<i64>,
26}
27
28#[derive(Clone, Default)]
31pub struct ActiveRun {
32 inner: Arc<Mutex<ActiveRunState>>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct RunAlreadyActive {
39 pub run_id: i64,
40}
41
42impl ActiveRun {
43 pub async fn start(
52 &self,
53 run_id: i64,
54 cancel: CtrlCHandle,
55 task: impl Future<Output = ()> + Send + 'static,
56 ) -> Result<(), RunAlreadyActive> {
57 let mut guard = self.inner.lock().await;
58 if let Some(existing) = guard.exclusive {
59 return Err(RunAlreadyActive { run_id: existing });
60 }
61 if guard.tasks.contains_key(&run_id) {
62 return Err(RunAlreadyActive { run_id });
63 }
64 let task_handle = tokio::spawn(task);
65 let active = self.clone();
66 let handle = tokio::spawn(async move {
67 if let Err(error) = task_handle.await {
68 tracing::error!(run_id, %error, "tune task exited unexpectedly");
69 }
70 active.release(run_id).await;
71 });
72 guard.tasks.insert(run_id, ActiveTask { cancel, handle });
73 Ok(())
74 }
75
76 pub async fn reserve(&self, run_id: i64) -> Result<(), RunAlreadyActive> {
86 let mut guard = self.inner.lock().await;
87 if let Some(existing) = guard.exclusive {
88 return Err(RunAlreadyActive { run_id: existing });
89 }
90 if let Some((&existing, _)) = guard.tasks.first_key_value() {
91 return Err(RunAlreadyActive { run_id: existing });
92 }
93 guard.exclusive = Some(run_id);
94 Ok(())
95 }
96
97 pub async fn exclusive_id(&self) -> Option<i64> {
102 self.inner.lock().await.exclusive
103 }
104
105 pub async fn active_run_ids(&self) -> Vec<i64> {
108 self.inner.lock().await.tasks.keys().copied().collect()
109 }
110
111 pub async fn cancel(&self, run_id: i64) -> bool {
115 let guard = self.inner.lock().await;
116 if let Some(task) = guard.tasks.get(&run_id) {
117 task.cancel.trigger();
118 true
119 } else {
120 guard.exclusive == Some(run_id)
121 }
122 }
123
124 pub async fn release(&self, run_id: i64) {
129 let mut guard = self.inner.lock().await;
130 if guard.exclusive == Some(run_id) {
131 guard.exclusive = None;
132 } else {
133 guard.tasks.remove(&run_id);
134 }
135 }
136
137 pub async fn cancel_and_wait(&self, wait_timeout: Duration) {
141 let (tasks, _exclusive) = {
142 let mut guard = self.inner.lock().await;
143 let tasks = std::mem::take(&mut guard.tasks);
144 let exclusive = guard.exclusive.take();
145 (tasks, exclusive)
146 };
147 if tasks.is_empty() {
148 return;
149 }
150 let run_ids: Vec<i64> = tasks.keys().copied().collect();
151 let tasks: Vec<_> = tasks
152 .into_iter()
153 .map(|(run_id, task)| {
154 task.cancel.trigger();
155 (run_id, task.cancel, task.handle)
156 })
157 .collect();
158 let wait = async move {
159 for (run_id, _cancel, handle) in tasks {
160 if let Err(error) = handle.await {
161 tracing::error!(run_id, %error, "tune task exited unexpectedly");
162 }
163 }
164 };
165 if tokio::time::timeout(wait_timeout, wait).await.is_err() {
166 tracing::error!(
167 ?run_ids,
168 ?wait_timeout,
169 "tune tasks did not finish restoring within the shutdown grace period and were \
170 abandoned; affected loops may have been left mid-test -- check them by hand, \
171 or run `bhtune history revert` for the affected runs once this process has \
172 exited"
173 );
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::future::Future;
182 use std::pin::Pin;
183 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
184 use std::task::{Context, Poll};
185 use std::time::Duration;
186 use tokio::sync::oneshot;
187
188 async fn wait_until_inactive(active: &ActiveRun, run_id: i64) {
189 tokio::time::timeout(Duration::from_secs(1), async {
190 while active.active_run_ids().await.contains(&run_id) {
191 tokio::task::yield_now().await;
192 }
193 })
194 .await
195 .expect("the finished task should release its active-run registration");
196 }
197
198 #[tokio::test]
199 async fn start_registers_runs_and_releases_a_completed_task() {
200 let active = ActiveRun::default();
201 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
202 let ran = Arc::new(AtomicBool::new(false));
203 let ran_clone = ran.clone();
204 let (finish_tx, finish_rx) = oneshot::channel();
205 active
206 .start(1, handle, async move {
207 ran_clone.store(true, Ordering::SeqCst);
208 finish_rx.await.unwrap();
209 })
210 .await
211 .unwrap();
212 assert_eq!(active.active_run_ids().await, vec![1]);
213 finish_tx.send(()).unwrap();
214 wait_until_inactive(&active, 1).await;
215 assert!(ran.load(Ordering::SeqCst));
216 assert!(active.reserve(2).await.is_ok());
217 active.release(2).await;
218 }
219
220 #[tokio::test]
221 async fn start_allows_multiple_runs_while_they_are_active() {
222 let active = ActiveRun::default();
223 let (_ctrl_c_1, handle_1) = bhtune_cli::cancel::CtrlC::manual();
224 active
225 .start(1, handle_1, std::future::pending())
226 .await
227 .unwrap();
228
229 let (_ctrl_c_2, handle_2) = bhtune_cli::cancel::CtrlC::manual();
230 active
231 .start(2, handle_2, std::future::pending())
232 .await
233 .unwrap();
234 assert_eq!(active.active_run_ids().await, vec![1, 2]);
235 }
236
237 #[tokio::test]
238 async fn release_frees_the_slot_for_the_matching_run_id() {
239 let active = ActiveRun::default();
240 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
241 active
242 .start(1, handle, std::future::pending())
243 .await
244 .unwrap();
245 active.release(1).await;
246 assert!(active.active_run_ids().await.is_empty());
247 }
248
249 #[tokio::test]
250 async fn releasing_one_run_keeps_other_runs_registered() {
251 let active = ActiveRun::default();
252 let (_ctrl_c_1, handle_1) = bhtune_cli::cancel::CtrlC::manual();
253 let (_ctrl_c_2, handle_2) = bhtune_cli::cancel::CtrlC::manual();
254 active
255 .start(1, handle_1, std::future::pending())
256 .await
257 .unwrap();
258 active
259 .start(2, handle_2, std::future::pending())
260 .await
261 .unwrap();
262
263 active.release(1).await;
264
265 assert_eq!(active.active_run_ids().await, vec![2]);
266 }
267
268 #[tokio::test]
269 async fn release_is_a_no_op_for_a_non_matching_run_id() {
270 let active = ActiveRun::default();
271 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
272 active
273 .start(1, handle, std::future::pending())
274 .await
275 .unwrap();
276 active.release(999).await;
277 assert_eq!(active.active_run_ids().await, vec![1]);
278 }
279
280 #[tokio::test]
281 async fn reserve_registers_an_exclusive_operation_without_spawning_anything() {
282 let active = ActiveRun::default();
283 active.reserve(1).await.unwrap();
284 assert_eq!(active.exclusive_id().await, Some(1));
285 }
286
287 #[tokio::test]
288 async fn reserve_refuses_while_a_spawned_task_is_active() {
289 let active = ActiveRun::default();
290 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
291 active
292 .start(1, handle, std::future::pending())
293 .await
294 .unwrap();
295 let err = active.reserve(2).await.unwrap_err();
296 assert_eq!(err, RunAlreadyActive { run_id: 1 });
297 }
298
299 #[tokio::test]
300 async fn start_refuses_while_a_reservation_is_active() {
301 let active = ActiveRun::default();
302 active.reserve(1).await.unwrap();
303 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
304 let err = active
305 .start(2, handle, std::future::pending())
306 .await
307 .unwrap_err();
308 assert_eq!(err, RunAlreadyActive { run_id: 1 });
309 }
310
311 #[tokio::test]
312 async fn rejected_start_drops_the_unscheduled_task_and_keeps_the_reservation() {
313 struct DropProbe(Arc<AtomicBool>);
314
315 impl Drop for DropProbe {
316 fn drop(&mut self) {
317 self.0.store(true, Ordering::SeqCst);
318 }
319 }
320
321 let active = ActiveRun::default();
322 active.reserve(1).await.unwrap();
323 let dropped = Arc::new(AtomicBool::new(false));
324 let probe = DropProbe(dropped.clone());
325 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
326 let mut task = std::future::poll_fn(move |_| {
327 let _ = &probe;
328 Poll::Pending
329 });
330 let waker = std::task::Waker::noop();
331 let mut context = Context::from_waker(waker);
332 assert!(Pin::new(&mut task).poll(&mut context).is_pending());
333
334 let err = active.start(2, handle, task).await.unwrap_err();
335
336 assert_eq!(err, RunAlreadyActive { run_id: 1 });
337 assert!(dropped.load(Ordering::SeqCst));
338 assert_eq!(active.exclusive_id().await, Some(1));
339 assert!(active.active_run_ids().await.is_empty());
340 }
341
342 #[tokio::test]
343 async fn cancel_and_wait_logs_an_unexpected_registration_task_failure() {
344 let active = ActiveRun::default();
345 let (_ctrl_c, cancel) = bhtune_cli::cancel::CtrlC::manual();
346 let handle = tokio::spawn(async {
347 panic!("simulated registration task failure");
348 });
349 active
350 .inner
351 .lock()
352 .await
353 .tasks
354 .insert(1, ActiveTask { cancel, handle });
355
356 active.cancel_and_wait(Duration::from_secs(1)).await;
357
358 assert!(active.active_run_ids().await.is_empty());
359 }
360
361 #[tokio::test]
362 async fn start_refuses_a_duplicate_run_id_while_the_task_is_active() {
363 let active = ActiveRun::default();
364 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
365 active
366 .start(1, handle, std::future::pending())
367 .await
368 .unwrap();
369
370 let (_ctrl_c_duplicate, duplicate_handle) = bhtune_cli::cancel::CtrlC::manual();
371 let err = active
372 .start(1, duplicate_handle, std::future::pending())
373 .await
374 .unwrap_err();
375
376 assert_eq!(err, RunAlreadyActive { run_id: 1 });
377 }
378
379 #[tokio::test]
380 async fn reserve_refuses_a_second_reservation_while_one_is_active() {
381 let active = ActiveRun::default();
382 active.reserve(1).await.unwrap();
383 let err = active.reserve(2).await.unwrap_err();
384 assert_eq!(err, RunAlreadyActive { run_id: 1 });
385 }
386
387 #[tokio::test]
388 async fn release_frees_a_reserved_slot() {
389 let active = ActiveRun::default();
390 active.reserve(1).await.unwrap();
391 active.release(1).await;
392 assert_eq!(active.exclusive_id().await, None);
393 }
394
395 #[tokio::test]
396 async fn cancel_returns_true_for_a_reservation_but_has_nothing_to_trigger() {
397 let active = ActiveRun::default();
402 active.reserve(1).await.unwrap();
403 assert!(active.cancel(1).await);
404 assert_eq!(active.exclusive_id().await, Some(1));
406 }
407
408 #[tokio::test]
409 async fn cancel_and_wait_clears_a_reservation_immediately_without_waiting() {
410 let active = ActiveRun::default();
411 active.reserve(1).await.unwrap();
412 tokio::time::timeout(
415 Duration::from_millis(200),
416 active.cancel_and_wait(Duration::from_secs(5)),
417 )
418 .await
419 .expect("cancel_and_wait must not block on a reservation with no task to await");
420 assert_eq!(active.exclusive_id().await, None);
421 }
422
423 #[tokio::test]
424 async fn cancel_returns_true_for_the_matching_run_id() {
425 let active = ActiveRun::default();
433 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
434 active
435 .start(1, handle, std::future::pending())
436 .await
437 .unwrap();
438 assert!(active.cancel(1).await);
439 }
440
441 #[tokio::test]
442 async fn cancel_returns_false_for_a_non_matching_run_id() {
443 let active = ActiveRun::default();
444 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
445 active
446 .start(1, handle, std::future::pending())
447 .await
448 .unwrap();
449 assert!(!active.cancel(999).await);
450 }
451
452 #[tokio::test]
453 async fn cancel_returns_false_when_nothing_is_active() {
454 let active = ActiveRun::default();
455 assert!(!active.cancel(1).await);
456 }
457
458 #[tokio::test]
459 async fn cancel_and_wait_is_a_no_op_when_nothing_is_active() {
460 let active = ActiveRun::default();
461 tokio::time::timeout(
463 Duration::from_millis(200),
464 active.cancel_and_wait(Duration::from_secs(5)),
465 )
466 .await
467 .expect("cancel_and_wait must not block when no run is active");
468 }
469
470 #[tokio::test]
471 async fn cancel_and_wait_waits_for_the_spawned_task_to_actually_finish() {
472 let active = ActiveRun::default();
473 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
474 let finished = Arc::new(AtomicBool::new(false));
475 let finished_clone = finished.clone();
476 active
484 .start(1, handle, async move {
485 tokio::time::sleep(Duration::from_millis(50)).await;
486 finished_clone.store(true, Ordering::SeqCst);
487 })
488 .await
489 .unwrap();
490
491 tokio::time::timeout(
492 Duration::from_millis(500),
493 active.cancel_and_wait(Duration::from_secs(5)),
494 )
495 .await
496 .expect("cancel_and_wait should resolve once the task finishes");
497 assert!(
498 finished.load(Ordering::SeqCst),
499 "cancel_and_wait must not return before the task's future has fully resolved"
500 );
501 assert!(active.active_run_ids().await.is_empty());
504 }
505
506 #[tokio::test]
507 async fn cancel_and_wait_waits_for_all_spawned_tasks() {
508 let active = ActiveRun::default();
509 let finished = Arc::new(AtomicUsize::new(0));
510
511 for run_id in [1, 2] {
512 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
513 let finished_clone = finished.clone();
514 active
515 .start(run_id, handle, async move {
516 tokio::time::sleep(Duration::from_millis(50)).await;
517 finished_clone.fetch_add(1, Ordering::SeqCst);
518 })
519 .await
520 .unwrap();
521 }
522
523 tokio::time::timeout(
524 Duration::from_millis(500),
525 active.cancel_and_wait(Duration::from_secs(5)),
526 )
527 .await
528 .expect("cancel_and_wait should resolve after all tasks finish");
529 assert_eq!(finished.load(Ordering::SeqCst), 2);
530 assert!(active.active_run_ids().await.is_empty());
531 }
532
533 #[tokio::test]
534 async fn cancel_and_wait_abandons_a_task_that_does_not_finish_within_the_timeout() {
535 let active = ActiveRun::default();
536 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
537 active
540 .start(1, handle, std::future::pending())
541 .await
542 .unwrap();
543
544 tokio::time::timeout(
545 Duration::from_millis(500),
546 active.cancel_and_wait(Duration::from_millis(50)),
547 )
548 .await
549 .expect("cancel_and_wait must respect its own timeout even if the task never exits");
550 }
551
552 #[tokio::test]
553 async fn cancel_and_wait_logs_and_consumes_a_panicking_task() {
554 let active = ActiveRun::default();
555 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
556 active
557 .start(1, handle, async {
558 panic!("simulated task failure");
559 })
560 .await
561 .unwrap();
562
563 active.cancel_and_wait(Duration::from_secs(1)).await;
564 assert!(active.active_run_ids().await.is_empty());
565 }
566
567 #[tokio::test]
568 async fn a_panicking_task_releases_its_registration_without_shutdown() {
569 let active = ActiveRun::default();
570 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
571 active
572 .start(1, handle, async {
573 panic!("simulated task failure");
574 })
575 .await
576 .unwrap();
577
578 wait_until_inactive(&active, 1).await;
579 assert!(active.reserve(2).await.is_ok());
580 active.release(2).await;
581 }
582
583 #[tokio::test]
584 async fn a_task_that_reaches_its_own_timeout_releases_its_registration() {
585 let active = ActiveRun::default();
586 let (_ctrl_c, handle) = bhtune_cli::cancel::CtrlC::manual();
587 active
588 .start(1, handle, async {
589 assert!(
590 tokio::time::timeout(Duration::from_millis(10), std::future::pending::<()>(),)
591 .await
592 .is_err()
593 );
594 })
595 .await
596 .unwrap();
597
598 wait_until_inactive(&active, 1).await;
599 assert!(active.reserve(2).await.is_ok());
600 active.release(2).await;
601 }
602}