Skip to content

Commit 1d68c9d

Browse files
authored
Merge pull request #3050 from itowlson/component-download-race-condition
Fix concurrent HTTP component downloads race condition
2 parents a70116a + fa7ccce commit 1d68c9d

File tree

2 files changed

+58
-11
lines changed

2 files changed

+58
-11
lines changed

crates/loader/src/http.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,34 @@ use anyhow::{ensure, Context, Result};
44
use sha2::Digest;
55
use tokio::io::AsyncWriteExt;
66

7+
/// Describes the naming convention that `verified_download` is permitted
8+
/// to assume in the directory it saves downloads to.
9+
///
10+
/// Consumers (direct or indirect) of `verified_download` are expected to check
11+
/// if the file is already downloaded before calling it. This enum exists
12+
/// to address race conditions when the same blob is downloaded several times
13+
/// concurrently.
14+
///
15+
/// The significance of this is for when the destination file turns out to already
16+
/// exist after all (that is, has been created since the caller originally checked
17+
/// existence). In the ContentIndexed case, the name already existing guarantees that
18+
/// the file matches the download. If a caller uses `verified_download` for a
19+
/// *non*-content-indexed case then they must provide and handle a new variant
20+
/// of the enum.
21+
pub enum DestinationConvention {
22+
/// The download destination is content-indexed; therefore, in the event
23+
/// of a race, the loser of the race can be safely discarded.
24+
ContentIndexed,
25+
}
26+
727
/// Downloads content from `url` which will be verified to match `digest` and
828
/// then moved to `dest`.
9-
pub async fn verified_download(url: &str, digest: &str, dest: &Path) -> Result<()> {
29+
pub async fn verified_download(
30+
url: &str,
31+
digest: &str,
32+
dest: &Path,
33+
convention: DestinationConvention,
34+
) -> Result<()> {
1035
tracing::debug!("Downloading content from {url:?}");
1136

1237
// Prepare tempfile destination
@@ -38,9 +63,16 @@ pub async fn verified_download(url: &str, digest: &str, dest: &Path) -> Result<(
3863
);
3964

4065
// Move to final destination
41-
temp_path
42-
.persist_noclobber(dest)
43-
.with_context(|| format!("Failed to save download from {url} to {}", dest.display()))?;
66+
let persist_result = temp_path.persist_noclobber(dest);
4467

45-
Ok(())
68+
persist_result.or_else(|e| {
69+
let file_already_exists = e.error.kind() == std::io::ErrorKind::AlreadyExists;
70+
if file_already_exists && matches!(convention, DestinationConvention::ContentIndexed) {
71+
Ok(())
72+
} else {
73+
Err(e).with_context(|| {
74+
format!("Failed to save download from {url} to {}", dest.display())
75+
})
76+
}
77+
})
4678
}

crates/loader/src/local.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,14 @@ impl LocalLoader {
381381

382382
self.cache.ensure_dirs().await?;
383383
let dest = self.cache.wasm_path(digest);
384-
verified_download(url, digest, &dest)
385-
.await
386-
.with_context(|| format!("Error fetching source URL {url:?}"))?;
384+
verified_download(
385+
url,
386+
digest,
387+
&dest,
388+
crate::http::DestinationConvention::ContentIndexed,
389+
)
390+
.await
391+
.with_context(|| format!("Error fetching source URL {url:?}"))?;
387392
dest
388393
};
389394
file_content_ref(path)
@@ -675,14 +680,24 @@ fn explain_file_mount_source_error(e: anyhow::Error, src: &Path) -> anyhow::Erro
675680
}
676681

677682
#[cfg(feature = "async-io")]
678-
async fn verified_download(url: &str, digest: &str, dest: &Path) -> Result<()> {
679-
crate::http::verified_download(url, digest, dest)
683+
async fn verified_download(
684+
url: &str,
685+
digest: &str,
686+
dest: &Path,
687+
convention: crate::http::DestinationConvention,
688+
) -> Result<()> {
689+
crate::http::verified_download(url, digest, dest, convention)
680690
.await
681691
.with_context(|| format!("Error fetching source URL {url:?}"))
682692
}
683693

684694
#[cfg(not(feature = "async-io"))]
685-
async fn verified_download(_url: &str, _digest: &str, _dest: &Path) -> Result<()> {
695+
async fn verified_download(
696+
_url: &str,
697+
_digest: &str,
698+
_dest: &Path,
699+
_convention: crate::http::DestinationConvention,
700+
) -> Result<()> {
686701
panic!("async-io feature is required for downloading Wasm sources")
687702
}
688703

0 commit comments

Comments
 (0)