gevent input hook #2785

Closed
ellisonbg opened this Issue Jan 14, 2013 · 26 comments

8 participants

@ellisonbg
IPython member

We would like to have integration with gevent through our inputhook. Work was begun on this, but stalled out with some design issues. Here is the original PR: #1654. This issue should be used for further discussing what needs to happen to get this feature done. When work continues, the original PR can be reopened or a new one created.

@adgaudio

As a workaround until we solve this issue, this was (basically) bfroehle's suggestion:

python -m gevent.monkey $(which ipython)

Also, there's a pretty good explanation for why "gevent.monkey.patch_thread()" must occur before "import threading" at stackoverflow:

http://stackoverflow.com/questions/8774958/keyerror-in-module-threading-after-a-successful-py-test-run#answer-12639040

@adgaudio

Following up to my previous comment, is the input hook approach appropriate for applying gevent.monkey.patch_thread()? There are three specific points I noticed:

  1. gevent doesn't support a monkey.unpatch_thread() option, which means we'd have no way of disabling the patch like we can for qt, gtk and wx, etc.
  2. gevent isn't at all a gui, but it does utilize the libevent event loop.
  3. I believe (could be wrong) the current approach won't work for any app launchers that import threading or 3rd party libraries like zmq that import threading before importing IPython
    • this would include parallel/apps/launcher.py

Maybe what we want, rather than --gui=gevent, is a --gevent-safe flag? Or perhaps a dirtier solution would be to patch the executable and do this gevent.monkey business before any ipython code gets called. In any case, I am interested in helping to solve this problem, so I'd love to revive the discussion and hear your feedback!

@minrk
IPython member

For point 1., that doesn't seem like a big problem. gevent doesn't play nice with the rest of the world, so I think it is safe to assume that "once you go green, you never go...back" in a given process.

For point 2, there's really nothing GUI about our integration with any eventloop. --gui should probably just be called --eventloop, as the GUI-ness is totally irrelevant to anything that we actually do with the flag, even when the eventloop is from a GUI toolkit.

I don't know about point 3 in general, but I do know that the tornado + zmq eventloop we use in the kernel is fully compatible with gevent, as long as you do import zmq.green as zmq instead of import zmq. I don't think there's any reason to consider gevent compatibility for code outside the InteractiveShell eventloop integration (parallel launchers run in ipcontroller, which has no reason to deal with gevent). The only reason for gevent compatibility is to be able to run user code that deals with gevent in an IPython session.

@adgaudio

Hi minrk,

Thanks for responding so quickly! So it seems the main problem remaining with daf's PR is that the gui/eventloop code (which does the gevent.monkey patching) should get executed before history is initiated. After a few hours familiarizing myself with the code, I think this means that init_gui_pylab() (which eventually calls inputhook.enable_gui) must get called before init_shell() (which eventually calls init_history) in these two apps' initialize method:

frontend.terminal.ipapp.TerminalIPythonApp.initialize
kernel.zmq.kernelapp.IPKernalApp.initialize

I'm going to try testing this out and then bring daf's code up to date with this additional change, but I have two questions about the zmq shell: Based on your previous comment that the kernel is compatible with gevent, do I need to concern myself with the order of init_history and init_gui_pylab in IPKernalApp.initialize?

Also, why do kernel.zmq.eventloops and lib.inputhooks both exist separately? Is the kernel version the new way of doing things?

Thanks again, Alex

@minrk
IPython member

They are separate because the inputhook is only active when there is a terminal REPL involved (plain old IPython). The kernel (used by the notebook, qtconsole, etc.) handles things a bit differently.

@adgaudio

I seem to have rediscovered daf's stated problem. If you were to execute this code:

   if 'threading' in sys.modules:
        raise ImportWarning('threading module loaded before gevent monkey patch')

immediately after all imports in any one of the .py files in the frontend/terminal dir and then tried to run './ipython.py', the program raises the exception. This means that monkey patching must happen before certain IPython modules get loaded (obviously), and (perhaps less obviously) the --gui=gevent flag must be parsed and must load inputhookgevent.py before it loads parts (or arguably all) of the IPython core.

At this point I'm not sure if it's worth it to proceed with this pr. As far as I understand, proceeding means we decide to add some special case in IPython loaders/code that parses the --gui flag and imports gevent before any threading imports, which is certainly a hack. Otherwise, we close the pr and tell the end-user to wrap calls to ipython using the above workaround. What do you think?

@minrk
IPython member

