Skip to content

Read and replace the request under one lock in SetKeepDisplayAwake [minor] - #42

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/39-atomic-request
Nov 4, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/39-atomic-request

Conversation

@matt-edmondson

Copy link
Copy Markdown
Collaborator

Fixes #39

What changed

SetKeepDisplayAwake read Request under gate, released the lock, and then took it again in SetRequest to write a modified copy. If another request change landed in that gap, it was overwritten, and StateChanged still fired for both calls. This is the request-path counterpart of #30. The fix has the same shape as #31: the decision is made inside the lock.

  • New public overload SetRequest(Func<SleepBlockRequest, SleepBlockRequest> update). It runs update on the request in force under gate, then applies the result. Behaviour is otherwise unchanged: re-acquire when active, no event when nothing changed. A null result throws ArgumentNullException and leaves the request alone. Because the overload is public, it's a [minor] change. Callers get atomic partial edits such as SetRequest(current => current with { Reason = "Rendering" }).
  • SetKeepDisplayAwake and SetRequest(SleepBlockRequest) now both go through it.
  • The tool's --reason option used the same racy SetRequest(controller.Request with { … }) shape. It now uses the overload.
  • The README API table lists the new overload.

Tests

  • SetKeepDisplayAwake_RacingAReasonUpdate_KeepsBothChanges runs SetKeepDisplayAwake(true) against a reason-only update over 3000 trials. Each trial uses a fresh controller, and two long-lived threads are released together by spinning on shared counters. It asserts that every trial ends with both changes. Starting a task per trial (the Decide and apply a Toggle under one lock, so concurrent toggles each flip once [patch] #31 Toggle-test style) staggers the two sides too much: with that design the old code passed 200/200, so the test uses long-lived threads instead. The threads are background threads and every wait has a 10 s timeout, so a failure can't hang the test host.
  • SetRequest_WithAnUpdate_AppliesItToTheRequestInForce and SetRequest_WithAnUpdateThatReturnsNull_ThrowsAndKeepsTheRequest cover the new overload directly.
  • I put back the old SetKeepDisplayAwake body and kept everything else. The race test then failed on three runs out of three, losing a change in 8 to 13 of every 2000 trials (4 cores). With the fix, the full suite passes on repeated runs (32/32, about 6 s).

Generated by Claude Code

…inor]

SetKeepDisplayAwake read Request under the controller's lock, released it,
then took it again in SetRequest to write a modified copy. A concurrent
request change landing in between was overwritten while StateChanged
still reported both. Add SetRequest(Func<SleepBlockRequest,
SleepBlockRequest>), which computes the new request from the current one
inside the lock, and route SetKeepDisplayAwake, SetRequest(request) and
the tool's --reason option through it.

Fixes #39

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

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 2213194 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

Development

Successfully merging this pull request may close these issues.

Concurrent SetKeepDisplayAwake and SetRequest lose one change: the request is read and written under separate locks (the #30 race, outside Toggle)

2 participants