feat(peer): journal download file ownership
Track the exact regular files owned by each completed and in-flight peer download instead of sweeping every non-reserved path after cancellation. Bind the record to the canonical games directory, publish pending ownership before payload mutation, and use the final version.ini rename as the recovery commit point. Make replacement, cancellation, and startup recovery preserve unknown files and install state while removing stale or partial downloader-owned bytes. Add a new-format baseline so legacy discarded sentinels cannot make partially modified payloads ready, sync payload and journal state in transaction order, and serialize startup recovery against operation admission. Document ambiguous legacy target adoption, portable alias transitions, and the other ownership tradeoffs in the refactor decision log. Test Plan: - `just clippy` -- passed - `just test` -- passed (182 peer-core tests plus the full workspace) - `just fmt` -- Rust, TOML, and Prettier formatting completed; the command then stopped on 40 pre-existing rumdl findings in unrelated Markdown content - `git diff --cached --check` -- passed
This commit is contained in:
14 files changed
+1478
-265
No files matched your search
@@ -6,16 +6,18 @@ use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{
|
||||
manifest::ValidatedDownloadManifest,
|
||||
ownership::DownloadOwnershipTransaction,
|
||||
planning::{ChunkDownloadResult, DownloadChunk, build_peer_plans, extract_version_descriptor},
|
||||
progress::{DownloadProgressTracker, sample_download_progress},
|
||||
retry::{RetryContext, retry_failed_chunks},
|
||||
storage::{discard_cancelled_download, prepare_game_storage},
|
||||
storage::{prepare_game_storage, sync_game_storage},
|
||||
transport::download_from_peer,
|
||||
version_ini::{
|
||||
VersionIniBuffer,
|
||||
VersionIniCommit,
|
||||
begin_version_ini_transaction,
|
||||
commit_version_ini_buffer,
|
||||
rollback_version_ini_transaction,
|
||||
restore_unjournaled_version_ini_transaction,
|
||||
},
|
||||
};
|
||||
use crate::{PeerEvent, config::MAX_RETRY_COUNT};
|
||||
@@ -24,6 +26,7 @@ use crate::{PeerEvent, config::MAX_RETRY_COUNT};
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) async fn download_game_files(
|
||||
manifest: ValidatedDownloadManifest,
|
||||
state_dir: &Path,
|
||||
peers: Vec<SocketAddr>,
|
||||
file_peer_map: HashMap<String, Vec<SocketAddr>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
@@ -46,30 +49,46 @@ pub(crate) async fn download_game_files(
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let game_root = manifest.game_root().to_path_buf();
|
||||
let ownership = DownloadOwnershipTransaction::prepare(state_dir, &manifest).await?;
|
||||
|
||||
begin_version_ini_transaction(&game_root).await?;
|
||||
if let Err(error) = begin_version_ini_transaction(&game_root).await {
|
||||
if let Err(restore_error) = restore_before_ownership_journal(&game_root).await {
|
||||
return Err(error.wrap_err(format!(
|
||||
"sentinel parking failed and rollback also failed: {restore_error}"
|
||||
)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
restore_before_ownership_journal(&game_root).await?;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
if let Err(error) = ownership.journal_pending().await {
|
||||
if let Err(restore_error) = restore_before_ownership_journal(&game_root).await {
|
||||
return Err(error.wrap_err(format!(
|
||||
"ownership journal failed and sentinel restore also failed: {restore_error}"
|
||||
)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(err) = prepare_game_storage(&manifest).await {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
if cancel_token.is_cancelled() {
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
|
||||
if let Err(error) = tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
|
||||
id: game_id.clone(),
|
||||
})?;
|
||||
}) {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
let progress_tracker = DownloadProgressTracker::new(total_download_bytes(&transfer_descs));
|
||||
let transfer_ctx = TransferContext {
|
||||
@@ -91,30 +110,63 @@ pub(crate) async fn download_game_files(
|
||||
.await;
|
||||
|
||||
if let Err(err) = transfer_result {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
if cancel_token.is_cancelled() {
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
}
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if cancel_token.is_cancelled() {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
discard_cancelled_download_best_effort(&games_folder, &game_id).await;
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
if let Err(err) = commit_version_ini_buffer(&game_root, &version_buffer).await {
|
||||
rollback_version_ini_transaction(&game_root).await;
|
||||
return Err(err);
|
||||
if let Err(error) = sync_game_storage(&manifest).await {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
return Err(error.wrap_err("failed to make downloaded payload durable"));
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
if let Err(error) = ownership.remove_stale().await {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
return Err(error.wrap_err("failed to remove stale download-owned files"));
|
||||
}
|
||||
if cancel_token.is_cancelled() {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
eyre::bail!("download cancelled for game {game_id}");
|
||||
}
|
||||
|
||||
match commit_version_ini_buffer(&game_root, &version_buffer).await {
|
||||
Ok(VersionIniCommit::Durable) => {}
|
||||
Ok(VersionIniCommit::NeedsRecovery(error)) => {
|
||||
// The visible sentinel makes rollback unsafe. Keep pending ownership
|
||||
// so startup recovery can decide from the durable filesystem state.
|
||||
return Err(eyre::eyre!(
|
||||
"version.ini was renamed but its durability could not be established: {error}"
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
abort_download_best_effort(&ownership, &game_id).await;
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if let Err(error) = ownership.finalize().await {
|
||||
// The sentinel rename is the commit point. Pending ownership lets the
|
||||
// next recovery or download finish this idempotently.
|
||||
log::error!("Downloaded {game_id}, but ownership finalization must be recovered: {error}");
|
||||
}
|
||||
log::info!("all files downloaded for game: {game_id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn discard_cancelled_download_best_effort(games_folder: &Path, game_id: &str) {
|
||||
if let Err(err) = discard_cancelled_download(games_folder, game_id).await {
|
||||
log::warn!("Failed to discard cancelled download payload for {game_id}: {err}");
|
||||
async fn restore_before_ownership_journal(game_root: &Path) -> eyre::Result<()> {
|
||||
restore_unjournaled_version_ini_transaction(game_root).await
|
||||
}
|
||||
|
||||
async fn abort_download_best_effort(ownership: &DownloadOwnershipTransaction, game_id: &str) {
|
||||
if let Err(err) = ownership.abort().await {
|
||||
log::warn!("Failed to abort download-owned payload for {game_id}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user