Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async support #323

Merged
merged 36 commits into from
Sep 11, 2018
Merged

async support #323

merged 36 commits into from
Sep 11, 2018

Conversation

minrk
Copy link
Member

@minrk minrk commented May 24, 2018

allow handlers to return anything acceptable to gen.maybe_future

this should include tornado/asyncio coroutines and Futures

counterpart to ipython/ipython#11155

closes jupyter/notebook#3397

@minrk
Copy link
Member Author

minrk commented May 24, 2018

In action: asyncio integration works by default with asyncio already running:

async-ipython

@Carreau
Copy link
Member

Carreau commented May 30, 2018

The following restart and run all leads to a sql error when storing history

import asyncio
await asyncio.sleep(1)
1+1
ERROR! Session/line number was not unique in database. History logging moved to new session 14506

@Carreau
Copy link
Member

Carreau commented May 30, 2018

That also leads the the busy/idle indicator to be wrong.

@Carreau
Copy link
Member

Carreau commented May 30, 2018

Got also this weird things which looks like killing the kernel:

[(await get_build(i))['@type'] for i in range(first, first+10)]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
cell_name in async-def-wrapper()

TypeError: must be str, not int

@Carreau
Copy link
Member

Carreau commented May 31, 2018

I'm starting to be convinced that submitting cells should block submission of subsequent cells unless said otherwise i have the following:

# cells 1
from collections import Counter
c = Counter([(await get_build(i))['@type'] for i in range(first,last)])
# cell 2
c

Cell 1 is ran async, thus cell 2 execute immediately and raise an error c is undefined.

@axil
Copy link

axil commented Jun 12, 2018

Hi, @minrk. Trying to launch some async code in jupyter notebook.

Running latest jupyter-notebook (5.5.0) with your version of ipykernel (run-async branch), getting this:
image

What am I doing wrong?

Update:
Got it. IPython should also be customized (ipython/ipython#11155).
image

A happy beta-tester so far :)

@Carreau
Copy link
Member

Carreau commented Aug 13, 2018

@minrk, do you remember the discussion we had at scipy about execution messages being consumed while code is already running ? Could you give me some pointers on how to fix this ?

I've also pushed some compatibility with IPython 7.0.

@Carreau
Copy link
Member

Carreau commented Aug 14, 2018

I have a solution which I'm sure is wrong:

--- a/ipykernel/kernelbase.py
+++ b/ipykernel/kernelbase.py
@@ -23,6 +23,7 @@ from signal import signal, default_int_handler, SIGINT
 import zmq
 from tornado import ioloop
 from tornado import gen
+from tornado.locks import Semaphore
 from zmq.eventloop.zmqstream import ZMQStream

 from traitlets.config.configurable import SingletonConfigurable
@@ -38,6 +39,13 @@ from jupyter_client.session import Session

 from ._version import kernel_protocol_version

+from collections import defaultdict
+
+# shell handlers are now coroutine (for async await code),
+# so we may not want to consume all the message and block while it yields
+_msg_locks = defaultdict(Semaphore)
+
+
 class Kernel(SingletonConfigurable):

     #---------------------------------------------------------------------------
@@ -233,7 +241,9 @@ class Kernel(SingletonConfigurable):
             self.log.debug("%s: %s", msg_type, msg)
             self.pre_handler_hook()
             try:
-                yield gen.maybe_future(handler(stream, idents, msg))
+                lock = _msg_locks[msg_type]
+                with (yield lock.acquire()):
+                    yield gen.maybe_future(handler(stream, idents, msg))
             except Exception:
                 self.log.error("Exception in message handler:", exc_info=True)
             finally:
@@ -376,7 +386,6 @@ class Kernel(SingletonConfigurable):
     @gen.coroutine
     def execute_request(self, stream, ident, parent):

I'm going to assume the locks will of course not always be release in the order they've been taken right ?

See also the new PR #11265 on IPython side that fix and document a couple of other things.

@Carreau
Copy link
Member

