Skip to content

Commit a2db48d

Browse files
authored
fix async windows file persist retries (astral-sh#11008)
The previous two versions of the code were bugged and would always produce None when you retried (producing a hard LostState error).
1 parent c1a2ef1 commit a2db48d

File tree

1 file changed

+35
-12
lines changed

1 file changed

+35
-12
lines changed

crates/uv-fs/src/lib.rs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -310,22 +310,45 @@ pub async fn persist_with_retry(
310310
// See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
311311
let to = to.as_ref();
312312

313-
// the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError`
313+
// Ok there's a lot of complex ownership stuff going on here.
314+
//
315+
// the `NamedTempFile` `persist` method consumes `self`, and returns it back inside
316+
// the Error in case of `PersistError`:
314317
// https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
315-
// So we will update the `from` optional value in safe and borrow-checker friendly way every retry
316-
// Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry
317-
let mut from = Some(from);
318-
let persist = move || {
319-
// Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block
320-
let mut from: Option<NamedTempFile> = from.take();
318+
// So every time we fail, we need to reset the `NamedTempFile` to try again.
319+
//
320+
// Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
321+
// be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
322+
// totally owned `Future` (so effectively it returns a `FnOnce`).
323+
//
324+
// But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
325+
// to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s
326+
// with interior mutability (`Mutex`) to have the closure and all the Futures it creates share
327+
// a single memory location that the `NamedTempFile` can be shuttled in and out of.
328+
//
329+
// In spite of the Mutex all of this code will run logically serially, so there shouldn't be a
330+
// chance for a race where we try to get the `NamedTempFile` but it's actually None. The code
331+
// is just written pedantically/robustly.
332+
let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
333+
let persist = || {
334+
// Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value
335+
let from2 = from.clone();
321336

322337
async move {
323-
if let Some(file) = from.take() {
338+
let maybe_file: Option<NamedTempFile> = from2
339+
.lock()
340+
.map_err(|_| PersistRetryError::LostState)?
341+
.take();
342+
if let Some(file) = maybe_file {
324343
file.persist(to).map_err(|err| {
325-
let error_message = err.to_string();
326-
// Set back the NamedTempFile returned back by the Error
327-
from = Some(err.file);
328-
PersistRetryError::Persist(error_message)
344+
let error_message: String = err.to_string();
345+
// Set back the `NamedTempFile` returned back by the Error
346+
if let Ok(mut guard) = from2.lock() {
347+
*guard = Some(err.file);
348+
PersistRetryError::Persist(error_message)
349+
} else {
350+
PersistRetryError::LostState
351+
}
329352
})
330353
} else {
331354
Err(PersistRetryError::LostState)

0 commit comments

Comments
 (0)