it sounds like there is no sensible fix that really belongs in IPython. I guess gevent is just too disruptive for it to be anything other than the very first thing you do, so gevent compatibility would have to be a separately maintained entry point that wraps IPython before IPython is imported.

@adgaudio

Thanks for your time, minrk. To recap: the significant problem holding is back is gevent.monkey must be imported before threading.

If that import order requirement suddenly did not matter anymore, we could certainly consider reviving and merging daf's work, but until then, we are stuck with a workaround.

@fperez
IPython member

Yes @adgaudio: after reading this thread, I'm certainly -1 on putting anything in IPython that depends on a third-party's idea of how early in the life of sys.modules it must enter the import chain. The main issue is that something like that is bound to be brittle, and in that case it should be maintained by those who keep an eye on that library, not by us. Special-case hacks like that are sometimes unavoidable, but then I'd say that a wrapper entry point is the right solution.

We deliberately keep the IPython top-level entry scripts short and simple precisely so that if anyone has to simply copy them to hack their startup process, they are literally only carrying one or two lines of code and it's not something likely to ever go significantly out of sync with IPython.

@fperez
IPython member

Given this, I'm closing this one as the solution seems to be the entry point already indicated above.

@fperez fperez closed this Feb 2, 2013
@takluyver
IPython member
@adgaudio

@takluyver - Thanks for your suggestion. I emailed the IPython-dev list a couple days ago but haven't received any responses. If someone were to modify the IPython launcher, it could look like below. A fancier version I guess could use argparse and also launch ipcluster, etc.

 $ cat ~/.virtualenvs/t/bin/ipython 
#!/home/alexwork/.virtualenvs/t/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'ipython==0.13.1','console_scripts','ipython'
__requires__ = 'ipython==0.13.1'
import sys
from pkg_resources import load_entry_point

import gevent.monkey ; gevent.monkey.patch_all()

sys.exit(
   load_entry_point('ipython==0.13.1', 'console_scripts', 'ipython')()
)
@takluyver
IPython member

Thanks @adgaudio. I saw your message on the mailing list, but I guess there hasn't been an immediate interest. That's probably expected - if lots of people were after ipython+gevent, the PR would have got more attention. Posting the launcher here so it's easy for other people to find is probably the best move.

@daf
daf commented Feb 8, 2013

Hi folks! @adgaudio, cheers for taking this on - I had let it languish too much. I too agree that a specialized entry script will probably be ok. However, from what I had gathered in my initial work, isn't the "gui" eventloop necessary in order to prevent ipython blocking during readline (which is, typically, the majority of usage)? I think it would need to be both, and take out the parts for being able to do --gui=gevent on the command line.

A couple more points raised in this discussion:

@minrk, we might be behind in our zmq version, but that version lacked proper device support - the heartbeat used for connecting to a remote ipython kernel had to be patched out (was a simple fix in the end). I hope there is better support in mainline zmq now, and will advocate updating our project to use it.

Also, I think there is a non-official way of "unmonkeypatching" - import the modules you know gevent touches (['os', 'time', 'thread', 'socket', 'select', 'ssl', 'httplib']), pull their references out of sys.modules to store somewhere, run the monkey patcher, then when it comes time to restore, replace entries in sys.modules (and probably a reload somewhere). Of course, this is so hacky I'm not recommending anyone DO this, just giving an option...

@minrk
IPython member

@minrk, we might be behind in our zmq version, but that version lacked proper device support - the heartbeat used for connecting to a remote ipython kernel had to be patched out (was a simple fix in the end). I hope there is better support in mainline zmq now, and will advocate updating our project to use it.

That's correct, 13.0, which will be out in the next few weeks (RC1 here) should have green versions of everything.

@bbirand

Hi all,

I'm also interested in having gevent integration with the IPython shell. I read all the discussion both here and in the mailing list, and tried out a few of the suggestions. Yet I can't get it to work. My base case is the simple example that was sent to the mailing list a while back:

# gtest.py
from gevent import spawn, wait
import time
def work():
    while True:
        time.sleep(2)
        print "Hello"

Then I have the following gipython.py file, as suggested above (with the more recent version):

#!/usr/bin/env python
# EASY-INSTALL-ENTRY-SCRIPT: 'ipython==2.1.0','console_scripts','ipython'
__requires__ = 'ipython==2.1.0'
import sys
from pkg_resources import load_entry_point

import gevent.monkey ; gevent.monkey.patch_all()

sys.exit(
   load_entry_point('ipython==2.1.0', 'console_scripts', 'ipython')()
   )