Carreau commented Aug 14, 2018

One other thing I'd like to figure out when using IPykernel is how to run user code in other eventloops like trio and curio. Which so far I've managed to do this way, but block the tornado eventloop:

diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py
index 129d163..9a283cc 100644
--- a/ipykernel/ipkernel.py
+++ b/ipykernel/ipkernel.py
@@ -214,7 +214,16 @@ class IPythonKernel(KernelBase):
             def run_cell(*args, **kwargs):
                 return shell.run_cell(*args, **kwargs)
         try:
-            res = yield run_cell(code, store_history=store_history, silent=silent)
+            # TODO: here we need to hook into the right event loop to run user
+            # code using trio/curio.
+            from IPython.core.interactiveshell import _asyncio_runner
+            if shell.loop_runner is _asyncio_runner:
+                res = yield run_cell(code, store_history=store_history, silent=silent)
+            else:
+                @gen.coroutine
+                def run_cell(*args, **kwargs):
+                    return shell.run_cell(*args, **kwargs)
+                res = yield run_cell(code, store_history=store_history, silent=silent)
         finally:
             self._restore_input()

I've heard that trio has some asyncio integration support, so it would be nice to have better integration.

@minrk
Copy link
Member Author

minrk commented Aug 16, 2018

do you remember the discussion we had at scipy about execution messages being consumed while code is already running ?

Yes, it was to switch the message handler triggers from on_recv callbacks to while true coroutines:

while True:
    msg = await socket.recv_multipart()
    await self.dispatch...

This would require pyzmq >= 16 for asyncio support in .recv_multipart.

Alternately, a smaller change could be to keep the on_recv triggers, but hand off to a coroutine, so that message processing is kept in order:

async def on_recv(msg):
    await self.msg_queue.put(msg)

async def process_queue():
    while True:
        msg = await self.msg_queue.get()
        await self.dispatch(msg)

This would have the slight possible downside that messages would be popped off the zmq queue into Python before they are handled. Not sure if that's a real issue, though. This pattern would give us the option to run the message handlers in an IO thread, since handoff could be threadsafe. This might be valuable if we want to switch runners.

One other thing I'd like to figure out when using IPykernel is how to run user code in other eventloops like trio and curio.

I think we ought to be able to use the existing eventloop integration for qt, etc. to do this. That's where I would start.

@Carreau
Copy link
Member

Carreau commented Aug 28, 2018

I've updated this to work with the latest iteration of ipython/ipython#11265

@minrk
Copy link
Member Author

minrk commented Sep 4, 2018

@Carreau I've re-added the queue and fixed some handling of ExecutionResult objects, and now it seems to work with my testing for asyncio and trio, at least.

@Carreau
Copy link
Member

Carreau commented Sep 4, 2018 via email

@Carreau
Copy link
Member

Carreau commented Sep 5, 2018

Thanks I had a chance to look at the code. Should the aborts also flush the newly created queue instead of just looking at ZMQ ? I suppose that's what the current failure are from.

@minrk
Copy link
Member Author

minrk commented Sep 5, 2018

ok, I think the last hurdle is dealing with eventloop integration and the message queue. Now that handling messages is async, we cannot allow the eventloop integration to work the way it does, witch pausing the main eventloop and pumping kernel.do_one_iteration(). Instead, we need eventloop integration to work the other way - tornado is unconditionally and always running, and we pump the app eventloop while tornado is idle. That should eliminate the flushing issues.

@minrk
Copy link
Member Author

minrk commented Sep 7, 2018

This now relies on ipython/ipython#11289

with async code, KeyboardInterrupt is raised in the main eventloop by default,
which would halt the kernel.

