@@ -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+
83114def _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
112135def _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
284288def _prune_if_stat_unchanged (path : Path , st_before : os .stat_result ) -> None :
0 commit comments