nbclient icon indicating copy to clipboard operation
nbclient copied to clipboard

when cell contains ipywidget , execute cell will sometimes blocks

Open hjliu0206 opened this issue 1 year ago • 1 comments

package version

nbformat                  5.10.4
nbclient                     0.10.0

here is the code which can reproduce the bug

import nbformat
from nbclient import NotebookClient
def execute_ipynb(ipynb_file):
    with open(ipynb_file, 'r', encoding='utf-8') as f:
        nb_node = nbformat.read(f, as_version=4)
    client = NotebookClient(nb_node)

    try:
        with client.setup_kernel():
            info_msg = client.wait_for_reply(client.kc.kernel_info())
            for index, cell in enumerate(nb_node.cells):
                client.execute_cell(cell, index)
    except Exception as e:
        print("An error occurred during notebook execution:{}", e)
ipynb_file = 'test.ipynb'
execute_ipynb(ipynb_file)

content of test.ipynb

{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "655816ee-ac2e-44d0-8b1f-4d2686ef0942",
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as widgets\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import IPython\n",
    "\n",
    "%matplotlib widget\n",
    "\n",
    "sr = 44100\n",
    "duration = 1.0\n",
    "\n",
    "with plt.ioff():\n",
    "    fig = plt.figure(figsize=(14, 7))\n",
    "\n",
    "@widgets.interact(\n",
    "    freq=widgets.FloatLogSlider(base=np.e, min=np.log(20), max=np.log(20000), step=0.001, value=440),\n",
    "    autoplay=False,\n",
    ")\n",
    "def audio(freq: float, autoplay: bool):\n",
    "    print(f\"Frequency: {freq}\")\n",
    "    \n",
    "    length = int(sr * duration)\n",
    "    t = np.linspace(0, duration, length)\n",
    "    y = 0.1 * np.sin(2 * np.pi * freq * t)\n",
    "\n",
    "    fig.clear() \n",
    "    ax = fig.add_subplot(1, 1, 1)\n",
    "    ax.plot(y)\n",
    "\n",
    "    fig.tight_layout()\n",
    "    IPython.display.display(\n",
    "        fig.canvas,\n",
    "        IPython.display.Audio(y, rate=sr, autoplay=autoplay, normalize=False),\n",
    "    )\n",
    "import time\n",
    "# time.sleep(0.1)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}

execute above python code will sometimes blocks

Reason: If the kernel does not pass in the timeout parameter when calling the poll api, and sends a send message to the shell channel at the same time, it will always be blocked.

i have raise an merge request to fix the issue

hjliu0206 avatar May 13 '24 03:05 hjliu0206

here is my fix merge request https://github.com/jupyter/nbclient/pull/311

hjliu0206 avatar May 13 '24 03:05 hjliu0206

Looks like I hit this problem while debugging https://github.com/voila-dashboards/voila/issues/1522. I don't understand why this isn't working, but after adding a ton of logging both for nbclient and jupyter_client packages, I see that in my case AsyncKernelClient is used, that one uses AsyncZMQSocketChannel for the shell_channel: https://github.com/jupyter/jupyter_client/blob/1f36fbf4d0e2e067129552d9e69212f7bf7c0624/jupyter_client/asynchronous/client.py#L54

I have modified this method https://github.com/jupyter/jupyter_client/blob/1f36fbf4d0e2e067129552d9e69212f7bf7c0624/jupyter_client/channels.py#L302C1-L313C24 to look like this:

    async def get_msg(  # type:ignore[override]
        self, timeout: t.Optional[float] = None
    ) -> t.Dict[str, t.Any]:
        """Gets a message if there is one that is ready."""
        assert self.socket is not None
        timeout_ms = None if timeout is None else int(timeout * 1000)  # seconds to ms
        ready = await self.socket.poll(timeout_ms)
        if ready:
            res = await self._recv()
            print(f"Got result from call #{timeout}, {res=}")
            return res
        else:
            raise Empty

notice that I am printing timeout in the log, but since this is an argument coming in - it can be used as an id to make sure I can differentiate between calls and track them across the board, e.g. see global_counter_value_get below.

Another piece of the puzzle is from nbclient, I have modified this method https://github.com/jupyter/nbclient/blob/2650b217c4c9965110ceb4de193092d7eb36c2bb/nbclient/client.py#L761-L806 like this:

import threading
global_counter = 10000
counter_lock = threading.Lock()

def global_counter_value_get():
    global global_counter
    with counter_lock:
        global_counter += 1
        return global_counter

    async def _async_poll_for_reply(
       ##################### not changed above
        while True:
            try:
                if error_on_timeout_execute_reply:
                    msg = error_on_timeout_execute_reply  # type:ignore[unreachable]
                    msg["parent_header"] = {"msg_id": msg_id}
                else:
                    id_value = global_counter_value_get()
                    print(f"About to call into get_msg #{id_value}")
                    msg = await ensure_async(self.kc.shell_channel.get_msg(timeout=id_value ))
                    print(f"Got response for #{id_value}; {msg=}")
                if msg["parent_header"].get("msg_id") == msg_id:
     ####################### not changed below

And after making the changes above I can see that jupyter_client seems to return a value just fine, e.g. the log message right above the return is printed out; but that response never arrives to _async_poll_for_reply, e.g. it's like the evenloop is blocked and await never works.

I have switchinged from using AsyncMappingKernelManager to a blocking one and it solved the problem, which is one more reason to suspect that there is something off with the event loop here. This is the config I am using now: --NotebookApp.kernel_manager_class=jupyter_server.services.kernels.kernelmanager.MappingKernelManager

Aleksei-Poliakov avatar Jun 17 '25 19:06 Aleksei-Poliakov