Skip to content

[v1.0] Bound and classify Windows atomic-replace retries #315

Description

@codeforester

Goal

Retry genuinely transient Windows sharing/lock failures without imposing multi-second latency on permanent permission errors.

Background

Current _replace_with_retry() catches every PermissionError on Windows, retries up to 50 times, and sleeps with linear backoff:

def _replace_with_retry(source: Path, destination: Path) -> None:
"""Replace a private file, tolerating transient Windows sharing races."""
# Antivirus/indexer handles and concurrent writers can hold the destination
# briefly on Windows. Use a bounded, linear backoff long enough for those
# transient sharing violations without making a persistent permission error
# unbounded.
attempts = 1 if os.name != "nt" else 50
for attempt in range(attempts):
try:
os.replace(source, destination)
return
except PermissionError:
if attempt == attempts - 1:
raise
time.sleep(0.005 * (attempt + 1))

The worst-case sleep before the final failure is:

0.005 * (1 + 2 + ... + 49) = 6.125 seconds

The code does not distinguish a transient sharing/lock violation from permanent access denied, a read-only destination, or another non-retryable permission condition. Managed metadata is written in the invocation lifecycle, so a permanent filesystem policy error can turn a fast actionable failure into an unexplained six-second stall. The only regression test covers one transient failure followed by success; it does not assert error classification or a maximum elapsed budget:

def test_windows_replace_retries_transient_sharing_failure(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
source = Path(tmpdir) / "source"
destination = Path(tmpdir) / "destination"
source.write_text("payload", encoding="utf-8")
with (
mock.patch.object(private_files.os, "name", "nt"),
mock.patch.object(
private_files.os,
"replace",
side_effect=[PermissionError("busy"), lambda src, dst: Path(dst).write_text(Path(src).read_text())],
) as replace,
mock.patch.object(private_files.time, "sleep") as sleep,
):
private_files._replace_with_retry(source, destination) # pylint: disable=protected-access
self.assertEqual(replace.call_count, 2)
sleep.assert_called_once()

Scope

  • Classify retryable Windows sharing/lock errors from winerror/documented platform signals.
  • Fail immediately on permanent permission and path-policy errors.
  • Replace attempt-count-only behavior with an explicit maximum elapsed budget.
  • Make delay/clock behavior deterministic in tests and observable in debug diagnostics.
  • Preserve atomic replacement and the old destination on failure.

Acceptance criteria

  • Sharing/lock violations retry within a documented total deadline.
  • Access denied and other non-retryable permission errors fail without sleeping.
  • Worst-case retry latency is asserted in tests.
  • Successful transient recovery, deadline exhaustion, and permanent failure retain correct temporary-file cleanup.
  • Native Windows and mounted/network-filesystem cases are covered or explicitly documented.

Validation

Run focused fault-injection tests plus native Windows lifecycle and metadata persistence jobs.

Project fields

  • Status: Backlog
  • Priority: P2
  • Area: Runtime
  • Initiative: Adoption Polish
  • Size: S

Ownership

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething is not working

Type

No type

Projects

  • Status
    Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions