use std::time::{SystemTime, UNIX_EPOCH}; use super::{ CoreError, DownloadEngine, DownloadId, DownloadStatus, RequestGroup, Result, RetryAttempt, RetryHistoryEntry, RuntimeConfig, RuntimeEvent, RuntimeEventKind, ScheduleDecision, SegmentAssignment, SegmentState, effective_piece_length, infer_total_length, usize_to_u64, }; impl DownloadEngine { /// Advances the scheduler clock and emits a scheduler tick event. pub fn scheduler_tick(&mut self) -> Result<()> { let _ = self.scheduler.tick(); self.events .emit(RuntimeEvent::new(RuntimeEventKind::SchedulerTick)); Ok(()) } /// Prepares a single HTTP download by applying the scheduler decision for its current state. pub fn prepare_http_download(&mut self, gid: DownloadId) -> Result { let decision = { let group = self .registry .get(gid) .ok_or(CoreError::UnknownDownloadId(gid))?; match group.status() { DownloadStatus::Waiting => ScheduleDecision::Queue(gid), DownloadStatus::Active => ScheduleDecision::RunNow(gid), DownloadStatus::Error => ScheduleDecision::RetryLater(gid), DownloadStatus::Paused | DownloadStatus::Complete | DownloadStatus::Removed => { ScheduleDecision::Noop } } }; self.apply_schedule_decision(gid, &decision); self.registry .get(gid) .cloned() .ok_or(CoreError::UnknownDownloadId(gid)) } /// Runs one scheduler pass and returns the first actionable decision. #[must_use] pub fn schedule_once(&mut self) -> ScheduleDecision { self.scheduler.record_schedule_run(); let _ = self.scheduler.tick(); let mut gids = self .registry .groups .iter() .filter_map(|(gid, group)| (group.status() == &DownloadStatus::Active).then_some(*gid)) .collect::>(); gids.sort_by_key(|gid| gid.as_u64()); gids.extend( self.reserved_queue .iter() .copied() .filter(|gid| self.registry.get(*gid).is_some()), ); let mut retry_gids = self .registry .groups .iter() .filter_map(|(gid, group)| (group.status() == &DownloadStatus::Error).then_some(*gid)) .collect::>(); retry_gids.sort_by_key(|gid| gid.as_u64()); gids.extend(retry_gids); for gid in gids { let Some(group) = self.registry.get(gid) else { continue; }; let decision = self.scheduler.decide(group); match decision { ScheduleDecision::RunNow(_) | ScheduleDecision::Queue(_) | ScheduleDecision::RetryLater(_) => { self.apply_schedule_decision(gid, &decision); self.events .emit(RuntimeEvent::new(RuntimeEventKind::SchedulerTick).with_gid(gid)); return decision; } ScheduleDecision::Pause(_) | ScheduleDecision::Remove(_) => { self.scheduler.record_decision(&decision); return decision; } ScheduleDecision::Noop => {} } } self.scheduler.record_decision(&ScheduleDecision::Noop); ScheduleDecision::Noop } } /// Converts retry attempts into scheduler-facing retry-history entries. pub(super) fn retry_history_from_group(attempts: &[RetryAttempt]) -> Vec { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map_or(0, |duration| duration.as_secs()); attempts .iter() .map(|attempt| RetryHistoryEntry { at_unix_secs: now, reason: attempt.error.clone().unwrap_or_else(|| "retry".to_string()), }) .collect() } /// Builds runtime segment assignments for the scheduler-selected active segment count. pub(super) fn build_segment_assignments( group: &RequestGroup, runtime: &RuntimeConfig, desired_segments: usize, ) -> Vec { if desired_segments == 0 { return Vec::new(); } let total_length = infer_total_length(group, effective_piece_length(group)); let start_offset = group .resume_state() .map_or(0, |state| state.resume_offset) .max(group.completed_length()) .min(total_length); let remaining = total_length.saturating_sub(start_offset); if remaining == 0 { return Vec::new(); } let piece_length = effective_piece_length(group); let alignment = piece_length.max(runtime.min_split_size.max(1)); let segment_count = desired_segments .min( usize::try_from(remaining.div_ceil(runtime.min_split_size.max(1))) .unwrap_or(usize::MAX), ) .max(1); let target_span = remaining.div_ceil(usize_to_u64(segment_count)); let mut cursor = start_offset; let mut assignments = Vec::with_capacity(segment_count); for slot in 0..segment_count { if cursor >= total_length { break; } let end = if slot + 1 == segment_count { total_length } else { let raw_end = cursor.saturating_add(target_span).min(total_length); let aligned_end = raw_end .div_ceil(alignment) .saturating_mul(alignment) .min(total_length); aligned_end.max(cursor.saturating_add(1)) }; let mut assignment = SegmentAssignment::new(slot, crate::piece::PieceRange::new(cursor, end)); assignment.state = SegmentState::Active; assignments.push(assignment); cursor = end; } assignments }