Skip to content

Widen pexpect close/terminate delay to avoid free-threaded teardown race - #354

Merged
blink1073 merged 1 commit into
Calysto:mainfrom
blink1073:widen-pexpect-close-delay
Jul 28, 2026
Merged

Widen pexpect close/terminate delay to avoid free-threaded teardown race#354
blink1073 merged 1 commit into
Calysto:mainfrom
blink1073:widen-pexpect-close-delay

Conversation

@blink1073

Copy link
Copy Markdown
Contributor

References

Surfaced by oct2py's CI on the test (3.14t) matrix leg:
https://github.com/blink1073/oct2py/actions/runs/30259504522/job/89956026163

Description

On the free-threaded (no-GIL) Python 3.14t build, OctaveEngine consumers
occasionally see pexpect.exceptions.ExceptionPexpect: Could not terminate the child raised out of REPLWrapper.terminate() during teardown, even
though the Octave subprocess was in fact terminating normally.

Root cause: ptyprocess.PtyProcess.close() closes the fd, sleeps
delayafterclose (default 0.1s), then checks isalive() before raising.
Under the free-threaded build, process reaping/signal delivery appears to
be racier, so isalive() can still report True immediately after the
termination signal was sent — a false "could not terminate", not an actual
leaked process.

This kernel's own _cleanup() (registered via atexit) already tolerates
any exception from repl.terminate(), so kernel shutdown itself is
unaffected. But any caller that terminates the REPL directly — e.g.
oct2py's Oct2Py.exit()/restart(), which unregister _cleanup from
atexit and call self._engine.repl.terminate() themselves — sees the
exception propagate.

Changes

  • Widen delayafterclose/delayafterterminate on the pexpect child to
    0.5s in OctaveEngine._create_repl(), giving the kernel more headroom
    before isalive() is checked. This is the same spot that already
    removes the default delaybeforesend delay, so it benefits every
    OctaveEngine consumer rather than requiring each one to reach into
    repl.child itself.
  • Add regression tests in tests/test_engine.py::TestCreateRepl asserting
    both delays are set on the constructed REPL's child.

Backwards-incompatible changes

None. delayafterclose/delayafterterminate only affect the tail end of
close()'s wait-then-check sequence during teardown; going from 0.1s to
0.5s in the (rare) case where the process is still shutting down does not
change behavior for any caller not currently affected by this race.

Testing

  • poetry run pytest: 174 passed.
  • just typing: no issues.
  • just pre-commit: all hooks pass.

AI usage

  • Some or all of the content of this PR was generated by AI.
  • The human author has carefully reviewed this PR and run this code.
  • AI tools and models used: Claude Code

…own race

ptyprocess's close() sleeps delayafterclose (default 0.1s) then checks
isalive() before raising ExceptionPexpect("Could not terminate the
child"). Under the free-threaded (no-GIL) Python build, process reaping
can lag past that window even though the termination signal was already
sent, producing a false "could not terminate" failure during teardown
rather than an actual leaked process.

Widen delayafterclose/delayafterterminate on the pexpect child to 0.5s
in OctaveEngine._create_repl(), giving the kernel more headroom before
isalive() is checked. This benefits every OctaveEngine consumer, not
just this kernel's own _cleanup(), which already tolerates the
exception.

Observed via oct2py's CI on the 3.14t matrix leg:
https://github.com/blink1073/oct2py/actions/runs/30259504522/job/89956026163
@blink1073 blink1073 added the bug label Jul 28, 2026
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.93%. Comparing base (35cf8c0) to head (d6616d7).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #354      +/-   ##
==========================================
+ Coverage   92.90%   92.93%   +0.03%     
==========================================
  Files           6        6              
  Lines         437      439       +2     
==========================================
+ Hits          406      408       +2     
  Misses         31       31              
Flag Coverage Δ
linux 92.71% <100.00%> (+0.03%) ⬆️
windows 92.93% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
octave_kernel/kernel.py 93.94% <100.00%> (+0.03%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blink1073
blink1073 merged commit cd925fa into Calysto:main Jul 28, 2026
30 of 31 checks passed
@blink1073
blink1073 deleted the widen-pexpect-close-delay branch July 28, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant