From 9b9ed50134e59c328629dc64446cc1873af44022 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 10 Jun 2026 15:09:09 -0700 Subject: [PATCH] Do not implicitly shut down the shared reusable executor All runners with executor=None now share loky's reusable executor singleton, so a finishing (or cancelled) runner must not shut it down: another runner may still be submitting work to it, which raised loky.process_executor.ShutdownExecutorError (seen in the Read the Docs build of tutorial.advanced-topics.md). The executor is only shut down when the user explicitly passes shutdown_executor=True; idle loky workers time out on their own. --- adaptive/runner.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/adaptive/runner.py b/adaptive/runner.py index 9ee5a0aa..99f1f4e4 100644 --- a/adaptive/runner.py +++ b/adaptive/runner.py @@ -92,8 +92,9 @@ class BaseRunner(metaclass=abc.ABCMeta): If True, record the method calls made to the learner by this runner. shutdown_executor : bool, default: False If True, shutdown the executor when the runner has completed. If - `executor` is not provided then the executor created internally - by the runner is shut down, regardless of this parameter. + `executor` is not provided then the runner uses loky's reusable + executor, which is shared between runners and is therefore not + shut down unless this parameter is True. retries : int, default: 0 Maximum amount of retries of a certain point ``x`` in ``learner.function(x)``. After `retries` is reached for ``x`` @@ -159,9 +160,11 @@ def __init__( self._pending_tasks: dict[FutureTypes, int] = {} - # if we instantiate our own executor, then we are also responsible - # for calling 'shutdown' - self.shutdown_executor = shutdown_executor or (executor is None) + # The internally created executor (when `executor is None`) is loky's + # reusable executor, a singleton shared between all runners, so we + # must not shut it down implicitly: another runner may still be using + # it. Pass `shutdown_executor=True` to shut it down anyway. + self.shutdown_executor = shutdown_executor self.learner = learner self.log: list | None = [] if log else None @@ -378,8 +381,9 @@ class BlockingRunner(BaseRunner): If True, record the method calls made to the learner by this runner. shutdown_executor : bool, default: False If True, shutdown the executor when the runner has completed. If - `executor` is not provided then the executor created internally - by the runner is shut down, regardless of this parameter. + `executor` is not provided then the runner uses loky's reusable + executor, which is shared between runners and is therefore not + shut down unless this parameter is True. retries : int, default: 0 Maximum amount of retries of a certain point ``x`` in ``learner.function(x)``. After `retries` is reached for ``x`` @@ -524,8 +528,9 @@ class AsyncRunner(BaseRunner): If True, record the method calls made to the learner by this runner. shutdown_executor : bool, default: False If True, shutdown the executor when the runner has completed. If - `executor` is not provided then the executor created internally - by the runner is shut down, regardless of this parameter. + `executor` is not provided then the runner uses loky's reusable + executor, which is shared between runners and is therefore not + shut down unless this parameter is True. ioloop : ``asyncio.AbstractEventLoop``, optional The ioloop in which to run the learning algorithm. If not provided, the default event loop is used.