From 9713b38d7f2ffa5d946244df98d34e46f064e2e6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 06:11:58 -0500 Subject: [PATCH 01/21] Move the mod_wsgi tests from Evergreen to GitHub Actions The Evergreen tasks depend on toolchain builds of mod_wsgi.so for each Python version. The new Mod WSGI jobs install mod_wsgi with pip on an ubuntu runner and test the latest stable CPython against a replica set in both daemon and embedded mode. mod_wsgi is pinned to 4.9.4 because releases 5 and newer crash Apache children in embedded mode under load. --- .evergreen/generated_configs/tasks.yml | 74 ----------------- .evergreen/generated_configs/variants.yml | 10 --- .evergreen/scripts/generate_config.py | 33 -------- .evergreen/scripts/mod_wsgi_smoke_test.sh | 80 +++++++++++++++++++ .evergreen/scripts/mod_wsgi_tester.py | 39 ++++++--- .github/workflows/test-python.yml | 43 ++++++++++ CONTRIBUTING.md | 13 ++- justfile | 5 ++ test/mod_wsgi_test/README.rst | 50 +++++------- test/mod_wsgi_test/apache22amazon.conf | 34 -------- test/mod_wsgi_test/apache22ubuntu1204.conf | 29 ------- ...4ubuntu161404.conf => apache24ubuntu.conf} | 2 +- 12 files changed, 185 insertions(+), 227 deletions(-) create mode 100755 .evergreen/scripts/mod_wsgi_smoke_test.sh delete mode 100644 test/mod_wsgi_test/apache22amazon.conf delete mode 100644 test/mod_wsgi_test/apache22ubuntu1204.conf rename test/mod_wsgi_test/{apache24ubuntu161404.conf => apache24ubuntu.conf} (97%) diff --git a/.evergreen/generated_configs/tasks.yml b/.evergreen/generated_configs/tasks.yml index 942967bbec..e2677e7399 100644 --- a/.evergreen/generated_configs/tasks.yml +++ b/.evergreen/generated_configs/tasks.yml @@ -316,80 +316,6 @@ tasks: TOPOLOGY: sharded_cluster tags: [test-min-support] - # Mod wsgi tests - - name: mod-wsgi-replica-set-python3.10 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.10" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: standalone - TOOLCHAIN_VERSION: "3.10" - tags: [mod_wsgi, pr] - - name: mod-wsgi-embedded-mode-replica-set-python3.11 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.11" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: embedded - TOOLCHAIN_VERSION: "3.11" - tags: [mod_wsgi, pr] - - name: mod-wsgi-replica-set-python3.12 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.12" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: standalone - TOOLCHAIN_VERSION: "3.12" - tags: [mod_wsgi, pr] - - name: mod-wsgi-embedded-mode-replica-set-python3.13 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.13" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: embedded - TOOLCHAIN_VERSION: "3.13" - tags: [mod_wsgi, pr] - - name: mod-wsgi-embedded-mode-replica-set-python3.14 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.14" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: embedded - TOOLCHAIN_VERSION: "3.14" - tags: [mod_wsgi, pr] - - name: mod-wsgi-embedded-mode-replica-set-python3.15 - commands: - - func: run server - vars: - TOPOLOGY: replica_set - TOOLCHAIN_VERSION: "3.15" - - func: run tests - vars: - TEST_NAME: mod_wsgi - SUB_TEST_NAME: embedded - TOOLCHAIN_VERSION: "3.15" - tags: [mod_wsgi, pr] - # No orchestration tests - name: test-no-orchestration-python3.10-min-deps commands: diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index de2ce0e5ce..26027888fe 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -384,16 +384,6 @@ buildvariants: TEST_NAME: mockupdb tags: [pr] - # Mod wsgi tests - - name: mod_wsgi-ubuntu-22 - tasks: - - name: .mod_wsgi - display_name: Mod_WSGI Ubuntu-22 - run_on: - - ubuntu2204-small - expansions: - MOD_WSGI_VERSION: "4" - # No c ext tests - name: no-c-ext-rhel8 tasks: diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index f8f8be140a..97fd9b0176 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -400,14 +400,6 @@ def create_no_c_ext_variants(): return [create_variant(tasks, display_name, host=host, expansions=expansions)] -def create_mod_wsgi_variants(): - host = HOSTS["ubuntu22"] - tasks = [".mod_wsgi"] - expansions = dict(MOD_WSGI_VERSION="4") - display_name = get_variant_name("Mod_WSGI", host) - return [create_variant(tasks, display_name, host=host, expansions=expansions)] - - def create_disable_test_commands_variants(): host = DEFAULT_HOST expansions = dict(AUTH="auth", SSL="ssl", DISABLE_TEST_COMMANDS="1") @@ -1012,31 +1004,6 @@ def create_oidc_tasks(): return tasks -def create_mod_wsgi_tasks(): - tasks = [] - for (test, topology), python in zip_cycle( - product(["standalone", "embedded-mode"], ["standalone", "replica_set"]), CPYTHONS - ): - if "t" in python: - continue - if test == "standalone": - task_name = "mod-wsgi-" - else: - task_name = "mod-wsgi-embedded-mode-" - task_name += topology.replace("_", "-") - task_name = get_task_name(task_name, python=python) - server_vars = dict(TOPOLOGY=topology, TOOLCHAIN_VERSION=python) - server_func = FunctionCall(func="run server", vars=server_vars) - vars = dict( - TEST_NAME="mod_wsgi", SUB_TEST_NAME=test.split("-")[0], TOOLCHAIN_VERSION=python - ) - test_func = FunctionCall(func="run tests", vars=vars) - tags = ["mod_wsgi", "pr"] - commands = [server_func, test_func] - tasks.append(EvgTask(name=task_name, tags=tags, commands=commands)) - return tasks - - def _create_ocsp_tasks(algo, variant, server_type, base_task_name): tasks = [] file_name = f"{algo}-basic-tls-ocsp-{variant}.json" diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh new file mode 100755 index 0000000000..c90ac3566e --- /dev/null +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Smoke test the mod_wsgi tests in an ubuntu:24.04 container, mirroring the +# Mod WSGI job in .github/workflows/test-python.yml. +set -eu + +SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0}) +ROOT=$(dirname "$(dirname "$SCRIPT_DIR")") + +# Re-exec the script inside a container when run on the host. +if [ ! -f /.dockerenv ]; then + if ! command -v docker >/dev/null; then + echo "docker is required to run the mod_wsgi smoke test" + exit 1 + fi + exec docker run --rm -v "$ROOT":/src:ro ubuntu:24.04 bash /src/.evergreen/scripts/mod_wsgi_smoke_test.sh +fi + +if [ "$(id -u)" = "0" ]; then + # Apache and mongod must not run as root. Install system packages, copy the + # checkout into a home directory, and re-run this script unprivileged. + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq apache2 apache2-dev build-essential curl jq git >/dev/null + useradd -m smoke + mkdir -p /home/smoke/src + # Leave out the generated env files so they cannot override the versions + # set below. + tar -C /src -cf - --exclude=.git --exclude=.venv --exclude=.evergreen/scripts/env.sh \ + --exclude=.evergreen/scripts/test-env.sh . | tar -C /home/smoke/src -xf - + chown -R smoke:smoke /home/smoke/src + exec su - smoke -c "bash /src/.evergreen/scripts/mod_wsgi_smoke_test.sh" +fi + +curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null +export PATH="$HOME/.local/bin:$PATH" +uv tool install rust-just >/dev/null + +# Mirror the GHA job and test the latest stable CPython. +LATEST_PYTHON=$(curl -fsSL https://endoflife.date/api/python.json | jq -r '.[0].latest') +echo "Testing with CPython $LATEST_PYTHON" +uv python install "$LATEST_PYTHON" >/dev/null + +cd /home/smoke/src + +export UV_PYTHON=$LATEST_PYTHON +export PYMONGO_C_EXT_MUST_BUILD=1 +just install + +# Start a single-node replica set. +MARCH=$(uname -m) +if [ "$MARCH" != "aarch64" ] && [ "$MARCH" != "x86_64" ]; then + echo "Unsupported architecture: $MARCH" + exit 1 +fi +MONGODB_URL=$(curl -fsSL https://downloads.mongodb.org/current.json | jq -r " + .versions[] | select(.version | startswith(\"8.0\")) | .downloads[] | + select(.target==\"ubuntu2404\" and .arch==\"$MARCH\") | .archive.url" | grep -v enterprise | head -1) +curl -fsSL "$MONGODB_URL" -o /tmp/mongo.tgz +MEMBER=$(tar -tzf /tmp/mongo.tgz | grep "bin/mongod$") +tar -xz -C /tmp --strip-components=2 -f /tmp/mongo.tgz "$MEMBER" +mkdir -p /tmp/db +/tmp/mongod --replSet rs0 --bind_ip 127.0.0.1 --port 27017 --dbpath /tmp/db --fork --logpath /tmp/mongod.log +uv run python -c " +from pymongo import MongoClient +client = MongoClient('127.0.0.1:27017', directConnection=True) +client.admin.command('replSetInitiate', {'_id': 'rs0', 'members': [{'_id': 0, 'host': '127.0.0.1:27017'}]}) +MongoClient().admin.command('hello') +" + +# uv sync prunes ad-hoc packages, so install mod_wsgi before each setup. Call +# the test scripts directly: the just recipes run "uv sync", which would +# remove mod_wsgi before teardown can stop Apache. +for MODE in standalone embedded; do + # mod_wsgi >= 5 segfaults Apache children in embedded mode under load. + uv pip install -q "mod_wsgi==4.9.4" + bash .evergreen/scripts/setup-tests.sh mod_wsgi $MODE + bash .evergreen/run-tests.sh + bash .evergreen/scripts/teardown-tests.sh +done +echo "mod_wsgi smoke test passed" diff --git a/.evergreen/scripts/mod_wsgi_tester.py b/.evergreen/scripts/mod_wsgi_tester.py index 5968849068..7c2ddb6db5 100644 --- a/.evergreen/scripts/mod_wsgi_tester.py +++ b/.evergreen/scripts/mod_wsgi_tester.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import socket import sys import time import urllib.error @@ -22,6 +23,17 @@ def make_request(url, timeout=10): raise TimeoutError(f"Failed to access {url}") +def find_mod_wsgi_so() -> str: + # pip builds the Apache module against the installing interpreter, so the + # .so lives inside the installed package. + import mod_wsgi + + so_files = list((Path(mod_wsgi.__file__).parent / "server").glob("mod_wsgi*.so")) + if len(so_files) != 1: + raise ValueError(f"Expected one mod_wsgi.so in {mod_wsgi.__file__}, found {so_files}") + return str(so_files[0]) + + def setup_mod_wsgi(sub_test_name: str) -> None: env = os.environ.copy() if sub_test_name == "embedded": @@ -34,20 +46,14 @@ def setup_mod_wsgi(sub_test_name: str) -> None: apache = which("apache2") if not apache and Path("/usr/lib/apache2/mpm-prefork/apache2").exists(): apache = "/usr/lib/apache2/mpm-prefork/apache2" - if apache: - apache_config = "apache24ubuntu161404.conf" - else: - apache = which("httpd") - if not apache: - raise ValueError("Could not find apache2 or httpd") - apache_config = "apache22amazon.conf" - python_version = ".".join(str(val) for val in sys.version_info[:2]) - mod_wsgi_version = 4 - so_file = f"/opt/python/mod_wsgi/python_version/{python_version}/mod_wsgi_version/{mod_wsgi_version}/mod_wsgi.so" + if not apache: + raise ValueError("Could not find apache2") + apache_config = "apache24ubuntu.conf" + so_file = find_mod_wsgi_so() write_env("MOD_WSGI_SO", so_file) env["MOD_WSGI_SO"] = so_file - env["PYTHONHOME"] = f"/opt/python/{python_version}" env["PROJECT_DIRECTORY"] = project_directory = str(ROOT) + write_env("PROJECT_DIRECTORY", project_directory) write_env("APACHE_BINARY", apache) write_env("APACHE_CONFIG", apache_config) uri1 = f"http://localhost:8080/interpreter1{project_directory}" @@ -88,6 +94,17 @@ def teardown_mod_wsgi() -> None: run_command(f"{apache} -k stop -f {ROOT}/test/mod_wsgi_test/{apache_config}") + # -k stop signals the master process and returns before it exits. Wait for + # the port to be released so another instance can bind it. + deadline = time.time() + 10 + while time.time() < deadline: + try: + socket.create_connection(("127.0.0.1", 8080), timeout=1).close() + except OSError: + return + time.sleep(0.5) + raise ValueError("Apache did not release port 8080") + if __name__ == "__main__": setup_mod_wsgi() diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 252c9972de..76a013af82 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -141,6 +141,49 @@ jobs: just setup-tests doctest just run-tests + mod_wsgi: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + mode: ["standalone", "embedded"] + name: Mod WSGI (${{ matrix.mode }}) + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + # endoflife.date returns releases newest-first, so the first entry is + # the latest stable CPython. + - id: latest-python + run: echo "version=$(curl -fsSL https://endoflife.date/api/python.json | jq -r '.[0].latest')" >> "$GITHUB_OUTPUT" + - name: Install Python tooling + uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 + with: + python-version: ${{ steps.latest-python.outputs.version }} + - name: Install Apache + run: sudo apt-get install -y apache2 apache2-dev + - name: Install dependencies + run: | + export PYMONGO_C_EXT_MUST_BUILD=1 + just install + # mod_wsgi >= 5 segfaults Apache children in embedded mode under load. + uv pip install "mod_wsgi==4.9.4" + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: "8.0" + topology: "replica_set" + - name: Run tests + # Call the test scripts directly: the just recipes run "uv sync", + # which would remove the ad-hoc mod_wsgi install before teardown can + # stop Apache. + run: | + bash .evergreen/scripts/setup-tests.sh mod_wsgi ${{ matrix.mode }} + bash .evergreen/run-tests.sh + - name: Teardown + if: always() + run: bash .evergreen/scripts/teardown-tests.sh + docs: name: Docs Checks runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ef90224c2..fa5f755623 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -351,14 +351,19 @@ You will need to set up access to the `drivers-test-secrets-role`, see the [Wiki ### mod_wsgi tests -Note: these tests can only be run from an Evergreen Linux host that has the Python toolchain. +Continuous integration runs the tests on every pull request in the Mod WSGI job of +`.github/workflows/test-python.yml`. To run the same steps locally in an ubuntu container, +run `just smoke-mod-wsgi`. -- Run `just run-server`. +To run the tests by hand, install Apache and mod_wsgi (`sudo apt-get install -y apache2 +apache2-dev` and `uv pip install "mod_wsgi==4.9.4"` on Ubuntu; mod_wsgi 5+ crashes in embedded +mode), then: + +- Run `TOPOLOGY=replica_set just run-server`. - Run `just setup-tests mod_wsgi `. - Run `just run-tests`. -The `mode` can be `standalone` or `embedded`. For the `replica_set` version of the tests, use -`TOPOLOGY=replica_set just run-server`. +The `mode` can be `standalone` or `embedded`. ### OCSP tests diff --git a/justfile b/justfile index df678cdbac..80b4ca53e4 100644 --- a/justfile +++ b/justfile @@ -124,6 +124,11 @@ coverage-xml: uv tool run --with "coverage[toml]" coverage xml @echo "Coverage report generated in coverage.xml" +# Run the mod_wsgi tests in an ubuntu container +[group('test')] +smoke-mod-wsgi: + bash .evergreen/scripts/mod_wsgi_smoke_test.sh + # Start a MongoDB server via drivers-evergreen-tools [group('server')] run-server *args="": diff --git a/test/mod_wsgi_test/README.rst b/test/mod_wsgi_test/README.rst index e96db9406c..f231cb886b 100644 --- a/test/mod_wsgi_test/README.rst +++ b/test/mod_wsgi_test/README.rst @@ -13,44 +13,31 @@ mod_wsgi creates. Test Matrix ----------- -PyMongo should be tested with several versions of mod_wsgi and a selection -of Python versions. Each combination of mod_wsgi and Python version should -be tested with a standalone and a replica set. ``mod_wsgi_test.py`` -detects if the deployment is a replica set and connects to the whole set. +Continuous integration tests the latest stable CPython with the mod_wsgi +release from PyPI, against a replica set, using both daemon and embedded +mode. Other combinations of mod_wsgi and Python versions can be tested +manually. Setup ----- -Compile Python +Install Apache .............. -We need a Python interpreter built as a shared library. Download the -source tarball for each Python version tested, untar it, and run:: +On Ubuntu, install Apache and the headers used to build mod_wsgi:: - ./configure --prefix=/some/directory --enable-shared LDFLAGS="-Wl,--rpath=/some/directory/lib" - make - make install + sudo apt-get install -y apache2 apache2-dev -This results in an executable named "python" or "python3" and a shared -library named something like "libpython2.7.so.1.0" or "libpython3.3m.so.1.0". - -Compile mod_wsgi +Install mod_wsgi ................ -Compile mod_wsgi for each combination for Python and mod_wsgi version in the -test matrix. For example, to compile mod_wsgi 3.4 for Python 2.7 on a -RedHat-like Linux:: +Install mod_wsgi 4.9.4 into the project virtualenv. pip builds mod_wsgi +against the interpreter it is installed with:: - sudo yum install -y httpd httpd-devel - wget https://modwsgi.googlecode.com/files/mod_wsgi-3.4.tar.gz - tar xzf mod_wsgi-3.4.tar.gz - cd mod_wsgi-3.4 - ./configure --with-python=/some/directory/bin/python LDFLAGS="-Wl,--rpath=/some/directory/lib" - make - make install + uv pip install "mod_wsgi==4.9.4" -To ease testing of several matrix combinations, copy the resulting -``mod_wsgi.so`` to a safe place. +mod_wsgi releases 5 and newer crash Apache children in embedded mode under +load, so the test pins the 4.9 series. Start mongod ............ @@ -64,7 +51,7 @@ Configure Apache Set a MOD_WSGI_SO environment variable so our ``mod_wsgi_test.conf`` can find and load mod_wsgi.so:: - export MOD_WSGI_SO=/path/to/mod_wsgi.so + export MOD_WSGI_SO=$(find .venv -name "mod_wsgi*.so") Start Apache with one of the config files in this directory. @@ -104,7 +91,8 @@ the workaround added in `PYTHON-569 Automation ---------- -At MongoDB, Inc. we use a continuous integration job that tests each -combination in the matrix. The job starts up Apache, starts a single server -or replica set, and runs ``test_client.py`` with the proper arguments. -See `run-mod-wsgi-tests.sh `_ +Continuous integration runs the test on every pull request in the Mod WSGI +job of `.github/workflows/test-python.yml +`_. +To run the same steps locally, use ``just smoke-mod-wsgi``, which runs an +ubuntu container with Apache, a single-node replica set, and both test modes. diff --git a/test/mod_wsgi_test/apache22amazon.conf b/test/mod_wsgi_test/apache22amazon.conf deleted file mode 100644 index 7755336b07..0000000000 --- a/test/mod_wsgi_test/apache22amazon.conf +++ /dev/null @@ -1,34 +0,0 @@ -# This is a minimal httpd.conf file written for Apache 2.2 on Amazon Linux - -# The modules directory is here by default. -# ServerRoot "/etc/httpd" -DocumentRoot ${PWD} -PidFile ${PWD}/apache2.pid - -User nobody -Group nobody - -# Bind to localhost only, don't require sudo. -Listen 127.0.0.1:8080 - -# Required modules. -LoadModule authz_host_module modules/mod_authz_host.so -# Needed so we can set a custom log location. -LoadModule log_config_module modules/mod_log_config.so - -ErrorLog ${PWD}/error_log -CustomLog ${PWD}/access_log combined - - - AllowOverride None - Order Deny,Allow - Deny from All - - - - AllowOverride None - Order Allow,Deny - Allow from All - - -Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF} diff --git a/test/mod_wsgi_test/apache22ubuntu1204.conf b/test/mod_wsgi_test/apache22ubuntu1204.conf deleted file mode 100644 index 9fa4b2060b..0000000000 --- a/test/mod_wsgi_test/apache22ubuntu1204.conf +++ /dev/null @@ -1,29 +0,0 @@ -# This is a minimal httpd.conf file written for Apache 2.2 on Ubuntu 12.04 - -# The modules directory is here on Ubuntu. -ServerRoot "/usr/lib/apache2" -DocumentRoot ${PWD} -PidFile ${PWD}/apache2.pid - -# Bind to localhost only, don't require sudo. -Listen 127.0.0.1:8080 - -# Required modules. -LoadModule authz_host_module modules/mod_authz_host.so - -ErrorLog ${PWD}/error_log -CustomLog ${PWD}/access_log combined - - - AllowOverride None - Order Deny,Allow - Deny from All - - - - AllowOverride None - Order Allow,Deny - Allow from All - - -Include ${PROJECT_DIRECTORY}/test/mod_wsgi_test/${MOD_WSGI_CONF} diff --git a/test/mod_wsgi_test/apache24ubuntu161404.conf b/test/mod_wsgi_test/apache24ubuntu.conf similarity index 97% rename from test/mod_wsgi_test/apache24ubuntu161404.conf rename to test/mod_wsgi_test/apache24ubuntu.conf index eb5414f0f7..9776d95f19 100644 --- a/test/mod_wsgi_test/apache24ubuntu161404.conf +++ b/test/mod_wsgi_test/apache24ubuntu.conf @@ -1,4 +1,4 @@ -# This is a minimal httpd.conf file written for Apache 2.4 on Ubuntu 14.04/16.04 +# This is a minimal httpd.conf file written for Apache 2.4 on Ubuntu. # The modules directory is here on Ubuntu. ServerRoot "/usr/lib/apache2" From 5b4aad82b98c329f0db86df147a96464083519d7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 06:37:58 -0500 Subject: [PATCH 02/21] Create the venv in the Mod WSGI job, CI skips it in setup-dev-env --- .github/workflows/test-python.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 76a013af82..c54c22dc53 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -164,8 +164,10 @@ jobs: run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies run: | + # CI skips the venv setup in setup-dev-env.sh, so create it here. export PYMONGO_C_EXT_MUST_BUILD=1 just install + uv sync # mod_wsgi >= 5 segfaults Apache children in embedded mode under load. uv pip install "mod_wsgi==4.9.4" - id: setup-mongodb From b97c528daa8a8db3e6f66f2cf795bb7aa0e2aa4b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 07:10:18 -0500 Subject: [PATCH 03/21] Source the Mod WSGI matrix from the Evergreen version lists The jobs test the lowest supported MongoDB with the oldest CPython and minimum dependencies, and the latest MongoDB with the newest CPython. mod_wsgi moves into a dependency group pinned to 4.9.4, because releases 5 and newer crash Apache children in embedded mode under load. --- .evergreen/scripts/mod_wsgi_matrix.py | 24 ++ .evergreen/scripts/mod_wsgi_smoke_test.sh | 18 +- .github/workflows/test-python.yml | 35 ++- CONTRIBUTING.md | 3 +- pyproject.toml | 2 + test/mod_wsgi_test/README.rst | 19 +- uv.lock | 310 +++++++++++----------- 7 files changed, 227 insertions(+), 184 deletions(-) create mode 100644 .evergreen/scripts/mod_wsgi_matrix.py diff --git a/.evergreen/scripts/mod_wsgi_matrix.py b/.evergreen/scripts/mod_wsgi_matrix.py new file mode 100644 index 0000000000..fe482e9038 --- /dev/null +++ b/.evergreen/scripts/mod_wsgi_matrix.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import json + +from generate_config_utils import ALL_VERSIONS, CPYTHONS + +# The lowest supported MongoDB with the oldest supported CPython and minimum +# dependencies, and the latest MongoDB with the newest supported CPython. +MATRIX = { + "include": [ + { + "python-version": CPYTHONS[0], + "mongodb-version": ALL_VERSIONS[0], + "test-min-deps": "1", + }, + { + "python-version": CPYTHONS[-1], + "mongodb-version": ALL_VERSIONS[-1], + }, + ] +} + +if __name__ == "__main__": + print(json.dumps(MATRIX)) diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh index c90ac3566e..7c5d5a02c3 100755 --- a/.evergreen/scripts/mod_wsgi_smoke_test.sh +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -35,16 +35,17 @@ curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null export PATH="$HOME/.local/bin:$PATH" uv tool install rust-just >/dev/null -# Mirror the GHA job and test the latest stable CPython. -LATEST_PYTHON=$(curl -fsSL https://endoflife.date/api/python.json | jq -r '.[0].latest') +cd /home/smoke/src + +# Mirror the GHA job and test the newest supported CPython. +LATEST_PYTHON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py | jq -r '.include[-1]."python-version"') echo "Testing with CPython $LATEST_PYTHON" uv python install "$LATEST_PYTHON" >/dev/null -cd /home/smoke/src - export UV_PYTHON=$LATEST_PYTHON export PYMONGO_C_EXT_MUST_BUILD=1 just install +uv sync --group mod_wsgi # Start a single-node replica set. MARCH=$(uname -m) @@ -53,7 +54,7 @@ if [ "$MARCH" != "aarch64" ] && [ "$MARCH" != "x86_64" ]; then exit 1 fi MONGODB_URL=$(curl -fsSL https://downloads.mongodb.org/current.json | jq -r " - .versions[] | select(.version | startswith(\"8.0\")) | .downloads[] | + .versions[] | select(.current and .production_release) | .downloads[] | select(.target==\"ubuntu2404\" and .arch==\"$MARCH\") | .archive.url" | grep -v enterprise | head -1) curl -fsSL "$MONGODB_URL" -o /tmp/mongo.tgz MEMBER=$(tar -tzf /tmp/mongo.tgz | grep "bin/mongod$") @@ -67,12 +68,9 @@ client.admin.command('replSetInitiate', {'_id': 'rs0', 'members': [{'_id': 0, 'h MongoClient().admin.command('hello') " -# uv sync prunes ad-hoc packages, so install mod_wsgi before each setup. Call -# the test scripts directly: the just recipes run "uv sync", which would -# remove mod_wsgi before teardown can stop Apache. +# The mod_wsgi group is part of the synced environment, so both modes can run +# without reinstalling. for MODE in standalone embedded; do - # mod_wsgi >= 5 segfaults Apache children in embedded mode under load. - uv pip install -q "mod_wsgi==4.9.4" bash .evergreen/scripts/setup-tests.sh mod_wsgi $MODE bash .evergreen/run-tests.sh bash .evergreen/scripts/teardown-tests.sh diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index c54c22dc53..6fa0622fbc 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -141,25 +141,37 @@ jobs: just setup-tests doctest just run-tests + mod_wsgi_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - name: Install Python tooling + uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 + - id: matrix + # The Mod WSGI matrix comes from the Evergreen config's version lists. + run: echo "matrix=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py)" >> "$GITHUB_OUTPUT" + mod_wsgi: + needs: mod_wsgi_matrix runs-on: ubuntu-latest strategy: fail-fast: false matrix: mode: ["standalone", "embedded"] - name: Mod WSGI (${{ matrix.mode }}) + include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix) }} + name: Mod WSGI (${{ matrix.mode }} ${{ matrix.python-version }} ${{ matrix.mongodb-version }}) steps: - uses: actions/checkout@v7.0.1 with: persist-credentials: false - # endoflife.date returns releases newest-first, so the first entry is - # the latest stable CPython. - - id: latest-python - run: echo "version=$(curl -fsSL https://endoflife.date/api/python.json | jq -r '.[0].latest')" >> "$GITHUB_OUTPUT" - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.latest-python.outputs.version }} + python-version: ${{ matrix.python-version }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -167,18 +179,17 @@ jobs: # CI skips the venv setup in setup-dev-env.sh, so create it here. export PYMONGO_C_EXT_MUST_BUILD=1 just install - uv sync - # mod_wsgi >= 5 segfaults Apache children in embedded mode under load. - uv pip install "mod_wsgi==4.9.4" + uv sync --group mod_wsgi - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: "8.0" + version: ${{ matrix.mongodb-version }} topology: "replica_set" - name: Run tests + env: + TEST_MIN_DEPS: ${{ matrix.test-min-deps }} # Call the test scripts directly: the just recipes run "uv sync", - # which would remove the ad-hoc mod_wsgi install before teardown can - # stop Apache. + # which would drop the mod_wsgi group before teardown can stop Apache. run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi ${{ matrix.mode }} bash .evergreen/run-tests.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa5f755623..fe8e702ad5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -356,8 +356,7 @@ Continuous integration runs the tests on every pull request in the Mod WSGI job run `just smoke-mod-wsgi`. To run the tests by hand, install Apache and mod_wsgi (`sudo apt-get install -y apache2 -apache2-dev` and `uv pip install "mod_wsgi==4.9.4"` on Ubuntu; mod_wsgi 5+ crashes in embedded -mode), then: +apache2-dev` and `uv sync --group mod_wsgi` on Ubuntu), then: - Run `TOPOLOGY=replica_set just run-server`. - Run `just setup-tests mod_wsgi `. diff --git a/pyproject.toml b/pyproject.toml index 2adddcd29b..ca90b18e90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,8 @@ unasync = [ ] pip = ["pip>=20.2"] gevent = ["gevent>=21.12"] +# mod_wsgi >= 5 crashes Apache children in embedded mode under load. +mod_wsgi = ["mod_wsgi==4.9.4"] coverage = [ "coverage[toml]>=5,<=7.15.0" ] diff --git a/test/mod_wsgi_test/README.rst b/test/mod_wsgi_test/README.rst index f231cb886b..41690fbd3f 100644 --- a/test/mod_wsgi_test/README.rst +++ b/test/mod_wsgi_test/README.rst @@ -13,10 +13,11 @@ mod_wsgi creates. Test Matrix ----------- -Continuous integration tests the latest stable CPython with the mod_wsgi -release from PyPI, against a replica set, using both daemon and embedded -mode. Other combinations of mod_wsgi and Python versions can be tested -manually. +Continuous integration tests the oldest supported CPython against the oldest +supported MongoDB with minimum dependencies, and the newest supported CPython +against the latest MongoDB, in both daemon and embedded mode against a replica +set. The Python and MongoDB versions come from ``generate_config_utils.py`` in +``.evergreen/scripts``. Other combinations can be tested manually. Setup ----- @@ -31,13 +32,11 @@ On Ubuntu, install Apache and the headers used to build mod_wsgi:: Install mod_wsgi ................ -Install mod_wsgi 4.9.4 into the project virtualenv. pip builds mod_wsgi -against the interpreter it is installed with:: +The project defines a ``mod_wsgi`` dependency group that pins the version +used for testing. pip builds mod_wsgi against the interpreter it is +installed with:: - uv pip install "mod_wsgi==4.9.4" - -mod_wsgi releases 5 and newer crash Apache children in embedded mode under -load, so the test pins the 4.9 series. + uv sync --group mod_wsgi Start mongod ............ diff --git a/uv.lock b/uv.lock index f80e0c9a46..f19a7521df 100644 --- a/uv.lock +++ b/uv.lock @@ -58,9 +58,9 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup" }, + { name = "idna" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ @@ -76,9 +76,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.15'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ @@ -358,9 +358,9 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "botocore", version = "1.42.97", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jmespath", marker = "python_full_version < '3.10'" }, - { name = "s3transfer", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "botocore", version = "1.42.97", source = { registry = "https://pypi.org/simple" } }, + { name = "jmespath" }, + { name = "s3transfer", version = "0.16.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/7d/5c6fa0bb9fd5caf865b9356411793900304328bcd0bc1eda96a32a1368a6/boto3-1.42.97.tar.gz", hash = "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", size = 113217, upload-time = "2026-04-27T20:39:17.646Z" } wheels = [ @@ -376,9 +376,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "botocore", version = "1.43.89", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "jmespath", marker = "python_full_version >= '3.10'" }, - { name = "s3transfer", version = "0.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "botocore", version = "1.43.89", source = { registry = "https://pypi.org/simple" } }, + { name = "jmespath" }, + { name = "s3transfer", version = "0.19.2", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/26/48b3da85526a72a02df55e564481fc348e93699c15f0f502681b12ac2c8a/boto3-1.43.89.tar.gz", hash = "sha256:c28abbe472e9b7cad08807356311aeec51bde5218c18489da827045d2267bfd9", size = 112702, upload-time = "2026-09-04T19:24:57.143Z" } wheels = [ @@ -394,9 +394,9 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "jmespath", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, - { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/95/c37edb602948fad2253ffd1bb3dba5b938645bd1845ee4160350136a0f41/botocore-1.42.97.tar.gz", hash = "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", size = 15269348, upload-time = "2026-04-27T20:39:05.625Z" } wheels = [ @@ -412,9 +412,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "jmespath", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/06/f63fb1befdf77af18539fb24ea01f2da0f13965ed5de091061708ac96416/botocore-1.43.89.tar.gz", hash = "sha256:f0574942970742657b0e0716cf08c2dfe6bef8e6de5fbb7081c3424e262b4cca", size = 16074206, upload-time = "2026-09-04T19:24:52.464Z" } wheels = [ @@ -439,7 +439,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -537,7 +537,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ @@ -830,7 +830,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ @@ -976,7 +976,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "tomli" }, ] [[package]] @@ -1083,7 +1083,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -1353,8 +1353,8 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9' and platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version <= '3.9'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } wheels = [ @@ -1418,9 +1418,9 @@ resolution-markers = [ "python_full_version > '3.9' and python_full_version < '3.10'", ] dependencies = [ - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9' and python_full_version < '3.10' and platform_python_implementation != 'PyPy'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy'" }, { name = "cffi", version = "2.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version > '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ @@ -1520,7 +1520,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1554,10 +1554,10 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, - { name = "greenlet", version = "3.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and platform_python_implementation == 'CPython'" }, - { name = "zope-event", version = "6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "zope-interface", version = "8.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", version = "3.2.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event", version = "6.0", source = { registry = "https://pypi.org/simple" } }, + { name = "zope-interface", version = "8.0.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/5c/92002455a57cb3634383e2b822e3bccf409f43cde34528e46428971475cf/gevent-26.7.0.tar.gz", hash = "sha256:5b333a556e38a302b1b8c80525bef16d437e16f1e7767947789406841856a102", size = 6729213, upload-time = "2026-07-22T20:16:04.713Z" } wheels = [ @@ -1617,10 +1617,10 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "cffi", version = "2.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, - { name = "greenlet", version = "3.5.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and platform_python_implementation == 'CPython'" }, - { name = "zope-event", version = "6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "zope-interface", version = "8.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cffi", version = "2.1.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, + { name = "greenlet", version = "3.5.5", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'CPython'" }, + { name = "zope-event", version = "6.2", source = { registry = "https://pypi.org/simple" } }, + { name = "zope-interface", version = "8.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/eb/5f2db8013f1a4a6df2c23201f384a066f13ff5764a9f62a608c8a50ac8cc/gevent-26.8.0.tar.gz", hash = "sha256:96039f41bbde6dcd72559e5ffbd408a04f46774b47d991d4cf032da8fa79e5a0", size = 6625998, upload-time = "2026-08-10T18:02:28.038Z" } wheels = [ @@ -1903,7 +1903,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "zipp", version = "3.23.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "zipp", version = "3.23.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ @@ -1919,7 +1919,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "zipp", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "zipp", version = "4.1.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ @@ -2225,6 +2225,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/c0/bc0c3e82bf91993bf2337b125c4d49c3cadae39f5a4c642baada958f976e/mockupdb-1.8.1-py2.py3-none-any.whl", hash = "sha256:8eb76212963bad5df7f3ea8f26af916087c2dcc69a0457c75328abad44956dbb", size = 27578, upload-time = "2021-10-15T19:02:55.489Z" }, ] +[[package]] +name = "mod-wsgi" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/12/b3756f3b72ae3410a83a9b98862925aea64e854c93ef16c1e46b11e32d59/mod_wsgi-4.9.4.tar.gz", hash = "sha256:8e762662ea5b01afc386bbcfbaa079748eb6203ab1d6d3a3dac9237f5666cfc9", size = 497531, upload-time = "2022-09-12T03:12:20.178Z" } + [[package]] name = "mypy" version = "1.19.1" @@ -2463,6 +2469,9 @@ lint = [ mockupdb = [ { name = "mockupdb" }, ] +mod-wsgi = [ + { name = "mod-wsgi" }, +] perf = [ { name = "simplejson" }, ] @@ -2516,6 +2525,7 @@ dev = [] gevent = [{ name = "gevent", specifier = ">=21.12" }] lint = [{ name = "ruff", specifier = "==0.15.20" }] mockupdb = [{ name = "mockupdb", specifier = ">=1.8.1" }] +mod-wsgi = [{ name = "mod-wsgi", specifier = "==4.9.4" }] perf = [{ name = "simplejson", specifier = ">=3.17.0" }] pip = [{ name = "pip", specifier = ">=20.2" }] typing = [ @@ -2572,8 +2582,8 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "cryptography", version = "47.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version <= '3.9'" }, - { name = "typing-extensions", marker = "python_full_version <= '3.9'" }, + { name = "cryptography", version = "47.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } wheels = [ @@ -2590,8 +2600,8 @@ resolution-markers = [ "python_full_version > '3.9' and python_full_version < '3.10'", ] dependencies = [ - { name = "cryptography", version = "50.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version > '3.9'" }, - { name = "typing-extensions", marker = "python_full_version > '3.9' and python_full_version < '3.13'" }, + { name = "cryptography", version = "50.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } wheels = [ @@ -2620,13 +2630,13 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -2642,13 +2652,13 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -2664,9 +2674,9 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "backports-asyncio-runner" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } wheels = [ @@ -2682,9 +2692,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, - { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ @@ -2734,10 +2744,10 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "certifi", marker = "python_full_version < '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, - { name = "idna", marker = "python_full_version < '3.10'" }, - { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ @@ -2753,10 +2763,10 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.10'" }, - { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, - { name = "idna", marker = "python_full_version >= '3.10'" }, - { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -2777,7 +2787,7 @@ name = "roman-numerals-py" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, + { name = "roman-numerals" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } wheels = [ @@ -2818,7 +2828,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "botocore", version = "1.42.97", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "botocore", version = "1.42.97", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/29/af14f4ef3c11a50435308660e2cc68761c9a7742475e0585cd4396b91777/s3transfer-0.16.1.tar.gz", hash = "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524", size = 154801, upload-time = "2026-04-22T20:36:06.475Z" } wheels = [ @@ -2834,7 +2844,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "botocore", version = "1.43.89", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "botocore", version = "1.43.89", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } wheels = [ @@ -3020,24 +3030,24 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "babel", marker = "python_full_version < '3.10'" }, - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, - { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "jinja2", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" } }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" } }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" } }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } wheels = [ @@ -3052,23 +3062,23 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "babel", marker = "python_full_version == '3.10.*'" }, - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, - { name = "imagesize", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "jinja2", marker = "python_full_version == '3.10.*'" }, - { name = "packaging", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize", version = "2.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" } }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -3083,23 +3093,23 @@ resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize", version = "2.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" } }, + { name = "roman-numerals-py" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ @@ -3116,16 +3126,16 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "colorama" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "starlette", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "uvicorn", version = "0.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "uvicorn", version = "0.52.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "watchfiles", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "watchfiles", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "watchfiles", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "websockets", version = "16.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } @@ -3141,12 +3151,12 @@ resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "starlette", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "uvicorn", version = "0.52.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "watchfiles", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "websockets", version = "17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "colorama" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" } }, + { name = "starlette", version = "1.6.0", source = { registry = "https://pypi.org/simple" } }, + { name = "uvicorn", version = "0.52.4", source = { registry = "https://pypi.org/simple" } }, + { name = "watchfiles", version = "1.2.0", source = { registry = "https://pypi.org/simple" } }, + { name = "websockets", version = "17.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } wheels = [ @@ -3280,8 +3290,8 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } wheels = [ @@ -3297,8 +3307,8 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "anyio", version = "4.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, + { name = "anyio", version = "4.15.1", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ @@ -3426,9 +3436,9 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "h11", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" } }, + { name = "h11" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/4f/f9fdac7cf6dd79790eb165639b5c452ceeabc7bbabbba4569155470a287d/uvicorn-0.39.0.tar.gz", hash = "sha256:610512b19baa93423d2892d7823741f6d27717b642c8964000d7194dded19302", size = 82001, upload-time = "2025-12-21T13:05:17.973Z" } wheels = [ @@ -3444,9 +3454,9 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "h11", marker = "python_full_version >= '3.10'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" } }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } wheels = [ @@ -3462,7 +3472,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "anyio", version = "4.12.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ @@ -3585,7 +3595,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "anyio", version = "4.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "anyio", version = "4.15.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } wheels = [ @@ -4135,7 +4145,7 @@ resolution-markers = [ "python_full_version <= '3.9'", ] dependencies = [ - { name = "setuptools", version = "82.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "setuptools", version = "82.0.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/d8/9c8b0c6bb1db09725395618f68d3b8a08089fca0aed28437500caaf713ee/zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92", size = 18731, upload-time = "2025-09-12T07:10:13.551Z" } wheels = [ From a58e8abda63be3608c3313da07b9d0e6e8580362 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 10:23:45 -0500 Subject: [PATCH 04/21] Fix the mod_wsgi 6 crash and unpin the dependency mod_wsgi 6 destroys interpreters as child processes exit and deletes the thread states of still-running threads, crashing live pymongo monitor threads. Set WSGIDestroyInterpreter Off in the test Apache config to keep the mod_wsgi 4 lifecycle. The smoke script raises the nofile limit so mongod is not starved by the connection storm, and the test client now dumps the Apache error log on any failure. --- .evergreen/scripts/mod_wsgi_smoke_test.sh | 4 ++++ .evergreen/scripts/mod_wsgi_tester.py | 5 +++-- pyproject.toml | 6 ++++-- test/mod_wsgi_test/README.rst | 5 ++--- test/mod_wsgi_test/apache24ubuntu.conf | 5 +++++ uv.lock | 23 +++++++++++++++++++---- 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh index 7c5d5a02c3..408b371eb2 100755 --- a/.evergreen/scripts/mod_wsgi_smoke_test.sh +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -37,6 +37,10 @@ uv tool install rust-just >/dev/null cd /home/smoke/src +# mongod inherits the soft nofile limit; the 1024 default is exhausted by the +# connection storm the parallel test generates. +ulimit -n 65536 + # Mirror the GHA job and test the newest supported CPython. LATEST_PYTHON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py | jq -r '.include[-1]."python-version"') echo "Testing with CPython $LATEST_PYTHON" diff --git a/.evergreen/scripts/mod_wsgi_tester.py b/.evergreen/scripts/mod_wsgi_tester.py index 7c2ddb6db5..2c156a734c 100644 --- a/.evergreen/scripts/mod_wsgi_tester.py +++ b/.evergreen/scripts/mod_wsgi_tester.py @@ -83,9 +83,10 @@ def test_mod_wsgi() -> None: args = f"-n 25000 serial {uri1} {uri2}" main(*parse_args(args.split())) - except Exception as e: + except BaseException: + # The test client raises KeyboardInterrupt in the failing path. LOGGER.error(Path("error_log").read_text()) - raise e + raise def teardown_mod_wsgi() -> None: diff --git a/pyproject.toml b/pyproject.toml index ca90b18e90..1ad4dd70ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,8 +65,10 @@ unasync = [ ] pip = ["pip>=20.2"] gevent = ["gevent>=21.12"] -# mod_wsgi >= 5 crashes Apache children in embedded mode under load. -mod_wsgi = ["mod_wsgi==4.9.4"] +# The test config sets WSGIDestroyInterpreter Off; without it, mod_wsgi 6 +# crashes live pymongo monitor threads when it destroys interpreters as +# child processes exit. +mod_wsgi = ["mod_wsgi>=4.9.4"] coverage = [ "coverage[toml]>=5,<=7.15.0" ] diff --git a/test/mod_wsgi_test/README.rst b/test/mod_wsgi_test/README.rst index 41690fbd3f..73a0d8ce80 100644 --- a/test/mod_wsgi_test/README.rst +++ b/test/mod_wsgi_test/README.rst @@ -32,9 +32,8 @@ On Ubuntu, install Apache and the headers used to build mod_wsgi:: Install mod_wsgi ................ -The project defines a ``mod_wsgi`` dependency group that pins the version -used for testing. pip builds mod_wsgi against the interpreter it is -installed with:: +The project defines a ``mod_wsgi`` dependency group used for testing. pip +builds mod_wsgi against the interpreter it is installed with:: uv sync --group mod_wsgi diff --git a/test/mod_wsgi_test/apache24ubuntu.conf b/test/mod_wsgi_test/apache24ubuntu.conf index 9776d95f19..4d73183c56 100644 --- a/test/mod_wsgi_test/apache24ubuntu.conf +++ b/test/mod_wsgi_test/apache24ubuntu.conf @@ -2,6 +2,11 @@ # The modules directory is here on Ubuntu. ServerRoot "/usr/lib/apache2" + +# mod_wsgi 6 deletes thread states of still-running threads when it destroys +# interpreters as child processes exit, which crashes pymongo monitor threads. +# Keep the mod_wsgi 4 lifecycle where child processes do not finalize Python. +WSGIDestroyInterpreter Off DocumentRoot ${PWD} PidFile ${PWD}/apache2.pid diff --git a/uv.lock b/uv.lock index f19a7521df..932a6e68b6 100644 --- a/uv.lock +++ b/uv.lock @@ -2227,9 +2227,23 @@ wheels = [ [[package]] name = "mod-wsgi" -version = "4.9.4" +version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/12/b3756f3b72ae3410a83a9b98862925aea64e854c93ef16c1e46b11e32d59/mod_wsgi-4.9.4.tar.gz", hash = "sha256:8e762662ea5b01afc386bbcfbaa079748eb6203ab1d6d3a3dac9237f5666cfc9", size = 497531, upload-time = "2022-09-12T03:12:20.178Z" } +resolution-markers = [ + "python_full_version > '3.9' and python_full_version < '3.10'", + "python_full_version <= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/4d/275853efa4d2a4efe1614aa0df06295e71f2082cebccc4d40d0f0a551c24/mod_wsgi-5.0.2.tar.gz", hash = "sha256:6fe5557c3ef25c184c2c994905fe52be1c06ae5c46235ecafe558e84789e2159", size = 498627, upload-time = "2024-11-22T08:53:22.752Z" } + +[[package]] +name = "mod-wsgi" +version = "6.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/ce/fdbfe51f596e37c657176ea64d637737074214bfcd21c2177aa6b1f1da14/mod_wsgi-6.0.6.tar.gz", hash = "sha256:897f58ee961be7dcfe527074dd64d43d18ca108cf919d4c3bcf10fa6a4c12921", size = 607559, upload-time = "2026-08-19T09:51:18.52Z" } [[package]] name = "mypy" @@ -2470,7 +2484,8 @@ mockupdb = [ { name = "mockupdb" }, ] mod-wsgi = [ - { name = "mod-wsgi" }, + { name = "mod-wsgi", version = "5.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mod-wsgi", version = "6.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] perf = [ { name = "simplejson" }, @@ -2525,7 +2540,7 @@ dev = [] gevent = [{ name = "gevent", specifier = ">=21.12" }] lint = [{ name = "ruff", specifier = "==0.15.20" }] mockupdb = [{ name = "mockupdb", specifier = ">=1.8.1" }] -mod-wsgi = [{ name = "mod-wsgi", specifier = "==4.9.4" }] +mod-wsgi = [{ name = "mod-wsgi", specifier = ">=4.9.4" }] perf = [{ name = "simplejson", specifier = ">=3.17.0" }] pip = [{ name = "pip", specifier = ">=20.2" }] typing = [ From b9cfae4bbe4c5818e11e62004911833c03f7cccb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 10:40:39 -0500 Subject: [PATCH 05/21] Split the Mod WSGI jobs into standalone and embedded --- .github/workflows/test-python.yml | 50 ++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 6fa0622fbc..a37268b693 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -155,15 +155,57 @@ jobs: # The Mod WSGI matrix comes from the Evergreen config's version lists. run: echo "matrix=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py)" >> "$GITHUB_OUTPUT" - mod_wsgi: + mod_wsgi_standalone: needs: mod_wsgi_matrix runs-on: ubuntu-latest strategy: fail-fast: false matrix: - mode: ["standalone", "embedded"] - include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix) }} - name: Mod WSGI (${{ matrix.mode }} ${{ matrix.python-version }} ${{ matrix.mongodb-version }}) + mode: ["standalone"] + include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix).include }} + name: Mod WSGI Standalone (${{ matrix.python-version }} ${{ matrix.mongodb-version }}) + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + - name: Install Python tooling + uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Apache + run: sudo apt-get install -y apache2 apache2-dev + - name: Install dependencies + run: | + # CI skips the venv setup in setup-dev-env.sh, so create it here. + export PYMONGO_C_EXT_MUST_BUILD=1 + just install + uv sync --group mod_wsgi + - id: setup-mongodb + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: ${{ matrix.mongodb-version }} + topology: "replica_set" + - name: Run tests + env: + TEST_MIN_DEPS: ${{ matrix.test-min-deps }} + # Call the test scripts directly: the just recipes run "uv sync", + # which would drop the mod_wsgi group before teardown can stop Apache. + run: | + bash .evergreen/scripts/setup-tests.sh mod_wsgi ${{ matrix.mode }} + bash .evergreen/run-tests.sh + - name: Teardown + if: always() + run: bash .evergreen/scripts/teardown-tests.sh + + mod_wsgi_embedded: + needs: mod_wsgi_matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + mode: ["embedded"] + include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix).include }} + name: Mod WSGI Embedded (${{ matrix.python-version }} ${{ matrix.mongodb-version }}) steps: - uses: actions/checkout@v7.0.1 with: From eee671215eb7a0ae36e86d753e694e4a4af211c2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 10:59:58 -0500 Subject: [PATCH 06/21] Run the min and max deps tests in each Mod WSGI job --- .evergreen/scripts/mod_wsgi_matrix.py | 25 +++-- .evergreen/scripts/mod_wsgi_smoke_test.sh | 2 +- .github/workflows/test-python.yml | 108 ++++++++++++++-------- 3 files changed, 82 insertions(+), 53 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_matrix.py b/.evergreen/scripts/mod_wsgi_matrix.py index fe482e9038..2d327df54c 100644 --- a/.evergreen/scripts/mod_wsgi_matrix.py +++ b/.evergreen/scripts/mod_wsgi_matrix.py @@ -6,19 +6,16 @@ # The lowest supported MongoDB with the oldest supported CPython and minimum # dependencies, and the latest MongoDB with the newest supported CPython. -MATRIX = { - "include": [ - { - "python-version": CPYTHONS[0], - "mongodb-version": ALL_VERSIONS[0], - "test-min-deps": "1", - }, - { - "python-version": CPYTHONS[-1], - "mongodb-version": ALL_VERSIONS[-1], - }, - ] -} +VERSIONS = [ + { + "python-version": CPYTHONS[0], + "mongodb-version": ALL_VERSIONS[0], + }, + { + "python-version": CPYTHONS[-1], + "mongodb-version": ALL_VERSIONS[-1], + }, +] if __name__ == "__main__": - print(json.dumps(MATRIX)) + print(json.dumps(VERSIONS)) diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh index 408b371eb2..deb8b6472e 100755 --- a/.evergreen/scripts/mod_wsgi_smoke_test.sh +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -42,7 +42,7 @@ cd /home/smoke/src ulimit -n 65536 # Mirror the GHA job and test the newest supported CPython. -LATEST_PYTHON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py | jq -r '.include[-1]."python-version"') +LATEST_PYTHON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py | jq -r '.[-1]."python-version"') echo "Testing with CPython $LATEST_PYTHON" uv python install "$LATEST_PYTHON" >/dev/null diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index a37268b693..e008599fdf 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -141,37 +141,28 @@ jobs: just setup-tests doctest just run-tests - mod_wsgi_matrix: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - steps: - - uses: actions/checkout@v7.0.1 - with: - persist-credentials: false - - name: Install Python tooling - uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 - - id: matrix - # The Mod WSGI matrix comes from the Evergreen config's version lists. - run: echo "matrix=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py)" >> "$GITHUB_OUTPUT" - mod_wsgi_standalone: - needs: mod_wsgi_matrix runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - mode: ["standalone"] - include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix).include }} - name: Mod WSGI Standalone (${{ matrix.python-version }} ${{ matrix.mongodb-version }}) + name: Mod WSGI Standalone steps: - uses: actions/checkout@v7.0.1 with: persist-credentials: false + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - id: versions + # The versions come from the Evergreen config's version lists. + run: | + JSON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py) + echo "min-python=$(jq -r '.[0]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + # Test the minimum dependencies on the oldest supported Python and + # MongoDB, then the newest on the newest. - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ steps.versions.outputs.min-python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -183,37 +174,60 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ matrix.mongodb-version }} + version: ${{ steps.versions.outputs.min-mongodb }} topology: "replica_set" - - name: Run tests + - name: Run min deps tests env: - TEST_MIN_DEPS: ${{ matrix.test-min-deps }} + TEST_MIN_DEPS: "1" # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | - bash .evergreen/scripts/setup-tests.sh mod_wsgi ${{ matrix.mode }} + bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh + - name: Install max Python tooling + uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 + with: + python-version: ${{ steps.versions.outputs.max-python }} + - name: Rebuild virtual environment + run: rm -rf .venv && uv sync --group mod_wsgi + - id: setup-mongodb-max + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: ${{ steps.versions.outputs.max-mongodb }} + topology: "replica_set" + - name: Run max deps tests + run: | + bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone + bash .evergreen/run-tests.sh + - name: Teardown max + if: always() + run: bash .evergreen/scripts/teardown-tests.sh mod_wsgi_embedded: - needs: mod_wsgi_matrix runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - mode: ["embedded"] - include: ${{ fromJSON(needs.mod_wsgi_matrix.outputs.matrix).include }} - name: Mod WSGI Embedded (${{ matrix.python-version }} ${{ matrix.mongodb-version }}) + name: Mod WSGI Embedded steps: - uses: actions/checkout@v7.0.1 with: persist-credentials: false + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - id: versions + # The versions come from the Evergreen config's version lists. + run: | + JSON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py) + echo "min-python=$(jq -r '.[0]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + # Test the minimum dependencies on the oldest supported Python and + # MongoDB, then the newest on the newest. - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ steps.versions.outputs.min-python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -225,19 +239,37 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ matrix.mongodb-version }} + version: ${{ steps.versions.outputs.min-mongodb }} topology: "replica_set" - - name: Run tests + - name: Run min deps tests env: - TEST_MIN_DEPS: ${{ matrix.test-min-deps }} + TEST_MIN_DEPS: "1" # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | - bash .evergreen/scripts/setup-tests.sh mod_wsgi ${{ matrix.mode }} + bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded bash .evergreen/run-tests.sh - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh + - name: Install max Python tooling + uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 + with: + python-version: ${{ steps.versions.outputs.max-python }} + - name: Rebuild virtual environment + run: rm -rf .venv && uv sync --group mod_wsgi + - id: setup-mongodb-max + uses: mongodb-labs/drivers-evergreen-tools@master + with: + version: ${{ steps.versions.outputs.max-mongodb }} + topology: "replica_set" + - name: Run max deps tests + run: | + bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded + bash .evergreen/run-tests.sh + - name: Teardown max + if: always() + run: bash .evergreen/scripts/teardown-tests.sh docs: name: Docs Checks From dcd8738573f4a19a8bf2f6791456afb518159aa6 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 11:09:51 -0500 Subject: [PATCH 07/21] Test MongoDB 8.0 as the lowest version; ubuntu runners cannot install older --- .evergreen/scripts/mod_wsgi_matrix.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_matrix.py b/.evergreen/scripts/mod_wsgi_matrix.py index 2d327df54c..f529df3238 100644 --- a/.evergreen/scripts/mod_wsgi_matrix.py +++ b/.evergreen/scripts/mod_wsgi_matrix.py @@ -4,12 +4,13 @@ from generate_config_utils import ALL_VERSIONS, CPYTHONS -# The lowest supported MongoDB with the oldest supported CPython and minimum -# dependencies, and the latest MongoDB with the newest supported CPython. +# MongoDB 8.0 with the oldest supported CPython and minimum dependencies, and +# the latest MongoDB with the newest supported CPython. ubuntu-24.04 runners +# cannot install MongoDB older than 8.0, so no earlier version can be tested. VERSIONS = [ { "python-version": CPYTHONS[0], - "mongodb-version": ALL_VERSIONS[0], + "mongodb-version": "8.0", }, { "python-version": CPYTHONS[-1], From 06e4fa0bf37116dccbab6bad6779efc8b36e2a3c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 11:59:28 -0500 Subject: [PATCH 08/21] Force a fresh C extension build in both Mod WSGI rounds --- .github/workflows/test-python.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index e008599fdf..a7a3e3af62 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -170,7 +170,8 @@ jobs: # CI skips the venv setup in setup-dev-env.sh, so create it here. export PYMONGO_C_EXT_MUST_BUILD=1 just install - uv sync --group mod_wsgi + uv sync --group mod_wsgi --refresh-package pymongo + uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: @@ -192,7 +193,11 @@ jobs: with: python-version: ${{ steps.versions.outputs.max-python }} - name: Rebuild virtual environment - run: rm -rf .venv && uv sync --group mod_wsgi + run: | + rm -rf .venv + export PYMONGO_C_EXT_MUST_BUILD=1 + uv sync --group mod_wsgi --refresh-package pymongo + uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb-max uses: mongodb-labs/drivers-evergreen-tools@master with: @@ -235,7 +240,8 @@ jobs: # CI skips the venv setup in setup-dev-env.sh, so create it here. export PYMONGO_C_EXT_MUST_BUILD=1 just install - uv sync --group mod_wsgi + uv sync --group mod_wsgi --refresh-package pymongo + uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: @@ -257,7 +263,11 @@ jobs: with: python-version: ${{ steps.versions.outputs.max-python }} - name: Rebuild virtual environment - run: rm -rf .venv && uv sync --group mod_wsgi + run: | + rm -rf .venv + export PYMONGO_C_EXT_MUST_BUILD=1 + uv sync --group mod_wsgi --refresh-package pymongo + uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb-max uses: mongodb-labs/drivers-evergreen-tools@master with: From 9dec93a40e23aa778d9f2d94b73d2f08064aaf2e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 14:03:33 -0500 Subject: [PATCH 09/21] Run the min deps round last; it rewrites the lock file --- .github/workflows/test-python.yml | 54 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index a7a3e3af62..b484b43c5f 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -157,12 +157,13 @@ jobs: echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - # Test the minimum dependencies on the oldest supported Python and - # MongoDB, then the newest on the newest. + # Test the newest dependencies on the newest Python and MongoDB, then + # the minimum dependencies on the oldest supported Python and MongoDB. + # The min deps test rewrites the lock file, so it runs last. - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.min-python }} + python-version: ${{ steps.versions.outputs.max-python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -175,11 +176,9 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.min-mongodb }} + version: ${{ steps.versions.outputs.max-mongodb }} topology: "replica_set" - - name: Run min deps tests - env: - TEST_MIN_DEPS: "1" + - name: Run max deps tests # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | @@ -188,26 +187,28 @@ jobs: - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh - - name: Install max Python tooling + - name: Install min Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.max-python }} + python-version: ${{ steps.versions.outputs.min-python }} - name: Rebuild virtual environment run: | rm -rf .venv export PYMONGO_C_EXT_MUST_BUILD=1 uv sync --group mod_wsgi --refresh-package pymongo uv run --no-sync python tools/fail_if_no_c.py - - id: setup-mongodb-max + - id: setup-mongodb-min uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.max-mongodb }} + version: ${{ steps.versions.outputs.min-mongodb }} topology: "replica_set" - - name: Run max deps tests + - name: Run min deps tests + env: + TEST_MIN_DEPS: "1" run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh - - name: Teardown max + - name: Teardown min if: always() run: bash .evergreen/scripts/teardown-tests.sh @@ -227,12 +228,13 @@ jobs: echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - # Test the minimum dependencies on the oldest supported Python and - # MongoDB, then the newest on the newest. + # Test the newest dependencies on the newest Python and MongoDB, then + # the minimum dependencies on the oldest supported Python and MongoDB. + # The min deps test rewrites the lock file, so it runs last. - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.min-python }} + python-version: ${{ steps.versions.outputs.max-python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -245,11 +247,9 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.min-mongodb }} + version: ${{ steps.versions.outputs.max-mongodb }} topology: "replica_set" - - name: Run min deps tests - env: - TEST_MIN_DEPS: "1" + - name: Run max deps tests # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | @@ -258,26 +258,28 @@ jobs: - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh - - name: Install max Python tooling + - name: Install min Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.max-python }} + python-version: ${{ steps.versions.outputs.min-python }} - name: Rebuild virtual environment run: | rm -rf .venv export PYMONGO_C_EXT_MUST_BUILD=1 uv sync --group mod_wsgi --refresh-package pymongo uv run --no-sync python tools/fail_if_no_c.py - - id: setup-mongodb-max + - id: setup-mongodb-min uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.max-mongodb }} + version: ${{ steps.versions.outputs.min-mongodb }} topology: "replica_set" - - name: Run max deps tests + - name: Run min deps tests + env: + TEST_MIN_DEPS: "1" run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded bash .evergreen/run-tests.sh - - name: Teardown max + - name: Teardown min if: always() run: bash .evergreen/scripts/teardown-tests.sh From 950c310d8a919716d8b2bb4bf8a4737c640171c7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 17:07:14 -0500 Subject: [PATCH 10/21] Run the Mod WSGI jobs on ubuntu-22.04 and test MongoDB 6.0 for min deps --- .evergreen/scripts/mod_wsgi_matrix.py | 8 ++++---- .github/workflows/test-python.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_matrix.py b/.evergreen/scripts/mod_wsgi_matrix.py index f529df3238..ea8add69e9 100644 --- a/.evergreen/scripts/mod_wsgi_matrix.py +++ b/.evergreen/scripts/mod_wsgi_matrix.py @@ -4,13 +4,13 @@ from generate_config_utils import ALL_VERSIONS, CPYTHONS -# MongoDB 8.0 with the oldest supported CPython and minimum dependencies, and -# the latest MongoDB with the newest supported CPython. ubuntu-24.04 runners -# cannot install MongoDB older than 8.0, so no earlier version can be tested. +# MongoDB 6.0 with the oldest supported CPython and minimum dependencies, and +# the latest MongoDB with the newest supported CPython. The jobs run on +# ubuntu-22.04 runners, where MongoDB 6.0 is the oldest version that installs. VERSIONS = [ { "python-version": CPYTHONS[0], - "mongodb-version": "8.0", + "mongodb-version": "6.0", }, { "python-version": CPYTHONS[-1], diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index b484b43c5f..2b2d03e681 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -142,7 +142,7 @@ jobs: just run-tests mod_wsgi_standalone: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Mod WSGI Standalone steps: - uses: actions/checkout@v7.0.1 @@ -213,7 +213,7 @@ jobs: run: bash .evergreen/scripts/teardown-tests.sh mod_wsgi_embedded: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Mod WSGI Embedded steps: - uses: actions/checkout@v7.0.1 From bb3697261b4fc70c261b6d94ec1b69ca0dae29fd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 18 Sep 2026 18:06:07 -0500 Subject: [PATCH 11/21] Split the Mod WSGI jobs by round: min deps and latest --- .github/workflows/test-python.yml | 84 ++++++++----------------------- 1 file changed, 22 insertions(+), 62 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 2b2d03e681..b7c43583c1 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -141,9 +141,9 @@ jobs: just setup-tests doctest just run-tests - mod_wsgi_standalone: + mod_wsgi_min_deps: runs-on: ubuntu-22.04 - name: Mod WSGI Standalone + name: Mod WSGI Min Deps steps: - uses: actions/checkout@v7.0.1 with: @@ -153,17 +153,12 @@ jobs: # The versions come from the Evergreen config's version lists. run: | JSON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py) - echo "min-python=$(jq -r '.[0]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - # Test the newest dependencies on the newest Python and MongoDB, then - # the minimum dependencies on the oldest supported Python and MongoDB. - # The min deps test rewrites the lock file, so it runs last. + echo "python=$(jq -r '.[0]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.max-python }} + python-version: ${{ steps.versions.outputs.python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -176,9 +171,11 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.max-mongodb }} + version: ${{ steps.versions.outputs.mongodb }} topology: "replica_set" - - name: Run max deps tests + - name: Run standalone tests + env: + TEST_MIN_DEPS: "1" # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | @@ -187,34 +184,19 @@ jobs: - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh - - name: Install min Python tooling - uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 - with: - python-version: ${{ steps.versions.outputs.min-python }} - - name: Rebuild virtual environment - run: | - rm -rf .venv - export PYMONGO_C_EXT_MUST_BUILD=1 - uv sync --group mod_wsgi --refresh-package pymongo - uv run --no-sync python tools/fail_if_no_c.py - - id: setup-mongodb-min - uses: mongodb-labs/drivers-evergreen-tools@master - with: - version: ${{ steps.versions.outputs.min-mongodb }} - topology: "replica_set" - - name: Run min deps tests + - name: Run embedded tests env: TEST_MIN_DEPS: "1" run: | - bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone + bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded bash .evergreen/run-tests.sh - - name: Teardown min + - name: Teardown embedded if: always() run: bash .evergreen/scripts/teardown-tests.sh - mod_wsgi_embedded: + mod_wsgi_latest: runs-on: ubuntu-22.04 - name: Mod WSGI Embedded + name: Mod WSGI Latest steps: - uses: actions/checkout@v7.0.1 with: @@ -224,17 +206,12 @@ jobs: # The versions come from the Evergreen config's version lists. run: | JSON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py) - echo "min-python=$(jq -r '.[0]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "min-mongodb=$(jq -r '.[0]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "max-python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - echo "max-mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - # Test the newest dependencies on the newest Python and MongoDB, then - # the minimum dependencies on the oldest supported Python and MongoDB. - # The min deps test rewrites the lock file, so it runs last. + echo "python=$(jq -r '.[1]."python-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" + echo "mongodb=$(jq -r '.[1]."mongodb-version"' <<<"$JSON")" >> "$GITHUB_OUTPUT" - name: Install Python tooling uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 with: - python-version: ${{ steps.versions.outputs.max-python }} + python-version: ${{ steps.versions.outputs.python }} - name: Install Apache run: sudo apt-get install -y apache2 apache2-dev - name: Install dependencies @@ -247,39 +224,22 @@ jobs: - id: setup-mongodb uses: mongodb-labs/drivers-evergreen-tools@master with: - version: ${{ steps.versions.outputs.max-mongodb }} + version: ${{ steps.versions.outputs.mongodb }} topology: "replica_set" - - name: Run max deps tests + - name: Run standalone tests # Call the test scripts directly: the just recipes run "uv sync", # which would drop the mod_wsgi group before teardown can stop Apache. run: | - bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded + bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh - name: Teardown if: always() run: bash .evergreen/scripts/teardown-tests.sh - - name: Install min Python tooling - uses: mongodb-labs/drivers-github-tools/python/setup@1a2d86bd5fed4b0090453bcd510e4c2ba3b92356 # v3.0.4 - with: - python-version: ${{ steps.versions.outputs.min-python }} - - name: Rebuild virtual environment - run: | - rm -rf .venv - export PYMONGO_C_EXT_MUST_BUILD=1 - uv sync --group mod_wsgi --refresh-package pymongo - uv run --no-sync python tools/fail_if_no_c.py - - id: setup-mongodb-min - uses: mongodb-labs/drivers-evergreen-tools@master - with: - version: ${{ steps.versions.outputs.min-mongodb }} - topology: "replica_set" - - name: Run min deps tests - env: - TEST_MIN_DEPS: "1" + - name: Run embedded tests run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi embedded bash .evergreen/run-tests.sh - - name: Teardown min + - name: Teardown embedded if: always() run: bash .evergreen/scripts/teardown-tests.sh From 7ac7b4080bb5731d36761f1d92687b010468e90c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 12:25:50 -0500 Subject: [PATCH 12/21] PYTHON-6114 Pin drivers-evergreen-tools in the Mod WSGI jobs Pin the action to a commit hash so the PR's CodeQL check no longer flags the new setup-mongodb steps for using an unpinned third-party action ref. --- .github/workflows/test-python.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index b7c43583c1..3efd660cf2 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -169,7 +169,8 @@ jobs: uv sync --group mod_wsgi --refresh-package pymongo uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb - uses: mongodb-labs/drivers-evergreen-tools@master + # Pinned to a commit hash to satisfy CodeQL's ref-pin check. + uses: mongodb-labs/drivers-evergreen-tools@63fb7765ba349b1c8495f722e1942ef39e1b05b9 with: version: ${{ steps.versions.outputs.mongodb }} topology: "replica_set" @@ -222,7 +223,8 @@ jobs: uv sync --group mod_wsgi --refresh-package pymongo uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb - uses: mongodb-labs/drivers-evergreen-tools@master + # Pinned to a commit hash to satisfy CodeQL's ref-pin check. + uses: mongodb-labs/drivers-evergreen-tools@63fb7765ba349b1c8495f722e1942ef39e1b05b9 with: version: ${{ steps.versions.outputs.mongodb }} topology: "replica_set" From e1881e66da752cf5765a27f2ddf643e366d8caf3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 20 Sep 2026 12:04:44 +0000 Subject: [PATCH 13/21] PYTHON-6114 Stop executor threads on subinterpreter shutdown PeriodicExecutor now falls back to a non-daemon thread when daemon threads are disallowed (subinterpreters). The monitor shutdown handler is also registered with threading._register_atexit so executors are stopped and joined before interpreter teardown. Adds regression tests for PeriodicExecutor in a real subinterpreter and for concurrent MongoClients across subinterpreters (Python 3.14+). --- pymongo/asynchronous/monitor.py | 7 ++ pymongo/periodic_executor.py | 9 +- pymongo/synchronous/monitor.py | 7 ++ test/asynchronous/test_periodic_executor.py | 43 ++++++++ test/test_periodic_executor.py | 43 ++++++++ test/test_threads.py | 104 ++++++++++++++++++++ 6 files changed, 212 insertions(+), 1 deletion(-) diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index ba0e3804a6..179f459837 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -18,6 +18,7 @@ import asyncio import atexit +import threading import time import weakref from typing import TYPE_CHECKING, Any, Optional @@ -492,3 +493,9 @@ def _shutdown_resources() -> None: if _IS_SYNC: atexit.register(_shutdown_resources) + # Also register with threading so executors are stopped before the + # interpreter tries to join their (possibly non-daemon) threads. Unlike + # atexit, this runs for subinterpreters, where daemon threads are + # generally not allowed and CPython is strict about background threads. + if hasattr(threading, "_register_atexit"): + threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index 4b979ca9f9..c6f444480c 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -185,7 +185,14 @@ def open(self) -> None: if not started: thread = threading.Thread(target=self._run, name=self._name) - thread.daemon = True + try: + # Daemon threads are disabled in subinterpreters unless the + # interpreter was created with allow_daemon_threads=True. + # _shutdown_executors stops and joins the thread during + # interpreter shutdown, so a non-daemon thread is safe. + thread.daemon = True + except RuntimeError: + pass self._thread = weakref.proxy(thread) _register_executor(self) # Mitigation to RuntimeError firing when thread starts on shutdown diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index 9a25757f03..8a445fce5d 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -18,6 +18,7 @@ import asyncio import atexit +import threading import time import weakref from typing import TYPE_CHECKING, Any, Optional @@ -490,3 +491,9 @@ def _shutdown_resources() -> None: if _IS_SYNC: atexit.register(_shutdown_resources) + # Also register with threading so executors are stopped before the + # interpreter tries to join their (possibly non-daemon) threads. Unlike + # atexit, this runs for subinterpreters, where daemon threads are + # generally not allowed and CPython is strict about background threads. + if hasattr(threading, "_register_atexit"): + threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] diff --git a/test/asynchronous/test_periodic_executor.py b/test/asynchronous/test_periodic_executor.py index 15186c4f88..97a2e1fd13 100644 --- a/test/asynchronous/test_periodic_executor.py +++ b/test/asynchronous/test_periodic_executor.py @@ -17,17 +17,28 @@ from __future__ import annotations import asyncio +import importlib +import os import sys +import textwrap import threading import time +from typing import Any + +_interpreters: Any = None +if sys.version_info >= (3, 14): + _interpreters = importlib.import_module("concurrent.interpreters") sys.path[0:0] = [""] +import pymongo from pymongo.periodic_executor import AsyncPeriodicExecutor from test.asynchronous import AsyncUnitTest, unittest _IS_SYNC = False +_PYMONGO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(pymongo.__file__))) + class TestAsyncPeriodicExecutor(AsyncUnitTest): def _make_executor(self, interval=30.0, min_interval=0.01, target=None, name="test"): @@ -178,6 +189,38 @@ async def target(): executor._task.exception() self.assertEqual(call_count, 2, "executor should run again after re-open") + async def test_subinterpreter_shutdown(self): + if not _IS_SYNC: + self.skipTest("subinterpreters are only used with the sync driver") + if _interpreters is None: + self.skipTest("concurrent.interpreters requires Python 3.14+") + return + + root = _PYMONGO_ROOT + code = textwrap.dedent( + f""" + import sys + sys.path.insert(0, {root!r}) + from pymongo.periodic_executor import PeriodicExecutor + + def target(): + return True + + executor = PeriodicExecutor( + interval=30.0, min_interval=0.05, target=target, name="subinterp" + ) + executor.open() + """ + ) + interp = _interpreters.create() + try: + interp.exec(code) + finally: + # Destroying the subinterpreter must stop and join the executor + # thread without crashing or hanging. Regression test for + # PYTHON-6114. + interp.close() + if __name__ == "__main__": unittest.main() diff --git a/test/test_periodic_executor.py b/test/test_periodic_executor.py index e6bbb31a72..622e9ad070 100644 --- a/test/test_periodic_executor.py +++ b/test/test_periodic_executor.py @@ -17,17 +17,28 @@ from __future__ import annotations import asyncio +import importlib +import os import sys +import textwrap import threading import time +from typing import Any + +_interpreters: Any = None +if sys.version_info >= (3, 14): + _interpreters = importlib.import_module("concurrent.interpreters") sys.path[0:0] = [""] +import pymongo from pymongo.periodic_executor import PeriodicExecutor from test import UnitTest, unittest _IS_SYNC = True +_PYMONGO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(pymongo.__file__))) + class TestPeriodicExecutor(UnitTest): def _make_executor(self, interval=30.0, min_interval=0.01, target=None, name="test"): @@ -178,6 +189,38 @@ def target(): executor._task.exception() self.assertEqual(call_count, 2, "executor should run again after re-open") + def test_subinterpreter_shutdown(self): + if not _IS_SYNC: + self.skipTest("subinterpreters are only used with the sync driver") + if _interpreters is None: + self.skipTest("concurrent.interpreters requires Python 3.14+") + return + + root = _PYMONGO_ROOT + code = textwrap.dedent( + f""" + import sys + sys.path.insert(0, {root!r}) + from pymongo.periodic_executor import PeriodicExecutor + + def target(): + return True + + executor = PeriodicExecutor( + interval=30.0, min_interval=0.05, target=target, name="subinterp" + ) + executor.open() + """ + ) + interp = _interpreters.create() + try: + interp.exec(code) + finally: + # Destroying the subinterpreter must stop and join the executor + # thread without crashing or hanging. Regression test for + # PYTHON-6114. + interp.close() + if __name__ == "__main__": unittest.main() diff --git a/test/test_threads.py b/test/test_threads.py index 1b241751ee..11a1bfe8e6 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -16,7 +16,15 @@ from __future__ import annotations +import sys +import textwrap import threading +import uuid + +try: + from concurrent import interpreters +except ImportError: # pragma: no cover - Python < 3.14 + interpreters = None # type: ignore[assignment] from test import IntegrationTest, client_context, unittest from test.utils import joinall @@ -160,6 +168,102 @@ def test_safe_update(self): error.join() okay.join() + @staticmethod + def _get_n(queue, n, errors): + try: + return sorted(queue.get(timeout=30) for _ in range(n)) + except interpreters.QueueEmpty: + raise AssertionError(f"subinterpreters failed to run: {errors!r}") from None + + @unittest.skipUnless( + sys.version_info >= (3, 14), "concurrent.interpreters requires Python 3.14+" + ) + def test_subinterpreters(self): + if interpreters is None: + self.skipTest("concurrent.interpreters is not available") + + # Run live MongoClients in more than one subinterpreter at the same + # time. This mirrors the mod_wsgi test, which mounts the same app in + # two interpreters, and covers pymongo shutting down its background + # threads when an interpreter is destroyed (PYTHON-6114). + n_interpreters = 2 + coll_name = f"subinterp-{uuid.uuid4().hex}" + self.addCleanup(self.db.drop_collection, coll_name) + + ready = interpreters.create_queue() + release = interpreters.create_queue() + done = interpreters.create_queue() + code = textwrap.dedent( + """ + import sys + sys.path[:0] = path + + from pymongo import MongoClient + + client = MongoClient(uri, serverSelectionTimeoutMS=30000) + collection = client.get_database(db_name).get_collection(coll_name) + collection.insert_one({"subinterp": i}) + assert collection.find_one({"subinterp": i}) is not None + ready.put(i) + # Hold the client open until every interpreter has connected, so + # that all of the clients are live at the same time. + release.get(timeout=60) + assert collection.find_one({"subinterp": i}) is not None + done.put(i) + """ + ) + + errors: list[BaseException] = [] + + def run(interp): + try: + interp.exec(code) + except BaseException as exc: + errors.append(exc) + + interps = [] + threads = [] + try: + for i in range(n_interpreters): + interp = interpreters.create() + interp.prepare_main( + uri=client_context.uri, + db_name=self.db.name, + coll_name=coll_name, + i=i, + path=tuple(sys.path), + ready=ready, + release=release, + done=done, + ) + interps.append(interp) + thread = threading.Thread(target=run, args=(interp,), name=f"subinterp-{i}") + threads.append(thread) + thread.start() + + started = self._get_n(ready, n_interpreters, errors) + self.assertEqual(started, list(range(n_interpreters))) + for _ in range(n_interpreters): + release.put(True) + + for thread in threads: + thread.join(60) + self.assertFalse(thread.is_alive(), f"{thread.name} did not exit") + + finished = self._get_n(done, n_interpreters, errors) + self.assertEqual(finished, list(range(n_interpreters))) + if errors: + self.fail(f"subinterpreter errors: {errors!r}") + finally: + # Unblock any interpreter still waiting, then destroy them all. + for _ in range(n_interpreters): + release.put(True) + for interp in interps: + interp.close() + + found = sorted(doc["subinterp"] for doc in self.db[coll_name].find({}, {"subinterp": 1})) + self.assertEqual(found, list(range(n_interpreters))) + if __name__ == "__main__": unittest.main() From ccb71a7a29f0b156ce0fd8142d3c5a1b1c9258a9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 13:41:15 -0500 Subject: [PATCH 14/21] PYTHON-5418 Add a test using InterpreterPoolExecutor Runs live MongoClients inside interpreters managed by the standard InterpreterPoolExecutor, mirroring the existing multi-threaded checks. The pool's interpreters do not allow daemon threads, so this exercises the non-daemon monitor thread fallback and its shutdown handling. --- test/test_threads.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/test_threads.py b/test/test_threads.py index 11a1bfe8e6..edd2dd85f8 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -26,6 +26,11 @@ except ImportError: # pragma: no cover - Python < 3.14 interpreters = None # type: ignore[assignment] +try: + from concurrent.futures import InterpreterPoolExecutor +except ImportError: # pragma: no cover - Python < 3.14 + InterpreterPoolExecutor = None # type: ignore[assignment,misc] + from test import IntegrationTest, client_context, unittest from test.utils import joinall @@ -35,6 +40,20 @@ def setUpModule(): pass +def _interpreter_pool_worker(i, uri, db_name, coll_name, path): + import sys + + sys.path[:0] = list(path) + + from pymongo import MongoClient + + client: MongoClient = MongoClient(uri, serverSelectionTimeoutMS=30000) + collection = client.get_database(db_name).get_collection(coll_name) + collection.insert_one({"interp-pool": i}) + assert collection.find_one({"interp-pool": i}) is not None + return i + + class AutoAuthenticateThreads(threading.Thread): def __init__(self, collection, num): threading.Thread.__init__(self) @@ -264,6 +283,34 @@ def run(interp): found = sorted(doc["subinterp"] for doc in self.db[coll_name].find({}, {"subinterp": 1})) self.assertEqual(found, list(range(n_interpreters))) + @unittest.skipUnless( + sys.version_info >= (3, 14), "InterpreterPoolExecutor requires Python 3.14+" + ) + def test_interpreter_pool_executor(self): + if InterpreterPoolExecutor is None: + self.skipTest("InterpreterPoolExecutor is not available") + + # Run live MongoClients inside interpreters managed by the standard + # InterpreterPoolExecutor (PYTHON-5418). The pool's interpreters do + # not allow daemon threads, so pymongo must start non-daemon monitor + # threads and stop them when the interpreter is destroyed. + n_interpreters = 2 + coll_name = f"interp-pool-{uuid.uuid4().hex}" + self.addCleanup(self.db.drop_collection, coll_name) + + args = (client_context.uri, self.db.name, coll_name, tuple(sys.path)) + with InterpreterPoolExecutor(max_workers=n_interpreters) as executor: + futures = [ + executor.submit(_interpreter_pool_worker, i, *args) for i in range(n_interpreters) + ] + for i, future in enumerate(futures): + self.assertEqual(future.result(timeout=120), i) + + found = sorted( + doc["interp-pool"] for doc in self.db[coll_name].find({}, {"interp-pool": 1}) + ) + self.assertEqual(found, list(range(n_interpreters))) + if __name__ == "__main__": unittest.main() From 63cc5ac4efba0fb3e2ab553658308cf097d12be0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 14:41:42 -0500 Subject: [PATCH 15/21] PYTHON-5418 Submit exec to the InterpreterPoolExecutor The worker interpreters may not have the repo root on sys.path (the stdlib test package shadows the repo's), so the pickled worker function failed to unpickle. Submit the builtin exec with a code string that inserts the main interpreter's sys.path before importing pymongo. --- test/test_threads.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/test_threads.py b/test/test_threads.py index edd2dd85f8..fa015856a2 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -40,20 +40,6 @@ def setUpModule(): pass -def _interpreter_pool_worker(i, uri, db_name, coll_name, path): - import sys - - sys.path[:0] = list(path) - - from pymongo import MongoClient - - client: MongoClient = MongoClient(uri, serverSelectionTimeoutMS=30000) - collection = client.get_database(db_name).get_collection(coll_name) - collection.insert_one({"interp-pool": i}) - assert collection.find_one({"interp-pool": i}) is not None - return i - - class AutoAuthenticateThreads(threading.Thread): def __init__(self, collection, num): threading.Thread.__init__(self) @@ -298,13 +284,26 @@ def test_interpreter_pool_executor(self): coll_name = f"interp-pool-{uuid.uuid4().hex}" self.addCleanup(self.db.drop_collection, coll_name) - args = (client_context.uri, self.db.name, coll_name, tuple(sys.path)) + # The worker runs in an interpreter whose sys.path has not picked up + # the repo root, so pass the callable as a builtin (exec) and insert + # the main interpreter's sys.path before importing pymongo. + code = textwrap.dedent( + f""" + import sys + sys.path[:0] = {tuple(sys.path)!r} + + from pymongo import MongoClient + + client = MongoClient({client_context.uri!r}, serverSelectionTimeoutMS=30000) + collection = client.get_database({self.db.name!r}).get_collection({coll_name!r}) + collection.insert_one({{"interp-pool": i}}) + assert collection.find_one({{"interp-pool": i}}) is not None + """ + ) with InterpreterPoolExecutor(max_workers=n_interpreters) as executor: - futures = [ - executor.submit(_interpreter_pool_worker, i, *args) for i in range(n_interpreters) - ] - for i, future in enumerate(futures): - self.assertEqual(future.result(timeout=120), i) + futures = [executor.submit(exec, code, {"i": i}) for i in range(n_interpreters)] + for future in futures: + future.result(timeout=120) found = sorted( doc["interp-pool"] for doc in self.db[coll_name].find({}, {"interp-pool": 1}) From e1588fa739c3cefcd8f3adcc2ce908147acead54 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:09:30 -0500 Subject: [PATCH 16/21] PYTHON-6114 Address review feedback - Pin uv to an immutable release with checksum verification in the smoke test instead of piping the mutable install script into a shell. - Install the minimum mod_wsgi version (4.9.4) in the min deps job; the lock file has the latest version and TEST_MIN_DEPS does not cover the mod_wsgi group. - Only register the monitor shutdown handler with threading._register_atexit in interpreters that disallow daemon threads, preserving normal main interpreter shutdown ordering. - Pin actions/checkout to a commit hash to satisfy the semgrep mutable-action-tag check. - Clarify the test matrix documentation: MongoDB 6.0 is hardcoded because ubuntu-22.04 runners cannot install older versions. - Add timeout-minutes and apt-get update to the new jobs, guard interpreter cleanup in the tests, and fix the rationale for calling the test scripts directly in the CONTRIBUTING instructions. --- .evergreen/scripts/mod_wsgi_smoke_test.sh | 21 +++++++++-- .github/workflows/test-python.yml | 43 ++++++++++++++--------- CONTRIBUTING.md | 9 +++-- pymongo/asynchronous/monitor.py | 17 +++++---- pymongo/periodic_executor.py | 5 ++- pymongo/synchronous/monitor.py | 17 +++++---- test/mod_wsgi_test/README.rst | 10 +++--- test/test_threads.py | 3 +- 8 files changed, 84 insertions(+), 41 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh index deb8b6472e..17c8715b33 100755 --- a/.evergreen/scripts/mod_wsgi_smoke_test.sh +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -31,15 +31,30 @@ if [ "$(id -u)" = "0" ]; then exec su - smoke -c "bash /src/.evergreen/scripts/mod_wsgi_smoke_test.sh" fi -curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null +# Install uv from a pinned release, verifying the published checksum, rather +# than piping a mutable installer script into a shell. +UV_VERSION=0.12.17 +case "$(uname -m)" in + x86_64) UV_TARGET=x86_64-unknown-linux-gnu ;; + aarch64 | arm64) UV_TARGET=aarch64-unknown-linux-gnu ;; + *) echo "Unsupported architecture" >&2; exit 1 ;; +esac +curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz" +curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz.sha256" +(cd /tmp && sha256sum -c "uv-${UV_TARGET}.tar.gz.sha256") +tar -xzf "/tmp/uv-${UV_TARGET}.tar.gz" -C /tmp +mkdir -p "$HOME/.local/bin" +install -m 0755 "/tmp/uv-${UV_TARGET}/uv" "$HOME/.local/bin/uv" +rm -rf "/tmp/uv-${UV_TARGET}.tar.gz" "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "/tmp/uv-${UV_TARGET}" export PATH="$HOME/.local/bin:$PATH" uv tool install rust-just >/dev/null cd /home/smoke/src # mongod inherits the soft nofile limit; the 1024 default is exhausted by the -# connection storm the parallel test generates. -ulimit -n 65536 +# connection storm the parallel test generates. Best effort: some container +# hosts set a lower hard limit. +ulimit -n 65536 2>/dev/null || true # Mirror the GHA job and test the newest supported CPython. LATEST_PYTHON=$(uv run --no-project --with 'shrub.py>=3.10.0' python .evergreen/scripts/mod_wsgi_matrix.py | jq -r '.[-1]."python-version"') diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 3efd660cf2..84b26f5d67 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -22,7 +22,7 @@ jobs: static: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -76,7 +76,7 @@ jobs: name: Python ${{ matrix.python-version }}-${{ matrix.os }} steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -98,7 +98,7 @@ jobs: name: Coverage steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -123,7 +123,7 @@ jobs: runs-on: ubuntu-latest name: DocTest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -143,9 +143,10 @@ jobs: mod_wsgi_min_deps: runs-on: ubuntu-22.04 + timeout-minutes: 60 name: Mod WSGI Min Deps steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -160,13 +161,18 @@ jobs: with: python-version: ${{ steps.versions.outputs.python }} - name: Install Apache - run: sudo apt-get install -y apache2 apache2-dev + run: | + sudo apt-get update + sudo apt-get install -y apache2 apache2-dev - name: Install dependencies run: | # CI skips the venv setup in setup-dev-env.sh, so create it here. export PYMONGO_C_EXT_MUST_BUILD=1 just install uv sync --group mod_wsgi --refresh-package pymongo + # Test the minimum mod_wsgi version declared in pyproject.toml; + # the lock file has the latest version. + uv pip install "mod_wsgi==4.9.4" uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb # Pinned to a commit hash to satisfy CodeQL's ref-pin check. @@ -177,8 +183,8 @@ jobs: - name: Run standalone tests env: TEST_MIN_DEPS: "1" - # Call the test scripts directly: the just recipes run "uv sync", - # which would drop the mod_wsgi group before teardown can stop Apache. + # Call the test scripts directly: the just recipes run an exact + # "uv sync", which would drop the mod_wsgi group. run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh @@ -197,9 +203,10 @@ jobs: mod_wsgi_latest: runs-on: ubuntu-22.04 + timeout-minutes: 60 name: Mod WSGI Latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 @@ -214,7 +221,9 @@ jobs: with: python-version: ${{ steps.versions.outputs.python }} - name: Install Apache - run: sudo apt-get install -y apache2 apache2-dev + run: | + sudo apt-get update + sudo apt-get install -y apache2 apache2-dev - name: Install dependencies run: | # CI skips the venv setup in setup-dev-env.sh, so create it here. @@ -229,8 +238,8 @@ jobs: version: ${{ steps.versions.outputs.mongodb }} topology: "replica_set" - name: Run standalone tests - # Call the test scripts directly: the just recipes run "uv sync", - # which would drop the mod_wsgi group before teardown can stop Apache. + # Call the test scripts directly: the just recipes run an exact + # "uv sync", which would drop the mod_wsgi group. run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh @@ -249,7 +258,7 @@ jobs: name: Docs Checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -268,7 +277,7 @@ jobs: matrix: python: ["3.10", "3.11"] steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -285,7 +294,7 @@ jobs: runs-on: ubuntu-latest name: Integration Tests steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling @@ -311,7 +320,7 @@ jobs: runs-on: ubuntu-latest name: "Make an sdist" steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-python@v7.0.0 @@ -371,7 +380,7 @@ jobs: runs-on: ubuntu-latest name: Test minimum dependencies and Python steps: - - uses: actions/checkout@v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install Python tooling diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe8e702ad5..8389a06c0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -359,10 +359,13 @@ To run the tests by hand, install Apache and mod_wsgi (`sudo apt-get install -y apache2-dev` and `uv sync --group mod_wsgi` on Ubuntu), then: - Run `TOPOLOGY=replica_set just run-server`. -- Run `just setup-tests mod_wsgi `. -- Run `just run-tests`. +- Run `bash .evergreen/scripts/setup-tests.sh mod_wsgi `. +- Run `bash .evergreen/run-tests.sh`. +- Run `bash .evergreen/scripts/teardown-tests.sh`. -The `mode` can be `standalone` or `embedded`. +The `mode` can be `standalone` or `embedded`. Call the test scripts directly +rather than using the `just` recipes: the recipes run an exact `uv sync`, +which drops the `mod_wsgi` group. ### OCSP tests diff --git a/pymongo/asynchronous/monitor.py b/pymongo/asynchronous/monitor.py index 179f459837..406bf11249 100644 --- a/pymongo/asynchronous/monitor.py +++ b/pymongo/asynchronous/monitor.py @@ -493,9 +493,14 @@ def _shutdown_resources() -> None: if _IS_SYNC: atexit.register(_shutdown_resources) - # Also register with threading so executors are stopped before the - # interpreter tries to join their (possibly non-daemon) threads. Unlike - # atexit, this runs for subinterpreters, where daemon threads are - # generally not allowed and CPython is strict about background threads. - if hasattr(threading, "_register_atexit"): - threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] + # In subinterpreters, daemon threads are not allowed and the executors' + # threads are joined (unlike atexit, threading._register_atexit runs for + # subinterpreters), so the executors must be stopped before the + # interpreter tries to join them. Probe for that restriction: in the + # main interpreter the assignment always succeeds and the normal atexit + # ordering is preserved. + try: + threading.Thread().daemon = True + except RuntimeError: + if hasattr(threading, "_register_atexit"): + threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] diff --git a/pymongo/periodic_executor.py b/pymongo/periodic_executor.py index c6f444480c..bf99469b81 100644 --- a/pymongo/periodic_executor.py +++ b/pymongo/periodic_executor.py @@ -189,7 +189,10 @@ def open(self) -> None: # Daemon threads are disabled in subinterpreters unless the # interpreter was created with allow_daemon_threads=True. # _shutdown_executors stops and joins the thread during - # interpreter shutdown, so a non-daemon thread is safe. + # interpreter shutdown, so a non-daemon thread is safe. Note + # that if the join times out, interpreter shutdown still + # joins the thread without a timeout, so a stuck target can + # block interpreter teardown. thread.daemon = True except RuntimeError: pass diff --git a/pymongo/synchronous/monitor.py b/pymongo/synchronous/monitor.py index 8a445fce5d..ab86559a1d 100644 --- a/pymongo/synchronous/monitor.py +++ b/pymongo/synchronous/monitor.py @@ -491,9 +491,14 @@ def _shutdown_resources() -> None: if _IS_SYNC: atexit.register(_shutdown_resources) - # Also register with threading so executors are stopped before the - # interpreter tries to join their (possibly non-daemon) threads. Unlike - # atexit, this runs for subinterpreters, where daemon threads are - # generally not allowed and CPython is strict about background threads. - if hasattr(threading, "_register_atexit"): - threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] + # In subinterpreters, daemon threads are not allowed and the executors' + # threads are joined (unlike atexit, threading._register_atexit runs for + # subinterpreters), so the executors must be stopped before the + # interpreter tries to join them. Probe for that restriction: in the + # main interpreter the assignment always succeeds and the normal atexit + # ordering is preserved. + try: + threading.Thread().daemon = True + except RuntimeError: + if hasattr(threading, "_register_atexit"): + threading._register_atexit(_shutdown_resources) # type: ignore[attr-defined] diff --git a/test/mod_wsgi_test/README.rst b/test/mod_wsgi_test/README.rst index 73a0d8ce80..539ab87f51 100644 --- a/test/mod_wsgi_test/README.rst +++ b/test/mod_wsgi_test/README.rst @@ -13,10 +13,12 @@ mod_wsgi creates. Test Matrix ----------- -Continuous integration tests the oldest supported CPython against the oldest -supported MongoDB with minimum dependencies, and the newest supported CPython -against the latest MongoDB, in both daemon and embedded mode against a replica -set. The Python and MongoDB versions come from ``generate_config_utils.py`` in +Continuous integration tests the oldest supported CPython against MongoDB 6.0 +with minimum dependencies, and the newest supported CPython against the latest +MongoDB, in both daemon and embedded mode against a replica set. MongoDB 6.0 +is used instead of the oldest supported version because the ubuntu-22.04 +runners cannot install older MongoDB versions. The Python versions and the +latest MongoDB version come from ``generate_config_utils.py`` in ``.evergreen/scripts``. Other combinations can be tested manually. Setup diff --git a/test/test_threads.py b/test/test_threads.py index fa015856a2..f2fa8e9b91 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -264,7 +264,8 @@ def run(interp): for _ in range(n_interpreters): release.put(True) for interp in interps: - interp.close() + if interp.is_running(): + interp.close() found = sorted(doc["subinterp"] for doc in self.db[coll_name].find({}, {"subinterp": 1})) self.assertEqual(found, list(range(n_interpreters))) From f77d46e380f4a1545ad98aac755585440def74cb Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:29:28 -0500 Subject: [PATCH 17/21] PYTHON-6114 Resolve the mod_wsgi group in the min deps job Add mod_wsgi to GROUP_MAP so the group is included in the --resolution=lowest-direct resolution, testing the minimum version from pyproject.toml rather than hardcoding it in the workflow. --- .evergreen/scripts/setup_tests.py | 2 +- .github/workflows/test-python.yml | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index a8839533c1..60935b953e 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -47,7 +47,7 @@ # Map the test name to test group. -GROUP_MAP = dict(mockupdb="mockupdb", perf="perf") +GROUP_MAP = dict(mockupdb="mockupdb", perf="perf", mod_wsgi="mod_wsgi") # The python version used for perf tests. PERF_PYTHON_VERSION = "3.10.11" diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 84b26f5d67..4c40aa1ac7 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -170,9 +170,6 @@ jobs: export PYMONGO_C_EXT_MUST_BUILD=1 just install uv sync --group mod_wsgi --refresh-package pymongo - # Test the minimum mod_wsgi version declared in pyproject.toml; - # the lock file has the latest version. - uv pip install "mod_wsgi==4.9.4" uv run --no-sync python tools/fail_if_no_c.py - id: setup-mongodb # Pinned to a commit hash to satisfy CodeQL's ref-pin check. @@ -184,7 +181,9 @@ jobs: env: TEST_MIN_DEPS: "1" # Call the test scripts directly: the just recipes run an exact - # "uv sync", which would drop the mod_wsgi group. + # "uv sync", which would drop the mod_wsgi group. The group is + # resolved with TEST_MIN_DEPS via GROUP_MAP in setup_tests.py, so + # mod_wsgi is tested at the minimum version from pyproject.toml. run: | bash .evergreen/scripts/setup-tests.sh mod_wsgi standalone bash .evergreen/run-tests.sh From 2b70790670fa6970ca25dab506dddb03269c05cc Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:34:19 -0500 Subject: [PATCH 18/21] PYTHON-6114 Install uv once in the smoke test root phase The root phase already runs before the unprivileged re-exec and has curl, so install the pinned uv to /usr/local/bin there instead of installing it again as the smoke user. --- .evergreen/scripts/mod_wsgi_smoke_test.sh | 29 +++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/.evergreen/scripts/mod_wsgi_smoke_test.sh b/.evergreen/scripts/mod_wsgi_smoke_test.sh index 17c8715b33..d7630cfc25 100755 --- a/.evergreen/scripts/mod_wsgi_smoke_test.sh +++ b/.evergreen/scripts/mod_wsgi_smoke_test.sh @@ -21,6 +21,20 @@ if [ "$(id -u)" = "0" ]; then export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq apache2 apache2-dev build-essential curl jq git >/dev/null + # Install uv to a system path from a pinned release, verifying the published + # checksum, rather than piping a mutable installer script into a shell. + UV_VERSION=0.12.17 + case "$(uname -m)" in + x86_64) UV_TARGET=x86_64-unknown-linux-gnu ;; + aarch64 | arm64) UV_TARGET=aarch64-unknown-linux-gnu ;; + *) echo "Unsupported architecture" >&2; exit 1 ;; + esac + curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz" + curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz.sha256" + (cd /tmp && sha256sum -c "uv-${UV_TARGET}.tar.gz.sha256") + tar -xzf "/tmp/uv-${UV_TARGET}.tar.gz" -C /tmp + install -m 0755 "/tmp/uv-${UV_TARGET}/uv" /usr/local/bin/uv + rm -rf "/tmp/uv-${UV_TARGET}.tar.gz" "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "/tmp/uv-${UV_TARGET}" useradd -m smoke mkdir -p /home/smoke/src # Leave out the generated env files so they cannot override the versions @@ -31,21 +45,6 @@ if [ "$(id -u)" = "0" ]; then exec su - smoke -c "bash /src/.evergreen/scripts/mod_wsgi_smoke_test.sh" fi -# Install uv from a pinned release, verifying the published checksum, rather -# than piping a mutable installer script into a shell. -UV_VERSION=0.12.17 -case "$(uname -m)" in - x86_64) UV_TARGET=x86_64-unknown-linux-gnu ;; - aarch64 | arm64) UV_TARGET=aarch64-unknown-linux-gnu ;; - *) echo "Unsupported architecture" >&2; exit 1 ;; -esac -curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz" -curl -fsSL -o "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_TARGET}.tar.gz.sha256" -(cd /tmp && sha256sum -c "uv-${UV_TARGET}.tar.gz.sha256") -tar -xzf "/tmp/uv-${UV_TARGET}.tar.gz" -C /tmp -mkdir -p "$HOME/.local/bin" -install -m 0755 "/tmp/uv-${UV_TARGET}/uv" "$HOME/.local/bin/uv" -rm -rf "/tmp/uv-${UV_TARGET}.tar.gz" "/tmp/uv-${UV_TARGET}.tar.gz.sha256" "/tmp/uv-${UV_TARGET}" export PATH="$HOME/.local/bin:$PATH" uv tool install rust-just >/dev/null From 0890a329955b97f587d7210ed9d6cf4c5f29f053 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:37:36 -0500 Subject: [PATCH 19/21] PYTHON-5418 Note subinterpreter support in the changelog --- doc/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 5fb2fb4731..e9da8c6678 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,6 +7,13 @@ Changes in Version 4.19.0 (2026/XX/XX) PyMongo 4.19 brings a number of changes including: - Added support for Python 3.15. +- Added support for running PyMongo in subinterpreters, including inside + ``concurrent.futures.InterpreterPoolExecutor`` (Python 3.14+). In + interpreters that do not allow daemon threads, monitor threads now start as + non-daemon threads and are stopped and joined when the interpreter is torn + down. Note that because these threads are non-daemon, an interpreter may + block on teardown until any in-flight monitor work completes. Subinterpreter + support is currently only exercised with the synchronous client. Bug fixes ......... From 8ef3a0b723286c3592b0cd8dbe028d8110594442 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:39:34 -0500 Subject: [PATCH 20/21] PYTHON-5418 Test the async client in subinterpreters Runs live AsyncMongoClients inside interpreters managed by InterpreterPoolExecutor. The async client runs its background tasks on the interpreter's own event loop rather than in threads, covering the other shutdown path. --- test/test_threads.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/test_threads.py b/test/test_threads.py index f2fa8e9b91..712fad05bf 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -311,6 +311,66 @@ def test_interpreter_pool_executor(self): ) self.assertEqual(found, list(range(n_interpreters))) + @unittest.skipUnless( + sys.version_info >= (3, 14), "InterpreterPoolExecutor requires Python 3.14+" + ) + def test_interpreter_pool_executor_async(self): + if InterpreterPoolExecutor is None: + self.skipTest("InterpreterPoolExecutor is not available") + + # Run live AsyncMongoClients inside interpreters managed by the + # standard InterpreterPoolExecutor. The async client runs its + # background tasks on the interpreter's own event loop instead of in + # threads. + n_interpreters = 2 + coll_name = f"interp-pool-async-{uuid.uuid4().hex}" + self.addCleanup(self.db.drop_collection, coll_name) + + # The worker runs in an interpreter whose sys.path has not picked up + # the repo root, so pass the callable as a builtin (exec) and insert + # the main interpreter's sys.path before importing pymongo. + code = textwrap.dedent( + """ + import asyncio + import sys + sys.path[:0] = path + + from pymongo import AsyncMongoClient + + async def main(): + client = AsyncMongoClient(uri, serverSelectionTimeoutMS=30000) + collection = client.get_database(db_name).get_collection(coll_name) + await collection.insert_one({"interp-pool-async": i}) + assert await collection.find_one({"interp-pool-async": i}) is not None + await client.close() + + asyncio.run(main()) + """ + ) + with InterpreterPoolExecutor(max_workers=n_interpreters) as executor: + futures = [ + executor.submit( + exec, + code, + { + "i": i, + "path": tuple(sys.path), + "uri": client_context.uri, + "db_name": self.db.name, + "coll_name": coll_name, + }, + ) + for i in range(n_interpreters) + ] + for future in futures: + future.result(timeout=120) + + found = sorted( + doc["interp-pool-async"] + for doc in self.db[coll_name].find({}, {"interp-pool-async": 1}) + ) + self.assertEqual(found, list(range(n_interpreters))) + if __name__ == "__main__": unittest.main() From 316f07ee0b3f936c71ca420450df0cee0e26adde Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 21 Sep 2026 16:46:36 -0500 Subject: [PATCH 21/21] PYTHON-5418 Finalize idle interpreters and update the changelog Invert the is_running guard in test_subinterpreters: closing an idle interpreter runs threading._shutdown, which stops and joins pymongo's monitor threads, so the successful path must close them. Note in the changelog that both clients are covered by the subinterpreter tests. --- doc/changelog.rst | 11 ++++++----- test/test_threads.py | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index e9da8c6678..ddf56b3a9d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,11 +9,12 @@ PyMongo 4.19 brings a number of changes including: - Added support for Python 3.15. - Added support for running PyMongo in subinterpreters, including inside ``concurrent.futures.InterpreterPoolExecutor`` (Python 3.14+). In - interpreters that do not allow daemon threads, monitor threads now start as - non-daemon threads and are stopped and joined when the interpreter is torn - down. Note that because these threads are non-daemon, an interpreter may - block on teardown until any in-flight monitor work completes. Subinterpreter - support is currently only exercised with the synchronous client. + interpreters that do not allow daemon threads, the synchronous client's + monitor threads now start as non-daemon threads and are stopped and joined + when the interpreter is torn down. Note that because these threads are + non-daemon, an interpreter may block on teardown until any in-flight + monitor work completes. Tests cover both the synchronous and asynchronous + clients running in subinterpreters. Bug fixes ......... diff --git a/test/test_threads.py b/test/test_threads.py index 712fad05bf..9b4585de7e 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -264,7 +264,11 @@ def run(interp): for _ in range(n_interpreters): release.put(True) for interp in interps: - if interp.is_running(): + # Finalize the idle interpreters: closing one runs + # threading._shutdown, which stops and joins pymongo's + # monitor threads. Skip any that are still executing to + # avoid masking the original error. + if not interp.is_running(): interp.close() found = sorted(doc["subinterp"] for doc in self.db[coll_name].find({}, {"subinterp": 1}))