@@ -3,9 +3,9 @@ mod common;
33use std:: io:: { Read , Write } ;
44use std:: net:: { TcpListener , TcpStream } ;
55use std:: path:: { Path , PathBuf } ;
6- use std:: process:: { Child , Command , Stdio } ;
7- use std:: sync:: Arc ;
6+ use std:: process:: { Child , Command , ExitStatus , Stdio } ;
87use std:: sync:: atomic:: { AtomicBool , Ordering } ;
8+ use std:: sync:: { Arc , Condvar , Mutex } ;
99use std:: thread;
1010use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
1111
@@ -16,19 +16,64 @@ fn run_cli_args(args: &[&str]) -> (i32, String, String) {
1616 run_cli_args_with_env ( args, & [ ] )
1717}
1818
19- fn run_cli_args_with_env ( args : & [ & str ] , envs : & [ ( & str , & str ) ] ) -> ( i32 , String , String ) {
20- let cwd = exe_temp_path ( "empty-cwd" ) ;
21- std:: fs:: create_dir_all ( & cwd) . expect ( "empty cwd should create" ) ;
22- let output = cli_command ( args, envs, & cwd)
23- . output ( )
24- . expect ( "codetrial should exit" ) ;
25- let _ = std:: fs:: remove_dir_all ( cwd) ;
19+ /// Written by `wait_for_http` and read by `spawn_server`; the pair is pinned
20+ /// in `only_sigkill_is_retryable`.
21+ const SIGKILL_PREFIX : & str = "SIGKILL: " ;
2622
27- (
28- output. status . code ( ) . unwrap_or_default ( ) ,
29- String :: from_utf8 ( output. stdout ) . expect ( "stdout should be UTF-8" ) ,
30- String :: from_utf8 ( output. stderr ) . expect ( "stderr should be UTF-8" ) ,
31- )
23+ fn was_sigkill ( status : & ExitStatus ) -> bool {
24+ #[ cfg( unix) ]
25+ {
26+ use std:: os:: unix:: process:: ExitStatusExt ;
27+
28+ status. signal ( ) == Some ( 9 )
29+ }
30+ #[ cfg( not( unix) ) ]
31+ {
32+ let _ = status;
33+ false
34+ }
35+ }
36+
37+ #[ cfg( unix) ]
38+ #[ test]
39+ fn only_sigkill_is_retryable ( ) {
40+ use std:: os:: unix:: process:: ExitStatusExt ;
41+
42+ assert ! ( was_sigkill( & ExitStatus :: from_raw( 9 ) ) ) ;
43+ assert ! ( !was_sigkill( & ExitStatus :: from_raw( 11 ) ) ) ;
44+ }
45+
46+ fn run_cli_args_with_env ( args : & [ & str ] , envs : & [ ( & str , & str ) ] ) -> ( i32 , String , String ) {
47+ // Retried for the one reason a run says nothing about the binary: the
48+ // machine ran out of memory and the kernel chose this process. `code()` is
49+ // `None` for a death by signal, and defaulting that to 0 handed every
50+ // caller a successful exit status for a process that never ran, so
51+ // `reject_usage_errors_with_exit_two` failed reporting 0 against 2 and read
52+ // as the binary having accepted a usage error it had refused.
53+ for attempt in 0 ..5 {
54+ let cwd = exe_temp_path ( "empty-cwd" ) ;
55+ std:: fs:: create_dir_all ( & cwd) . expect ( "empty cwd should create" ) ;
56+ let output = cli_command ( args, envs, & cwd)
57+ . output ( )
58+ . expect ( "codetrial should exit" ) ;
59+ let _ = std:: fs:: remove_dir_all ( cwd) ;
60+
61+ let Some ( code) = output. status . code ( ) else {
62+ assert ! (
63+ was_sigkill( & output. status) ,
64+ "{args:?}: killed by an unexpected signal: {}" ,
65+ output. status
66+ ) ;
67+ assert ! ( attempt < 4 , "{args:?}: killed by a signal five times over" ) ;
68+ continue ;
69+ } ;
70+ return (
71+ code,
72+ String :: from_utf8 ( output. stdout ) . expect ( "stdout should be UTF-8" ) ,
73+ String :: from_utf8 ( output. stderr ) . expect ( "stderr should be UTF-8" ) ,
74+ ) ;
75+ }
76+ unreachable ! ( "the loop returns or asserts" )
3277}
3378
3479/// The same invocation, but the process is required to stop by itself.
@@ -41,6 +86,19 @@ fn run_cli_args_with_env(args: &[&str], envs: &[(&str, &str)]) -> (i32, String,
4186/// CI as a 33-second timeout. Bounded, so a guard that stopped guarding is a
4287/// red test.
4388fn run_cli_until_exit ( args : & [ & str ] , envs : & [ ( & str , & str ) ] ) -> ( i32 , String , String ) {
89+ // Retried on a kill for the same reason the other runner is: the machine
90+ // running out of memory says nothing about the binary, and this helper's
91+ // whole point is that a refusal is observed rather than assumed.
92+ for attempt in 0 ..5 {
93+ if let Some ( result) = run_cli_until_exit_once ( args, envs) {
94+ return result;
95+ }
96+ assert ! ( attempt < 4 , "{args:?}: killed by a signal five times over" ) ;
97+ }
98+ unreachable ! ( "the loop returns or asserts" )
99+ }
100+
101+ fn run_cli_until_exit_once ( args : & [ & str ] , envs : & [ ( & str , & str ) ] ) -> Option < ( i32 , String , String ) > {
44102 // Generous for a loaded runner, and well inside the timeout `cargo mutants`
45103 // derives from the baseline (33s when this was written). A bound above that
46104 // would score a caught mutant as a timeout again, which is the failure this
@@ -92,7 +150,17 @@ fn run_cli_until_exit(args: &[&str], envs: &[(&str, &str)]) -> (i32, String, Str
92150 let stderr = err. join ( ) . expect ( "stderr reader should finish" ) ;
93151 let _ = std:: fs:: remove_dir_all ( & cwd) ;
94152
95- ( status. code ( ) . unwrap_or_default ( ) , stdout, stderr)
153+ // Not `unwrap_or_default`: `code()` is `None` for a death by signal, and
154+ // zero is the one value that reads as a clean exit. An out-of-memory kill
155+ // reported that way turned "the binary refused" into "the binary accepted".
156+ let Some ( code) = status. code ( ) else {
157+ assert ! (
158+ was_sigkill( & status) ,
159+ "codetrial {args:?} died of an unexpected signal: {status}"
160+ ) ;
161+ return None ;
162+ } ;
163+ Some ( ( code, stdout, stderr) )
96164}
97165
98166/// One place builds the invocation, so the two runners cannot drift about which
@@ -249,8 +317,13 @@ fn wait_for_http(addr: &str, child: &mut Child) -> Result<(), String> {
249317 if let Some ( stream) = child. stderr . as_mut ( ) {
250318 let _ = stream. read_to_string ( & mut stderr) ;
251319 }
320+ let cause = if was_sigkill ( & status) {
321+ SIGKILL_PREFIX
322+ } else {
323+ ""
324+ } ;
252325 return Err ( format ! (
253- "server exited before listening: {status}; stderr={stderr}"
326+ "{cause} server exited before listening: {status}; stderr={stderr}"
254327 ) ) ;
255328 }
256329 thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
@@ -262,16 +335,30 @@ fn wait_for_http(addr: &str, child: &mut Child) -> Result<(), String> {
262335/// its listener, so another socket can take it first. Retry on that specific
263336/// loss instead of reporting it as the behavior under test failing.
264337fn spawn_server ( build : impl Fn ( & str ) -> Command ) -> ( String , ServerProcess ) {
338+ let mut last = String :: new ( ) ;
265339 for _ in 0 ..5 {
266340 let addr = free_addr ( ) ;
267341 let mut child = ServerProcess :: spawn ( & mut build ( & addr) ) ;
268342 match wait_for_http ( & addr, & mut child) {
269343 Ok ( ( ) ) => return ( addr, child) ,
270- Err ( error) if error. contains ( "Address already in use" ) => continue ,
344+
345+ // Two startup losses that say nothing about the binary. The port
346+ // was taken between `free_addr` closing its listener and the child
347+ // binding it. Or the machine ran out of memory and the kernel chose
348+ // this process: `MAX_LIVE_SERVERS` is what keeps that rare, and a
349+ // suite that reported it as the behavior under test failing would
350+ // be reporting the size of the machine it ran on.
351+ Err ( error)
352+ if error. contains ( "Address already in use" )
353+ || error. starts_with ( SIGKILL_PREFIX ) =>
354+ {
355+ last = error;
356+ continue ;
357+ }
271358 Err ( error) => panic ! ( "{error}" ) ,
272359 }
273360 }
274- panic ! ( "could not start the server on a free port " ) ;
361+ panic ! ( "no attempt started a server; the last one said: {last} " ) ;
275362}
276363
277364/// A cold-start server: the binary beside `dir`, with no config file on any
@@ -326,10 +413,160 @@ fn http_request(addr: &str, request: &str) -> String {
326413 response
327414}
328415
416+ /// How many servers may be live at once.
417+ ///
418+ /// Most tests in this file start a real `codetrial web`, and the test harness
419+ /// runs one thread per core, so the default is as many live servers as the
420+ /// machine has cores. Each is a whole binary with the WebRTC stack linked in,
421+ /// which on a small machine is more resident memory than there is to give: the
422+ /// kernel picks servers off with SIGKILL, and `wait_for_http` reports the death
423+ /// as the behavior under test failing. Retrying would not help, because the
424+ /// next attempt asks the same machine for the same thing.
425+ ///
426+ /// Four rather than a number derived from the hardware. The cap only has to be
427+ /// below what the smallest machine running this suite can hold, and a bound
428+ /// that varies by host is a suite that is flaky on exactly the hosts nobody
429+ /// tests on. Tests that need no server never queue here.
430+ const MAX_LIVE_SERVERS : usize = 4 ;
431+
432+ fn server_slots ( ) -> & ' static ( Mutex < usize > , Condvar ) {
433+ static SLOTS : std:: sync:: OnceLock < ( Mutex < usize > , Condvar ) > = std:: sync:: OnceLock :: new ( ) ;
434+ SLOTS . get_or_init ( || ( Mutex :: new ( MAX_LIVE_SERVERS ) , Condvar :: new ( ) ) )
435+ }
436+
437+ // How many servers this thread is already holding, so the slot is charged to
438+ // the test rather than to each server it starts.
439+ //
440+ // `stop_child` kills a server without dropping the `ServerProcess` that owns
441+ // it, so a test that stops one and starts another holds two at once. Charging
442+ // both would let enough such tests take every slot and then wait on each other
443+ // for the second one they each need, which is a hung suite rather than a slow
444+ // one. A test pays once and keeps paying until it ends.
445+ thread_local ! {
446+ static SERVERS_HELD : std:: cell:: Cell <usize > = const { std:: cell:: Cell :: new( 0 ) } ;
447+ }
448+
449+ // How many times this thread has handed the shared slot back, so a test can say
450+ // when the release happened rather than only that the count moved. Per thread
451+ // because the release is, which keeps it clear of every other test taking and
452+ // returning slots at the same time.
453+ thread_local ! {
454+ static SLOTS_RELEASED : std:: cell:: Cell <usize > = const { std:: cell:: Cell :: new( 0 ) } ;
455+ }
456+
457+ /// Held for as long as the server is alive, because the memory is held that
458+ /// long. Taken before the spawn and returned by `ServerProcess`'s `Drop`, which
459+ /// already owns reaping the child, so a panicking test releases it by unwinding
460+ /// rather than leaving the suite one slot poorer for the rest of the run.
461+ struct ServerSlot {
462+ /// Whether this one took the shared slot, or joined the one its thread
463+ /// already holds.
464+ owns : bool ,
465+ }
466+
467+ impl ServerSlot {
468+ fn take ( ) -> Self {
469+ let owns = SERVERS_HELD . with ( |held| {
470+ let depth = held. get ( ) ;
471+ held. set ( depth + 1 ) ;
472+ depth == 0
473+ } ) ;
474+ if owns {
475+ let ( free, released) = server_slots ( ) ;
476+ let mut free = free. lock ( ) . unwrap_or_else ( |error| error. into_inner ( ) ) ;
477+ while * free == 0 {
478+ free = released
479+ . wait ( free)
480+ . unwrap_or_else ( |error| error. into_inner ( ) ) ;
481+ }
482+ * free -= 1 ;
483+ }
484+ Self { owns }
485+ }
486+ }
487+
488+ impl Drop for ServerSlot {
489+ fn drop ( & mut self ) {
490+ // Released when the thread's last server goes, not when the one that
491+ // took the slot goes. A test holding two and dropping the first-created
492+ // one first would otherwise hand the slot back while its second server
493+ // is still running, and the cap this exists to hold would be one short.
494+ let last = SERVERS_HELD . with ( |held| {
495+ let depth = held. get ( ) . saturating_sub ( 1 ) ;
496+ held. set ( depth) ;
497+ depth == 0
498+ } ) ;
499+ if !last {
500+ return ;
501+ }
502+ SLOTS_RELEASED . with ( |count| count. set ( count. get ( ) + 1 ) ) ;
503+ let ( free, released) = server_slots ( ) ;
504+ * free. lock ( ) . unwrap_or_else ( |error| error. into_inner ( ) ) += 1 ;
505+ released. notify_one ( ) ;
506+ }
507+ }
508+
509+ /// Asserted on the flags rather than on the free count, because every other
510+ /// test in this file is taking and returning slots while this one runs and the
511+ /// count is theirs too.
512+ /// Drop order is the test author's, not this file's: a test can stop one
513+ /// server and drop it before the one it started afterwards. The slot belongs to
514+ /// the thread for as long as any of its servers is alive, so the release has to
515+ /// wait for the last of them rather than for the one that took it.
516+ #[ test]
517+ fn the_slot_outlives_a_server_dropped_out_of_order ( ) {
518+ let before = SLOTS_RELEASED . with ( |count| count. get ( ) ) ;
519+ let first = ServerSlot :: take ( ) ;
520+ let second = ServerSlot :: take ( ) ;
521+ assert ! ( first. owns) ;
522+ assert ! ( !second. owns) ;
523+
524+ drop ( first) ;
525+ assert_eq ! (
526+ SLOTS_RELEASED . with( |count| count. get( ) ) ,
527+ before,
528+ "the slot went back while this thread still held a server"
529+ ) ;
530+
531+ drop ( second) ;
532+ assert_eq ! (
533+ SLOTS_RELEASED . with( |count| count. get( ) ) ,
534+ before + 1 ,
535+ "the last server went and the slot did not go back"
536+ ) ;
537+ }
538+
539+ #[ test]
540+ fn a_thread_running_two_servers_pays_for_one_slot ( ) {
541+ let outer = ServerSlot :: take ( ) ;
542+ let inner = ServerSlot :: take ( ) ;
543+ assert ! (
544+ outer. owns,
545+ "the first server on a thread takes the shared slot"
546+ ) ;
547+ assert ! (
548+ !inner. owns,
549+ "a second server on the same thread joins that slot rather than waiting \
550+ for one the thread it is waiting on cannot give back"
551+ ) ;
552+ drop ( inner) ;
553+ drop ( outer) ;
554+ assert_eq ! (
555+ SERVERS_HELD . with( |held| held. get( ) ) ,
556+ 0 ,
557+ "both are given back"
558+ ) ;
559+ }
560+
329561/// Kills the spawned server on drop. Without this a failing assertion unwinds
330562/// past `stop_child` and leaves a `codetrial web` process listening for the
331563/// rest of the machine's uptime; several accumulated during development.
332- struct ServerProcess ( Child ) ;
564+ struct ServerProcess {
565+ child : Child ,
566+ /// Never read. It is here so the slot is given back when the server it
567+ /// stands for is reaped, and not one statement sooner.
568+ _slot : ServerSlot ,
569+ }
333570
334571impl ServerProcess {
335572 /// stderr is piped here rather than left to each caller, because
@@ -341,32 +578,36 @@ impl ServerProcess {
341578 /// `binary_web_does_not_require_github_oauth_config` did: the one spawning
342579 /// test that did not pipe was the one whose retry never fired.
343580 fn spawn ( command : & mut Command ) -> Self {
344- Self (
345- command
581+ // The slot is taken before the spawn, not after, or the bound would be
582+ // on servers this suite admits to rather than on servers running.
583+ let slot = ServerSlot :: take ( ) ;
584+ Self {
585+ child : command
346586 . stderr ( Stdio :: piped ( ) )
347587 . spawn ( )
348588 . expect ( "codetrial web should start" ) ,
349- )
589+ _slot : slot,
590+ }
350591 }
351592}
352593
353594impl std:: ops:: Deref for ServerProcess {
354595 type Target = Child ;
355596 fn deref ( & self ) -> & Child {
356- & self . 0
597+ & self . child
357598 }
358599}
359600
360601impl std:: ops:: DerefMut for ServerProcess {
361602 fn deref_mut ( & mut self ) -> & mut Child {
362- & mut self . 0
603+ & mut self . child
363604 }
364605}
365606
366607impl Drop for ServerProcess {
367608 fn drop ( & mut self ) {
368- let _ = self . 0 . kill ( ) ;
369- let _ = self . 0 . wait ( ) ;
609+ let _ = self . child . kill ( ) ;
610+ let _ = self . child . wait ( ) ;
370611 }
371612}
372613
0 commit comments