| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- """A kernel manager for multiple kernels"""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- from __future__ import annotations
- import asyncio
- import json
- import os
- import socket
- import typing as t
- import uuid
- from functools import wraps
- from pathlib import Path
- import zmq
- from traitlets import Any, Bool, Dict, DottedObjectName, Instance, Unicode, default, observe
- from traitlets.config.configurable import LoggingConfigurable
- from traitlets.utils.importstring import import_item
- from .connect import KernelConnectionInfo
- from .kernelspec import NATIVE_KERNEL_NAME, KernelSpecManager
- from .manager import KernelManager
- from .utils import ensure_async, run_sync, utcnow
- class DuplicateKernelError(Exception):
- pass
- def kernel_method(f: t.Callable) -> t.Callable:
- """decorator for proxying MKM.method(kernel_id) to individual KMs by ID"""
- @wraps(f)
- def wrapped(
- self: t.Any, kernel_id: str, *args: t.Any, **kwargs: t.Any
- ) -> t.Callable | t.Awaitable:
- # get the kernel
- km = self.get_kernel(kernel_id)
- method = getattr(km, f.__name__)
- # call the kernel's method
- r = method(*args, **kwargs)
- # last thing, call anything defined in the actual class method
- # such as logging messages
- f(self, kernel_id, *args, **kwargs)
- # return the method result
- return r
- return wrapped
- class MultiKernelManager(LoggingConfigurable):
- """A class for managing multiple kernels."""
- default_kernel_name = Unicode(
- NATIVE_KERNEL_NAME, help="The name of the default kernel to start"
- ).tag(config=True)
- kernel_spec_manager = Instance(KernelSpecManager, allow_none=True)
- kernel_manager_class = DottedObjectName(
- "jupyter_client.ioloop.IOLoopKernelManager",
- help="""The kernel manager class. This is configurable to allow
- subclassing of the KernelManager for customized behavior.
- """,
- ).tag(config=True)
- @observe("kernel_manager_class")
- def _kernel_manager_class_changed(self, change: t.Any) -> None:
- self.kernel_manager_factory = self._create_kernel_manager_factory()
- kernel_manager_factory = Any(help="this is kernel_manager_class after import")
- @default("kernel_manager_factory")
- def _kernel_manager_factory_default(self) -> t.Callable:
- return self._create_kernel_manager_factory()
- def _create_kernel_manager_factory(self) -> t.Callable:
- kernel_manager_ctor = import_item(self.kernel_manager_class)
- def create_kernel_manager(*args: t.Any, **kwargs: t.Any) -> KernelManager:
- if self.shared_context:
- if self.context.closed:
- # recreate context if closed
- self.context = self._context_default()
- kwargs.setdefault("context", self.context)
- km = kernel_manager_ctor(*args, **kwargs)
- return km
- return create_kernel_manager
- shared_context = Bool(
- True,
- help="Share a single zmq.Context to talk to all my kernels",
- ).tag(config=True)
- context = Instance("zmq.Context")
- _created_context = Bool(False)
- _pending_kernels = Dict()
- @property
- def _starting_kernels(self) -> dict:
- """A shim for backwards compatibility."""
- return self._pending_kernels
- @default("context")
- def _context_default(self) -> zmq.Context:
- self._created_context = True
- return zmq.Context()
- connection_dir = Unicode("")
- external_connection_dir = Unicode(None, allow_none=True)
- _kernels = Dict()
- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
- super().__init__(*args, **kwargs)
- self.kernel_id_to_connection_file: dict[str, Path] = {}
- def __del__(self) -> None:
- """Handle garbage collection. Destroy context if applicable."""
- if self._created_context and self.context and not self.context.closed:
- if self.log:
- self.log.debug("Destroying zmq context for %s", self)
- self.context.destroy(linger=1000)
- try:
- super_del = super().__del__ # type:ignore[misc]
- except AttributeError:
- pass
- else:
- super_del()
- def list_kernel_ids(self) -> list[str]:
- """Return a list of the kernel ids of the active kernels."""
- if self.external_connection_dir is not None:
- external_connection_dir = Path(self.external_connection_dir)
- if external_connection_dir.is_dir():
- connection_files = [p for p in external_connection_dir.iterdir() if p.is_file()]
- # remove kernels (whose connection file has disappeared) from our list
- k = list(self.kernel_id_to_connection_file.keys())
- v = list(self.kernel_id_to_connection_file.values())
- for connection_file in list(self.kernel_id_to_connection_file.values()):
- if connection_file not in connection_files:
- kernel_id = k[v.index(connection_file)]
- del self.kernel_id_to_connection_file[kernel_id]
- del self._kernels[kernel_id]
- # add kernels (whose connection file appeared) to our list
- for connection_file in connection_files:
- if connection_file in self.kernel_id_to_connection_file.values():
- continue
- try:
- connection_info: KernelConnectionInfo = json.loads(
- connection_file.read_text()
- )
- except Exception: # noqa: S112
- continue
- self.log.debug("Loading connection file %s", connection_file)
- if not ("kernel_name" in connection_info and "key" in connection_info):
- continue
- # it looks like a connection file
- kernel_id = self.new_kernel_id()
- self.kernel_id_to_connection_file[kernel_id] = connection_file
- km = self.kernel_manager_factory(
- parent=self,
- log=self.log,
- owns_kernel=False,
- )
- km.load_connection_info(connection_info)
- km.last_activity = utcnow()
- km.execution_state = "idle"
- km.connections = 1
- km.kernel_id = kernel_id
- km.kernel_name = connection_info["kernel_name"]
- km.ready.set_result(None)
- self._kernels[kernel_id] = km
- # Create a copy so we can iterate over kernels in operations
- # that delete keys.
- return list(self._kernels.keys())
- def __len__(self) -> int:
- """Return the number of running kernels."""
- return len(self.list_kernel_ids())
- def __contains__(self, kernel_id: str) -> bool:
- return kernel_id in self._kernels
- def pre_start_kernel(
- self, kernel_name: str | None, kwargs: t.Any
- ) -> tuple[KernelManager, str, str]:
- # kwargs should be mutable, passing it as a dict argument.
- kernel_id = kwargs.pop("kernel_id", self.new_kernel_id(**kwargs))
- if kernel_id in self:
- raise DuplicateKernelError("Kernel already exists: %s" % kernel_id)
- if kernel_name is None:
- kernel_name = self.default_kernel_name
- # kernel_manager_factory is the constructor for the KernelManager
- # subclass we are using. It can be configured as any Configurable,
- # including things like its transport and ip.
- constructor_kwargs = {}
- if self.kernel_spec_manager:
- constructor_kwargs["kernel_spec_manager"] = self.kernel_spec_manager
- km = self.kernel_manager_factory(
- connection_file=os.path.join(self.connection_dir, "kernel-%s.json" % kernel_id),
- parent=self,
- log=self.log,
- kernel_name=kernel_name,
- **constructor_kwargs,
- )
- return km, kernel_name, kernel_id
- def update_env(self, *, kernel_id: str, env: t.Dict[str, str]) -> None:
- """
- Allow to update the environment of the given kernel.
- Forward the update env request to the corresponding kernel.
- .. version-added: 8.5
- """
- if kernel_id in self:
- self._kernels[kernel_id].update_env(env=env)
- async def _add_kernel_when_ready(
- self, kernel_id: str, km: KernelManager, kernel_awaitable: t.Awaitable
- ) -> None:
- try:
- await kernel_awaitable
- self._kernels[kernel_id] = km
- self._pending_kernels.pop(kernel_id, None)
- except Exception as e:
- self.log.exception(e)
- async def _remove_kernel_when_ready(
- self, kernel_id: str, kernel_awaitable: t.Awaitable
- ) -> None:
- try:
- await kernel_awaitable
- self.remove_kernel(kernel_id)
- self._pending_kernels.pop(kernel_id, None)
- except Exception as e:
- self.log.exception(e)
- def _using_pending_kernels(self) -> bool:
- """Returns a boolean; a clearer method for determining if
- this multikernelmanager is using pending kernels or not
- """
- return getattr(self, "use_pending_kernels", False)
- async def _async_start_kernel(self, *, kernel_name: str | None = None, **kwargs: t.Any) -> str:
- """Start a new kernel.
- The caller can pick a kernel_id by passing one in as a keyword arg,
- otherwise one will be generated using new_kernel_id().
- The kernel ID for the newly started kernel is returned.
- """
- km, kernel_name, kernel_id = self.pre_start_kernel(kernel_name, kwargs)
- if not isinstance(km, KernelManager):
- self.log.warning( # type:ignore[unreachable]
- f"Kernel manager class ({self.kernel_manager_class.__class__}) is not an instance of 'KernelManager'!"
- )
- kwargs["kernel_id"] = kernel_id # Make kernel_id available to manager and provisioner
- starter = ensure_async(km.start_kernel(**kwargs))
- task = asyncio.create_task(self._add_kernel_when_ready(kernel_id, km, starter))
- self._pending_kernels[kernel_id] = task
- # Handling a Pending Kernel
- if self._using_pending_kernels():
- # If using pending kernels, do not block
- # on the kernel start.
- self._kernels[kernel_id] = km
- else:
- await task
- # raise an exception if one occurred during kernel startup.
- if km.ready.exception():
- raise km.ready.exception() # type: ignore[misc]
- return kernel_id
- start_kernel = run_sync(_async_start_kernel)
- async def _async_shutdown_kernel(
- self,
- kernel_id: str,
- now: bool | None = False,
- restart: bool | None = False,
- ) -> None:
- """Shutdown a kernel by its kernel uuid.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel to shutdown.
- now : bool
- Should the kernel be shutdown forcibly using a signal.
- restart : bool
- Will the kernel be restarted?
- """
- self.log.info("Kernel shutdown: %s", kernel_id)
- # If the kernel is still starting, wait for it to be ready.
- if kernel_id in self._pending_kernels:
- task = self._pending_kernels[kernel_id]
- try:
- await task
- km = self.get_kernel(kernel_id)
- await t.cast(asyncio.Future, km.ready)
- except asyncio.CancelledError:
- pass
- except Exception:
- self.remove_kernel(kernel_id)
- return
- km = self.get_kernel(kernel_id)
- # If a pending kernel raised an exception, remove it.
- if not km.ready.cancelled() and km.ready.exception():
- self.remove_kernel(kernel_id)
- return
- stopper = ensure_async(km.shutdown_kernel(now, restart))
- fut = asyncio.ensure_future(self._remove_kernel_when_ready(kernel_id, stopper))
- self._pending_kernels[kernel_id] = fut
- # Await the kernel if not using pending kernels.
- if not self._using_pending_kernels():
- await fut
- # raise an exception if one occurred during kernel shutdown.
- if km.ready.exception():
- raise km.ready.exception() # type: ignore[misc]
- shutdown_kernel = run_sync(_async_shutdown_kernel)
- @kernel_method
- def request_shutdown(self, kernel_id: str, restart: bool | None = False) -> None:
- """Ask a kernel to shut down by its kernel uuid"""
- @kernel_method
- def finish_shutdown(
- self,
- kernel_id: str,
- waittime: float | None = None,
- pollinterval: float | None = 0.1,
- ) -> None:
- """Wait for a kernel to finish shutting down, and kill it if it doesn't"""
- self.log.info("Kernel shutdown: %s", kernel_id)
- @kernel_method
- def cleanup_resources(self, kernel_id: str, restart: bool = False) -> None:
- """Clean up a kernel's resources"""
- def remove_kernel(self, kernel_id: str) -> KernelManager:
- """remove a kernel from our mapping.
- Mainly so that a kernel can be removed if it is already dead,
- without having to call shutdown_kernel.
- The kernel object is returned, or `None` if not found.
- """
- return self._kernels.pop(kernel_id, None)
- async def _async_shutdown_all(self, now: bool = False) -> None:
- """Shutdown all kernels."""
- kids = self.list_kernel_ids()
- kids += list(self._pending_kernels)
- kms = list(self._kernels.values())
- futs = [self._async_shutdown_kernel(kid, now=now) for kid in set(kids)]
- await asyncio.gather(*futs)
- # If using pending kernels, the kernels will not have been fully shut down.
- if self._using_pending_kernels():
- for km in kms:
- try:
- await km.ready
- except asyncio.CancelledError:
- self._pending_kernels[km.kernel_id].cancel()
- except Exception:
- # Will have been logged in _add_kernel_when_ready
- pass
- shutdown_all = run_sync(_async_shutdown_all)
- def interrupt_kernel(self, kernel_id: str) -> None:
- """Interrupt (SIGINT) the kernel by its uuid.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel to interrupt.
- """
- kernel = self.get_kernel(kernel_id)
- if not kernel.ready.done():
- msg = "Kernel is in a pending state. Cannot interrupt."
- raise RuntimeError(msg)
- out = kernel.interrupt_kernel()
- self.log.info("Kernel interrupted: %s", kernel_id)
- return out
- @kernel_method
- def signal_kernel(self, kernel_id: str, signum: int) -> None:
- """Sends a signal to the kernel by its uuid.
- Note that since only SIGTERM is supported on Windows, this function
- is only useful on Unix systems.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel to signal.
- signum : int
- Signal number to send kernel.
- """
- self.log.info("Signaled Kernel %s with %s", kernel_id, signum)
- async def _async_restart_kernel(self, kernel_id: str, now: bool = False) -> None:
- """Restart a kernel by its uuid, keeping the same ports.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel to interrupt.
- now : bool, optional
- If True, the kernel is forcefully restarted *immediately*, without
- having a chance to do any cleanup action. Otherwise the kernel is
- given 1s to clean up before a forceful restart is issued.
- In all cases the kernel is restarted, the only difference is whether
- it is given a chance to perform a clean shutdown or not.
- """
- kernel = self.get_kernel(kernel_id)
- if self._using_pending_kernels() and not kernel.ready.done():
- msg = "Kernel is in a pending state. Cannot restart."
- raise RuntimeError(msg)
- await ensure_async(kernel.restart_kernel(now=now))
- self.log.info("Kernel restarted: %s", kernel_id)
- restart_kernel = run_sync(_async_restart_kernel)
- @kernel_method
- def is_alive(self, kernel_id: str) -> bool: # type:ignore[empty-body]
- """Is the kernel alive.
- This calls KernelManager.is_alive() which calls Popen.poll on the
- actual kernel subprocess.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel.
- """
- def _check_kernel_id(self, kernel_id: str) -> None:
- """check that a kernel id is valid"""
- if kernel_id not in self:
- raise KeyError("Kernel with id not found: %s" % kernel_id)
- def get_kernel(self, kernel_id: str) -> KernelManager:
- """Get the single KernelManager object for a kernel by its uuid.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel.
- """
- self._check_kernel_id(kernel_id)
- return self._kernels[kernel_id]
- @kernel_method
- def add_restart_callback(
- self, kernel_id: str, callback: t.Callable, event: str = "restart"
- ) -> None:
- """add a callback for the KernelRestarter"""
- @kernel_method
- def remove_restart_callback(
- self, kernel_id: str, callback: t.Callable, event: str = "restart"
- ) -> None:
- """remove a callback for the KernelRestarter"""
- @kernel_method
- def get_connection_info(self, kernel_id: str) -> dict[str, t.Any]: # type:ignore[empty-body]
- """Return a dictionary of connection data for a kernel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel.
- Returns
- =======
- connection_dict : dict
- A dict of the information needed to connect to a kernel.
- This includes the ip address and the integer port
- numbers of the different channels (stdin_port, iopub_port,
- shell_port, hb_port).
- """
- @kernel_method
- def connect_iopub( # type:ignore[empty-body]
- self, kernel_id: str, identity: bytes | None = None
- ) -> socket.socket:
- """Return a zmq Socket connected to the iopub channel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel
- identity : bytes (optional)
- The zmq identity of the socket
- Returns
- =======
- stream : zmq Socket or ZMQStream
- """
- @kernel_method
- def connect_shell( # type:ignore[empty-body]
- self, kernel_id: str, identity: bytes | None = None
- ) -> socket.socket:
- """Return a zmq Socket connected to the shell channel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel
- identity : bytes (optional)
- The zmq identity of the socket
- Returns
- =======
- stream : zmq Socket or ZMQStream
- """
- @kernel_method
- def connect_control( # type:ignore[empty-body]
- self, kernel_id: str, identity: bytes | None = None
- ) -> socket.socket:
- """Return a zmq Socket connected to the control channel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel
- identity : bytes (optional)
- The zmq identity of the socket
- Returns
- =======
- stream : zmq Socket or ZMQStream
- """
- @kernel_method
- def connect_stdin( # type:ignore[empty-body]
- self, kernel_id: str, identity: bytes | None = None
- ) -> socket.socket:
- """Return a zmq Socket connected to the stdin channel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel
- identity : bytes (optional)
- The zmq identity of the socket
- Returns
- =======
- stream : zmq Socket or ZMQStream
- """
- @kernel_method
- def connect_hb( # type:ignore[empty-body]
- self, kernel_id: str, identity: bytes | None = None
- ) -> socket.socket:
- """Return a zmq Socket connected to the hb channel.
- Parameters
- ==========
- kernel_id : uuid
- The id of the kernel
- identity : bytes (optional)
- The zmq identity of the socket
- Returns
- =======
- stream : zmq Socket or ZMQStream
- """
- def new_kernel_id(self, **kwargs: t.Any) -> str:
- """
- Returns the id to associate with the kernel for this request. Subclasses may override
- this method to substitute other sources of kernel ids.
- :param kwargs:
- :return: string-ized version 4 uuid
- """
- return str(uuid.uuid4())
- class AsyncMultiKernelManager(MultiKernelManager):
- kernel_manager_class = DottedObjectName(
- "jupyter_client.ioloop.AsyncIOLoopKernelManager",
- config=True,
- help="""The kernel manager class. This is configurable to allow
- subclassing of the AsyncKernelManager for customized behavior.
- """,
- )
- use_pending_kernels = Bool(
- False,
- help="""Whether to make kernels available before the process has started. The
- kernel has a `.ready` future which can be awaited before connecting""",
- ).tag(config=True)
- context = Instance("zmq.asyncio.Context")
- @default("context")
- def _context_default(self) -> zmq.asyncio.Context:
- self._created_context = True
- return zmq.asyncio.Context()
- start_kernel: t.Callable[..., t.Awaitable] = MultiKernelManager._async_start_kernel # type:ignore[assignment]
- restart_kernel: t.Callable[..., t.Awaitable] = MultiKernelManager._async_restart_kernel # type:ignore[assignment]
- shutdown_kernel: t.Callable[..., t.Awaitable] = MultiKernelManager._async_shutdown_kernel # type:ignore[assignment]
- shutdown_all: t.Callable[..., t.Awaitable] = MultiKernelManager._async_shutdown_all # type:ignore[assignment]
|