Skip to content

Commit 8f0b2b3

Browse files
krassowskiCopilot
andauthored
Prevent reconnect stalls from orphaned kernel_info requests (#1632)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 12ae156 commit 8f0b2b3

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

jupyter_server/services/kernels/connection/channels.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,18 @@ def disconnect(self):
461461
# start_buffering early-return below, otherwise the channel leaks.
462462
if self.kernel_info_channel is not None and not self.kernel_info_channel.closed():
463463
self.kernel_info_channel.close()
464+
# If this connection owned the shared kernel_info future and we
465+
# are closing its channel before a reply arrives, unblock any
466+
# reconnect path waiting on that pending future.
467+
if not self._kernel_info_future.done():
468+
self._kernel_info_future.set_result({})
469+
# Allow a future connection to issue a fresh kernel_info request
470+
# rather than inheriting an orphaned/empty cached future.
471+
if (
472+
getattr(self.kernel_manager, "_kernel_info_future", None)
473+
is self._kernel_info_future
474+
):
475+
del self.kernel_manager._kernel_info_future
464476
self.kernel_info_channel = None
465477

466478
if self.kernel_id in self.multi_kernel_manager:

tests/services/kernels/test_connection.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,40 @@ async def test_no_fd_leak_on_disconnect_with_orphaned_kernel_info_channel(
222222
f"FD leak detected: {final_fds - baseline_fds} FDs leaked after 100 "
223223
f"disconnects with orphaned kernel_info_channel"
224224
)
225+
226+
227+
@pytest.mark.skipif(sys.platform == "win32", reason="Times out on Windows")
228+
async def test_disconnect_resolves_orphaned_kernel_info_future(jp_serverapp: ServerApp) -> None:
229+
"""Disconnecting with an orphaned kernel_info channel should not leave
230+
kernel_manager._kernel_info_future pending, which would block reconnects
231+
waiting for a never-arriving kernel_info reply."""
232+
app = jp_serverapp
233+
km = app.kernel_manager
234+
kernel_id = await km.start_kernel()
235+
kernel = km.get_kernel(kernel_id)
236+
237+
conn1 = _make_connection(app, kernel, timeout=5.0)
238+
conn1.create_stream()
239+
conn1.session.key = kernel.session.key
240+
conn1.session_key = f"{kernel_id}:{conn1.session.session}"
241+
242+
# Simulate an in-flight kernel_info request that never gets a reply.
243+
conn1.kernel_info_channel = km.connect_shell(kernel_id)
244+
km._kernel_info_future = conn1._kernel_info_future
245+
246+
# Force the buffering early-return path in disconnect().
247+
# disconnect() first decrements the connection count via
248+
# notify_disconnect(), so start at 1 to reach the == 0 branch.
249+
km._kernel_connections[kernel_id] = 1
250+
ZMQChannelsWebsocketConnection._open_sockets.add(conn1)
251+
conn1.disconnect()
252+
253+
# Regression check: pending shared future must be resolved by disconnect.
254+
assert conn1._kernel_info_future.done()
255+
256+
# A new connection should not block waiting on the stale pending future.
257+
conn2 = _make_connection(app, kernel, timeout=0.2)
258+
conn2.create_stream()
259+
conn2.session.key = kernel.session.key
260+
conn2.kernel_info_timeout = 0.2
261+
await asyncio.wait_for(asyncio.wrap_future(conn2.request_kernel_info()), timeout=1.0)

0 commit comments

Comments
 (0)