• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Neptune-Crypto / neptune-core / 14607972517

23 Apr 2025 01:20AM UTC coverage: 84.99% (+0.5%) from 84.452%
14607972517

push

github

dan-da
test: fix race_condition_with_one_new_block

This test was hanging forever in CI.

There were two problems:

1. The CLI args were using the default auto-detected TxProvingCapability which
results in PrimitiveWitness capability on the CI machine which causes the proof
upgrader job to log an error and return (abort).

2. The test was passing a cloned channel sender to handle_upgrade() then calling
recv() on the receiver prior to dropping the original sender.  So after
handle_upgrade() aborts and returns the test waits for this message which will
never come because handle_upgrade() already dropped that sender.  But the test
is holding the original sender so it never gets dropped.  deadlock.

The fix for (1) is set the CLI args capability to match the capability in each
iteration of the loop [ProofCollection, SingleProof].

The fix for (2) is simply to *move* the sender into handle_upgrade() instead of
cloning it, so that it gets dropped when the fn ends.  THen the recv().await
immediately returns an error because the channel is closed.

6 of 6 new or added lines in 1 file covered. (100.0%)

2475 existing lines in 50 files now uncovered.

54831 of 64515 relevant lines covered (84.99%)

165910.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.14
/src/job_queue/queue.rs
1
use std::collections::VecDeque;
2
use std::sync::Arc;
3
use std::sync::Mutex;
4

5
use tokio::sync::mpsc;
6
use tokio::sync::mpsc::UnboundedReceiver;
7
use tokio::sync::oneshot;
8
use tokio::sync::watch;
9
use tokio::task::JoinHandle;
10

11
use super::errors::JobHandleError;
12
use super::errors::JobQueueError;
13
use super::traits::Job;
14
use super::traits::JobCancelReceiver;
15
use super::traits::JobCancelSender;
16
use super::traits::JobCompletion;
17
use super::traits::JobResult;
18
use super::traits::JobResultReceiver;
19
use super::traits::JobResultSender;
20

21
/// A job-handle enables cancelling a job and awaiting results
22
#[derive(Debug)]
23
pub struct JobHandle {
24
    result_rx: JobResultReceiver,
25
    cancel_tx: JobCancelSender,
26
}
27
impl JobHandle {
28
    /// wait for job to complete
29
    ///
30
    /// a completed job may either be finished, cancelled, or panicked.
31
    pub async fn complete(self) -> Result<JobCompletion, JobHandleError> {
1,323✔
32
        Ok(self.result_rx.await?)
1,323✔
33
    }
1,323✔
34

35
    /// wait for job result, or err if cancelled or a panic occurred within job.
36
    pub async fn result(self) -> Result<Box<dyn JobResult>, JobHandleError> {
1,321✔
37
        match self.complete().await? {
1,321✔
38
            JobCompletion::Finished(r) => Ok(r),
1,319✔
UNCOV
39
            JobCompletion::Cancelled => Err(JobHandleError::JobCancelled),
×
40
            JobCompletion::Panicked(e) => Err(JobHandleError::JobPanicked(e)),
2✔
41
        }
42
    }
1,321✔
43

44
    /// cancel job and return immediately.
45
    pub fn cancel(&self) -> Result<(), JobHandleError> {
×
UNCOV
46
        Ok(self.cancel_tx.send(())?)
×
UNCOV
47
    }
×
48

49
    /// cancel job and wait for it to complete.
50
    pub async fn cancel_and_await(self) -> Result<JobCompletion, JobHandleError> {
2✔
51
        self.cancel_tx.send(())?;
2✔
52
        self.complete().await
2✔
53
    }
2✔
54

55
    /// channel receiver for job results
56
    pub fn result_rx(self) -> JobResultReceiver {
54✔
57
        self.result_rx
54✔
58
    }
54✔
59

60
    /// channel sender for cancelling job.
61
    pub fn cancel_tx(&self) -> &JobCancelSender {
1,297✔
62
        &self.cancel_tx
1,297✔
63
    }
1,297✔
64
}
65

66
/// messages that can be sent to job-queue inner task.
67
enum JobQueueMsg<P> {
68
    AddJob(AddJobMsg<P>),
69
    Stop,
70
}
71

72
/// represents a msg to add a job to the queue.
73
struct AddJobMsg<P> {
74
    job: Box<dyn Job>,
75
    result_tx: JobResultSender,
76
    cancel_tx: JobCancelSender,
77
    cancel_rx: JobCancelReceiver,
78
    priority: P,
79
}
80