While running async code, register a special sigint handler that cancels the relevant task
instead of halting the eventloop.
@minrk minrk requested a review from Carreau September 7, 2018 13:57
yaml doesn't like 4 spaces
which has different asyncio integration (specifically, not on asyncio by default)
it's only relevant on tornado 5 where asyncio is already running
not CancelledError, which is only for the true async runs
needed to get a later version
@pytest.mark.parametrize("asynclib", ["asyncio", "trio", "curio"])
@skip_without_async
def test_async_interrupt(asynclib, request):
asynclib = "asyncio"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: remove I guess ?

@Carreau
Copy link
Member

Carreau commented Sep 8, 2018

The test failure seem to appear at the time of @takluyver's input transformer pr, more especially
( ipython/ipython@f55a4a5 ; ipython/ipython#11041)

A bit tricky to bisect as one need to switch PTK version.

It may be a miss-test, as I'm not sure why range? would send a set_next_input.

Payload on 6.0.0, where the set_next_input is wrong anyway as it sets only one line:

{
  "status": "ok",
  "execution_count": 1,
  "user_expressions": {},
  "payload": [
    {
      "source": "set_next_input",
      "text": "   x=range",
      "replace": false
    },
    {
      "source": "page",
      "data": {
        "text/plain": "\u001b[0;31mInit signature:...           type\n"
      },
      "start": 0
    }
  ]
}

Payload on master:

{
  "status": "ok",
  "execution_count": 1,
  "user_expressions": {},
  "payload": [
    {
      "source": "page",
      "data": {
        "text/plain": "\u001b[0;31mInit signature:...           type\n"
      },
      "start": 0
    }
  ]
}

@Carreau
Copy link
Member

Carreau commented Sep 8, 2018

Test was added pre big split, currently at commit 71fca97 (used to be ipython/ipython@e9db6d5), in PR ipython/ipython#4363, see issue ipython/ipython#1349 for background of why this was done, and why set_next_input was send originally.

@Carreau
Copy link
Member

Carreau commented Sep 8, 2018

Also I've started to draft some Blog post (https://medium.com/@mbussonn/ipython-7-0-async-repl-a35ce050f7f7) still really early, and many parts to re-write, in particular if we want to make IPython/ipykernel release at the same time.

@Carreau
Copy link
Member

Carreau commented Sep 9, 2018

The change in behavior of ? is very much on purpose if you look at the code.
That is to say, the set_next_input is called only iif the ? is alone on its line.

I'm not sure if this really make sens (@takluyver ?), I'm tempted to say that the set_next_input makes lots of sens, especially in multiline....

Test of set_next input were testing not only that several set_next_input
were not set several time; but was doing so via the side effect that
`?` was triggering set_next_input.

Use directly ip.set_next_input to not rely on the behavior or `?` which
anyway is buggy
@Carreau
Copy link
Member

Carreau commented Sep 9, 2018

I'm not sure if this really make sens (@takluyver ?), I'm tempted to say that the set_next_input makes lots of sens, especially in multiline....

Meh, it's broken also on 6.4 and before in a different way, and only insert the current line, so InputTransformer2 behavior suits me. I've push a commit that fixes the test (test the actual behavior instead of testing via the fact that ? use the behavior)

@Carreau
Copy link
Member

Carreau commented Sep 9, 2018

Test are now passing !

@minrk
Copy link
Member Author

minrk commented Sep 10, 2018

Change in set_next_input multi-call behavior seems fine to me, I'm happy with the updated test. Shall we land this and get ready to make prereleases?

@Carreau
Copy link
Member

Carreau commented Sep 10, 2018 via email

@minrk minrk merged commit e32d7f9 into ipython:master Sep 11, 2018
@minrk
Copy link
Member Author

minrk commented Sep 11, 2018

OK! Landing this one, and we can make 5.0 beta to go with the IPython 7.0 beta

@minrk minrk deleted the run-async branch September 11, 2018 15:49
@Carreau
Copy link
Member

Carreau commented Sep 11, 2018

Side, dumb question. Should we leapfrog 5.0 and 6.0 and do a 7.0 to sync the version number of IPython and IPykernel ?

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.

Can't invoke asyncio event_loop after tornado 5.0 update
5 participants