Skip to content

Commit dbbb528

Browse files
committed
refactor(core.utils): collapse Windows sharing-retry loops
Replace, stat-and-read, and unlink each carried their own copy of the _REPLACE_RETRY_DELAYS / sleep / try-op / PermissionError loop. Centralise the loop as _with_sharing_retry(op, on_exhausted=...) and let each caller plug in its own success-on-success and exhausted-budget behaviour. Net behaviour is unchanged (exhaustion semantics for each public helper are preserved via the on_exhausted callback).
1 parent f1502af commit dbbb528

1 file changed

Lines changed: 46 additions & 42 deletions

File tree

cuda_core/cuda/core/utils/_program_cache/_file_stream.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ def _default_cache_dir() -> Path:
8080
return root / "cuda-python" / "program-cache"
8181

8282

83+
def _with_sharing_retry(op, *args, on_exhausted=None, **kwargs):
84+
"""Run ``op(*args, **kwargs)`` retrying transient Windows sharing
85+
violations under the bounded ``_REPLACE_RETRY_DELAYS`` budget.
86+
87+
On Windows, ``os.replace``/``read_bytes``/``unlink`` can surface
88+
winerror 5/32/33 (or bare EACCES via ``_is_windows_sharing_violation``)
89+
while another process briefly holds the file open without share-delete
90+
rights. The retry hides that contention. Other ``PermissionError``s
91+
(real ACLs, unexpected winerror) propagate immediately.
92+
93+
Successful returns and any non-``PermissionError`` exceptions
94+
(including ``FileNotFoundError``) bubble up unchanged. After the
95+
budget is exhausted, the helper either calls ``on_exhausted(last_exc)``
96+
if provided, or re-raises the last sharing-violation exception.
97+
"""
98+
last_exc: PermissionError | None = None
99+
for delay in _REPLACE_RETRY_DELAYS:
100+
if delay:
101+
time.sleep(delay)
102+
try:
103+
return op(*args, **kwargs)
104+
except PermissionError as exc:
105+
if not _is_windows_sharing_violation(exc):
106+
raise
107+
last_exc = exc
108+
if on_exhausted is not None:
109+
return on_exhausted(last_exc)
110+
assert last_exc is not None # at least one iteration ran and caught a PermissionError
111+
raise last_exc
112+
113+
83114
def _replace_with_sharing_retry(tmp_path: Path, target: Path) -> bool:
84115
"""Atomic rename with Windows-specific retry on sharing/lock violations.
85116
@@ -93,20 +124,12 @@ def _replace_with_sharing_retry(tmp_path: Path, target: Path) -> bool:
93124
``FILE_SHARE_WRITE`` (Python's default for ``open(p, "wb")``) or while
94125
a previous unlink is in ``PENDING_DELETE`` -- both are transient.
95126
"""
96-
for i, delay in enumerate(_REPLACE_RETRY_DELAYS):
97-
if delay:
98-
time.sleep(delay)
99-
try:
100-
os.replace(tmp_path, target)
101-
return True
102-
except PermissionError as exc:
103-
if not _IS_WINDOWS or getattr(exc, "winerror", None) not in _SHARING_VIOLATION_WINERRORS:
104-
raise
105-
# Windows sharing violation; loop and try again unless this was the
106-
# last attempt, in which case fall through and return False.
107-
if i == len(_REPLACE_RETRY_DELAYS) - 1:
108-
return False
109-
return False
127+
128+
def _do_replace() -> bool:
129+
os.replace(tmp_path, target)
130+
return True
131+
132+
return _with_sharing_retry(_do_replace, on_exhausted=lambda _exc: False)
110133

111134

112135
def _stat_and_read_with_sharing_retry(path: Path) -> tuple[os.stat_result, bytes]:
@@ -128,19 +151,14 @@ def _stat_and_read_with_sharing_retry(path: Path) -> tuple[os.stat_result, bytes
128151
would surface consistently; the bounded retry budget keeps the cost
129152
of treating them as transient negligible.
130153
"""
131-
last_exc: BaseException | None = None
132-
for delay in _REPLACE_RETRY_DELAYS:
133-
if delay:
134-
time.sleep(delay)
135-
try:
136-
return path.stat(), path.read_bytes()
137-
except FileNotFoundError:
138-
raise
139-
except PermissionError as exc:
140-
if not _is_windows_sharing_violation(exc):
141-
raise
142-
last_exc = exc
143-
raise FileNotFoundError(path) from last_exc
154+
155+
def _do_stat_and_read() -> tuple[os.stat_result, bytes]:
156+
return path.stat(), path.read_bytes()
157+
158+
def _exhausted(last_exc):
159+
raise FileNotFoundError(path) from last_exc
160+
161+
return _with_sharing_retry(_do_stat_and_read, on_exhausted=_exhausted)
144162

145163

146164
_UTIME_SUPPORTS_FD = os.utime in os.supports_fd
@@ -264,21 +282,7 @@ def _unlink_with_sharing_retry(path: Path) -> None:
264282
:func:`_is_windows_sharing_violation` to filter the exhausted-retry
265283
case and re-raise any other ``PermissionError``.
266284
"""
267-
last_exc: PermissionError | None = None
268-
for delay in _REPLACE_RETRY_DELAYS:
269-
if delay:
270-
time.sleep(delay)
271-
try:
272-
path.unlink()
273-
return
274-
except FileNotFoundError:
275-
raise
276-
except PermissionError as exc:
277-
if not _is_windows_sharing_violation(exc):
278-
raise
279-
last_exc = exc
280-
if last_exc is not None:
281-
raise last_exc
285+
_with_sharing_retry(path.unlink)
282286

283287

284288
def _prune_if_stat_unchanged(path: Path, st_before: os.stat_result) -> None:

0 commit comments

Comments
 (0)