Skip to content

Commit bdb8c26

Browse files
authored
Add UV_COMPILE_BYTECODE_TIMEOUT environment variable (#14369)
## Summary When installing packages on _very_ slow/overloaded systems it'spossible to trigger bytecode compilation timeouts, which tends to happen in environments such as Qemu (especially without KVM/virtio), but also on systems that are simply overloaded. I've seen this in my Nix builds if I for example am compiling a Linux kernel at the same time as a few other concurrent builds. By making the bytecode compilation timeout adjustable you can work around such issues. I plan to set `UV_COMPILE_BYTECODE_TIMEOUT=0` in the [pyproject.nix builders](https://pyproject-nix.github.io/pyproject.nix/build.html) to make them more reliable. - Related issues * #6105 ## Test Plan Only manual testing was applied in this instance. There is no existing automated tests for bytecode compilation timeout afaict.
1 parent 09fc943 commit bdb8c26

File tree

3 files changed

+56
-12
lines changed

3 files changed

+56
-12
lines changed

crates/uv-installer/src/compile.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::panic::AssertUnwindSafe;
22
use std::path::{Path, PathBuf};
33
use std::process::Stdio;
44
use std::time::Duration;
5-
use std::{io, panic};
5+
use std::{env, io, panic};
66

77
use async_channel::{Receiver, SendError};
88
use tempfile::tempdir_in;
@@ -20,7 +20,7 @@ use uv_warnings::warn_user;
2020

2121
const COMPILEALL_SCRIPT: &str = include_str!("pip_compileall.py");
2222
/// This is longer than any compilation should ever take.
23-
const COMPILE_TIMEOUT: Duration = Duration::from_secs(60);
23+
const DEFAULT_COMPILE_TIMEOUT: Duration = Duration::from_secs(60);
2424

2525
#[derive(Debug, Error)]
2626
pub enum CompileError {
@@ -55,6 +55,8 @@ pub enum CompileError {
5555
},
5656
#[error("Python startup timed out ({}s)", _0.as_secs_f32())]
5757
StartupTimeout(Duration),
58+
#[error("Got invalid value from environment for {var}: {message}.")]
59+
EnvironmentError { var: &'static str, message: String },
5860
}
5961

6062
/// Bytecode compile all file in `dir` using a pool of Python interpreters running a Python script
@@ -88,6 +90,29 @@ pub async fn compile_tree(
8890
let tempdir = tempdir_in(cache).map_err(CompileError::TempFile)?;
8991
let pip_compileall_py = tempdir.path().join("pip_compileall.py");
9092

93+
let timeout: Option<Duration> = match env::var(EnvVars::UV_COMPILE_BYTECODE_TIMEOUT) {
94+
Ok(value) => {
95+
if value == "0" {
96+
debug!("Disabling bytecode compilation timeout");
97+
None
98+
} else {
99+
if let Ok(duration) = value.parse::<u64>().map(Duration::from_secs) {
100+
debug!(
101+
"Using bytecode compilation timeout of {}s",
102+
duration.as_secs()
103+
);
104+
Some(duration)
105+
} else {
106+
return Err(CompileError::EnvironmentError {
107+
var: "UV_COMPILE_BYTECODE_TIMEOUT",
108+
message: format!("Expected an integer number of seconds, got \"{value}\""),
109+
});
110+
}
111+
}
112+
}
113+
Err(_) => Some(DEFAULT_COMPILE_TIMEOUT),
114+
};
115+
91116
debug!("Starting {} bytecode compilation workers", worker_count);
92117
let mut worker_handles = Vec::new();
93118
for _ in 0..worker_count {
@@ -98,6 +123,7 @@ pub async fn compile_tree(
98123
python_executable.to_path_buf(),
99124
pip_compileall_py.clone(),
100125
receiver.clone(),
126+
timeout,
101127
);
102128

103129
// Spawn each worker on a dedicated thread.
@@ -189,6 +215,7 @@ async fn worker(
189215
interpreter: PathBuf,
190216
pip_compileall_py: PathBuf,
191217
receiver: Receiver<PathBuf>,
218+
timeout: Option<Duration>,
192219
) -> Result<(), CompileError> {
193220
fs_err::tokio::write(&pip_compileall_py, COMPILEALL_SCRIPT)
194221
.await
@@ -208,12 +235,17 @@ async fn worker(
208235
}
209236
}
210237
};
238+
211239
// Handle a broken `python` by using a timeout, one that's higher than any compilation
212240
// should ever take.
213241
let (mut bytecode_compiler, child_stdin, mut child_stdout, mut child_stderr) =
214-
tokio::time::timeout(COMPILE_TIMEOUT, wait_until_ready)
215-
.await
216-
.map_err(|_| CompileError::StartupTimeout(COMPILE_TIMEOUT))??;
242+
if let Some(duration) = timeout {
243+
tokio::time::timeout(duration, wait_until_ready)
244+
.await
245+
.map_err(|_| CompileError::StartupTimeout(timeout.unwrap()))??
246+
} else {
247+
wait_until_ready.await?
248+
};
217249

218250
let stderr_reader = tokio::task::spawn(async move {
219251
let mut child_stderr_collected: Vec<u8> = Vec::new();
@@ -223,7 +255,7 @@ async fn worker(
223255
Ok(child_stderr_collected)
224256
});
225257

226-
let result = worker_main_loop(receiver, child_stdin, &mut child_stdout).await;
258+
let result = worker_main_loop(receiver, child_stdin, &mut child_stdout, timeout).await;
227259
// Reap the process to avoid zombies.
228260
let _ = bytecode_compiler.kill().await;
229261

@@ -340,6 +372,7 @@ async fn worker_main_loop(
340372
receiver: Receiver<PathBuf>,
341373
mut child_stdin: ChildStdin,
342374
child_stdout: &mut BufReader<ChildStdout>,
375+
timeout: Option<Duration>,
343376
) -> Result<(), CompileError> {
344377
let mut out_line = String::new();
345378
while let Ok(source_file) = receiver.recv().await {
@@ -372,12 +405,16 @@ async fn worker_main_loop(
372405

373406
// Handle a broken `python` by using a timeout, one that's higher than any compilation
374407
// should ever take.
375-
tokio::time::timeout(COMPILE_TIMEOUT, python_handle)
376-
.await
377-
.map_err(|_| CompileError::CompileTimeout {
378-
elapsed: COMPILE_TIMEOUT,
379-
source_file: source_file.clone(),
380-
})??;
408+
if let Some(duration) = timeout {
409+
tokio::time::timeout(duration, python_handle)
410+
.await
411+
.map_err(|_| CompileError::CompileTimeout {
412+
elapsed: duration,
413+
source_file: source_file.clone(),
414+
})??;
415+
} else {
416+
python_handle.await?;
417+
}
381418

382419
// This is a sanity check, if we don't get the path back something has gone wrong, e.g.
383420
// we're not actually running a python interpreter.

crates/uv-static/src/env_vars.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ impl EnvVars {
162162
/// will compile Python source files to bytecode after installation.
163163
pub const UV_COMPILE_BYTECODE: &'static str = "UV_COMPILE_BYTECODE";
164164

165+
/// Timeout (in seconds) for bytecode compilation.
166+
pub const UV_COMPILE_BYTECODE_TIMEOUT: &'static str = "UV_COMPILE_BYTECODE_TIMEOUT";
167+
165168
/// Equivalent to the `--no-editable` command-line argument. If set, uv
166169
/// installs any editable dependencies, including the project and any workspace members, as
167170
/// non-editable

docs/reference/environment.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ directory for caching instead of the default cache directory.
2626
Equivalent to the `--compile-bytecode` command-line argument. If set, uv
2727
will compile Python source files to bytecode after installation.
2828

29+
### `UV_COMPILE_BYTECODE_TIMEOUT`
30+
31+
Timeout (in seconds) for bytecode compilation.
32+
2933
### `UV_CONCURRENT_BUILDS`
3034

3135
Sets the maximum number of source distributions that uv will build

0 commit comments

Comments
 (0)