Skip to content

tests: fix three stop and clock races (isp1040, ioc3, ioc3_eth) - #380

Merged
techomancer merged 3 commits into
techomancer:mainfrom
jamesbraid:isp1040-hang
Oct 31, 2026
Merged

techomancer merged 3 commits into
techomancer:mainfrom
jamesbraid:isp1040-hang

Conversation

@jamesbraid

@jamesbraid jamesbraid commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

isp1040: move the test clock under the state lock

Cause: Rig::clock stored the fake time and notified the condition variable without taking the state lock. A move that landed after the ring thread read the clock and before it started waiting was lost, and the test hung in wait_idle (6 runs in 1000 when run alone).

Fix: the clock now moves through Core::set_test_clock, which takes the state lock first. A new test moves the clock past 2000 selection timeouts; without the lock it fails in 91 of 100 runs.

Test-only: guests never use the fake clock, so their behavior is unchanged.

ioc3_eth: wait for the device to be freed in the stop test

Cause: the test asserted that a Weak to the device could not be upgraded the instant the caller dropped its Arc. The receive thread can hold a strong reference during a poll, so the device is freed a moment later (about 3 runs in 100 under load). The device itself behaves correctly.

Fix: the test waits, bounded, for the device to be freed and for its running flag to clear. A new test holds a reference on another thread to pin the case deterministically.

Test-only: the change touches only test code, so guests are unaffected.

ioc3: read the pending timer only between callbacks in the stop test

Cause: a timer callback clears its armed-table slot on entry and arms the next deadline before it returns, so a repeating output can show no pending timer for a short span. The test read the table while a callback could be in flight (about 1 run in 300 under load).

Fix: the table is read through a helper that holds the callback gate, so the read sees the table at rest. A new test reads it 20000 times against a running periodic output; without the gate it fails 29 runs in 30.

Test-only: the change touches only test code, so guests are unaffected.

Tested with plain cargo test --release --lib, with and without jitv2.

The tests drive the adapter on a fake clock. While that clock is in
use, the ring thread reads it with the state locked and then waits on
the condition variable with no timeout, because only the test can move
it. Rig::clock stored the new time and notified without taking the
state lock. A move that landed after the thread read the clock and
before it waited was lost: the thread slept through the time that ended
its selection, and the test's wait_idle then waited for that selection
forever. Run alone, this test hung 6 times in 1000:

  a_reset_naming_a_missing_bus_leaves_a_waiting_selection_and_is_noted

with the ring thread in wait_selection and the test in wait_idle, both
parked on the same condition variable.

The clock now moves through Core::set_test_clock, which takes the state
lock first. Guests never use the fake clock: on the host's clock each
wait for a time to pass has a timeout, and the waits without one are
for state that changes under the lock, so guests could not hang this
way.

The new test moves the clock past 2000 selection timeouts, spacing the
two moves across the thread's wake-up. Without the lock it fails within
its 5 s limit in 91 of 100 runs.
@Samueltgg12

Copy link
Copy Markdown
Contributor

Waiting on it.

…stop test

stopping_the_mac_joins_its_threads asserted that a Weak to the device
could no longer be upgraded the instant the caller dropped its Arc. The
receive thread holds a strong reference for the length of each poll, so
under load it can still be inside one when the caller drops; the thread
then releases the last reference and the device is freed a moment later.
That failed about 3 runs in 100 when run alone on a loaded host, with
"assertion failed: probe.upgrade().is_none()". The device behaves
correctly: Drop calls stop, which skips joining the current thread.

Wait (bounded) for the device to be freed and check that its running
flag clears, which is what dropping a running device must achieve. A new
test pins the case deterministically: a reference held on another thread
keeps the device alive and running past the caller's drop, and releasing
it there stops the device.
stop_joins_the_threads_and_removes_the_timers failed about once in 300
runs with siblings loaded: "start arms the deadlines the chip holds". The
chip was fine. A timer callback clears its slot in the armed table on
entry and arms the next deadline before it returns, so a repeating output
shows no pending timer for that short span. The test read the table
without regard to a callback in flight, and with a 1000-tick period a
callback is often in flight under load.

Read the table through a helper that holds the callback gate, which a
callback holds from entry to return, so the read sees the table at rest.
The assertion is unchanged. A second test reads it 20000 times against a
running periodic output; without the gate it fails 29 runs in 30.
@jamesbraid jamesbraid changed the title isp1040: move the test clock under the state lock tests: fix three stop and clock races (isp1040, ioc3, ioc3_eth) Oct 31, 2026
@techomancer
techomancer merged commit 9b55194 into techomancer:main Oct 31, 2026
7 checks passed
@jamesbraid
jamesbraid deleted the isp1040-hang branch November 1, 2026 22:52
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.

3 participants