81
/// implements a job queue that sends result of each job to a listener.
82
pub struct JobQueue<P> {
83
    tx: mpsc::UnboundedSender<JobQueueMsg<P>>,
84

85
    process_jobs_task_handle: JoinHandle<()>, // Store the job processing task handle
86
    add_job_task_handle: JoinHandle<()>,      // store the job addition task handle.
87
}
88

89
// useful for detecting when a receiver gets dropped.
90
struct LoggedReceiver<T>(UnboundedReceiver<T>);
91

92
impl<T> Drop for LoggedReceiver<T> {
93
    fn drop(&mut self) {
1,263✔
94
        tracing::debug!("LoggedReceiver dropped!");
1,263✔
95
    }
1,263✔
96
}
97

98
impl<P> std::fmt::Debug for JobQueue<P> {
UNCOV
99
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
100
        f.debug_struct("JobQueue")
×
UNCOV
101
            .field("tx", &"mpsc::Sender")
×
UNCOV
102
            .finish()
×
UNCOV
103
    }
×
104
}
105

106
impl<P> Drop for JobQueue<P> {
107
    fn drop(&mut self) {
1,258✔
108
        tracing::debug!("in JobQueue::drop()");
1,258✔
109

110
        let _ = self.tx.send(JobQueueMsg::Stop);
1,258✔
111

1,258✔
112
        // not really necessary, but it avoids a dead-code warning.
1,258✔
113
        self.process_jobs_task_handle.abort();
1,258✔
114
        self.add_job_task_handle.abort();
1,258✔
115
    }
1,258✔
116
}
117

118
impl<P: Ord + Send + Sync + 'static> JobQueue<P> {
119
    /// creates job queue and starts it processing.  returns immediately.
120
    pub fn start() -> Self {
1,263✔
121
        struct CurrentJob {
122
            job_num: usize,
123
            cancel_tx: JobCancelSender,
124
        }
125
        struct Shared<P: Ord> {
126
            jobs: VecDeque<AddJobMsg<P>>,
127
            current_job: Option<CurrentJob>,
128
        }
129

130
        let (tx, rx) = mpsc::unbounded_channel::<JobQueueMsg<P>>();
1,263✔
131
        let mut rx = LoggedReceiver(rx);
1,263✔
132

1,263✔
133
        let shared = Shared {
1,263✔
134
            jobs: VecDeque::new(),
1,263✔
135
            current_job: None,
1,263✔
136
        };
1,263✔
137
        let jobs: Arc<Mutex<Shared<P>>> = Arc::new(Mutex::new(shared));
1,263✔
138

1,263✔
139
        let (tx_deque, mut rx_deque) = tokio::sync::mpsc::unbounded_channel();
1,263✔
140

1,263✔
141
        // spawns background task that adds incoming jobs to job-queue
1,263✔
142
        let jobs_rc1 = jobs.clone();
1,263✔
143
        let add_job_task_handle = tokio::spawn(async move {
1,263✔
144
            while let Some(msg) = rx.0.recv().await {
1,601✔
145
                match msg {
1,400✔
146
                    JobQueueMsg::AddJob(m) => {
1,382✔
147
                        let (num_jobs, job_running) = {
1,382✔
148
                            let mut guard = jobs_rc1.lock().unwrap();
1,382✔
149
                            guard.jobs.push_back(m);
1,382✔
150
                            let job_running = match &guard.current_job {
1,382✔
UNCOV
151
                                Some(j) => format!("#{}", j.job_num),
×
152
                                None => "none".to_string(),
1,382✔
153
                            };
154
                            (guard.jobs.len(), job_running)
1,382✔
155
                        };
1,382✔
156
                        tracing::info!(
1,382✔
157
                            "JobQueue: job added.  {} queued job(s).  job running: {}",
×
158
                            num_jobs,
159
                            job_running
160
                        );
161
                        let _ = tx_deque.send(());
1,382✔
162
                    }
163
                    JobQueueMsg::Stop => {
164
                        tracing::info!("JobQueue: received stop message.  stopping.");
18✔
165
                        drop(tx_deque); // close queue
18✔
166

18✔
167
                        // if there is a presently executing job we need to cancel it.
18✔
168
                        let guard = jobs_rc1.lock().unwrap();
18✔
169
                        if let Some(current_job) = &guard.current_job {
18✔
UNCOV
170
                            if !current_job.cancel_tx.is_closed() {
×
UNCOV
171
                                current_job.cancel_tx.send(()).unwrap();
×
UNCOV
172
                                tracing::info!("JobQueue: notified current job to stop.");
×
UNCOV
173
                            }
×
174
                        }
18✔
175
                        break;
18✔
176
                    }
177
                }
178
            }
179
            tracing::debug!("task add/stop exiting");
18✔
180
        });
1,263✔
181

1,263✔
182
        // spawns background task that processes job queue and runs jobs.
1,263✔
183
        let jobs_rc2 = jobs.clone();
1,263✔
184
        let process_jobs_task_handle = tokio::spawn(async move {
1,263✔
185
            let mut job_num: usize = 1;
219✔
186

187
            while rx_deque.recv().await.is_some() {
1,596✔
188
                let (msg, pending) = {
1,381✔
189
                    let mut guard = jobs_rc2.lock().unwrap();
1,381✔
190
                    guard
1,381✔
191
                        .jobs
1,381✔
192
                        .make_contiguous()
1,381✔
193
                        .sort_by(|a, b| b.priority.cmp(&a.priority));
1,381✔
194
                    let job = guard.jobs.pop_front().unwrap();
1,381✔
195
                    guard.current_job = Some(CurrentJob {
1,381✔
196
                        job_num,
1,381✔
197
                        cancel_tx: job.cancel_tx.clone(),
1,381✔
198
                    });
1,381✔
199
                    (job, guard.jobs.len())
1,381✔
200
                };
1,381✔
201

1,381✔
202
                tracing::info!(
1,381✔
UNCOV
203
                    "  *** JobQueue: begin job #{} - {} queued job(s) ***",
×
204
                    job_num,
205
                    pending
206
                );
207
                let timer = tokio::time::Instant::now();
1,381✔
208
                let task_handle = if msg.job.is_async() {
1,381✔
209
                    tokio::spawn(async move { msg.job.run_async_cancellable(msg.cancel_rx).await })
1,338✔
210
                } else {
211
                    tokio::task::spawn_blocking(move || msg.job.run(msg.cancel_rx))
43✔
212
                };
213

214
                let job_completion = match task_handle.await {
1,381✔
215
                    Ok(jc) => jc,
1,375✔
216
                    Err(e) => {
2✔
217
                        if e.is_panic() {
2✔
218
                            JobCompletion::Panicked(e.into_panic())
2✔
UNCOV
219
                        } else if e.is_cancelled() {
×
220
                            JobCompletion::Cancelled
×
221
                        } else {
222
                            unreachable!()
×
223
                        }
224
                    }
225
                };
226

227
                tracing::info!(
1,377✔
UNCOV
228
                    "  *** JobQueue: ended job #{} - Completion: {} - {} secs ***",
×
UNCOV
229
                    job_num,
×
UNCOV
230
                    job_completion,
×
UNCOV
231
                    timer.elapsed().as_secs_f32()
×
232
                );
233
                job_num += 1;
1,377✔
234

1,377✔
235
                jobs_rc2.lock().unwrap().current_job = None;
1,377✔
236

1,377✔
237
                let _ = msg.result_tx.send(job_completion);
1,377✔
238
            }
239
            tracing::debug!("task process_jobs exiting");
21✔
240
        });
1,263✔
241

1,263✔
242
        tracing::info!("JobQueue: started new queue.");
1,263✔
243

244
        Self {
1,263✔
245
            tx,
1,263✔
246
            process_jobs_task_handle,
1,263✔
247
            add_job_task_handle,
1,263✔
248
        }
1,263✔
249
    }
