Reload jupyter serverextension

Hi guys, I’m working on a jupyter server extension. My in developing extension is installed by pip install -e . and I noticed that everytime I change my code, I have to restart my jupyter server. It’s ok if i’m using a simple jupyter notebook server, but it’s very painful when using jupyter lab --watch since it takes a very long time to restart.

So if there is a way to gracefully reload my server extension?

Tornado has some support for this (when running in its debug mode), but I’m not sure if this is exposed on the jupyter app, or how reliable it is for notebook extensions. Try to look in the tornado docs, and see if you are able to turn it on in your dev env.

1 Like

@telamonian recently did some work on exposing tornado hot reloading to the notebook server. I’m not sure of the current state of it, but see Autoreload changes to a server extension in Jupyterlab and https://github.com/jupyter/notebook/pull/4795 .

2 Likes

Many thanks, I will try this PR branch to see if it’s ok. :slight_smile:

The autoreloading PR works (if you pass the --autoreload flag). The current state is that it will restart the entire Notebook/Jlab server when any changes to the Python code are seen. So in the right situation it can be more handy than restarting the server yourself, but there are probably ways to refine it more.

For a different approach, requiring no restarting (or PRs), launch a second notebook server from inside an IPython kernel. Why should end users get to have all the fun with interactive computing?

Here’s the full notebook, but the basic idea is:

from notebook.notebookapp import NotebookApp
app = NotebookApp()
app.initialize(["--port", "9999", "--NotebookApp.token=<probably still need a strong token>"])

http://localhost:9999 will now host a second server, with full interactive access to the internals. Want a new route? app.webapp.add_handler(...). Change the file root? app.contents_manager.root_dir = .... All kinds of mischief are available.

5 Likes

Could you explain more about why and how the ipython can do this on the fly? Really want to know.

I do not know the details about ipython but, for what I know, the app is already listened before you add the handler. How does the app instance know there is a new handler added to it?

Sure.

For some years now, Jupyter has been using an event loop as the execution model for all Jupyter components: usually tornado for the web (as opposed to e.g. qt for the desktop). In this case, as both ipykernel and notebook are tornado apps, everything’s fine.

how the ipython can do this on the fly?

What happens at the tail end of the initialize is a call to listen, which schedules tornado to route requests whenever it is contacted on its assigned OS port, but otherwise not do much.

Both applications run in the same OS thread, politely scheduling their next activity after the next expensive thing (like waiting for a user to make a request, reading a file, querying a database).

How does the app instance know there is a new handler added to it?

By interacting directly with the live NotebookApp object, we can change what the next routed response will do. The downside is that one “bad read” can deadlock everything. In the above example, you really don’t want to make a blocking call (e.g. requests.get) to localhost:9999… python will be waiting on itself for a response that will never come.

This meant we could run other tornado apps, but that was pretty much it. The present is much more exciting. Once Python 3.6 rolled around, tornado transparently dropped its “custom” loop and use the built-in asyncio loop. Now a great many loop-agnostic (or at least, aware) applications can be used together: aiohttp, fastapi, ariadne, tartiflette, home-assistant … the list will grow.

Most incumbent Python web environments, (e.g. pylons, flask, django) won’t be able to be used this way for a time, if ever. Historically, they did not use an event loop… django is starting, though it’s going to take a long time to get the database story squared, as the unit-of-work pattern of the Django ORM (or most any ORM) is not generally not compatible with asynchronous execution, without some pretty dark hackery (e.g. gevent).

There is a bit of a dark shadow, though, at least for windows users: zmq and asyncio loops aren’t going to play very nicely together anytime soon, so a lot of cool features just aren’t going to work there.

1 Like

From my understanding, since the ioloop is already started, we can created another app without calling ioloop.start(), so there is no hanging, we can use that app instance and do things on the fly. Am I right about this? This seems like great approch to achive something like hot reloading for me.

Yes, your understanding is correct: by the time your client sends the execute_request through your notebook sever (on process A, running on a tornado/zmq loop), to your ipykernel (on process B, running on another tornado/zmq loop), you are already well loop-ed, and don’t need to do any particular work.

Unfortunately, the loop is not magic. Most top-level applications expect to be running on the main loop, all by themselves, and certain things can only occur there (like starting new asyncio.create_subprocess_execs, which won’t work right now with windows notebook or ipykernel, to my knowledge). So like I said, it’s best if apps are loop-agnostic (will run in asyncio, trio, curio… and on main or sub loop), but it’s not even possible if they’re not loop-aware.