Skip to content

Commit 352d66d

Browse files
committed
force use of "spawn" process creation method at the start of extension.initialize_settings
1 parent 779f088 commit 352d66d

File tree

5 files changed

+22
-38
lines changed

5 files changed

+22
-38
lines changed

jupyter_scheduler/extension.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import asyncio
22
import multiprocessing
33

4-
multiprocessing.set_start_method("spawn", force=True)
5-
64
from jupyter_core.paths import jupyter_data_dir
75
from jupyter_server.extension.application import ExtensionApp
86
from jupyter_server.transutils import _i18n
@@ -72,6 +70,15 @@ def _db_url_default(self):
7270
)
7371

7472
def initialize_settings(self):
73+
# Forces new processes to not be forked on Linux.
74+
# This is necessary because `asyncio.get_event_loop()` is bugged in
75+
# forked processes in Python versions below 3.12. This method is
76+
# called by `jupyter_core` by `nbconvert` in the default executor.
77+
78+
# See: https://github.com/python/cpython/issues/66285
79+
# See also: https://github.com/jupyter/jupyter_core/pull/362
80+
multiprocessing.set_start_method("spawn", force=True)
81+
7582
super().initialize_settings()
7683

7784
create_tables(self.db_url, self.drop_tables)

jupyter_scheduler/job_files_manager.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import multiprocessing as mp
1+
from multiprocessing import Process
22
import os
33
import random
44
import tarfile
@@ -23,15 +23,7 @@ async def copy_from_staging(self, job_id: str, redownload: Optional[bool] = Fals
2323
output_filenames = self.scheduler.get_job_filenames(job)
2424
output_dir = self.scheduler.get_local_output_path(model=job, root_dir_relative=True)
2525

26-
# The MP context forces new processes to not be forked on Linux.
27-
# This is necessary because `asyncio.get_event_loop()` is bugged in
28-
# forked processes in Python versions below 3.12. This method is
29-
# called by `jupyter_core` by `nbconvert` in the default executor.
30-
#
31-
# See: https://github.com/python/cpython/issues/66285
32-
# See also: https://github.com/jupyter/jupyter_core/pull/362
33-
mp_ctx = mp.get_context("spawn")
34-
p = mp_ctx.Process(
26+
p = Process(
3527
target=Downloader(
3628
output_formats=job.output_formats,
3729
output_filenames=output_filenames,

jupyter_scheduler/scheduler.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import multiprocessing as mp
1+
from multiprocessing import Process
22
import os
33
import random
44
import shutil
@@ -481,15 +481,7 @@ def create_job(self, model: CreateJob) -> str:
481481
else:
482482
self.copy_input_file(model.input_uri, staging_paths["input"])
483483

484-
# The MP context forces new processes to not be forked on Linux.
485-
# This is necessary because `asyncio.get_event_loop()` is bugged in
486-
# forked processes in Python versions below 3.12. This method is
487-
# called by `jupyter_core` by `nbconvert` in the default executor.
488-
#
489-
# See: https://github.com/python/cpython/issues/66285
490-
# See also: https://github.com/jupyter/jupyter_core/pull/362
491-
mp_ctx = mp.get_context("spawn")
492-
p = mp_ctx.Process(
484+
p = Process(
493485
target=self.execution_manager_class(
494486
job_id=job.job_id,
495487
staging_paths=staging_paths,

jupyter_scheduler/tests/test_job_files_manager.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import tarfile
55
import time
66
from pathlib import Path
7-
from unittest.mock import MagicMock, patch
7+
from unittest.mock import patch
88

99
import pytest
1010

@@ -40,20 +40,13 @@ async def test_copy_from_staging():
4040
"input": "helloworld.ipynb",
4141
}
4242
output_dir = "jobs/1"
43-
44-
mock_scheduler = MagicMock()
45-
mock_scheduler.get_job.return_value = job
46-
mock_scheduler.get_staging_paths.return_value = staging_paths
47-
mock_scheduler.get_local_output_path.return_value = output_dir
48-
mock_scheduler.get_job_filenames.return_value = job_filenames
49-
50-
mock_context = MagicMock()
51-
mock_process = MagicMock()
52-
mock_context.Process.return_value = mock_process
53-
5443
with patch("jupyter_scheduler.job_files_manager.Downloader") as mock_downloader:
55-
with patch("jupyter_scheduler.scheduler.Scheduler", return_value=mock_scheduler):
56-
with patch("multiprocessing.get_context", return_value=mock_context):
44+
with patch("jupyter_scheduler.job_files_manager.Process") as mock_process:
45+
with patch("jupyter_scheduler.scheduler.Scheduler") as mock_scheduler:
46+
mock_scheduler.get_job.return_value = job
47+
mock_scheduler.get_staging_paths.return_value = staging_paths
48+
mock_scheduler.get_local_output_path.return_value = output_dir
49+
mock_scheduler.get_job_filenames.return_value = job_filenames
5750
manager = JobFilesManager(scheduler=mock_scheduler)
5851
await manager.copy_from_staging(1)
5952

@@ -65,7 +58,6 @@ async def test_copy_from_staging():
6558
redownload=False,
6659
include_staging_files=None,
6760
)
68-
mock_process.start.assert_called_once()
6961

7062

7163
HERE = Path(__file__).parent.resolve()

jupyter_scheduler/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import multiprocessing
23
import os
34
import shutil
45
from datetime import datetime, timezone
@@ -93,7 +94,7 @@ def copy_directory(
9394
exclude_files: Optional[List[str]] = [],
9495
) -> List[str]:
9596
"""Copies content of source_dir to destination_dir excluding exclude_files.
96-
Returns a list of relative paths to copied files from destination_dir.
97+
Returns a list of relative paths to copied files from destination_dir
9798
"""
9899
copied_files = []
99100
for item in os.listdir(source_dir):

0 commit comments

Comments
 (0)