Skip to content

Commit bfb88da

Browse files
committed
Fix XGBoost model download hanging indefinitely on network stalls
requests.get() had no timeout, so a stalled connection to Zenodo could hang a worker forever with no error — this is very likely what caused "stuck" predictions on the HF Space, since its ephemeral storage means this download runs fresh on every restart. Adds connect/read timeouts, retries on transient failures, a clear error message, and downloads to a temp file first so a killed/timed-out attempt never leaves a corrupt file behind that a later run would mistake for a valid cache hit.
1 parent d9bc42f commit bfb88da

1 file changed

Lines changed: 56 additions & 25 deletions

File tree

protac_splitter/protac_splitter.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,65 @@ def load_graph_edge_classifier_from_cache(
5757
model_path = cache_path / model_filename
5858

5959
if not model_path.exists():
60-
logging.info(f"Downloading XGBoost model → {model_path} ...")
61-
response = requests.get(download_url, stream=True)
62-
response.raise_for_status()
63-
expected_size = int(response.headers.get("Content-Length", -1))
64-
65-
with model_path.open("wb") as f:
66-
for chunk in response.iter_content(chunk_size=1024 * 1024):
67-
if chunk:
68-
f.write(chunk)
69-
70-
if expected_size != -1:
71-
actual = model_path.stat().st_size
72-
if actual != expected_size:
73-
model_path.unlink(missing_ok=True)
60+
_download_xgboost_model(download_url, model_path)
61+
62+
return GraphEdgeClassifier.load(model_path)
63+
64+
65+
def _download_xgboost_model(
66+
download_url: str,
67+
model_path: Path,
68+
num_attempts: int = 3,
69+
connect_timeout: float = 10.0,
70+
read_timeout: float = 30.0,
71+
) -> None:
72+
"""Download the XGBoost model to `model_path`, retrying on transient network errors.
73+
74+
Downloads to a temporary file first and only renames it into place once fully
75+
verified, so a killed or timed-out download never leaves a corrupt file behind
76+
that a later run would mistake for a valid cache hit.
77+
"""
78+
tmp_path = model_path.with_suffix(model_path.suffix + ".part")
79+
last_error: Optional[Exception] = None
80+
81+
for attempt in range(1, num_attempts + 1):
82+
try:
83+
logging.info(f"Downloading XGBoost model → {model_path} (attempt {attempt}/{num_attempts}) ...")
84+
response = requests.get(download_url, stream=True, timeout=(connect_timeout, read_timeout))
85+
response.raise_for_status()
86+
expected_size = int(response.headers.get("Content-Length", -1))
87+
88+
with tmp_path.open("wb") as f:
89+
for chunk in response.iter_content(chunk_size=1024 * 1024):
90+
if chunk:
91+
f.write(chunk)
92+
93+
if expected_size != -1:
94+
actual = tmp_path.stat().st_size
95+
if actual != expected_size:
96+
raise RuntimeError(
97+
f"Download incomplete: got {actual} bytes, expected {expected_size}."
98+
)
99+
100+
h = hashlib.sha256(tmp_path.read_bytes()).hexdigest()
101+
if h != _XGBOOST_SHA256:
74102
raise RuntimeError(
75-
f"Download incomplete: got {actual} bytes, expected {expected_size}."
103+
f"Downloaded model checksum mismatch: got {h}, expected {_XGBOOST_SHA256}."
76104
)
77105

78-
h = hashlib.sha256(model_path.read_bytes()).hexdigest()
79-
if h != _XGBOOST_SHA256:
80-
model_path.unlink(missing_ok=True)
81-
raise RuntimeError(
82-
f"Downloaded model checksum mismatch: got {h}, expected {_XGBOOST_SHA256}. "
83-
"The file has been removed — please try again."
84-
)
85-
logging.info("XGBoost model downloaded and verified.")
86-
87-
return GraphEdgeClassifier.load(model_path)
106+
tmp_path.rename(model_path)
107+
logging.info("XGBoost model downloaded and verified.")
108+
return
109+
except (requests.exceptions.RequestException, RuntimeError) as e:
110+
last_error = e
111+
tmp_path.unlink(missing_ok=True)
112+
logging.warning(f"XGBoost model download attempt {attempt}/{num_attempts} failed: {e}")
113+
114+
raise RuntimeError(
115+
f"Failed to download the XGBoost model from {download_url} after {num_attempts} attempts: "
116+
f"{last_error}. You can also download it manually and place it at {model_path} "
117+
"(or point PROTAC_SPLITTER_CACHE_DIR at a directory that already contains it)."
118+
) from last_error
88119

89120

90121
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)