You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cloudxr: possible exec race in background.start_and_wait() (needs owner triage) #1378
While fixing a flaky test in #1376, we found that start_and_wait() might have the same race. We have not reproduced it and are not sure it is a real problem. Raising it so you can decide.
The race in the test
Popen() can return while the child is still in exec. Until exec finishes, /proc/<pid>/cmdline reads empty. Under parallel load on a 48-core x86_64 host, a bare Popen() followed by an immediate cmdline read came back empty about 11% of the time. This is what made test_reads_real_cmdline_without_a_trailing_empty flaky; #1376 fixes the tests only.
Where the product code might hit the same window
In src/python/isaaccapture/cloudxr/background.py:
spawn() calls Popen(), writes the pid file and returns at once.
On the first pass of its loop, start_and_wait() calls is_runtime_live(), which is still False, then read_pid(run_dir).
read_pid() → _is_our_service() reads /proc/<pid>/cmdline. If it is still empty, the child is not recognised and read_pid() returns None.
start_and_wait() then raises "The CloudXR service exited during startup", even though the detached service may still be starting and keep running.
The gap from Popen() returning to the cmdline read looks about as short as in the flaky test. This is from reading the code only; we have not seen it happen in service start.
Open questions
Is this a real problem in practice, and is it worth fixing?
If so, one option is to keep the Popen object and detect exit with poll() instead of inferring it from the cmdline. _is_our_service()'s whole-argument match would stay as it is.
Setup:main at a74babb, 48-core x86_64, Linux 7.0, Python 3.12. The real start_and_wait(), spawn(), read_pid() and is_runtime_live(). The service is replaced by a script that sleeps 30 s, since the real one needs a GPU.
read_pid() on the first loop pass after spawn(), for a child that is alive the whole time:
Host
Missed
No other load (8 parallel drivers)
716 of 316,033 calls (0.23%)
stress-ng --cpu 96 --fork 16
545 of 37,388 calls (1.5%)
End to end, with only is_runtime_live() stubbed to report the runtime up on the second pass, the unmodified start_and_wait() raised The CloudXR service exited during startup. See …/service.log 3 times in 1,654 calls with no other load. Under the same stress-ng load, all 4 workers raised it within their first 15 to 54 calls. Every time, the child was still running 0.5 s later, and read_pid() then returned its pid.
What a user sees:
service start exits non-zero saying the service exited, while the detached service it just started keeps running and goes on to hold the GPU.
CloudXRLauncher catches only AlreadyServingError around start_and_wait(), so the RuntimeError propagates and the _STARTED_SERVICE message with the pid and log path is never printed, though the service keeps running.
Fix direction: the poll() option above holds up: keep the Popen from spawn() and treat proc.poll() is not None as exited. Do not make an empty cmdline mean "still starting": a zombie's cmdline is empty too, and terminate() and test_sigterm_and_clears_the_pid_file rely on empty meaning gone. TestStartRaces mocks spawn(), so nothing covers this path today; the fix wants a test with a real spawn.
Reproduction script
Run with a built isaaccapture on PYTHONPATH; several in parallel make the miss rate visible sooner.
"""Count start_and_wait() first-pass read_pid() misses on a live child.Run with a built isaaccapture on PYTHONPATH: python repro_1378.py [seconds]"""importosimportshutilimportsignalimportsysimporttempfileimporttimefrompathlibimportPathfromunittest.mockimportpatchfromisaaccapture.cloudxrimportbackgroundtmp=Path(tempfile.mkdtemp())
(tmp/"stand_in.py").write_text("import time\ntime.sleep(30)\n")
os.environ["PYTHONPATH"] =f"{tmp}{os.pathsep}{os.environ.get('PYTHONPATH', '')}"background._MODULE="stand_in"# the real service needs a GPUrun_dir, logs_dir=str(tmp/"run"), tmp/"logs"real_read_pid=background.read_pidclassFirstPass(Exception):
passdeffirst_read_pid(rd): # record the first post-spawn result, then leave the loopraiseFirstPass(real_read_pid(rd))
calls=misses=0deadline=time.monotonic() +float(sys.argv[1] iflen(sys.argv) >1else30)
try:
withpatch.object(background, "read_pid", first_read_pid):
whiletime.monotonic() <deadline:
try:
background.start_and_wait([], run_dir, logs_dir)
exceptFirstPassasseen:
misses+=seen.args[0] isNonepid=int(background.pid_path(run_dir).read_text())
os.kill(pid, signal.SIGKILL)
os.waitpid(pid, 0)
calls+=1finally:
shutil.rmtree(tmp)
print(f"read_pid() missed a live child on {misses} of {calls} start_and_wait() calls")
Activity
ivany-nv commentedon Oct 28, 2026
Reproduced when PR review 1376:
Setup:
mainat a74babb, 48-core x86_64, Linux 7.0, Python 3.12. The realstart_and_wait(),spawn(),read_pid()andis_runtime_live(). The service is replaced by a script that sleeps 30 s, since the real one needs a GPU.read_pid()on the first loop pass afterspawn(), for a child that is alive the whole time:stress-ng --cpu 96 --fork 16End to end, with only
is_runtime_live()stubbed to report the runtime up on the second pass, the unmodifiedstart_and_wait()raisedThe CloudXR service exited during startup. See …/service.log3 times in 1,654 calls with no other load. Under the samestress-ngload, all 4 workers raised it within their first 15 to 54 calls. Every time, the child was still running 0.5 s later, andread_pid()then returned its pid.What a user sees:
service startexits non-zero saying the service exited, while the detached service it just started keeps running and goes on to hold the GPU.CloudXRLaunchercatches onlyAlreadyServingErroraroundstart_and_wait(), so theRuntimeErrorpropagates and the_STARTED_SERVICEmessage with the pid and log path is never printed, though the service keeps running.Fix direction: the
poll()option above holds up: keep thePopenfromspawn()and treatproc.poll() is not Noneas exited. Do not make an empty cmdline mean "still starting": a zombie's cmdline is empty too, andterminate()andtest_sigterm_and_clears_the_pid_filerely on empty meaning gone.TestStartRacesmocksspawn(), so nothing covers this path today; the fix wants a test with a real spawn.Reproduction script
Run with a built
isaaccaptureonPYTHONPATH; several in parallel make the miss rate visible sooner.