1,263✔
250

251
    /// adds job to job-queue and returns immediately.
252
    ///
253
    /// job-results can be obtained by via JobHandle::results().await
254
    /// The job can be cancelled by JobHandle::cancel()
255
    pub fn add_job(&self, job: Box<dyn Job>, priority: P) -> Result<JobHandle, JobQueueError> {
1,382✔
256
        let (result_tx, result_rx) = oneshot::channel();
1,382✔
257
        let (cancel_tx, cancel_rx) = watch::channel::<()>(());
1,382✔
258

1,382✔
259
        let msg = JobQueueMsg::AddJob(AddJobMsg {
1,382✔
260
            job,
1,382✔
261
            result_tx,
1,382✔
262
            cancel_tx: cancel_tx.clone(),
1,382✔
263
            cancel_rx: cancel_rx.clone(),
1,382✔
264
            priority,
1,382✔
265
        });
1,382✔
266
        if self.tx.is_closed() {
1,382✔
UNCOV
267
            tracing::error!("JobQueue::add_job() -- add-job channel is unexpectedly closed!!!");
×
268
        }
1,382✔
269
        self.tx
1,382✔
270
            .send(msg)
1,382✔
271
            .map_err(|e| JobQueueError::AddJobError(e.to_string()))?;
1,382✔
272

273
        Ok(JobHandle {
1,382✔
274
            result_rx,
1,382✔
275
            cancel_tx,
1,382✔
276
        })
1,382✔
277
    }
1,382✔
278
}
279

280
#[cfg(test)]
281
mod tests {
282
    use std::time::Instant;
283

284
    use tracing_test::traced_test;
285

286
    use super::*;
287

288
    #[tokio::test(flavor = "multi_thread")]
UNCOV
289
    #[traced_test]
×
290
    async fn run_sync_jobs_by_priority() -> anyhow::Result<()> {
1✔
291
        workers::run_jobs_by_priority(false).await
1✔
292
    }
1✔
293

294
    #[tokio::test(flavor = "multi_thread")]
UNCOV
295
    #[traced_test]
×
296
    async fn run_async_jobs_by_priority() -> anyhow::Result<()> {
1✔
297
        workers::run_jobs_by_priority(true).await
1✔
298
    }
1✔
299

300
    #[tokio::test(flavor = "multi_thread")]
UNCOV
301
    #[traced_test]
×
302
    async fn get_sync_job_result() -> anyhow::Result<()> {
1✔
303
        workers::get_job_result(false).await
1✔
304
    }
1✔
305

306
    #[tokio::test(flavor = "multi_thread")]
UNCOV
307
    #[traced_test]
×
308
    async fn get_async_job_result() -> anyhow::Result<()> {
1✔
309
        workers::get_job_result(true).await
1✔
310
    }
1✔
311

312
    #[tokio::test(flavor = "multi_thread")]
UNCOV
313
    #[traced_test]
×
314
    async fn cancel_sync_job() -> anyhow::Result<()> {
1✔
315
        workers::cancel_job(false).await
1✔
316
    }
1✔
317

318
    #[tokio::test(flavor = "multi_thread")]
UNCOV
319
    #[traced_test]
×
320
    async fn cancel_async_job() -> anyhow::Result<()> {
1✔
321
        workers::cancel_job(true).await
1✔
322
    }
1✔
323

324
    #[test]
UNCOV
325
    #[traced_test]
×
326
    fn runtime_shutdown_timeout_force_cancels_sync_job() -> anyhow::Result<()> {
1✔
327
        workers::runtime_shutdown_timeout_force_cancels_job(false)
1✔
328
    }
1✔
329

330
    #[test]
UNCOV
331
    #[traced_test]
×
332
    fn runtime_shutdown_timeout_force_cancels_async_job() -> anyhow::Result<()> {
1✔
333
        workers::runtime_shutdown_timeout_force_cancels_job(true)
1✔
334
    }
1✔
335

336
    #[test]
UNCOV
337
    #[traced_test]
×
338
    fn runtime_shutdown_cancels_sync_job() {
1✔
339
        let _ = workers::runtime_shutdown_cancels_job(false);
1✔
340
    }
1✔
341

342
    #[test]
UNCOV
343
    #[traced_test]
×
344
    fn runtime_shutdown_cancels_async_job() -> anyhow::Result<()> {
1✔
345
        workers::runtime_shutdown_cancels_job(true)
1✔
346
    }
1✔
347

348
    #[test]
UNCOV
349
    #[traced_test]
×
350
    fn spawned_tasks_live_as_long_as_jobqueue() -> anyhow::Result<()> {
1✔
351
        workers::spawned_tasks_live_as_long_as_jobqueue(true)
1✔
352
    }
1✔
353

354
    #[tokio::test(flavor = "multi_thread")]
UNCOV
355
    #[traced_test]
×
356
    async fn panic_in_async_job_ends_job_cleanly() -> anyhow::Result<()> {
1✔
357
        workers::panics::panic_in_job_ends_job_cleanly(true).await
1✔
358
    }
1✔
359

360
    #[tokio::test(flavor = "multi_thread")]
UNCOV
361
    #[traced_test]
×
362
    async fn panic_in_blocking_job_ends_job_cleanly() -> anyhow::Result<()> {
1✔
363
        workers::panics::panic_in_job_ends_job_cleanly(false).await
1✔
364
    }
1✔
365

366
    mod workers {
367
        use std::any::Any;
368

369
        use super::*;
370
        use crate::job_queue::errors::JobHandleErrorSync;
371

372
        #[derive(PartialEq, Eq, PartialOrd, Ord)]
373
        pub enum DoubleJobPriority {
374
            Low = 1,
375
            Medium = 2,
376
            High = 3,
377
        }
378

379
        #[derive(PartialEq, Debug, Clone)]
380
        struct DoubleJobResult(u64, u64, Instant);
381
        impl JobResult for DoubleJobResult {
382
            fn as_any(&self) -> &dyn Any {
472✔
383
                self
472✔
384
            }
472✔
385
            fn into_any(self: Box<Self>) -> Box<dyn Any> {
74✔
386
                self
74✔
387
            }
74✔
388
        }
389

390
        // represents a prover job.  implements Job.
391
        struct DoubleJob {
392
            data: u64,
393
            duration: std::time::Duration,
394
            is_async: bool,
395
        }
396

397
        #[async_trait::async_trait]
398
        impl Job for DoubleJob {
399
            fn is_async(&self) -> bool {
82✔
400
                self.is_async
82✔
401
            }
82✔
402

403
            fn run(&self, cancel_rx: JobCancelReceiver) -> JobCompletion {
42✔
404
                let start = Instant::now();
42✔
405
                let sleep_time =
42✔
406
                    std::cmp::min(std::time::Duration::from_micros(100), self.duration);
42✔
407

408
                let r = loop {
42✔
409
                    if start.elapsed() < self.duration {
5,831✔
410
                        match cancel_rx.has_changed() {
5,792✔
411
                            Ok(changed) if changed => break JobCompletion::Cancelled,
1✔
412
                            Err(_) => break JobCompletion::Cancelled,
2✔
413
                            _ => {}
5,789✔
414
                        }
5,789✔
415

5,789✔
416
                        std::thread::sleep(sleep_time);
5,789✔
417
                    } else {
418
                        break JobCompletion::Finished(Box::new(DoubleJobResult(
39✔
419
                            self.data,
39✔
420
                            self.data * 2,
39✔
421
                            Instant::now(),
39✔
422
                        )));
39✔
423
                    }
424
                };
425

426
                tracing::info!("results: {:?}", r);
42✔
427
                r
42✔
428
            }
42✔
429

430
            async fn run_async(&self) -> Box<dyn JobResult> {
40✔
431
                tokio::time::sleep(self.duration).await;
40✔
432
                let r = DoubleJobResult(self.data, self.data * 2, Instant::now());
37✔
433

37✔
434
                tracing::info!("results: {:?}", r);
37✔
435
                Box::new(r)
37✔
436
            }
77✔
437
        }
438

439
        // this test demonstrates/verifies that:
440
        //  1. jobs are run in priority order, highest priority first.
441
        //  2. when multiple jobs have the same priority, they run in FIFO order.
442
        pub(super) async fn run_jobs_by_priority(is_async: bool) -> anyhow::Result<()> {
2✔
443
            let start_of_test = Instant::now();
2✔
444

2✔
445
            // create a job queue
2✔
446
            let job_queue = JobQueue::start();
2✔
447

2✔
448
            let mut handles = vec![];
2✔
449
            let duration = std::time::Duration::from_millis(20);
2✔
450

451
            // create 30 jobs, 10 at each priority level.
452
            for i in (1..10).rev() {
18✔
453
                let job1 = Box::new(DoubleJob {
18✔
454
                    data: i,
18✔
455
                    duration,
18✔
456
                    is_async,
18✔
457
                });
18✔
458
                let job2 = Box::new(DoubleJob {
18✔
459
                    data: i * 100,
18✔
460
                    duration,
18✔
461
                    is_async,
18✔
462
                });
18✔
463
                let job3 = Box::new(DoubleJob {
18✔
464
                    data: i * 1000,
18✔
465
                    duration,
18✔
466
                    is_async,
18✔
467
                });
18✔
468

18✔
469
                // process job and print results.
18✔
470
                handles.push(job_queue.add_job(job1, DoubleJobPriority::Low)?.result_rx());
18✔
471
                handles.push(
18✔
472
                    job_queue
18✔
473
                        .add_job(job2, DoubleJobPriority::Medium)?
18✔
474
                        .result_rx(),
18✔
475
                );
18✔
476
                handles.push(
18✔
477
                    job_queue
18✔
478
                        .add_job(job3, DoubleJobPriority::High)?
18✔
479
                        .result_rx(),
18✔
480
                );
481
            }
482

483
            // wait for all jobs to complete.
484
            let mut results = futures::future::join_all(handles).await;
2✔
485

486
            // the results are in the same order as handles passed to join_all.
487
            // we sort them by the timestamp in job result, ascending.
488
            results.sort_by(
2✔
489
                |a_completion, b_completion| match (a_completion, b_completion) {
236✔
490
                    (Ok(JobCompletion::Finished(a_dyn)), Ok(JobCompletion::Finished(b_dyn))) => {
236✔
491
                        let a = a_dyn.as_any().downcast_ref::<DoubleJobResult>().unwrap().2;
236✔
492

236✔
493
                        let b = b_dyn.as_any().downcast_ref::<DoubleJobResult>().unwrap().2;
236✔
494

236✔
495
                        a.cmp(&b)
236✔
496
                    }
UNCOV
497
                    _ => panic!("at least one job did not finish"),
×
498
                },
236✔
499
            );
2✔
500

2✔
501
            // iterate job results and verify that:
2✔
502
            //   timestamp of each is greater than prev.
2✔
503
            //   input value of each is greater than prev, except every 9th item which should be < prev
2✔
504
            //     because there are nine jobs per level.
2✔
505
            let mut prev = Box::new(DoubleJobResult(9999, 0, start_of_test));
2✔
506
            for (i, c) in results.into_iter().enumerate() {
54✔
507
                let Ok(JobCompletion::Finished(dyn_result)) = c else {
54✔
UNCOV
508
                    panic!("A job did not finish");
×
509
                };
510

511
                let job_result = dyn_result.into_any().downcast::<DoubleJobResult>().unwrap();
54✔
512

54✔
513
                assert!(job_result.2 > prev.2);
54✔
514

515
                // we don't do the assertion for the 2nd job because the job-queue starts
516
                // processing immediately and so a race condition is setup where it is possible
517
                // for either the Low priority or High job to start processing first.
518
                if i != 1 {
54✔
519
                    assert!(job_result.0 < prev.0);
52✔
520
                }
2✔
521

522
                prev = job_result;
54✔
523
            }
524

525
            Ok(())
2✔
526
        }
2✔
527

528
        // this test demonstrates/verifies that a job can return a result back to
529
        // the job initiator.
530
        pub(super) async fn get_job_result(is_async: bool) -> anyhow::Result<()> {
2✔
531
            // create a job queue
2✔
532
            let job_queue = JobQueue::start();
2✔
533
            let duration = std::time::Duration::from_millis(20);
2✔
534

535
            // create 10 jobs
536
            for i in 0..10 {
22✔
537
                let job = Box::new(DoubleJob {
20✔
538
                    data: i,
20✔
539
                    duration,
20✔
540
                    is_async,
20✔
541
                });
20✔
542

543
                let result = job_queue
20✔
544
                    .add_job(job, DoubleJobPriority::Low)?
20✔
545
                    .result()
20✔
546
                    .await
20✔
547
                    .map_err(|e| e.into_sync())?;
20✔
548

549
                let job_result = result.into_any().downcast::<DoubleJobResult>().unwrap();
20✔
550

20✔
551
                assert_eq!(i, job_result.0);
20✔
552
                assert_eq!(i * 2, job_result.1);
20✔
553
            }
554

555
            Ok(())
2✔
556
        }
2✔
557

558
        // tests/demonstrates that a long running job can be cancelled early.
559
        pub(super) async fn cancel_job(is_async: bool) -> anyhow::Result<()> {
2✔
560
            // create a job queue
2✔
561
            let job_queue = JobQueue::start();
2✔
562
            // start a 1 hour job.
2✔
563
            let duration = std::time::Duration::from_secs(3600); // 1 hour job.
2✔
564

2✔
565
            let job = Box::new(DoubleJob {
2✔
566
                data: 10,
2✔
567
                duration,
2✔
568
                is_async,
2✔
569
            });
2✔
570
            let job_handle = job_queue.add_job(job, DoubleJobPriority::Low)?;
2✔
571

572
            tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2✔
573

574
            let completion = job_handle.cancel_and_await().await.unwrap();
2✔
575
            assert!(matches!(completion, JobCompletion::Cancelled));
2✔
576

577
            Ok(())
2✔
578
        }
2✔
579

580
        // note: creates own tokio runtime.  caller must not use [tokio::test]
581
        //
582
        // this test starts a job that runs for 1 hour and then attempts to
583
        // shutdown tokio runtime via shutdown_timeout() with a 1 sec timeout.
584
        //
585
        // any async tasks should be aborted quickly.
586
        // any sync tasks will continue to run to completion.
587
        //
588
        // shutdown_timeout() will wait for tasks to abort for 1 sec and then
589
        // returns.  Any un-aborted tasks/threads become ignored/detached.
590
        // The OS can cleanup such threads when the process exits.
591
        //
592
        // the test checks that the shutdown completes in under 2 secs.
593
        //
594
        // the test demonstrates that shutdown_timeout() can be used to shutdown
595
        // tokio runtime even if sync (spawn_blocking) tasks/threads are still running
596
        // in the blocking threadpool.
597
        //
598
        // when called with is_async=true, it demonstrates that shutdown_timeout() also
599
        // aborts async jobs, as one would expect.
600
        pub(super) fn runtime_shutdown_timeout_force_cancels_job(
2✔
601
            is_async: bool,
2✔
602
        ) -> anyhow::Result<()> {
2✔
603
            let rt = tokio::runtime::Runtime::new()?;
2✔
604
            let result = rt.block_on(async {
2✔
605
                // create a job queue
2✔
606
                let job_queue = JobQueue::start();
2✔
607
                // start a 1 hour job.
2✔
608
                let duration = std::time::Duration::from_secs(3600); // 1 hour job.
2✔
609

2✔
610
                let job = Box::new(DoubleJob {
2✔
611
                    data: 10,
2✔
612
                    duration,
2✔
613
                    is_async,
2✔
614
                });
2✔
615
                let _rx = job_queue.add_job(job, DoubleJobPriority::Low)?;
2✔
616

617
                tokio::time::sleep(std::time::Duration::from_millis(20)).await;
2✔
618
                println!("finished scope");
2✔
619

2✔
620
                Ok(())
2✔
621
            });
2✔
622

2✔
623
            let start = std::time::Instant::now();
2✔
624

2✔
625
            println!("waiting 1 second for job before shutdown runtime");
2✔
626
            rt.shutdown_timeout(tokio::time::Duration::from_secs(1));
2✔
627

2✔
628
            assert!(start.elapsed() < std::time::Duration::from_secs(2));
2✔
629

630
            result
2✔
631
        }
2✔
632

633
        // note: creates own tokio runtime.  caller must not use [tokio::test]
634
        //
635
        // this test starts a job that runs for 5 secs and then attempts to
636
        // shutdown tokio runtime normally by dropping it.
637
        //
638
        // any async tasks should be aborted quickly.
639
        // any sync tasks will continue to run to completion.
640
        //
641
        // the tokio runtime does not complete the drop() until all tasks
642
        // have completed/aborted.
643
        //
644
        // the test checks that the job finishes in less than the 5 secs
645
        // required for full completion.  In other words, that it aborts.
646
        //
647
        // the test is expected to succeed for async jobs but fail for sync jobs.
648
        pub(super) fn runtime_shutdown_cancels_job(is_async: bool) -> anyhow::Result<()> {
2✔
649
            let rt = tokio::runtime::Runtime::new()?;
2✔
650
            let start = tokio::time::Instant::now();
2✔
651

2✔
652
            let result = rt.block_on(async {
2✔
653
                // create a job queue
2✔
654
                let job_queue = JobQueue::start();
2✔
655

2✔
656
                // this job takes at least 5 secs to complete.
2✔
657
                let duration = std::time::Duration::from_secs(5);
2✔
658

2✔
659
                let job = Box::new(DoubleJob {
2✔
660
                    data: 10,
2✔
661
                    duration,
2✔
662
                    is_async,
2✔
663
                });
2✔
664

665
                let rx_handle = job_queue.add_job(job, DoubleJobPriority::Low)?;
2✔
666
                drop(rx_handle);
2✔
667

2✔
668
                // sleep 50 ms to let job get started.
2✔
669
                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2✔
670

671
                Ok(())
2✔
672
            });
2✔
673

2✔
674
            // drop the tokio runtime. It will attempt to abort tasks.
2✔
675
            //   - async tasks can normally be aborted
2✔
676
            //   - spawn_blocking (sync) tasks cannot normally be aborted.
2✔
677
            drop(rt);
2✔
678

2✔
679
            // if test is successful, elapsed time should be less than the 5 secs
2✔
680
            // it takes for the job to complete.  (should be around 0.5 ms)
2✔
681

2✔
682
            // however it is expected/normal that sync tasks will not be aborted
2✔
683
            // and will run for full 5 secs.  thus this assert will fail for them.
2✔
684

2✔
685
            assert!(start.elapsed() < std::time::Duration::from_secs(5));
2✔
686

687
            result
2✔
688
        }
2✔
689

690
        // this test attempts to verify that the tasks spawned by the JobQueue
691
        // continue running until the JobQueue is dropped after the tokio
692
        // runtime is dropped.
693
        //
694
        // If the tasks are cencelled before JobQueue is dropped then a subsequent
695
        // api call that sends a msg will result in a "channel closed" error, which
696
        // is what the test checks for.
697
        //
698
        // note that the test has to do some tricky stuff to setup conditions
699
        // where the "channel closed" error can occur. It's a subtle issue.
700
        //
701
        // see description at:
702
        // https://github.com/tokio-rs/tokio/discussions/6961
703
        pub(super) fn spawned_tasks_live_as_long_as_jobqueue(is_async: bool) -> anyhow::Result<()> {
1✔
704
            let rt = tokio::runtime::Runtime::new()?;
1✔
705

706
            let result_ok: Arc<Mutex<bool>> = Arc::new(Mutex::new(true));
1✔
707

1✔
708
            let result_ok_clone = result_ok.clone();
1✔
709
            rt.block_on(async {
1✔
710
                // create a job queue
1✔
711
                let job_queue = Arc::new(JobQueue::start());
1✔
712

1✔
713
                // spawns background task that adds job
1✔
714
                let job_queue_cloned = job_queue.clone();
1✔
715
                let jh = tokio::spawn(async move {
1✔
716
                    // sleep 200 ms to let runtime finish.
1✔
717
                    // ie ensure drop(rt) will be reached and wait for us.
1✔
718
                    // note that we use std sleep.  if tokio sleep is used
1✔
719
                    // the test will always succeed due to the await point.
1✔
720
                    std::thread::sleep(std::time::Duration::from_millis(200));
1✔
721

1✔
722
                    let job = Box::new(DoubleJob {
1✔
723
                        data: 10,
1✔
724
                        duration: std::time::Duration::from_secs(1),
1✔
725
                        is_async,
1✔
726
                    });
1✔
727

1✔
728
                    let result = job_queue_cloned.add_job(job, DoubleJobPriority::Low);
1✔
729

1✔
730
                    // an assert on result.is_ok() would panic, but that panic would be
1✔
731
                    // printed and swallowed by tokio runtime, so the test would succeed
1✔
732
                    // despite the panic. instead we pass the result in a mutex so it
1✔
733
                    // can be asserted where it will be caught by the test runner.
1✔
734
                    *result_ok_clone.lock().unwrap() = result.is_ok();
1✔
735
                });
1✔
736

1✔
737
                // sleep 50 ms to let job get started.
1✔
738
                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1✔
739

740
                // note; awaiting the joinhandle makes the test succeed.
741

742
                jh.abort();
1✔
743
                let _ = jh.await;
1✔
744
            });
1✔
745

1✔
746
            // drop the tokio runtime. It will abort tasks.
1✔
747
            drop(rt);
1✔
748

1✔
749
            assert!(*result_ok.lock().unwrap());
1✔
750

751
            Ok(())
1✔
752
        }
1✔
753

754
        pub mod panics {
755
            use super::*;
756

757
            const PANIC_STR: &str = "job panics unexpectedly";
758

759
            struct PanicJob {
760
                is_async: bool,
761
            }
762

763
            #[async_trait::async_trait]
764
            impl Job for PanicJob {
765
                fn is_async(&self) -> bool {
2✔
766
                    self.is_async
2✔
767
                }
2✔
768

769
                fn run(&self, _cancel_rx: JobCancelReceiver) -> JobCompletion {
1✔
770
                    panic!("{}", PANIC_STR);
1✔
771
                }
772

773
                async fn run_async_cancellable(
774
                    &self,
775
                    _cancel_rx: JobCancelReceiver,
776
                ) -> JobCompletion {
1✔
777
                    panic!("{}", PANIC_STR);
1✔
778
                }
1✔
779
            }
780

781
            /// verifies that a job that panics will be ended properly.
782
            ///
783
            /// Properly means that:
784
            /// 1. an error is returned from job_handle.result() indicating job panicked.
785
            /// 2. caller is able to obtain panic info, which matches job's panic msg.
786
            /// 3. the job-queue continues accepting new jobs.
787
            /// 4. the job-queue continues processing jobs.
788
            ///
789
            /// async_job == true --> test an async job
790
            /// async_job == false --> test a blocking job
791
            pub async fn panic_in_job_ends_job_cleanly(async_job: bool) -> anyhow::Result<()> {
2✔
792
                // create a job queue
2✔
793
                let job_queue = JobQueue::start();
2✔
794

2✔
795
                let job = PanicJob {
2✔
796
                    is_async: async_job,
2✔
797
                };
2✔
798
                let job_handle = job_queue.add_job(Box::new(job), DoubleJobPriority::Low)?;
2✔
799

800
                let job_result = job_handle.result().await;
2✔
801

802
                println!("job_result: {:#?}", job_result);
2✔
803

2✔
804
                // verify that job_queue channel still open
2✔
805
                assert!(!job_queue.tx.is_closed());
2✔
806

807
                // verify that we get an error with the job's panic msg.
808
                assert!(matches!(
2✔
809
                    job_result.map_err(|e| e.into_sync()),
2✔
810
                    Err(JobHandleErrorSync::JobPanicked(e)) if e == *PANIC_STR
2✔
811
                ));
812

813
                // ensure we can still run another job afterwards.
814
                let newjob = Box::new(DoubleJob {
2✔
815
                    data: 10,
2✔
816
                    duration: std::time::Duration::from_millis(50),
2✔
817
                    is_async: false,
2✔
818
                });
2✔
819

820
                // ensure we can add another job.
821
                let new_job_handle = job_queue.add_job(newjob, DoubleJobPriority::Low)?;
2✔
822

823
                // ensure job processes and returns a result without error.
824
                assert!(new_job_handle.result().await.is_ok());
2✔
825

826
                Ok(())
2✔
827
            }
2✔
828
        }
829
    }
830
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc