Skip to content

Windows: consume_wake_signal drops wakes on transient sharing violation + de-flake race test#60

Open
danielhertz1999-bit wants to merge 2 commits into
CodeAbra:mainfrom
danielhertz1999-bit:fix/windows-wake-signal-sharing-violation
Open

Windows: consume_wake_signal drops wakes on transient sharing violation + de-flake race test#60
danielhertz1999-bit wants to merge 2 commits into
CodeAbra:mainfrom
danielhertz1999-bit:fix/windows-wake-signal-sharing-violation

Conversation

@danielhertz1999-bit

Copy link
Copy Markdown
Contributor

Problem

Two issues surfaced by tests/test_wake_handler.py::test_consume_atomic_no_race failing on macOS CI with assert 0 >= 1.

1. A real Windows bug in consume_wake_signal(). It does an unlink-to-consume inside a bare handler:

try:
    self._wake_signal_path.unlink()
except FileNotFoundError:
    return False
except OSError:          # <-- swallows PermissionError too
    return False
return True

On Windows, unlink() raises PermissionError (sharing violation, WinError 5/32) when a concurrent writer or reader momentarily holds the signal file open — Python's open() there doesn't request FILE_SHARE_DELETE. The bare except OSError swallows that transient failure as "no signal," dropping a real wake and leaving the daemon stranded in HIBERNATION with an unserved socket. On POSIX this never happens (you can unlink an open file).

2. The test itself is flaky. consumed_truthy_count >= 1 had no synchronization: the consumer ran a fixed 200 unlink iterations and could finish before any writer's first replace landed (adverse thread scheduling on a loaded runner), reading consumed=0. That's the assert 0 >= 1 seen on macOS — independent of the Windows bug, and unfixed by a Windows-only change.

Fix

  • consume_wake_signal: retry the unlink briefly on a Windows PermissionError (mirrors daemon_state._atomic_replace). On POSIX a PermissionError is a genuine permission fault, so it still returns False on the first failure — no behavior change off Windows.
  • De-flake test_consume_atomic_no_race: writers produce continuously and the consumer polls until it consumes (bounded by a deadline) instead of a fixed count. Keeps the atomicity intent (errors == [], no crash under concurrent write+consume) while making the >=1 liveness check deterministic on any scheduler. The writer harness also retries os.replace on a Windows PermissionError, mirroring production.
  • Two deterministic regression tests for the retry (simulated via monkeypatch, so they run on every platform): a transient Windows PermissionError is retried and consumed; a POSIX PermissionError is not retried.

Verification

wake_handler suite green; test_consume_atomic_no_race looped 10× locally on Windows with zero flakes. The two new deterministic tests fail against the pre-fix source (the retry path doesn't exist), confirming they gate the fix. macOS behavior is exercised by this PR's CI.

danielhertz1999-bit and others added 2 commits July 7, 2026 03:38
…lation

consume_wake_signal() did an unlink-to-consume inside a bare
`except OSError: return False`. On Windows, unlink() raises PermissionError
(sharing violation, WinError 5/32) when a concurrent writer or reader
momentarily holds the signal file open -- Python's open() there does not
request FILE_SHARE_DELETE. The bare handler swallowed that as "no signal",
dropping a real wake and stranding the daemon in HIBERNATION with an
unserved socket. Retry the unlink briefly on a Windows PermissionError
(mirrors daemon_state._atomic_replace); on POSIX a PermissionError is a
genuine permission fault, so it still returns False on the first failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Deterministic regression tests for the retry: a simulated Windows
  transient PermissionError is retried and consumed; a POSIX PermissionError
  is not retried. Both run on every platform via monkeypatch, so the fix is
  gated tightly rather than through the racy concurrency test.
- De-flake test_consume_atomic_no_race: writers now produce continuously and
  the consumer polls until it consumes (bounded by a deadline) instead of a
  fixed iteration count. The old fixed-count consumer could finish before any
  writer's first replace landed (adverse scheduling on a loaded runner),
  reading consumed=0 -- the macOS CI failure `assert 0 >= 1`.
- The writer harness retries os.replace on a Windows PermissionError,
  mirroring production so the writer thread can't die early and starve the
  consumer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CodeAbra

Copy link
Copy Markdown
Owner

Thanks. v2.0 (Rust rewrite) already has consume_wake_signal, so the wake-signal path carried over, I need to check whether the transient sharing-violation retry you added is in there or still missing. If it's missing it goes into v2.0.3 with the other Windows fixes. Keeping it open till I've confirmed. Nice de-flake on the race test too.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants