Skip to content

Commit c1d944e

Browse files
ianthomas23davidbrochartblink1073
authored
Remove control queue (#1210)
Co-authored-by: David Brochart <[email protected]> Co-authored-by: Steven Silvester <[email protected]>
1 parent 56a6372 commit c1d944e

File tree

5 files changed

+61
-60
lines changed

5 files changed

+61
-60
lines changed

ipykernel/inprocess/ipkernel.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,6 @@ def start(self):
8888
def _abort_queues(self):
8989
"""The in-process kernel doesn't abort requests."""
9090

91-
async def _flush_control_queue(self):
92-
"""No need to flush control queues for in-process"""
93-
9491
def _input_request(self, prompt, ident, parent, password=False):
9592
# Flush output before making the request.
9693
self.raw_input_str = None

ipykernel/kernelbase.py

Lines changed: 16 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from __future__ import annotations
66

77
import asyncio
8-
import concurrent.futures
98
import inspect
109
import itertools
1110
import logging
@@ -289,49 +288,16 @@ def __init__(self, **kwargs):
289288
for msg_type in self.control_msg_types:
290289
self.control_handlers[msg_type] = getattr(self, msg_type)
291290

292-
self.control_queue: Queue[t.Any] = Queue()
293-
294291
# Storing the accepted parameters for do_execute, used in execute_request
295292
self._do_exec_accepted_params = _accepts_parameters(
296293
self.do_execute, ["cell_meta", "cell_id"]
297294
)
298295

299-
def dispatch_control(self, msg):
300-
self.control_queue.put_nowait(msg)
301-
302-
async def poll_control_queue(self):
303-
while True:
304-
msg = await self.control_queue.get()
305-
# handle tracers from _flush_control_queue
306-
if isinstance(msg, (concurrent.futures.Future, asyncio.Future)):
307-
msg.set_result(None)
308-
continue
296+
async def dispatch_control(self, msg):
297+
# Ensure only one control message is processed at a time
298+
async with asyncio.Lock():
309299
await self.process_control(msg)
310300

311-
async def _flush_control_queue(self):
312-
"""Flush the control queue, wait for processing of any pending messages"""
313-
tracer_future: concurrent.futures.Future[object] | asyncio.Future[object]
314-
if self.control_thread:
315-
control_loop = self.control_thread.io_loop
316-
# concurrent.futures.Futures are threadsafe
317-
# and can be used to await across threads
318-
tracer_future = concurrent.futures.Future()
319-
awaitable_future = asyncio.wrap_future(tracer_future)
320-
else:
321-
control_loop = self.io_loop
322-
tracer_future = awaitable_future = asyncio.Future()
323-
324-
def _flush():
325-
# control_stream.flush puts messages on the queue
326-
if self.control_stream:
327-
self.control_stream.flush()
328-
# put Future on the queue after all of those,
329-
# so we can wait for all queued messages to be processed
330-
self.control_queue.put(tracer_future)
331-
332-
control_loop.add_callback(_flush)
333-
return awaitable_future
334-
335301
async def process_control(self, msg):
336302
"""dispatch control requests"""
337303
if not self.session:
@@ -387,8 +353,6 @@ async def dispatch_shell(self, msg):
387353
"""dispatch shell requests"""
388354
if not self.session:
389355
return
390-
# flush control queue before handling shell requests
391-
await self._flush_control_queue()
392356

393357
idents, msg = self.session.feed_identities(msg, copy=False)
394358
try:
@@ -531,6 +495,19 @@ async def process_one(self, wait=True):
531495
t, dispatch, args = self.msg_queue.get_nowait()
532496
except (asyncio.QueueEmpty, QueueEmpty):
533497
return
498+
499+
if self.control_thread is None and self.control_stream is not None:
500+
# If there isn't a separate control thread then this main thread handles both shell
501+
# and control messages. Before processing a shell message we need to flush all control
502+
# messages and allow them all to be processed.
503+
await asyncio.sleep(0)
504+
self.control_stream.flush()
505+
506+
socket = self.control_stream.socket
507+
while socket.poll(1):
508+
await asyncio.sleep(0)
509+
self.control_stream.flush()
510+
534511
await dispatch(*args)
535512

536513
async def dispatch_queue(self):
@@ -578,9 +555,6 @@ def start(self):
578555
if self.control_stream:
579556
self.control_stream.on_recv(self.dispatch_control, copy=False)
580557

581-
control_loop = self.control_thread.io_loop if self.control_thread else self.io_loop
582-
583-
asyncio.run_coroutine_threadsafe(self.poll_control_queue(), control_loop.asyncio_loop)
584558
if self.shell_stream:
585559
self.shell_stream.on_recv(
586560
partial(

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ dependencies = [
2424
"ipython>=7.23.1",
2525
"comm>=0.1.1",
2626
"traitlets>=5.4.0",
27-
"jupyter_client>=6.1.12",
27+
"jupyter_client>=8.0.0",
2828
"jupyter_core>=4.12,!=5.0.*",
2929
# For tk event loop support only.
3030
"nest_asyncio",
31-
"tornado>=6.1",
31+
"tornado>=6.2",
3232
"matplotlib-inline>=0.1",
3333
'appnope;platform_system=="Darwin"',
34-
"pyzmq>=24",
34+
"pyzmq>=25",
3535
"psutil",
3636
"packaging",
3737
]

tests/test_ipkernel_direct.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,41 +164,29 @@ def test_dispatch_debugpy(ipkernel: IPythonKernel) -> None:
164164

165165
async def test_start(ipkernel: IPythonKernel) -> None:
166166
shell_future: asyncio.Future = asyncio.Future()
167-
control_future: asyncio.Future = asyncio.Future()
168167

169168
async def fake_dispatch_queue():
170169
shell_future.set_result(None)
171170

172-
async def fake_poll_control_queue():
173-
control_future.set_result(None)
174-
175171
ipkernel.dispatch_queue = fake_dispatch_queue # type:ignore
176-
ipkernel.poll_control_queue = fake_poll_control_queue # type:ignore
177172
ipkernel.start()
178173
ipkernel.debugpy_stream = None
179174
ipkernel.start()
180175
await ipkernel.process_one(False)
181176
await shell_future
182-
await control_future
183177

184178

185179
async def test_start_no_debugpy(ipkernel: IPythonKernel) -> None:
186180
shell_future: asyncio.Future = asyncio.Future()
187-
control_future: asyncio.Future = asyncio.Future()
188181

189182
async def fake_dispatch_queue():
190183
shell_future.set_result(None)
191184

192-
async def fake_poll_control_queue():
193-
control_future.set_result(None)
194-
195185
ipkernel.dispatch_queue = fake_dispatch_queue # type:ignore
196-
ipkernel.poll_control_queue = fake_poll_control_queue # type:ignore
197186
ipkernel.debugpy_stream = None
198187
ipkernel.start()
199188

200189
await shell_future
201-
await control_future
202190

203191

204192
def test_create_comm():

tests/test_kernel.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import subprocess
1111
import sys
1212
import time
13+
from datetime import datetime, timedelta
1314
from subprocess import Popen
1415
from tempfile import TemporaryDirectory
1516

@@ -597,6 +598,47 @@ def test_control_thread_priority():
597598
assert control_dates[-1] <= shell_dates[0]
598599

599600

601+
def test_sequential_control_messages():
602+
with new_kernel() as kc:
603+
msg_id = kc.execute("import time")
604+
get_reply(kc, msg_id)
605+
606+
# Send multiple messages on the control channel.
607+
# Using execute messages to vary duration.
608+
sleeps = [0.6, 0.3, 0.1]
609+
610+
# Prepare messages
611+
msgs = [
612+
kc.session.msg("execute_request", {"code": f"time.sleep({sleep})"}) for sleep in sleeps
613+
]
614+
msg_ids = [msg["header"]["msg_id"] for msg in msgs]
615+
616+
# Submit messages
617+
for msg in msgs:
618+
kc.control_channel.send(msg)
619+
620+
# Get replies
621+
replies = [get_reply(kc, msg_id, channel="control") for msg_id in msg_ids]
622+
623+
# Check messages are processed in order, one at a time, and of a sensible duration.
624+
previous_end = None
625+
for reply, sleep in zip(replies, sleeps):
626+
start_str = reply["metadata"]["started"]
627+
if sys.version_info[:2] < (3, 11) and start_str.endswith("Z"):
628+
# Python < 3.11 doesn't support "Z" suffix in datetime.fromisoformat,
629+
# so use alternative timezone format.
630+
# https://github.com/python/cpython/issues/80010
631+
start_str = start_str[:-1] + "+00:00"
632+
start = datetime.fromisoformat(start_str)
633+
end = reply["header"]["date"] # Already a datetime
634+
635+
if previous_end is not None:
636+
assert start > previous_end
637+
previous_end = end
638+
639+
assert end >= start + timedelta(seconds=sleep)
640+
641+
600642
def _child():
601643
print("in child", os.getpid())
602644

0 commit comments

Comments
 (0)