The background coroutine only runs when wait() is called:

$ ./gpython.py

In [1]: from gtest import *

In [2]: spawn(work)
Out[2]: <Greenlet at 0x103152eb0: work>

In [3]: wait()
Hello
Hello
Hello

I checked out the (very old) branch by @daf, and that works great.

I have a project that uses the actor model for which gevent integration with the shell would be great. Is there still interest in having a separate executable that would allow this? Especially under the IPython notebook? What is the recommended approach?

Thanks!

@adgaudio

Hi Berk,

If you move the monkey patch line up so it gets called before import sys, your example works as you'd expect.

__requires__ = 'ipython==2.1.0'
import gevent.monkey ; gevent.monkey.patch_all()

import sys
from pkg_resources import load_entry_point

For the notebook (assuming recent ipython notebook version), you can just monkey patch in your notebook's first cell with threading=False:

import gevent.monkey
gevent.monkey.patch_all(thread=False)

As far as I know (haven't been up to date much on this front), injecting a monkey patch before other imports is still the recommended way to use gevent + other libraries like IPython.

@bbirand

Thanks @adgaudio for the reply!
I tried moving the monkey patching line to be the first import, and ran through the exact same steps as outlined above. Yet it is not working.

My expectation is that after I run spawn(work), the "Hello" string should be printed right away, while I can still type new commands. I shouldn't have to type wait() to see that printed. Is this the right expected behavior? Is it working in that way for you?

@adgaudio

Well your expectation that "Hello" prints after 2 seconds is correct, and it works for me (see below). You could try to use gevent.sleep and see if that works. If it does, you probably aren't monkey patching correctly. Also, I'm using anaconda ipython 2.1.0 and gevent 1.0.1 - not sure if that makes any difference.

This is what appears on my screen when I run the example:

In [1]: import gtest                                                                                                                 

In [2]: g = gtest.spawn(gtest.work)

In [3]: Hello
Hello
@bbirand

I still somehow can't get this behavior. Pretty frustrating, given that there are such few lines involved!

I created the following repo, which also explains what I am typing:
https://github.com/bbirand/gipython

I still have to issue the wait() call before seeing anything printed. Any ideas why we observe different behavior?

Thanks, @adgaudio!

@bbirand

Hi @adgaudio,
Did you get a chance to give a shot to this very short repo:
https://github.com/bbirand/gipython

I tried to retrace my steps for getting gevent integration, but it's not working on me. You seemed to suggest that it was working for you, is that correct?

Thanks!

@adgaudio

@bbirand Your example does work for me, though I cannot use gipython.py to start an ipython shell because I use the anaconda distribution of python (which uses a different launcher). I'd guess that your problem is an issue with how your ipython or system packages are installed. I'd recommend trying anaconda, since it's free and they seem to solve most of these kinds of problems.

@bbirand

Thanks again @adgaudio ,but it still is not working. I installed Anaconda, and made sure that I was running the right interpreter. I added an "Anaconda version" of my gipython script to the repo (https://github.com/bbirand/gipython), but still nothing. I really have no idea why it's working for you, but not for me. I also asked a question on the ipython-dev mailing list, hoping that at least I will get some feedback..

@ml31415

Is this loader still the recommended workaround? I get the following error with 2.3.0:

#!/usr/bin/env python
# EASY-INSTALL-ENTRY-SCRIPT: 'ipython==2.1.0','console_scripts','ipython'
__requires__ = 'ipython==2.3.0'
import gevent.monkey ; gevent.monkey.patch_all()
import sys
from pkg_resources import load_entry_point

sys.exit(
   load_entry_point('ipython==2.3.0', 'console_scripts', 'ipython')()
   )
Traceback (most recent call last):
  File "gipython.py", line 9, in <module>
    load_entry_point('ipython==2.3.0', 'console_scripts', 'ipython')()
  File "/usr/lib/python2.7/dist-packages/pkg_resources/__init__.py", line 558, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/lib/python2.7/dist-packages/pkg_resources/__init__.py", line 2681, in load_entry_point
    raise ImportError("Entry point %r not found" % ((group, name),))
ImportError: Entry point ('console_scripts', 'ipython') not found
@minrk
IPython member

@ml31415 setuptools writes entrypoints that require a specific version. They have to be regenerated on every installation/update. If you want to hand-write an entrypoint, it should look something like this:

#!/usr/bin/env python
import gevent.monkey
gevent.monkey.patch_all()

import IPython
IPython.start_ipython()
@ml31415

Ah, that does the trick! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment