1use crate::alert::AlertConfig;
7use crate::config::ScheduleError;
8use crate::history::{
9 CatchupPolicy, DependencyStatus, ExecutionRecord, ExecutionResult, ExecutionState,
10 HealthCheckResult, Jitter, RetryPolicy, ScheduleHealth, ScheduleVersion,
11};
12use crate::schedule::{BusinessCalendar, HolidayCalendar, Schedule};
13use crate::wfq::{TaskWeight, WFQState};
14use chrono::{DateTime, Duration, Utc};
15use serde::{Deserialize, Serialize};
16use std::collections::{HashMap, HashSet};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ScheduledTask {
21 pub name: String,
23
24 pub schedule: Schedule,
26
27 #[serde(default)]
29 pub args: Vec<serde_json::Value>,
30
31 #[serde(default)]
33 pub kwargs: HashMap<String, serde_json::Value>,
34
35 #[serde(default)]
37 pub options: TaskOptions,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub last_run_at: Option<DateTime<Utc>>,
42
43 #[serde(default)]
45 pub total_run_count: u64,
46
47 #[serde(default = "default_true")]
49 pub enabled: bool,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub jitter: Option<Jitter>,
54
55 #[serde(default)]
57 pub catchup_policy: CatchupPolicy,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub group: Option<String>,
62
63 #[serde(default)]
65 pub tags: HashSet<String>,
66
67 #[serde(default)]
69 pub retry_policy: RetryPolicy,
70
71 #[serde(default)]
73 pub retry_count: u32,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub last_failure_at: Option<DateTime<Utc>>,
78
79 #[serde(default)]
81 pub total_failure_count: u64,
82
83 #[serde(default)]
85 pub execution_history: Vec<ExecutionRecord>,
86
87 #[serde(default)]
89 pub max_history_size: usize,
90
91 #[serde(default)]
93 pub version_history: Vec<ScheduleVersion>,
94
95 #[serde(default = "default_version")]
97 pub current_version: u32,
98
99 #[serde(default)]
101 pub dependencies: HashSet<String>,
102
103 #[serde(default = "default_true")]
105 pub wait_for_dependencies: bool,
106
107 #[serde(skip)]
109 pub(crate) cached_next_run: Option<DateTime<Utc>>,
110
111 #[serde(default)]
113 pub alert_config: AlertConfig,
114
115 #[serde(default)]
117 pub execution_state: ExecutionState,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub wfq_state: Option<WFQState>,
122}
123
124#[allow(dead_code)]
125fn default_true() -> bool {
126 true
127}
128
129fn default_version() -> u32 {
130 1
131}
132
133impl ScheduledTask {
134 pub fn new(name: String, schedule: Schedule) -> Self {
135 let mut task = Self {
136 name,
137 schedule: schedule.clone(),
138 args: Vec::new(),
139 kwargs: HashMap::new(),
140 options: TaskOptions::default(),
141 last_run_at: None,
142 total_run_count: 0,
143 enabled: true,
144 jitter: None,
145 catchup_policy: CatchupPolicy::default(),
146 group: None,
147 tags: HashSet::new(),
148 retry_policy: RetryPolicy::default(),
149 retry_count: 0,
150 last_failure_at: None,
151 total_failure_count: 0,
152 execution_history: Vec::new(),
153 max_history_size: 0, version_history: Vec::new(),
155 current_version: 1,
156 dependencies: HashSet::new(),
157 wait_for_dependencies: true,
158 cached_next_run: None,
159 alert_config: AlertConfig::default(),
160 execution_state: ExecutionState::default(),
161 wfq_state: None,
162 };
163
164 let initial_version =
166 ScheduleVersion::from_task(&task, 1, Some("Initial creation".to_string()));
167 task.version_history.push(initial_version);
168
169 task
170 }
171
172 pub fn with_args(mut self, args: Vec<serde_json::Value>) -> Self {
173 self.args = args;
174 self
175 }
176
177 pub fn with_kwargs(mut self, kwargs: HashMap<String, serde_json::Value>) -> Self {
178 self.kwargs = kwargs;
179 self
180 }
181
182 pub fn disabled(mut self) -> Self {
183 self.enabled = false;
184 self
185 }
186
187 pub fn with_queue(mut self, queue: String) -> Self {
188 self.options.queue = Some(queue);
189 self
190 }
191
192 pub fn with_priority(mut self, priority: u8) -> Self {
193 self.options.priority = Some(priority);
194 self
195 }
196
197 pub fn with_expires(mut self, expires: u64) -> Self {
198 self.options.expires = Some(expires);
199 self
200 }
201
202 pub fn with_jitter(mut self, jitter: Jitter) -> Self {
204 self.jitter = Some(jitter);
205 self
206 }
207
208 pub fn with_catchup_policy(mut self, policy: CatchupPolicy) -> Self {
210 self.catchup_policy = policy;
211 self
212 }
213
214 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
216 self.retry_policy = policy;
217 self
218 }
219
220 pub fn with_group(mut self, group: String) -> Self {
222 self.group = Some(group);
223 self
224 }
225
226 pub fn with_tag(mut self, tag: String) -> Self {
228 self.tags.insert(tag);
229 self
230 }
231
232 pub fn with_tags(mut self, tags: HashSet<String>) -> Self {
234 self.tags = tags;
235 self
236 }
237
238 pub fn with_alert_config(mut self, config: AlertConfig) -> Self {
240 self.alert_config = config;
241 self
242 }
243
244 pub fn add_tag(&mut self, tag: String) {
246 self.tags.insert(tag);
247 }
248
249 pub fn remove_tag(&mut self, tag: &str) -> bool {
251 self.tags.remove(tag)
252 }
253
254 pub fn has_tag(&self, tag: &str) -> bool {
256 self.tags.contains(tag)
257 }
258
259 pub fn is_in_group(&self, group: &str) -> bool {
261 self.group.as_deref() == Some(group)
262 }
263
264 pub fn is_due(&self) -> Result<bool, ScheduleError> {
266 if self.last_run_at.is_none() {
268 return Ok(true);
269 }
270 let mut next_run = self.schedule.next_run(self.last_run_at)?;
271
272 if let Some(ref jitter) = self.jitter {
274 next_run = jitter.apply(next_run, &self.name);
275 }
276
277 Ok(Utc::now() >= next_run)
278 }
279
280 pub fn next_run_time(&self) -> Result<DateTime<Utc>, ScheduleError> {
282 if let Some(cached) = self.cached_next_run {
284 return Ok(cached);
285 }
286
287 let mut next_run = self.schedule.next_run(self.last_run_at)?;
289
290 if let Some(ref jitter) = self.jitter {
292 next_run = jitter.apply(next_run, &self.name);
293 }
294
295 Ok(next_run)
296 }
297
298 pub fn update_next_run_cache(&mut self) {
303 if let Ok(next_run) = self.next_run_time_uncached() {
304 self.cached_next_run = Some(next_run);
305 } else {
306 self.cached_next_run = None;
307 }
308 }
309
310 fn next_run_time_uncached(&self) -> Result<DateTime<Utc>, ScheduleError> {
312 let mut next_run = self.schedule.next_run(self.last_run_at)?;
313
314 if let Some(ref jitter) = self.jitter {
316 next_run = jitter.apply(next_run, &self.name);
317 }
318
319 Ok(next_run)
320 }
321
322 pub fn invalidate_next_run_cache(&mut self) {
326 self.cached_next_run = None;
327 }
328
329 pub fn is_enabled(&self) -> bool {
331 self.enabled
332 }
333
334 pub fn has_run(&self) -> bool {
336 self.last_run_at.is_some()
337 }
338
339 pub fn has_options(&self) -> bool {
341 self.options.has_queue() || self.options.has_priority() || self.options.has_expires()
342 }
343
344 pub fn age_since_last_run(&self) -> Option<chrono::Duration> {
346 self.last_run_at.map(|last_run| Utc::now() - last_run)
347 }
348
349 pub fn mark_success(&mut self) {
351 self.retry_count = 0; }
353
354 pub fn mark_failure(&mut self) {
356 self.last_failure_at = Some(Utc::now());
357 self.total_failure_count += 1;
358 self.retry_count += 1;
359 }
360
361 pub fn should_retry(&self) -> bool {
363 self.retry_policy.should_retry(self.retry_count)
364 }
365
366 pub fn begin_execution(&mut self, timeout_seconds: Option<u64>) {
374 let timeout_after = timeout_seconds.map(|secs| Utc::now() + Duration::seconds(secs as i64));
375 self.execution_state = ExecutionState::Running {
376 started_at: Utc::now(),
377 timeout_after,
378 };
379 }
380
381 pub fn complete_execution(&mut self) {
386 self.execution_state = ExecutionState::Idle;
387 }
388
389 pub fn detect_interrupted_execution(&self) -> bool {
398 match &self.execution_state {
399 ExecutionState::Idle => false,
400 ExecutionState::Running {
401 started_at,
402 timeout_after,
403 } => {
404 if let Some(timeout) = timeout_after {
406 if Utc::now() > *timeout {
407 return true;
408 }
409 }
410
411 let running_duration = Utc::now() - *started_at;
413 let max_reasonable_duration = Duration::hours(24); running_duration > max_reasonable_duration
415 }
416 }
417 }
418
419 pub fn recover_from_interruption(&mut self) -> Option<Duration> {
427 match &self.execution_state {
428 ExecutionState::Running { started_at, .. } => {
429 let duration = Utc::now() - *started_at;
430
431 let record = ExecutionRecord::completed(*started_at, ExecutionResult::Interrupted);
433 self.execution_history.push(record);
434
435 if self.max_history_size > 0 && self.execution_history.len() > self.max_history_size
437 {
438 let remove_count = self.execution_history.len() - self.max_history_size;
439 self.execution_history.drain(0..remove_count);
440 }
441
442 self.execution_state = ExecutionState::Idle;
444
445 self.retry_count += 1;
447
448 Some(duration)
449 }
450 ExecutionState::Idle => None,
451 }
452 }
453
454 pub fn is_ready_for_retry_after_crash(&self) -> bool {
456 if !self.execution_history.is_empty() {
458 if let Some(last_exec) = self.execution_history.last() {
459 if last_exec.is_interrupted() && self.should_retry() {
460 return true;
461 }
462 }
463 }
464 false
465 }
466
467 pub fn next_retry_delay(&self) -> Option<u64> {
469 self.retry_policy.next_retry_delay(self.retry_count)
470 }
471
472 pub fn next_retry_time(&self) -> Option<DateTime<Utc>> {
474 if let (Some(last_failure), Some(delay)) = (self.last_failure_at, self.next_retry_delay()) {
475 Some(last_failure + Duration::seconds(delay as i64))
476 } else {
477 None
478 }
479 }
480
481 pub fn is_ready_for_retry(&self) -> bool {
483 if !self.should_retry() {
484 return false;
485 }
486
487 if let Some(next_retry) = self.next_retry_time() {
488 Utc::now() >= next_retry
489 } else {
490 false
491 }
492 }
493
494 pub fn failure_rate(&self) -> f64 {
496 let total_attempts = self.total_run_count + self.total_failure_count;
497 if total_attempts == 0 {
498 0.0
499 } else {
500 self.total_failure_count as f64 / total_attempts as f64
501 }
502 }
503
504 pub fn with_max_history(mut self, max_size: usize) -> Self {
506 self.max_history_size = max_size;
507 self
508 }
509
510 pub fn add_execution_record(&mut self, record: ExecutionRecord) {
512 self.execution_history.push(record);
513
514 if self.max_history_size > 0 && self.execution_history.len() > self.max_history_size {
516 let remove_count = self.execution_history.len() - self.max_history_size;
517 self.execution_history.drain(0..remove_count);
518 }
519 }
520
521 pub fn get_last_executions(&self, n: usize) -> &[ExecutionRecord] {
523 let start = self.execution_history.len().saturating_sub(n);
524 &self.execution_history[start..]
525 }
526
527 pub fn get_all_executions(&self) -> &[ExecutionRecord] {
529 &self.execution_history
530 }
531
532 pub fn history_success_count(&self) -> usize {
534 self.execution_history
535 .iter()
536 .filter(|r| r.is_success())
537 .count()
538 }
539
540 pub fn history_failure_count(&self) -> usize {
542 self.execution_history
543 .iter()
544 .filter(|r| r.is_failure())
545 .count()
546 }
547
548 pub fn consecutive_failure_count(&self) -> u32 {
553 let mut count = 0u32;
554 for record in self.execution_history.iter().rev() {
555 if record.is_failure() {
556 count += 1;
557 } else {
558 break;
559 }
560 }
561 count
562 }
563
564 pub fn history_timeout_count(&self) -> usize {
566 self.execution_history
567 .iter()
568 .filter(|r| r.is_timeout())
569 .count()
570 }
571
572 pub fn average_duration_ms(&self) -> Option<u64> {
574 let durations: Vec<u64> = self
575 .execution_history
576 .iter()
577 .filter_map(|r| r.duration_ms)
578 .collect();
579
580 if durations.is_empty() {
581 None
582 } else {
583 Some(durations.iter().sum::<u64>() / durations.len() as u64)
584 }
585 }
586
587 pub fn min_duration_ms(&self) -> Option<u64> {
589 self.execution_history
590 .iter()
591 .filter_map(|r| r.duration_ms)
592 .min()
593 }
594
595 pub fn max_duration_ms(&self) -> Option<u64> {
597 self.execution_history
598 .iter()
599 .filter_map(|r| r.duration_ms)
600 .max()
601 }
602
603 pub fn history_success_rate(&self) -> f64 {
605 let total = self.execution_history.len();
606 if total == 0 {
607 0.0
608 } else {
609 self.history_success_count() as f64 / total as f64
610 }
611 }
612
613 pub fn clear_history(&mut self) {
615 self.execution_history.clear();
616 }
617
618 pub fn check_health(&self) -> HealthCheckResult {
620 let mut issues = Vec::new();
621
622 let next_run = match self.next_run_time() {
624 Ok(next) => Some(next),
625 Err(e) => {
626 issues.push(format!("Cannot calculate next run: {}", e));
627 None
628 }
629 };
630
631 if let Some(stuck_duration) = self.is_stuck() {
633 issues.push(format!(
634 "Task may be stuck (no execution in {} hours)",
635 stuck_duration.num_hours()
636 ));
637 }
638
639 let failure_rate = self.failure_rate();
641 let total_attempts = self.total_run_count + self.total_failure_count;
642 if failure_rate > 0.5 && total_attempts >= 10 {
643 issues.push(format!(
644 "High failure rate: {:.1}% ({} failures out of {} total)",
645 failure_rate * 100.0,
646 self.total_failure_count,
647 total_attempts
648 ));
649 }
650
651 if self.execution_history.len() >= 3 {
653 let last_3 = &self.execution_history[self.execution_history.len() - 3..];
654 if last_3.iter().all(|r| r.is_failure() || r.is_timeout()) {
655 issues.push("Last 3 executions failed".to_string());
656 }
657 }
658
659 let health = if !self.enabled {
661 ScheduleHealth::Warning {
662 issues: vec!["Task is disabled".to_string()],
663 }
664 } else if issues.is_empty() {
665 ScheduleHealth::Healthy
666 } else if issues.iter().any(|i| i.contains("Cannot calculate")) {
667 ScheduleHealth::Unhealthy { issues }
668 } else {
669 ScheduleHealth::Warning { issues }
670 };
671
672 let time_since_last_run = self.age_since_last_run();
673
674 let mut result = HealthCheckResult::new(self.name.clone(), health);
675 if let Some(next) = next_run {
676 result = result.with_next_run(next);
677 }
678 if let Some(duration) = time_since_last_run {
679 result = result.with_time_since_last_run(duration);
680 }
681 result
682 }
683
684 pub fn is_stuck(&self) -> Option<chrono::Duration> {
687 if !self.enabled {
688 return None;
689 }
690
691 if let Some(last_run) = self.last_run_at {
692 let age = Utc::now() - last_run;
693
694 let expected_interval = match &self.schedule {
696 Schedule::Interval { every } => Duration::seconds(*every as i64),
697 #[cfg(feature = "cron")]
698 Schedule::Crontab { .. } => Duration::hours(24), #[cfg(feature = "solar")]
700 Schedule::Solar { .. } => Duration::hours(24), Schedule::OneTime { .. } => return None, };
703
704 let stuck_threshold = expected_interval * 10;
706 if age > stuck_threshold {
707 return Some(age);
708 }
709 }
710
711 None
712 }
713
714 pub fn validate_schedule(&self) -> Result<(), ScheduleError> {
716 self.schedule.next_run(self.last_run_at)?;
718 Ok(())
719 }
720
721 pub fn update_schedule(&mut self, new_schedule: Schedule, change_reason: Option<String>) {
723 self.current_version += 1;
724 self.schedule = new_schedule;
725
726 self.update_next_run_cache();
728
729 let version = ScheduleVersion::from_task(self, self.current_version, change_reason);
730 self.version_history.push(version);
731 }
732
733 pub fn update_config(
735 &mut self,
736 enabled: Option<bool>,
737 jitter: Option<Option<Jitter>>,
738 catchup_policy: Option<CatchupPolicy>,
739 change_reason: Option<String>,
740 ) {
741 let mut jitter_changed = false;
742 if let Some(e) = enabled {
743 self.enabled = e;
744 }
745 if let Some(j) = jitter {
746 self.jitter = j;
747 jitter_changed = true;
748 }
749 if let Some(c) = catchup_policy {
750 self.catchup_policy = c;
751 }
752
753 if jitter_changed {
755 self.update_next_run_cache();
756 }
757
758 self.current_version += 1;
759 let version = ScheduleVersion::from_task(self, self.current_version, change_reason);
760 self.version_history.push(version);
761 }
762
763 pub fn rollback_to_version(&mut self, version_number: u32) -> Result<(), ScheduleError> {
765 let version = self
766 .version_history
767 .iter()
768 .find(|v| v.version == version_number)
769 .ok_or_else(|| {
770 ScheduleError::Invalid(format!("Version {} not found", version_number))
771 })?;
772
773 self.schedule = version.schedule.clone();
775 self.enabled = version.enabled;
776 self.jitter = version.jitter.clone();
777 self.catchup_policy = version.catchup_policy.clone();
778
779 self.current_version += 1;
781 let rollback_version = ScheduleVersion::from_task(
782 self,
783 self.current_version,
784 Some(format!("Rolled back to version {}", version_number)),
785 );
786 self.version_history.push(rollback_version);
787
788 Ok(())
789 }
790
791 pub fn get_version_history(&self) -> &[ScheduleVersion] {
793 &self.version_history
794 }
795
796 pub fn get_version(&self, version_number: u32) -> Option<&ScheduleVersion> {
798 self.version_history
799 .iter()
800 .find(|v| v.version == version_number)
801 }
802
803 pub fn get_previous_version(&self) -> Option<&ScheduleVersion> {
805 if self.current_version > 1 {
806 self.version_history.iter().rev().nth(1) } else {
808 None
809 }
810 }
811
812 pub fn add_dependency(&mut self, task_name: String) {
814 self.dependencies.insert(task_name);
815 }
816
817 pub fn remove_dependency(&mut self, task_name: &str) -> bool {
819 self.dependencies.remove(task_name)
820 }
821
822 pub fn clear_dependencies(&mut self) {
824 self.dependencies.clear();
825 }
826
827 pub fn has_dependencies(&self) -> bool {
829 !self.dependencies.is_empty()
830 }
831
832 pub fn depends_on(&self, task_name: &str) -> bool {
834 self.dependencies.contains(task_name)
835 }
836
837 pub fn with_dependencies(mut self, dependencies: HashSet<String>) -> Self {
839 self.dependencies = dependencies;
840 self
841 }
842
843 pub fn with_wait_for_dependencies(mut self, wait: bool) -> Self {
845 self.wait_for_dependencies = wait;
846 self
847 }
848
849 pub fn check_dependencies(&self, completed_tasks: &HashSet<String>) -> DependencyStatus {
851 if self.dependencies.is_empty() {
852 return DependencyStatus::Satisfied;
853 }
854
855 let pending: Vec<String> = self
856 .dependencies
857 .iter()
858 .filter(|dep| !completed_tasks.contains(*dep))
859 .cloned()
860 .collect();
861
862 if pending.is_empty() {
863 DependencyStatus::Satisfied
864 } else {
865 DependencyStatus::Waiting { pending }
866 }
867 }
868
869 pub fn check_dependencies_with_failures(
871 &self,
872 completed_tasks: &HashSet<String>,
873 failed_tasks: &HashSet<String>,
874 ) -> DependencyStatus {
875 if self.dependencies.is_empty() {
876 return DependencyStatus::Satisfied;
877 }
878
879 let failed: Vec<String> = self
880 .dependencies
881 .iter()
882 .filter(|dep| failed_tasks.contains(*dep))
883 .cloned()
884 .collect();
885
886 if !failed.is_empty() {
887 return DependencyStatus::Failed { failed };
888 }
889
890 let pending: Vec<String> = self
891 .dependencies
892 .iter()
893 .filter(|dep| !completed_tasks.contains(*dep))
894 .cloned()
895 .collect();
896
897 if pending.is_empty() {
898 DependencyStatus::Satisfied
899 } else {
900 DependencyStatus::Waiting { pending }
901 }
902 }
903}
904
905impl std::fmt::Display for ScheduledTask {
906 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907 write!(f, "ScheduledTask[{}] schedule={}", self.name, self.schedule)?;
908 if !self.enabled {
909 write!(f, " (disabled)")?;
910 }
911 if let Some(last_run) = self.last_run_at {
912 let age = Utc::now() - last_run;
913 write!(f, " last_run={}s ago", age.num_seconds())?;
914 }
915 write!(f, " runs={}", self.total_run_count)?;
916 Ok(())
917 }
918}
919
920#[derive(Debug, Clone, Serialize, Deserialize, Default)]
922pub struct TaskOptions {
923 pub queue: Option<String>,
925
926 pub priority: Option<u8>,
928
929 pub expires: Option<u64>,
931}
932
933impl TaskOptions {
934 pub fn has_queue(&self) -> bool {
936 self.queue.is_some()
937 }
938
939 pub fn has_priority(&self) -> bool {
941 self.priority.is_some()
942 }
943
944 pub fn has_expires(&self) -> bool {
946 self.expires.is_some()
947 }
948}
949
950impl std::fmt::Display for TaskOptions {
951 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
952 let mut parts = Vec::new();
953 if let Some(ref queue) = self.queue {
954 parts.push(format!("queue={}", queue));
955 }
956 if let Some(priority) = self.priority {
957 parts.push(format!("priority={}", priority));
958 }
959 if let Some(expires) = self.expires {
960 parts.push(format!("expires={}s", expires));
961 }
962 if parts.is_empty() {
963 write!(f, "TaskOptions[default]")
964 } else {
965 write!(f, "TaskOptions[{}]", parts.join(", "))
966 }
967 }
968}
969
970impl ScheduleVersion {
971 pub fn from_task(task: &ScheduledTask, version: u32, change_reason: Option<String>) -> Self {
973 Self {
974 version,
975 schedule: task.schedule.clone(),
976 created_at: Utc::now(),
977 change_reason,
978 enabled: task.enabled,
979 jitter: task.jitter.clone(),
980 catchup_policy: task.catchup_policy.clone(),
981 }
982 }
983}
984
985impl ScheduledTask {
986 pub fn with_business_calendar(self, _calendar: BusinessCalendar) -> Self {
991 self
993 }
994
995 pub fn with_holiday_calendar(self, _calendar: HolidayCalendar) -> Self {
1000 self
1002 }
1003}
1004
1005impl ScheduledTask {
1006 pub fn with_wfq_weight(mut self, weight: f64) -> Result<Self, String> {
1016 if self.wfq_state.is_none() {
1017 self.wfq_state = Some(WFQState::default());
1018 }
1019 self.wfq_state
1020 .as_mut()
1021 .expect("wfq_state initialized just above if None")
1022 .weight = TaskWeight::new(weight)?;
1023 Ok(self)
1024 }
1025
1026 pub fn wfq_weight(&self) -> f64 {
1028 self.wfq_state
1029 .as_ref()
1030 .map(|state| state.weight.value())
1031 .unwrap_or(1.0)
1032 }
1033
1034 pub fn wfq_finish_time(&self) -> f64 {
1036 self.wfq_state
1037 .as_ref()
1038 .map(|state| state.finish_time())
1039 .unwrap_or(0.0)
1040 }
1041}