Skip to content

Refuse an Acquire that was queued behind Dispose [patch] - #36

Merged
matt-edmondson merged 4 commits into
mainfrom
fix/33-acquire-after-dispose
Nov 4, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
fix/33-acquire-after-dispose

Conversation

@matt-edmondson

Copy link
Copy Markdown
Collaborator

Fixes #33

What was wrong

The Linux, Windows and macOS blockers each called ObjectDisposedException.ThrowIf(disposed, this) before taking gate. An Acquire could pass that check and then wait on the lock while Dispose held it. Once Dispose returned, the waiting Acquire started new holds on a blocker that was already disposed. IsBlocking stayed true, and a second Dispose was a no-op. On Linux the systemd-inhibit helper kept running until the process exited.

Change

  • All three blockers now check disposed again right after they take gate. The early check stays, so a disposed blocker still throws ObjectDisposedException before Linux reports PlatformNotSupportedException.
  • LinuxSleepBlocker gets an internal constructor that takes the helper paths. The public constructor resolves the paths from PATH and calls it. This lets a test point the blocker at a stand-in helper.

The Windows and macOS changes are the same one-line re-check. They were built here but not run, because those blockers only run on their own OS.

Tests

LinuxSleepBlockerTests.Acquire_QueuedBehindDispose_ThrowsAndStartsNoHelper uses a stand-in systemd-inhibit script. The script records its PID, then runs exec cat. The test makes the race deterministic:

  1. It holds the blocker's gate, which it reads by reflection.
  2. It starts an Acquire on another thread and waits until that thread is blocked on the lock.
  3. It calls Dispose on the test thread. Lock is re-entrant, so this works while the test holds gate.
  4. It releases gate.

It then checks three things: the queued Acquire threw ObjectDisposedException, IsBlocking is false, and no helper was started.

Results on Linux:

  • With the Linux re-check reverted, the test fails, because the Acquire started a helper.
  • With the fix, it passed 5 times out of 5, and the full suite passes (30/30).
  • On other platforms the test is inconclusive.

Generated by Claude Code

All three blockers checked disposed before taking gate, so an Acquire
that passed the check and then waited on the lock while Dispose ran
started new holds on a disposed blocker. IsBlocking stayed true and,
on Linux, the systemd-inhibit helper ran until the process exited.
Check disposed again once the lock is held.

Fixes #33

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW2EzJMFdK7ZQBB2keC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW2EzJMFdK7ZQBB2keC

Copy link
Copy Markdown
Collaborator Author

SonarCloud Code Analysis fails on new-code coverage: 75.0%, against a required 80%. It's not a test failure, and I'm leaving it as is.

The uncovered new lines are the ObjectDisposedException.ThrowIf(disposed, this) re-checks in WindowsSleepBlocker.Acquire and MacOsSleepBlocker.Acquire. Those blockers carry [SupportedOSPlatform] for their own OS and only run there, and the coverage run is on Linux. The Linux re-check, which is the same line, is covered by LinuxSleepBlockerTests.Acquire_QueuedBehindDispose_ThrowsAndStartsNoHelper. That test fails without the fix.

Re-running won't change the result, because coverage is collected the same way every time. CI tests pass on ubuntu and macOS. SonarCloud isn't a required check, so it doesn't block merging.


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5jpiuVuBRXSgitSY1
SonarCloud measures coverage on Windows, where the Linux-only race test is
inconclusive, so the new lines under the lock were uncovered. These tests
queue an Acquire behind Dispose for all three blockers; the check throws
before any platform call, so they run anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW5jpiuVuBRXSgitSY1
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit a264b9e into main Nov 4, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants