Merge branch 'master' of github.com:mongodb/mongo-python-driver
This commit is contained in:
commit
c0d52204c1
@ -207,7 +207,7 @@ functions:
|
||||
binary: bash
|
||||
working_dir: "src"
|
||||
include_expansions_in_env: [VERSION, TOPOLOGY, AUTH, SSL, ORCHESTRATION_FILE, PYTHON_BINARY, PYTHON_VERSION,
|
||||
STORAGE_ENGINE, REQUIRE_API_VERSION, DRIVERS_TOOLS, TEST_CRYPT_SHARED, AUTH_AWS, LOAD_BALANCER]
|
||||
STORAGE_ENGINE, REQUIRE_API_VERSION, DRIVERS_TOOLS, TEST_CRYPT_SHARED, AUTH_AWS, LOAD_BALANCER, LOCAL_ATLAS]
|
||||
args: [.evergreen/just.sh, run-server, "${TEST_NAME}"]
|
||||
- command: expansions.update
|
||||
params:
|
||||
@ -229,7 +229,7 @@ functions:
|
||||
include_expansions_in_env: [AUTH, SSL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
|
||||
AWS_SESSION_TOKEN, COVERAGE, PYTHON_BINARY, LIBMONGOCRYPT_URL, MONGODB_URI, PYTHON_VERSION,
|
||||
DISABLE_TEST_COMMANDS, GREEN_FRAMEWORK, NO_EXT, COMPRESSORS, MONGODB_API_VERSION, DEBUG_LOG,
|
||||
ORCHESTRATION_FILE, OCSP_SERVER_TYPE]
|
||||
ORCHESTRATION_FILE, OCSP_SERVER_TYPE, VERSION]
|
||||
binary: bash
|
||||
working_dir: "src"
|
||||
args: [.evergreen/just.sh, setup-tests, "${TEST_NAME}", "${SUB_TEST_NAME}"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -729,149 +729,29 @@ buildvariants:
|
||||
- rhel87-small
|
||||
|
||||
# Ocsp tests
|
||||
- name: ocsp-rhel8-v4.4-python3.9
|
||||
- name: ocsp-rhel8
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 v4.4 Python3.9
|
||||
display_name: OCSP RHEL8
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "4.4"
|
||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
||||
- name: ocsp-rhel8-v5.0-python3.10
|
||||
- name: ocsp-win64
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 v5.0 Python3.10
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "5.0"
|
||||
PYTHON_BINARY: /opt/python/3.10/bin/python3
|
||||
- name: ocsp-rhel8-v6.0-python3.11
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 v6.0 Python3.11
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "6.0"
|
||||
PYTHON_BINARY: /opt/python/3.11/bin/python3
|
||||
- name: ocsp-rhel8-v7.0-python3.12
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 v7.0 Python3.12
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "7.0"
|
||||
PYTHON_BINARY: /opt/python/3.12/bin/python3
|
||||
- name: ocsp-rhel8-v8.0-python3.13
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 v8.0 Python3.13
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "8.0"
|
||||
PYTHON_BINARY: /opt/python/3.13/bin/python3
|
||||
- name: ocsp-rhel8-rapid-pypy3.10
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 rapid PyPy3.10
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: rapid
|
||||
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3
|
||||
- name: ocsp-rhel8-latest-python3.9
|
||||
tasks:
|
||||
- name: .ocsp
|
||||
display_name: OCSP RHEL8 latest Python3.9
|
||||
run_on:
|
||||
- rhel87-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: latest
|
||||
PYTHON_BINARY: /opt/python/3.9/bin/python3
|
||||
- name: ocsp-win64-v4.4-python3.9
|
||||
tasks:
|
||||
- name: .ocsp-rsa !.ocsp-staple
|
||||
display_name: OCSP Win64 v4.4 Python3.9
|
||||
- name: .ocsp-rsa !.ocsp-staple .latest
|
||||
- name: .ocsp-rsa !.ocsp-staple .4.4
|
||||
display_name: OCSP Win64
|
||||
run_on:
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "4.4"
|
||||
PYTHON_BINARY: C:/python/Python39/python.exe
|
||||
- name: ocsp-win64-v8.0-python3.13
|
||||
- name: ocsp-macos
|
||||
tasks:
|
||||
- name: .ocsp-rsa !.ocsp-staple
|
||||
display_name: OCSP Win64 v8.0 Python3.13
|
||||
run_on:
|
||||
- windows-64-vsMulti-small
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "8.0"
|
||||
PYTHON_BINARY: C:/python/Python313/python.exe
|
||||
- name: ocsp-macos-v4.4-python3.9
|
||||
tasks:
|
||||
- name: .ocsp-rsa !.ocsp-staple
|
||||
display_name: OCSP macOS v4.4 Python3.9
|
||||
- name: .ocsp-rsa !.ocsp-staple .latest
|
||||
- name: .ocsp-rsa !.ocsp-staple .4.4
|
||||
display_name: OCSP macOS
|
||||
run_on:
|
||||
- macos-14
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "4.4"
|
||||
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3
|
||||
- name: ocsp-macos-v8.0-python3.13
|
||||
tasks:
|
||||
- name: .ocsp-rsa !.ocsp-staple
|
||||
display_name: OCSP macOS v8.0 Python3.13
|
||||
run_on:
|
||||
- macos-14
|
||||
batchtime: 10080
|
||||
expansions:
|
||||
AUTH: noauth
|
||||
SSL: ssl
|
||||
TOPOLOGY: server
|
||||
VERSION: "8.0"
|
||||
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.13/bin/python3
|
||||
|
||||
# Oidc auth tests
|
||||
- name: auth-oidc-ubuntu-22
|
||||
|
||||
@ -249,41 +249,22 @@ def generate_yaml(tasks=None, variants=None):
|
||||
|
||||
def create_ocsp_variants() -> list[BuildVariant]:
|
||||
variants = []
|
||||
batchtime = BATCHTIME_WEEK
|
||||
expansions = dict(AUTH="noauth", SSL="ssl", TOPOLOGY="server")
|
||||
base_display = "OCSP"
|
||||
|
||||
# OCSP tests on default host with all servers v4.4+ and all python versions.
|
||||
versions = get_versions_from("4.4")
|
||||
for version, python in zip_cycle(versions, ALL_PYTHONS):
|
||||
host = DEFAULT_HOST
|
||||
variant = create_variant(
|
||||
[".ocsp"],
|
||||
get_variant_name(base_display, host, version=version, python=python),
|
||||
python=python,
|
||||
version=version,
|
||||
host=host,
|
||||
expansions=expansions,
|
||||
batchtime=batchtime,
|
||||
)
|
||||
variants.append(variant)
|
||||
|
||||
# OCSP tests on Windows and MacOS.
|
||||
# MongoDB servers on these hosts do not staple OCSP responses and only support RSA.
|
||||
for host_name, version in product(["win64", "macos"], ["4.4", "8.0"]):
|
||||
# OCSP tests on default host with all servers v4.4+.
|
||||
# MongoDB servers on Windows and MacOS do not staple OCSP responses and only support RSA.
|
||||
# Only test with MongoDB 4.4 and latest.
|
||||
for host_name in ["rhel8", "win64", "macos"]:
|
||||
host = HOSTS[host_name]
|
||||
python = CPYTHONS[0] if version == "4.4" else CPYTHONS[-1]
|
||||
if host == DEFAULT_HOST:
|
||||
tasks = [".ocsp"]
|
||||
else:
|
||||
tasks = [".ocsp-rsa !.ocsp-staple .latest", ".ocsp-rsa !.ocsp-staple .4.4"]
|
||||
variant = create_variant(
|
||||
[".ocsp-rsa !.ocsp-staple"],
|
||||
get_variant_name(base_display, host, version=version, python=python),
|
||||
python=python,
|
||||
version=version,
|
||||
tasks,
|
||||
get_variant_name("OCSP", host),
|
||||
host=host,
|
||||
expansions=expansions,
|
||||
batchtime=batchtime,
|
||||
batchtime=BATCHTIME_WEEK,
|
||||
)
|
||||
variants.append(variant)
|
||||
|
||||
return variants
|
||||
|
||||
|
||||
@ -965,22 +946,34 @@ def create_mod_wsgi_tasks():
|
||||
return tasks
|
||||
|
||||
|
||||
def _create_ocsp_task(algo, variant, server_type, base_task_name):
|
||||
def _create_ocsp_tasks(algo, variant, server_type, base_task_name):
|
||||
tasks = []
|
||||
file_name = f"{algo}-basic-tls-ocsp-{variant}.json"
|
||||
|
||||
vars = dict(TEST_NAME="ocsp", ORCHESTRATION_FILE=file_name)
|
||||
server_func = FunctionCall(func="run server", vars=vars)
|
||||
for version in get_versions_from("4.4"):
|
||||
if version == "latest":
|
||||
python = MIN_MAX_PYTHON[-1]
|
||||
else:
|
||||
python = MIN_MAX_PYTHON[0]
|
||||
|
||||
vars = dict(ORCHESTRATION_FILE=file_name, OCSP_SERVER_TYPE=server_type, TEST_NAME="ocsp")
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
vars = dict(
|
||||
ORCHESTRATION_FILE=file_name,
|
||||
OCSP_SERVER_TYPE=server_type,
|
||||
TEST_NAME="ocsp",
|
||||
PYTHON_VERSION=python,
|
||||
VERSION=version,
|
||||
)
|
||||
test_func = FunctionCall(func="run tests", vars=vars)
|
||||
|
||||
tags = ["ocsp", f"ocsp-{algo}"]
|
||||
if "disableStapling" not in variant:
|
||||
tags.append("ocsp-staple")
|
||||
tags = ["ocsp", f"ocsp-{algo}", version]
|
||||
if "disableStapling" not in variant:
|
||||
tags.append("ocsp-staple")
|
||||
|
||||
task_name = f"test-ocsp-{algo}-{base_task_name}"
|
||||
commands = [server_func, test_func]
|
||||
return EvgTask(name=task_name, tags=tags, commands=commands)
|
||||
task_name = get_task_name(
|
||||
f"test-ocsp-{algo}-{base_task_name}", python=python, version=version
|
||||
)
|
||||
tasks.append(EvgTask(name=task_name, tags=tags, commands=[test_func]))
|
||||
return tasks
|
||||
|
||||
|
||||
def create_aws_lambda_tasks():
|
||||
@ -1092,8 +1085,8 @@ def create_ocsp_tasks():
|
||||
]
|
||||
for algo in ["ecdsa", "rsa"]:
|
||||
for variant, server_type, base_task_name in tests:
|
||||
task = _create_ocsp_task(algo, variant, server_type, base_task_name)
|
||||
tasks.append(task)
|
||||
new_tasks = _create_ocsp_tasks(algo, variant, server_type, base_task_name)
|
||||
tasks.extend(new_tasks)
|
||||
|
||||
return tasks
|
||||
|
||||
@ -1182,7 +1175,7 @@ def write_tasks_to_file():
|
||||
fid.write("tasks:\n")
|
||||
|
||||
for name, func in sorted(getmembers(mod, isfunction)):
|
||||
if not name.endswith("_tasks"):
|
||||
if name.startswith("_") or not name.endswith("_tasks"):
|
||||
continue
|
||||
if not name.startswith("create_"):
|
||||
raise ValueError("Task creators must start with create_")
|
||||
|
||||
@ -28,16 +28,6 @@ def start_server():
|
||||
elif test_name == "load_balancer":
|
||||
set_env("LOAD_BALANCER")
|
||||
|
||||
elif test_name == "ocsp":
|
||||
opts.ssl = True
|
||||
if "ORCHESTRATION_FILE" not in os.environ:
|
||||
found = False
|
||||
for opt in extra_opts:
|
||||
if opt.startswith("--orchestration-file"):
|
||||
found = True
|
||||
if not found:
|
||||
raise ValueError("Please provide an orchestration file")
|
||||
|
||||
elif test_name == "search_index":
|
||||
os.environ["TOPOLOGY"] = "replica_set"
|
||||
os.environ["MONGODB_VERSION"] = "7.0"
|
||||
|
||||
@ -19,28 +19,20 @@ fi
|
||||
# Ensure dependencies are installed.
|
||||
bash $HERE/install-dependencies.sh
|
||||
|
||||
# Set the location of the python bin dir.
|
||||
if [ "Windows_NT" = "${OS:-}" ]; then
|
||||
BIN_DIR=.venv/Scripts
|
||||
else
|
||||
BIN_DIR=.venv/bin
|
||||
fi
|
||||
# Get the appropriate UV_PYTHON.
|
||||
. $ROOT/.evergreen/utils.sh
|
||||
set -x
|
||||
|
||||
# Ensure there is a python venv.
|
||||
if [ ! -d $BIN_DIR ]; then
|
||||
. $ROOT/.evergreen/utils.sh
|
||||
|
||||
if [ -z "${PYTHON_BINARY:-}" ]; then
|
||||
if [ -n "${PYTHON_VERSION:-}" ]; then
|
||||
PYTHON_BINARY=$(get_python_binary $PYTHON_VERSION)
|
||||
else
|
||||
PYTHON_BINARY=$(find_python3)
|
||||
fi
|
||||
fi
|
||||
export UV_PYTHON=${PYTHON_BINARY}
|
||||
echo "export UV_PYTHON=$UV_PYTHON" >> $HERE/env.sh
|
||||
echo "Using python $UV_PYTHON"
|
||||
if [ -z "${PYTHON_BINARY:-}" ]; then
|
||||
if [ -n "${PYTHON_VERSION:-}" ]; then
|
||||
PYTHON_BINARY=$(get_python_binary $PYTHON_VERSION)
|
||||
else
|
||||
PYTHON_BINARY=$(find_python3)
|
||||
fi
|
||||
fi
|
||||
export UV_PYTHON=${PYTHON_BINARY}
|
||||
echo "export UV_PYTHON=$UV_PYTHON" >> $HERE/env.sh
|
||||
echo "Using python $UV_PYTHON"
|
||||
|
||||
# Add the default install path to the path if needed.
|
||||
if [ -z "${PYMONGO_BIN_DIR:-}" ]; then
|
||||
|
||||
@ -142,7 +142,6 @@ def handle_test_env() -> None:
|
||||
test_title = test_name
|
||||
if sub_test_name:
|
||||
test_title += f" {sub_test_name}"
|
||||
LOGGER.info(f"Setting up '{test_title}' with {AUTH=} and {SSL=}...")
|
||||
|
||||
# Create the test env file with the initial set of values.
|
||||
with ENV_FILE.open("w", newline="\n") as fid:
|
||||
@ -150,8 +149,6 @@ def handle_test_env() -> None:
|
||||
fid.write("set +x\n")
|
||||
ENV_FILE.chmod(ENV_FILE.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
write_env("AUTH", AUTH)
|
||||
write_env("SSL", SSL)
|
||||
write_env("PIP_QUIET") # Quiet by default.
|
||||
write_env("PIP_PREFER_BINARY") # Prefer binary dists by default.
|
||||
write_env("UV_FROZEN") # Do not modify lock files.
|
||||
@ -197,6 +194,13 @@ def handle_test_env() -> None:
|
||||
if test_name == "search_index":
|
||||
AUTH = "auth"
|
||||
|
||||
if test_name == "ocsp":
|
||||
SSL = "ssl"
|
||||
|
||||
write_env("AUTH", AUTH)
|
||||
write_env("SSL", SSL)
|
||||
LOGGER.info(f"Setting up '{test_title}' with {AUTH=} and {SSL=}...")
|
||||
|
||||
if test_name == "aws_lambda":
|
||||
UV_ARGS.append("--group pip")
|
||||
# Store AWS creds if they were given.
|
||||
@ -318,6 +322,22 @@ def handle_test_env() -> None:
|
||||
env["OCSP_ALGORITHM"] = ocsp_algo
|
||||
run_command(f"bash {DRIVERS_TOOLS}/.evergreen/ocsp/setup.sh", env=env)
|
||||
|
||||
# The mock OCSP responder MUST BE started before the mongod as the mongod expects that
|
||||
# a responder will be available upon startup.
|
||||
version = os.environ.get("VERSION", "latest")
|
||||
cmd = [
|
||||
"bash",
|
||||
f"{DRIVERS_TOOLS}/.evergreen/run-orchestration.sh",
|
||||
"--ssl",
|
||||
"--version",
|
||||
version,
|
||||
]
|
||||
if opts.verbose:
|
||||
cmd.append("-v")
|
||||
elif opts.quiet:
|
||||
cmd.append("-q")
|
||||
run_command(cmd, cwd=DRIVERS_TOOLS)
|
||||
|
||||
if SSL != "nossl":
|
||||
if not DRIVERS_TOOLS:
|
||||
raise RuntimeError("Missing DRIVERS_TOOLS")
|
||||
|
||||
@ -54,8 +54,8 @@ SUB_TEST_REQUIRED = ["auth_aws", "auth_oidc", "kms", "mod_wsgi", "perf"]
|
||||
|
||||
EXTRA_TESTS = ["mod_wsgi", "aws_lambda"]
|
||||
|
||||
# Tests that do not use run-orchestration.
|
||||
NO_RUN_ORCHESTRATION = ["auth_oidc", "atlas_connect", "data_lake", "mockupdb", "serverless"]
|
||||
# Tests that do not use run-orchestration directly.
|
||||
NO_RUN_ORCHESTRATION = ["auth_oidc", "atlas_connect", "data_lake", "mockupdb", "serverless", "ocsp"]
|
||||
|
||||
|
||||
def get_test_options(
|
||||
|
||||
@ -6,12 +6,12 @@ find_python3() {
|
||||
PYTHON=""
|
||||
# Find a suitable toolchain version, if available.
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
PYTHON="/Library/Frameworks/Python.Framework/Versions/Current/bin/python3"
|
||||
PYTHON="/Library/Frameworks/Python.Framework/Versions/3.9/bin/python3"
|
||||
elif [ "Windows_NT" = "${OS:-}" ]; then # Magic variable in cygwin
|
||||
PYTHON="C:/python/Current/python.exe"
|
||||
PYTHON="C:/python/Python39/python.exe"
|
||||
else
|
||||
# Prefer our own toolchain, fall back to mongodb toolchain if it has Python 3.9+.
|
||||
if [ -f "/opt/python/Current/bin/python3" ]; then
|
||||
if [ -f "/opt/python/3.9/bin/python3" ]; then
|
||||
PYTHON="/opt/python/Current/bin/python3"
|
||||
elif is_python_39 "$(command -v /opt/mongodbtoolchain/v5/bin/python3)"; then
|
||||
PYTHON="/opt/mongodbtoolchain/v5/bin/python3"
|
||||
|
||||
@ -335,7 +335,9 @@ You must have `docker` or `podman` installed locally.
|
||||
- Export the orchestration file, e.g. `export ORCHESTRATION_FILE=rsa-basic-tls-ocsp-disableStapling.json`.
|
||||
This corresponds to a config file in `$DRIVERS_TOOLS/.evergreen/orchestration/configs/servers`.
|
||||
MongoDB servers on MacOS and Windows do not staple OCSP responses and only support RSA.
|
||||
- Run `just run-server ocsp`.
|
||||
NOTE: because the mock ocsp responder MUST be started prior to the server starting, the ocsp tests start the server
|
||||
as part of `setup-tests`.
|
||||
|
||||
- Run `just setup-tests ocsp <sub test>` (options are "valid", "revoked", "valid-delegate", "revoked-delegate").
|
||||
- Run `just run-tests`
|
||||
|
||||
|
||||
@ -89,9 +89,15 @@ from pymongo.lock import (
|
||||
_async_create_lock,
|
||||
_release_locks,
|
||||
)
|
||||
from pymongo.logger import _CLIENT_LOGGER, _log_client_error, _log_or_warn
|
||||
from pymongo.logger import (
|
||||
_CLIENT_LOGGER,
|
||||
_COMMAND_LOGGER,
|
||||
_debug_log,
|
||||
_log_client_error,
|
||||
_log_or_warn,
|
||||
)
|
||||
from pymongo.message import _CursorAddress, _GetMore, _Query
|
||||
from pymongo.monitoring import ConnectionClosedReason
|
||||
from pymongo.monitoring import ConnectionClosedReason, _EventListeners
|
||||
from pymongo.operations import (
|
||||
DeleteMany,
|
||||
DeleteOne,
|
||||
@ -759,6 +765,8 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._port = port
|
||||
self._topology: Topology = None # type: ignore[assignment]
|
||||
self._timeout: float | None = None
|
||||
self._topology_settings: TopologySettings = None # type: ignore[assignment]
|
||||
self._event_listeners: _EventListeners | None = None
|
||||
|
||||
# _pool_class, _monitor_class, and _condition_class are for deep
|
||||
# customization of PyMongo, e.g. Motor.
|
||||
@ -2684,6 +2692,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._deprioritized_servers: list[Server] = []
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
|
||||
async def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2726,6 +2735,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
else:
|
||||
raise
|
||||
|
||||
@ -2747,6 +2757,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
self._attempt_number += 1
|
||||
if self._bulk:
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
@ -2825,6 +2836,14 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
self._retryable = False
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
_COMMAND_LOGGER,
|
||||
message=f"Retrying write attempt number {self._attempt_number}",
|
||||
clientId=self._client.client_id,
|
||||
commandName=self._operation,
|
||||
operationId=self._operation_id,
|
||||
)
|
||||
return await self._func(self._session, conn, self._retryable) # type: ignore
|
||||
except PyMongoError as exc:
|
||||
if not self._retryable:
|
||||
@ -2846,6 +2865,14 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
):
|
||||
if self._retrying and not self._retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
_COMMAND_LOGGER,
|
||||
message=f"Retrying read attempt number {self._attempt_number}",
|
||||
clientId=self._client._topology_settings._topology_id,
|
||||
commandName=self._operation,
|
||||
operationId=self._operation_id,
|
||||
)
|
||||
return await self._func(self._session, self._server, conn, read_pref) # type: ignore
|
||||
|
||||
|
||||
|
||||
@ -81,9 +81,15 @@ from pymongo.lock import (
|
||||
_create_lock,
|
||||
_release_locks,
|
||||
)
|
||||
from pymongo.logger import _CLIENT_LOGGER, _log_client_error, _log_or_warn
|
||||
from pymongo.logger import (
|
||||
_CLIENT_LOGGER,
|
||||
_COMMAND_LOGGER,
|
||||
_debug_log,
|
||||
_log_client_error,
|
||||
_log_or_warn,
|
||||
)
|
||||
from pymongo.message import _CursorAddress, _GetMore, _Query
|
||||
from pymongo.monitoring import ConnectionClosedReason
|
||||
from pymongo.monitoring import ConnectionClosedReason, _EventListeners
|
||||
from pymongo.operations import (
|
||||
DeleteMany,
|
||||
DeleteOne,
|
||||
@ -757,6 +763,8 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
|
||||
self._port = port
|
||||
self._topology: Topology = None # type: ignore[assignment]
|
||||
self._timeout: float | None = None
|
||||
self._topology_settings: TopologySettings = None # type: ignore[assignment]
|
||||
self._event_listeners: _EventListeners | None = None
|
||||
|
||||
# _pool_class, _monitor_class, and _condition_class are for deep
|
||||
# customization of PyMongo, e.g. Motor.
|
||||
@ -2670,6 +2678,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
self._deprioritized_servers: list[Server] = []
|
||||
self._operation = operation
|
||||
self._operation_id = operation_id
|
||||
self._attempt_number = 0
|
||||
|
||||
def run(self) -> T:
|
||||
"""Runs the supplied func() and attempts a retry
|
||||
@ -2712,6 +2721,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
raise
|
||||
self._retrying = True
|
||||
self._last_error = exc
|
||||
self._attempt_number += 1
|
||||
else:
|
||||
raise
|
||||
|
||||
@ -2733,6 +2743,7 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
raise self._last_error from exc
|
||||
else:
|
||||
raise
|
||||
self._attempt_number += 1
|
||||
if self._bulk:
|
||||
self._bulk.retrying = True
|
||||
else:
|
||||
@ -2811,6 +2822,14 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
# not support sessions raise the last error.
|
||||
self._check_last_error()
|
||||
self._retryable = False
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
_COMMAND_LOGGER,
|
||||
message=f"Retrying write attempt number {self._attempt_number}",
|
||||
clientId=self._client.client_id,
|
||||
commandName=self._operation,
|
||||
operationId=self._operation_id,
|
||||
)
|
||||
return self._func(self._session, conn, self._retryable) # type: ignore
|
||||
except PyMongoError as exc:
|
||||
if not self._retryable:
|
||||
@ -2832,6 +2851,14 @@ class _ClientConnectionRetryable(Generic[T]):
|
||||
):
|
||||
if self._retrying and not self._retryable:
|
||||
self._check_last_error()
|
||||
if self._retrying:
|
||||
_debug_log(
|
||||
_COMMAND_LOGGER,
|
||||
message=f"Retrying read attempt number {self._attempt_number}",
|
||||
clientId=self._client._topology_settings._topology_id,
|
||||
commandName=self._operation,
|
||||
operationId=self._operation_id,
|
||||
)
|
||||
return self._func(self._session, self._server, conn, read_pref) # type: ignore
|
||||
|
||||
|
||||
|
||||
@ -410,7 +410,14 @@ class APITestsMixin:
|
||||
expected_update_description = {"updatedFields": {"new": 1}, "removedFields": ["foo"]}
|
||||
if async_client_context.version.at_least(4, 5, 0):
|
||||
expected_update_description["truncatedArrays"] = []
|
||||
self.assertEqual(expected_update_description, change["updateDescription"])
|
||||
self.assertEqual(
|
||||
expected_update_description,
|
||||
{
|
||||
k: v
|
||||
for k, v in change["updateDescription"].items()
|
||||
if k in expected_update_description
|
||||
},
|
||||
)
|
||||
# Replace.
|
||||
await self.watched_collection().replace_one({"new": 1}, {"foo": "bar"})
|
||||
change = await change_stream.next()
|
||||
|
||||
@ -2692,7 +2692,7 @@ class TestLookupProse(AsyncEncryptionIntegrationTest):
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertTrue("not supported" in str(exc))
|
||||
self.assertIn("not supported", str(exc))
|
||||
|
||||
@async_client_context.require_version_max(8, 1, -1)
|
||||
async def test_9_error(self):
|
||||
@ -2721,7 +2721,7 @@ class TestLookupProse(AsyncEncryptionIntegrationTest):
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertTrue("Upgrade" in str(exc))
|
||||
self.assertIn("Upgrade", str(exc))
|
||||
|
||||
|
||||
# https://github.com/mongodb/specifications/blob/072601/source/client-side-encryption/tests/README.md#rewrap
|
||||
|
||||
@ -15,7 +15,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from test import unittest
|
||||
from test.asynchronous import AsyncIntegrationTest
|
||||
from test.asynchronous import AsyncIntegrationTest, async_client_context
|
||||
from unittest.mock import patch
|
||||
|
||||
from bson import json_util
|
||||
@ -97,6 +97,22 @@ class TestLogger(AsyncIntegrationTest):
|
||||
await c.db.test.insert_one({"x": "1"})
|
||||
self.assertGreater(len(cm.records), 0)
|
||||
|
||||
@async_client_context.require_failCommand_fail_point
|
||||
async def test_logging_retry_read_attempts(self):
|
||||
await self.db.test.insert_one({"x": "1"})
|
||||
|
||||
async with self.fail_point(
|
||||
{"mode": {"times": 1}, "data": {"failCommands": ["find"], "closeConnection": True}}
|
||||
):
|
||||
with self.assertLogs("pymongo.command", level="DEBUG") as cm:
|
||||
await self.db.test.find_one({"x": "1"})
|
||||
|
||||
retry_messages = [
|
||||
r.getMessage() for r in cm.records if "Retrying read attempt" in r.getMessage()
|
||||
]
|
||||
print(retry_messages)
|
||||
self.assertEqual(len(retry_messages), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -450,7 +450,7 @@ class TestPooling(_TestPoolingBase):
|
||||
with timeout(0.5):
|
||||
await client.db.t.find_one({"$where": delay(2)})
|
||||
|
||||
self.assertTrue("(configured timeouts: timeoutMS: 500.0ms" in str(error.exception))
|
||||
self.assertIn("(configured timeouts: timeoutMS: 500.0ms", str(error.exception))
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
async def test_socket_timeout_message(self):
|
||||
@ -475,9 +475,9 @@ class TestPooling(_TestPoolingBase):
|
||||
with self.assertRaises(Exception) as error:
|
||||
await client.db.t.find_one({"$where": delay(2)})
|
||||
|
||||
self.assertTrue(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 20000.0ms)"
|
||||
in str(error.exception)
|
||||
self.assertIn(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 20000.0ms)",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
@async_client_context.require_failCommand_appName
|
||||
@ -507,9 +507,9 @@ class TestPooling(_TestPoolingBase):
|
||||
with self.assertRaises(Exception) as error:
|
||||
await client.admin.command("ping")
|
||||
|
||||
self.assertTrue(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 500.0ms)"
|
||||
in str(error.exception)
|
||||
self.assertIn(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 500.0ms)",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -242,9 +242,9 @@ class TestAuthOIDCHuman(OIDCTestBase):
|
||||
authmechanismproperties=props,
|
||||
connect=False,
|
||||
)
|
||||
# Assert that a find operation fails with a client-side error.
|
||||
with self.assertRaises(ConfigurationError):
|
||||
client.test.test.find_one()
|
||||
# Assert that a find operation fails with a client-side error.
|
||||
with self.assertRaises(ConfigurationError):
|
||||
client.test.test.find_one()
|
||||
# Close the client.
|
||||
client.close()
|
||||
|
||||
|
||||
@ -42,70 +42,6 @@
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
{
|
||||
"description": "disambiguatedPaths is not present when showExpandedEvents is false/unset",
|
||||
"operations": [
|
||||
{
|
||||
"name": "insertOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"document": {
|
||||
"_id": 1,
|
||||
"a": {
|
||||
"1": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "createChangeStream",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"pipeline": []
|
||||
},
|
||||
"saveResultAsEntity": "changeStream0"
|
||||
},
|
||||
{
|
||||
"name": "updateOne",
|
||||
"object": "collection0",
|
||||
"arguments": {
|
||||
"filter": {
|
||||
"_id": 1
|
||||
},
|
||||
"update": {
|
||||
"$set": {
|
||||
"a.1": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "iterateUntilDocumentOrError",
|
||||
"object": "changeStream0",
|
||||
"expectResult": {
|
||||
"operationType": "update",
|
||||
"ns": {
|
||||
"db": "database0",
|
||||
"coll": "collection0"
|
||||
},
|
||||
"updateDescription": {
|
||||
"updatedFields": {
|
||||
"$$exists": true
|
||||
},
|
||||
"removedFields": {
|
||||
"$$exists": true
|
||||
},
|
||||
"truncatedArrays": {
|
||||
"$$exists": true
|
||||
},
|
||||
"disambiguatedPaths": {
|
||||
"$$exists": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "disambiguatedPaths is present on updateDescription when an ambiguous path is present",
|
||||
"operations": [
|
||||
|
||||
@ -181,7 +181,12 @@
|
||||
"field": "array",
|
||||
"newSize": 2
|
||||
}
|
||||
]
|
||||
],
|
||||
"disambiguatedPaths": {
|
||||
"$$unsetOrMatches": {
|
||||
"$$exists": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1408,6 +1413,11 @@
|
||||
"$$unsetOrMatches": {
|
||||
"$$exists": true
|
||||
}
|
||||
},
|
||||
"disambiguatedPaths": {
|
||||
"$$unsetOrMatches": {
|
||||
"$$exists": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
@ -38,15 +39,10 @@ OCSP_TLS_SHOULD_SUCCEED = os.environ.get("OCSP_TLS_SHOULD_SUCCEED") == "true"
|
||||
FORMAT = "%(asctime)s %(levelname)s %(module)s %(message)s"
|
||||
logging.basicConfig(format=FORMAT, level=logging.DEBUG)
|
||||
|
||||
if sys.platform == "win32":
|
||||
# The non-stapled OCSP endpoint check is slow on Windows.
|
||||
TIMEOUT_MS = 5000
|
||||
else:
|
||||
TIMEOUT_MS = 500
|
||||
|
||||
|
||||
def _connect(options):
|
||||
uri = f"mongodb://localhost:27017/?serverSelectionTimeoutMS={TIMEOUT_MS}&tlsCAFile={CA_FILE}&{options}"
|
||||
assert CA_FILE is not None
|
||||
uri = f"mongodb://localhost:27017/?serverSelectionTimeoutMS=10000&tlsCAFile={Path(CA_FILE).as_posix()}&{options}"
|
||||
print(uri)
|
||||
try:
|
||||
client = pymongo.MongoClient(uri)
|
||||
|
||||
@ -558,7 +558,7 @@ class TestBSON(unittest.TestCase):
|
||||
decode(bs)
|
||||
except Exception as exc:
|
||||
self.assertTrue(isinstance(exc, InvalidBSON))
|
||||
self.assertTrue(part in str(exc))
|
||||
self.assertIn(part, str(exc))
|
||||
else:
|
||||
self.fail("Failed to raise an exception.")
|
||||
|
||||
|
||||
@ -406,7 +406,14 @@ class APITestsMixin:
|
||||
expected_update_description = {"updatedFields": {"new": 1}, "removedFields": ["foo"]}
|
||||
if client_context.version.at_least(4, 5, 0):
|
||||
expected_update_description["truncatedArrays"] = []
|
||||
self.assertEqual(expected_update_description, change["updateDescription"])
|
||||
self.assertEqual(
|
||||
expected_update_description,
|
||||
{
|
||||
k: v
|
||||
for k, v in change["updateDescription"].items()
|
||||
if k in expected_update_description
|
||||
},
|
||||
)
|
||||
# Replace.
|
||||
self.watched_collection().replace_one({"new": 1}, {"foo": "bar"})
|
||||
change = change_stream.next()
|
||||
|
||||
@ -2676,7 +2676,7 @@ class TestLookupProse(EncryptionIntegrationTest):
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertTrue("not supported" in str(exc))
|
||||
self.assertIn("not supported", str(exc))
|
||||
|
||||
@client_context.require_version_max(8, 1, -1)
|
||||
def test_9_error(self):
|
||||
@ -2705,7 +2705,7 @@ class TestLookupProse(EncryptionIntegrationTest):
|
||||
]
|
||||
)
|
||||
)
|
||||
self.assertTrue("Upgrade" in str(exc))
|
||||
self.assertIn("Upgrade", str(exc))
|
||||
|
||||
|
||||
# https://github.com/mongodb/specifications/blob/072601/source/client-side-encryption/tests/README.md#rewrap
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from test import IntegrationTest, unittest
|
||||
from test import IntegrationTest, client_context, unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from bson import json_util
|
||||
@ -96,6 +96,22 @@ class TestLogger(IntegrationTest):
|
||||
c.db.test.insert_one({"x": "1"})
|
||||
self.assertGreater(len(cm.records), 0)
|
||||
|
||||
@client_context.require_failCommand_fail_point
|
||||
def test_logging_retry_read_attempts(self):
|
||||
self.db.test.insert_one({"x": "1"})
|
||||
|
||||
with self.fail_point(
|
||||
{"mode": {"times": 1}, "data": {"failCommands": ["find"], "closeConnection": True}}
|
||||
):
|
||||
with self.assertLogs("pymongo.command", level="DEBUG") as cm:
|
||||
self.db.test.find_one({"x": "1"})
|
||||
|
||||
retry_messages = [
|
||||
r.getMessage() for r in cm.records if "Retrying read attempt" in r.getMessage()
|
||||
]
|
||||
print(retry_messages)
|
||||
self.assertEqual(len(retry_messages), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -450,7 +450,7 @@ class TestPooling(_TestPoolingBase):
|
||||
with timeout(0.5):
|
||||
client.db.t.find_one({"$where": delay(2)})
|
||||
|
||||
self.assertTrue("(configured timeouts: timeoutMS: 500.0ms" in str(error.exception))
|
||||
self.assertIn("(configured timeouts: timeoutMS: 500.0ms", str(error.exception))
|
||||
|
||||
@client_context.require_failCommand_appName
|
||||
def test_socket_timeout_message(self):
|
||||
@ -473,9 +473,9 @@ class TestPooling(_TestPoolingBase):
|
||||
with self.assertRaises(Exception) as error:
|
||||
client.db.t.find_one({"$where": delay(2)})
|
||||
|
||||
self.assertTrue(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 20000.0ms)"
|
||||
in str(error.exception)
|
||||
self.assertIn(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 20000.0ms)",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
@client_context.require_failCommand_appName
|
||||
@ -505,9 +505,9 @@ class TestPooling(_TestPoolingBase):
|
||||
with self.assertRaises(Exception) as error:
|
||||
client.admin.command("ping")
|
||||
|
||||
self.assertTrue(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 500.0ms)"
|
||||
in str(error.exception)
|
||||
self.assertIn(
|
||||
"(configured timeouts: socketTimeoutMS: 500.0ms, connectTimeoutMS: 500.0ms)",
|
||||
str(error.exception),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ class TestServer(unittest.TestCase):
|
||||
hello = Hello({"ok": 1})
|
||||
sd = ServerDescription(("localhost", 27017), hello)
|
||||
server = Server(sd, pool=object(), monitor=object()) # type: ignore[arg-type]
|
||||
self.assertTrue("Standalone" in str(server))
|
||||
self.assertIn("Standalone", str(server))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user