Skip to content

fix: race tests for suspension ownership, and the three races they found (#47 § 4) - #53

Merged
novoj merged 1 commit into
mainfrom
test/47-race-barriers
Oct 31, 2026
Merged

novoj merged 1 commit into
mainfrom
test/47-race-barriers

Conversation

@novoj

@novoj novoj commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Closes #47. § 1–3 landed in #52. This PR is § 4: the deterministic barrier tests and the remaining live cases.

Barrier tests (SuspensionRaceBarrierTest)

Each test runs the competing action inside the mocked JDI call that marks its gap, on the thread the dispatcher, the watchdog or a job would run it on. Every run therefore takes the same interleaving, and every scenario ends on the suspend counts the target would really have (FakeSuspendModel).

Gap On the old code Outcome
A job still compiling → a frame tool passed none needed: a job counts from its start
A frame tool reserved and checked the thread → the dispatcher leases it failed: invoked code ran under the tool lease() waits while a frame tool reserves the thread (at most 2 s, then refused)
The hold check → a frame tool's retry failed: jdwp_set_local wrote into the frame of the thread's next stop a retry needs a hold only a tool ends: a published stop or a manual hold (userHoldsOn)
The watchdog's owner query → the owner leaves and stops at a new breakpoint passed none needed: unblock re-checks the owner
A job's compilation → its stop released and replaced passed none needed: the lease is refused

There are also two more tests: a positive control showing that a published stop still vouches for a retry, and a check that a frame tool which never finishes makes the lease refuse after the wait.

Live cases (SuspensionOwnershipLiveTest)

  • New: a stop-all released while its evaluation outlives the tool's wait. The other threads run at once; the evaluating thread runs once the evaluation ends. Passed.
  • New: the watchdog cancels a queued thread stop, then that thread stops again. The new stop keeps its own suspension. This case found the third race. JDWP shows a thread that invokes from inside its event's handling as RUNNING even while it waits for a monitor, so the watchdog never looked at it. A thread shown running is now asked for its contended monitor too; one that really runs reports none (unit test in SuspensionWatchdogTest).
  • Already present (from fix: 2.11.0 test-flight findings — probe hits read at exit, probes wait for their class, clearer replies, hint-free flights #52): reset during an evaluation, co-delivered arming, and disconnect during an evaluation that never returns.
  • Test fix: enableSuspended(request, thread) suspends only the request's filter thread. The VM-wide variant could invalidate the frame of an evaluation in flight ("Thread has been resumed"). That made Order 1 flaky; it failed in one run.

Tests

  • Unit: 1391 tests, 0 failures. The 45 skipped are the opt-in live tests.
  • Race, session, watchdog and listener tests on a single core (taskset -c 0): 196 tests, 0 failures; looped 6× before the watchdog fix.
  • SuspensionOwnershipLiveTest: 3 of 3 full-class runs green.
  • All live suites: 42 tests, 0 failures.

Docs: docs/event-pipeline.md § 14, docs/threading-and-safety.md § 5, docs/testing.md.

…und (#47)

#47 § 4 asked for deterministic barriers and native cases. Each barrier
runs the competing action inside the mocked JDI call that marks its gap
(SuspensionRaceBarrierTest); two of them failed on the old code:

- A frame tool and the dispatcher's lease: after the tool reserved the
  thread and found no evaluation, the dispatcher could lease it and run
  invoked code under the tool. lease() now waits while a frame tool
  reserves the thread (at most 2 s, then it is refused).
- The hold check and a frame tool's retry: a set the dispatcher still
  processed vouched for the retry, but the dispatcher could release it
  before the retry read, and jdwp_set_local wrote into the frame of the
  thread's next stop. A retry now needs a hold only a tool ends — a
  published stop or a manual hold (SuspensionSession.userHoldsOn).

The live case for a cancelled thread stop found the third: JDWP shows a
thread that invokes from inside its event's handling as RUNNING even
while it waits for a monitor, so the watchdog never looked at it. A
thread shown running is now asked for its contended monitor too.

Passing on the old code: a job still compiling refuses frame tools; the
owner query then a new stop of the owner; a job's compilation then a
replaced stop. Live: a stop-all released while its evaluation outlives
the tool's wait. The live tests enable a thread-filtered request by
suspending that thread only — a VM-wide resume broke evaluations in
flight and made Order 1 flaky.
@novoj
novoj merged commit f322374 into main Oct 31, 2026
1 check passed
@novoj
novoj deleted the test/47-race-barriers branch October 31, 2026 10:18
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.

Follow-ups from the PR #46 code review: per-stop reasons, per-invocation watchdog timing, late ring writes, race tests

1 participant