@@ -929,52 +929,60 @@ extension FileDescriptor {
929929extension SubprocessUnixTests {
930930 #if SubprocessFoundation
931931 @Test ( . requiresBash) func testConcurrentRun( ) async throws {
932- // Launch as many processes as we can
933- // Figure out the max open file limit
934- let limitResult = try await Subprocess . run (
935- . path( " /bin/sh " ) ,
936- arguments: [ " -c " , " ulimit -n " ] ,
937- output: . string( limit: 32 )
938- )
939- guard
940- let limitString = limitResult
941- . standardOutput?
942- . trimmingCharacters ( in: . whitespacesAndNewlines) ,
943- let ulimit = Int ( limitString)
944- else {
945- Issue . record ( " Failed to run ulimit -n " )
946- return
947- }
948- // Constrain to an ultimate upper limit of 4096, since Docker containers can have limits like 2^20 which is a bit too high for this test.
949- // Common defaults are 2560 for macOS and 1024 for Linux.
950- let limit = min ( ulimit, 4096 )
951- // Since we open two pipes per `run`, launch
952- // limit / 4 subprocesses should reveal any
953- // file descriptor leaks
954- let maxConcurrent = limit / 4
932+ // Read the soft fd limit via a C shim: RLIMIT_NOFILE's Swift type
933+ // varies across platforms and Swift versions, so calling getrlimit
934+ // directly from Swift is not reliably portable.
935+ // Cap at 4096: Docker containers can report limits like 2^20.
936+ let softLimit = Int ( min ( _subprocess_nofile_soft_limit ( ) , UInt64 ( 4096 ) ) )
937+
938+ // On Linux, account for any fds already open (e.g. from prior tests in
939+ // the same suite) to avoid hitting EMFILE during the concurrent spawn
940+ // burst. /proc/self/fd lists every open descriptor; subtracting the
941+ // current count plus a small margin gives the true available headroom.
942+ #if os(Linux) || os(Android)
943+ let currentFds = ( try ? FileManager . default. contentsOfDirectory ( atPath: " /proc/self/fd " ) ) ? . count ?? 50
944+ let available = max ( 32 , softLimit - currentFds - 50 )
945+ #else
946+ let available = softLimit
947+ #endif
948+ // Each concurrent spawn holds both ends of the stdout and stderr pipes
949+ // plus a temporary exec-error notification pipe while the child's
950+ // exec() completes — roughly 6–8 fds per in-flight spawn. Divide by
951+ // 8 to leave headroom and avoid EMFILE under high concurrency.
952+ let maxConcurrent = available / 8
955953 try await withThrowingTaskGroup ( of: Void . self) { group in
956954 var running = 0
957955 let byteCount = 1000
958956 for _ in 0 ..< maxConcurrent {
959957 group. addTask {
960- // This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
961- let r = try await Subprocess . run (
962- . name( " bash " ) ,
963- arguments: [
964- " -sc " , #"echo "$1" && echo "$1" >&2"# , " -- " , String ( repeating: " X " , count: byteCount) ,
965- ] ,
966- output: . data( limit: . max) ,
967- error: . data( limit: . max)
968- )
969- guard r. terminationStatus. isSuccess else {
970- Issue . record ( " Unexpected exit \( r. terminationStatus) from \( r. processIdentifier) " )
971- return
958+ // Catch errors so a single spawn/monitor failure doesn't
959+ // cascade-cancel sibling tasks (which would SIGKILL their
960+ // live subprocesses and flood the log with false failures).
961+ do {
962+ // This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
963+ let r = try await Subprocess . run (
964+ . name( " bash " ) ,
965+ arguments: [
966+ " -sc " , #"echo "$1" && echo "$1" >&2"# , " -- " , String ( repeating: " X " , count: byteCount) ,
967+ ] ,
968+ output: . data( limit: . max) ,
969+ error: . data( limit: . max)
970+ )
971+ guard r. terminationStatus. isSuccess else {
972+ Issue . record ( " Unexpected exit \( r. terminationStatus) from \( r. processIdentifier) " )
973+ return
974+ }
975+ #expect( r. standardOutput. count == byteCount + 1 , " \( r. standardOutput) " )
976+ #expect( r. standardError. count == byteCount + 1 , " \( r. standardError) " )
977+ } catch {
978+ Issue . record ( " Subprocess.run threw: \( error) " )
972979 }
973- #expect( r. standardOutput. count == byteCount + 1 , " \( r. standardOutput) " )
974- #expect( r. standardError. count == byteCount + 1 , " \( r. standardError) " )
975980 }
976981 running += 1
977- if running >= maxConcurrent / 4 {
982+ // Throttle to maxConcurrent/8 live subprocesses at a time
983+ // (rather than /4) to reduce peak memory pressure on
984+ // memory-constrained kernel-testing VMs (e.g. QEMU + 5.10).
985+ if running >= maxConcurrent / 8 {
978986 try await group. next ( )
979987 }
980988 }
0 commit comments