Skip to content

fix: probe kernel readiness without racing ipykernel initialization (ROADMAP.md #82) - #97

Merged
pablospe merged 2 commits into
mainfrom
task/b9dd2089-Kernel-readiness-probe-without
Aug 31, 2026
Merged

pablospe merged 2 commits into
mainfrom
task/b9dd2089-Kernel-readiness-probe-without

Conversation

@pablospe

@pablospe pablospe commented Aug 30, 2026 •

Copy link
Copy Markdown
Owner

What

start_session decided a kernel was ready from a single kernel_info_request
on the control channel, sent the instant kernel.json appeared. When that
request landed in a window inside ipykernel's startup it was dropped kernel-side
and never answered, so the start burned its whole remaining budget and failed
with "Kernel did not become ready within 30s". Readiness is now probed on the
shell channel, which cannot land in that window.

Why

The failure was a hard 59.5 s hang followed by a start error, on a kernel that
was in fact perfectly healthy — the mode's least recoverable failure, since the
kernel is killed on the way out and the diagnostic blames a slow machine.

How

ipykernel/kernelbase.py Kernel.start() registers the control dispatcher
(l.551) before it assigns _control_lock (l.559), and the control socket is
bound and kernel.json written long before start() runs. A control message
already queued when on_recv registers is delivered on the control thread while
the main thread is still on its way to l.559; dispatch_control opens with
async with self._control_lock (l.347) and raises TraitError on the unset
trait. The stream keeps polling — but the request is gone, and we only sent one.

The shell handler is registered at l.561, after the lock is assigned, so a
shell kernel_info_reply proves initialization finished. Nothing on the shell
dispatch path is assigned late (_main_asyncio_lock in Kernel.__init__,
ShellChannelThread.asyncio_lock in that thread's __init__).

Start here: _kernel_info_reply(kc, channel, timeout) in docx_editor/session.py
is the probe loop both helpers duplicated; _kernel_alive is now its control
wrapper and _kernel_idle is renamed _kernel_answers_shell. The one behavioural
line is the readiness check in start_session. _kernel_dead,
is_session_running, session_status's liveness check and exec_code's silence
probe deliberately keep control — they must answer for a busy kernel, which
only control does, and they run against an already-initialized kernel.

Testing

test_start_session_ready_when_control_never_answers uses a stand-in kernel that
answers kernel_info on shell only and leaves control bound but unserviced. It
discriminates: with the readiness check reverted to _kernel_alive it fails in
30.2 s with SessionError: Kernel did not become ready within 5.0s; with the
shell probe it passes in ~1 s. test_start_session_survives_control_probe_during_init
adds a real-kernel guard that fires a control probe the instant kernel.json
appears, then requires start, is_session_running and eval_code("1 + 1") to
succeed. That probe has to survive stop_channels(), which closes the socket
with linger=0: measured against a bound ROUTER, the probe arrived 2/20 times
when the socket was closed straight after send() and 20/20 when held open
until the start finished, so the guard holds it open.

tests/test_session.py serial: 90 passed on every run (173.6 s, 174.7 s, 174.4 s,
174.9 s, and 174.1 s after the last review fix), no flakes. Full suite -n 4:
1867 passed, 8 skipped, 2 xfailed (307.3 s, and 335.1 s on the final tree). ruff format --check and ruff check clean; ty check shows only the
7 pre-existing warnings, none in the touched files.

Summary by CodeRabbit

  • Bug Fixes
    • Improved session startup reliability when the control channel is slow or unresponsive.
    • Session readiness and state detection now use the shell channel for more accurate results.
    • Enhanced timeout messages to clearly identify shell-channel response issues.
    • Prevented unnecessary startup hangs caused by control-channel probing.

…ROADMAP.md #82)

start_session waited for readiness with a single kernel_info_request on the
control channel, sent the instant kernel.json appeared. ipykernel registers
the control dispatcher (kernelbase.py:551) before it assigns _control_lock
(:559), so a probe landing in that window raises TraitError kernel-side and
is dropped — and the one request we sent was then never answered, burning the
whole remaining budget (the 59.5s CI hang).

Readiness now rides the shell channel, whose handler is registered only after
_control_lock is assigned, so a shell reply proves initialization finished.
No execution can be in flight during a start, so the shell channel's
busy-serialization caveat does not apply there.

The probe loop the two helpers duplicated is extracted into
_kernel_info_reply(kc, channel, timeout); _kernel_alive stays the control
wrapper and _kernel_idle becomes _kernel_answers_shell. _kernel_dead,
is_session_running, session_status's liveness check and exec_code's silence
probe still use control: they must answer for a busy kernel, which only
control does.

Adds a stand-in kernel that answers kernel_info on shell only and leaves
control bound but unserviced, which fails in 30s against the old control
probe and passes in ~1s against the new one, plus a real-kernel guard that
fires a control probe the instant kernel.json appears.
@coderabbitai

coderabbitai Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e720ab2-81b1-4776-bf46-9e9c112b3768

📥 Commits

Reviewing files that changed from the base of the PR and between 794e943 and 8ea2d18.

📒 Files selected for processing (2)
  • docx_editor/session.py
  • tests/test_session.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Session readiness and channel probing

Layer / File(s) Summary
Kernel-info channel probes
docx_editor/session.py
The session uses a shared request-ID-matching kernel-info probe. Shell-channel replies determine readiness and state. Control-channel probes remain for liveness.
Shell-based startup validation
docx_editor/session.py, tests/test_session.py
start_session checks the shell channel and reports shell-channel timeout failures. Tests cover silent control channels, concurrent control probes, session state, and updated diagnostics.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 8ea2d

The change makes kernel startup readiness use a reply that reliably reflects initialization completion, with no actionable merge-blocking risk remaining after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant start_session
  participant shell_channel
  participant control_channel
  participant kernel
  start_session->>shell_channel: Send kernel_info_request
  shell_channel->>kernel: Deliver request
  kernel-->>shell_channel: Return kernel_info_reply
  start_session->>control_channel: Run liveness probe
  control_channel->>kernel: Deliver control request
Loading

Poem

A rabbit watched the shell reply,
While control kept watch nearby.
Startup hopped when replies came through,
Idle state knew what kernels do.
Timeout words grew clear and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: avoiding initialization races by probing kernel readiness correctly. It also identifies the related roadmap issue.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/b9dd2089-Kernel-readiness-probe-without

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.0%. Comparing base (794e943) to head (65862f0).

Additional details and impacted files
@@          Coverage Diff          @@
##            main     #97   +/-   ##
=====================================
  Coverage   97.9%   98.0%           
=====================================
  Files         23      23           
  Lines       5138    5131    -7     
  Branches     992     991    -1     
=====================================
- Hits        5035    5029    -6     
  Misses        49      49           
+ Partials      54      53    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

stop_channels() closes the client socket with linger=0, which discards
anything ZMQ has not yet handed over — and right after send() on loopback the
connect handshake is usually still in flight. Measured against a bound ROUTER
with the test's exact code shape: the probe arrived 2/20 times, so
test_start_session_survives_control_probe_during_init was mostly firing
nothing. Holding the socket open until the start finishes takes it to 20/20.

Also from review:
- the intruder now retries instead of giving up when it reads a half-written
  connection file, catches only the (OSError, ValueError) _client documents
  rather than everything, drops the exists()/stat() TOCTOU, and asserts it
  actually finished;
- start_session's comment names the ipykernel version and line numbers the
  shell-channel ordering was verified against, since pyproject sets no
  ipykernel ceiling;
- the stand-in's cleanup comment gives the real reason conftest's reaper never
  sees it (argv is `python -c`, not ipykernel_launcher);
- test_start_session_kernel_never_ready_stops_kernel's docstring names
  _kernel_answers_shell, the helper start_session now probes with.
@pablospe
pablospe merged commit 505dd80 into main Aug 31, 2026
15 checks passed
pablospe added a commit that referenced this pull request Aug 31, 2026
@pablospe
pablospe deleted the task/b9dd2089-Kernel-readiness-probe-without branch August 31, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant