refactor(peer): split download pipeline into modules
The download pipeline had grown into one large file that mixed sentinel transaction handling, peer planning, transport, retry, and top-level orchestration. Split it into a download/ module tree with one file per concern so future lifecycle changes can be reviewed at the right boundary. The public crate surface remains download::download_game_files. Helper types and functions are kept pub(super) or private so the refactor does not widen the API or encourage new callers to depend on internals. The version.ini transaction helpers stay local to version_ini.rs; the proposed fs_util extraction is intentionally left for the later atomic-index work, where a second caller exists. There is no intended runtime behavior change. Test Plan: - just fmt - just test - just clippy - just build Refs: none
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
|
||||
|
||||
use lanspread_db::db::GameFileDescription;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{
|
||||
planning::{DownloadChunk, build_peer_plans, extract_version_descriptor, prepare_game_storage},
|
||||
retry::retry_failed_chunks,
|
||||
transport::download_from_peer,
|
||||
version_ini::{
|
||||
VersionIniBuffer,
|
||||
begin_version_ini_transaction,
|
||||
commit_version_ini_buffer,
|
||||
rollback_version_ini_transaction,
|
||||
},
|
||||
};
|
||||
use crate::{PeerEvent, config::MAX_RETRY_COUNT};
|
||||
|
||||
/// Downloads all game files from available peers.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn download_game_files(
|
||||
game_id: &str,
|
||||
game_file_descs: Vec<GameFileDescription>,
|
||||
games_folder: PathBuf,
|
||||
peers: Vec<SocketAddr>,
|
||||
file_peer_map: HashMap<String, Vec<SocketAddr>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> eyre::Result<()> {
|
||||
if peers.is_empty() {
|
||||
eyre::bail!("no peers available for game {game_id}");
|
||||
}
|
||||
|
||||
if cancel_token.is_cancelled() {
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
let (version_desc, transfer_descs) =
|
||||
extract_version_descriptor(game_id, game_file_descs, &tx_notify_ui)?;
|
||||
let version_buffer = match VersionIniBuffer::new(&version_desc) {
|
||||
Ok(buffer) => Arc::new(buffer),
|
||||
Err(err) => {
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let game_root = games_folder.join(game_id);
|
||||
|
||||
if let Err(err) = begin_version_ini_transaction(&game_root).await {
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
return Err(err);
|
||||
}
|
||||
if let Err(err) = prepare_game_storage(&games_folder, &transfer_descs).await {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
return Err(err);
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
|
||||
let plans = build_peer_plans(&peers, &transfer_descs, &file_peer_map);
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for (peer_addr, plan) in plans {
|
||||
let base_dir = games_folder.clone();
|
||||
let game_id = game_id.to_string();
|
||||
let cancel_token = cancel_token.clone();
|
||||
let version_buffer = version_buffer.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
download_from_peer(
|
||||
peer_addr,
|
||||
&game_id,
|
||||
plan,
|
||||
base_dir,
|
||||
&cancel_token,
|
||||
Some(version_buffer),
|
||||
)
|
||||
.await
|
||||
}));
|
||||
}
|
||||
|
||||
let mut failed_chunks: Vec<DownloadChunk> = Vec::new();
|
||||
let mut last_err: Option<eyre::Report> = None;
|
||||
|
||||
for handle in tasks {
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
match handle.await {
|
||||
Ok(Ok(results)) => {
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
for chunk_result in results {
|
||||
if let Err(e) = chunk_result.result {
|
||||
log::warn!(
|
||||
"Failed to download chunk from {}: {e}",
|
||||
chunk_result.peer_addr
|
||||
);
|
||||
if chunk_result.chunk.retry_count < MAX_RETRY_COUNT {
|
||||
let mut retry_chunk = chunk_result.chunk;
|
||||
retry_chunk.retry_count += 1;
|
||||
retry_chunk.last_peer = Some(chunk_result.peer_addr);
|
||||
failed_chunks.push(retry_chunk);
|
||||
} else {
|
||||
last_err = Some(eyre::eyre!(
|
||||
"Max retries exceeded for chunk: {}",
|
||||
chunk_result.chunk.relative_path
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) | Err(_) if cancel_token.is_cancelled() => {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
Ok(Err(e)) => last_err = Some(e),
|
||||
Err(e) => last_err = Some(eyre::eyre!("task join error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
// Retry failed chunks if any
|
||||
if !failed_chunks.is_empty() && !peers.is_empty() {
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
log::info!("Retrying {} failed chunks", failed_chunks.len());
|
||||
|
||||
let retry_results = match retry_failed_chunks(
|
||||
failed_chunks,
|
||||
&peers,
|
||||
&games_folder,
|
||||
game_id,
|
||||
&file_peer_map,
|
||||
&cancel_token,
|
||||
Some(version_buffer.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(results) => results,
|
||||
Err(_) if cancel_token.is_cancelled() => {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
Err(err) => {
|
||||
last_err = Some(err);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
for chunk_result in retry_results {
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
if let Err(e) = chunk_result.result {
|
||||
log::error!("Retry failed for chunk: {e}");
|
||||
last_err = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
if let Some(err) = last_err {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if let Err(err) = commit_version_ini_buffer(&game_root, &version_buffer).await {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
return Err(err);
|
||||
}
|
||||
log::info!("all files downloaded for game: {game_id}");
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user