Skip to content

Commit d3eac5f

Browse files
committed
fix(test): every test in the ffi-thread file takes the pool lock, not just four
The first attempt locked only the tests whose ASSERTIONS depend on worker identity and exempted the rest on the reasoning that they hold under any scheduling. That is true of what they assert and irrelevant to what they do: they call on_plugin_thread too, so they take a worker, and a worker taken between the identity test's two calls is exactly the failure. It still failed under `cargo test --workspace` with ThreadId(21) vs ThreadId(22), adjacent ids, so the pool really had handed out a different worker. The file has seven tests, not four, and one of them deliberately holds four workers behind a barrier. Exempting a test from a lock because of what it asserts rather than what it touches is the mistake; the shared resource does not care why you touched it. Locking them costs nothing: none of the four newly-locked tests is about contention, so each holds just as strongly run alone. 0 failures in 10 full-suite runs, against roughly 1 in 3 before.
1 parent ecf7702 commit d3eac5f

1 file changed

Lines changed: 17 additions & 3 deletions

File tree

crates/plugin-loader/src/tests/ffi_thread_tests.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ use super::on_plugin_thread;
2525
/// here is that the obvious "fix" is to weaken the assertion to "some worker ran it", which would
2626
/// delete the only check that the pool reuses threads at all.
2727
///
28-
/// The lock is deliberately NOT taken by `work_never_runs_on_the_callers_thread` or
29-
/// `workers_are_named_for_what_they_are`: those hold under any scheduling, and serialising them
30-
/// would hide a regression that only appears under contention.
28+
/// EVERY test in this file takes the lock, including the two whose own assertions are
29+
/// scheduling-independent. The first attempt exempted them on the reasoning that they hold under
30+
/// any scheduling, which is true of what they ASSERT and irrelevant to what they DO: they call
31+
/// `on_plugin_thread` too, so they take a worker, and a worker taken between the identity test's
32+
/// two calls is exactly the failure. That version still failed under `cargo test --workspace`
33+
/// (ThreadId(21) vs ThreadId(22) — adjacent ids, so the pool really did hand out a different
34+
/// worker). Exempting a test from a lock because of what it asserts, rather than what it touches,
35+
/// is the mistake; the shared resource does not care why you touched it.
36+
///
37+
/// This does NOT weaken them. Neither test is about contention: one asserts plugin work is off the
38+
/// caller's thread, the other asserts the workers are named. Both hold just as strongly when run
39+
/// alone, so serialising costs nothing but four sequential tests.
3140
static IDENTITY: std::sync::Mutex<()> = std::sync::Mutex::new(());
3241

3342
/// Poisoning is irrelevant here: the guard protects scheduling, not data, so a panicking sibling
@@ -44,6 +53,7 @@ fn identity_lock() -> std::sync::MutexGuard<'static, ()> {
4453
/// image when that thread exits. The whole fix is this one fact.
4554
#[test]
4655
fn work_never_runs_on_the_callers_thread() {
56+
let _serial = identity_lock();
4757
let caller = std::thread::current().id();
4858
let ran_on = on_plugin_thread(|| std::thread::current().id()).expect("no panic");
4959
assert_ne!(
@@ -57,6 +67,7 @@ fn work_never_runs_on_the_callers_thread() {
5767
/// plainly whose threads these are.
5868
#[test]
5969
fn workers_are_named_for_what_they_are() {
70+
let _serial = identity_lock();
6071
let name =
6172
on_plugin_thread(|| std::thread::current().name().map(str::to_string)).expect("no panic");
6273
assert_eq!(name.as_deref(), Some("busbar-plugin-ffi"));
@@ -100,6 +111,7 @@ fn a_panicking_job_is_caught_and_the_worker_survives() {
100111
/// here, and a hung gateway is not an improvement on a crashing one.
101112
#[test]
102113
fn a_nested_call_takes_a_second_worker_and_does_not_deadlock() {
114+
let _serial = identity_lock();
103115
let (outer, inner) = on_plugin_thread(|| {
104116
let outer = std::thread::current().id();
105117
let inner = on_plugin_thread(|| std::thread::current().id()).expect("no panic");
@@ -116,6 +128,7 @@ fn a_nested_call_takes_a_second_worker_and_does_not_deadlock() {
116128
/// the caller blocking for the whole window, not the types.
117129
#[test]
118130
fn non_send_values_cross_the_rendezvous_in_both_directions() {
131+
let _serial = identity_lock();
119132
let borrowed = String::from("borrowed by the closure, never moved");
120133
let ptr: *const u8 = borrowed.as_ptr();
121134
let expected_len = borrowed.len();
@@ -132,6 +145,7 @@ fn non_send_values_cross_the_rendezvous_in_both_directions() {
132145
/// grows on demand, so making plugin calls thread-confined does not make them single-threaded.
133146
#[test]
134147
fn concurrent_callers_get_distinct_workers() {
148+
let _serial = identity_lock();
135149
use std::sync::{Arc, Barrier};
136150
let n = 4;
137151
let barrier = Arc::new(Barrier::new(n));

0 commit comments

Comments
 (0)