518 lines
18 KiB
Python
518 lines
18 KiB
Python
# Copyright 2024-present MongoDB, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
# may not use this file except in compliance with the License. You
|
|
# may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied. See the License for the specific language governing
|
|
# permissions and limitations under the License.
|
|
|
|
"""Pool options for AsyncMongoClient/MongoClient.
|
|
|
|
.. seealso:: This module is compatible with both the synchronous and asynchronous PyMongo APIs.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os
|
|
import platform
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, MutableMapping, Optional
|
|
|
|
import bson
|
|
from pymongo import __version__
|
|
from pymongo.common import (
|
|
MAX_CONNECTING,
|
|
MAX_IDLE_TIME_SEC,
|
|
MAX_POOL_SIZE,
|
|
MIN_POOL_SIZE,
|
|
WAIT_QUEUE_TIMEOUT,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pymongo.auth_shared import MongoCredential
|
|
from pymongo.compression_support import CompressionSettings
|
|
from pymongo.driver_info import DriverInfo
|
|
from pymongo.monitoring import _EventListeners
|
|
from pymongo.pyopenssl_context import SSLContext
|
|
from pymongo.server_api import ServerApi
|
|
|
|
|
|
_METADATA: dict[str, Any] = {"driver": {"name": "PyMongo", "version": __version__}}
|
|
|
|
if sys.platform.startswith("linux"):
|
|
# platform.linux_distribution was deprecated in Python 3.5
|
|
# and removed in Python 3.8. Starting in Python 3.5 it
|
|
# raises DeprecationWarning
|
|
# DeprecationWarning: dist() and linux_distribution() functions are deprecated in Python 3.5
|
|
_name = platform.system()
|
|
_METADATA["os"] = {
|
|
"type": _name,
|
|
"name": _name,
|
|
"architecture": platform.machine(),
|
|
# Kernel version (e.g. 4.4.0-17-generic).
|
|
"version": platform.release(),
|
|
}
|
|
elif sys.platform == "darwin":
|
|
_METADATA["os"] = {
|
|
"type": platform.system(),
|
|
"name": platform.system(),
|
|
"architecture": platform.machine(),
|
|
# (mac|i|tv)OS(X) version (e.g. 10.11.6) instead of darwin
|
|
# kernel version.
|
|
"version": platform.mac_ver()[0],
|
|
}
|
|
elif sys.platform == "win32":
|
|
_METADATA["os"] = {
|
|
"type": platform.system(),
|
|
# "Windows XP", "Windows 7", "Windows 10", etc.
|
|
"name": " ".join((platform.system(), platform.release())),
|
|
"architecture": platform.machine(),
|
|
# Windows patch level (e.g. 5.1.2600-SP3)
|
|
"version": "-".join(platform.win32_ver()[1:3]),
|
|
}
|
|
elif sys.platform.startswith("java"):
|
|
_name, _ver, _arch = platform.java_ver()[-1]
|
|
_METADATA["os"] = {
|
|
# Linux, Windows 7, Mac OS X, etc.
|
|
"type": _name,
|
|
"name": _name,
|
|
# x86, x86_64, AMD64, etc.
|
|
"architecture": _arch,
|
|
# Linux kernel version, OSX version, etc.
|
|
"version": _ver,
|
|
}
|
|
else:
|
|
# Get potential alias (e.g. SunOS 5.11 becomes Solaris 2.11)
|
|
_aliased = platform.system_alias(platform.system(), platform.release(), platform.version())
|
|
_METADATA["os"] = {
|
|
"type": platform.system(),
|
|
"name": " ".join([part for part in _aliased[:2] if part]),
|
|
"architecture": platform.machine(),
|
|
"version": _aliased[2],
|
|
}
|
|
|
|
if platform.python_implementation().startswith("PyPy"):
|
|
_METADATA["platform"] = " ".join(
|
|
(
|
|
platform.python_implementation(),
|
|
".".join(map(str, sys.pypy_version_info)), # type: ignore
|
|
"(Python %s)" % ".".join(map(str, sys.version_info)),
|
|
)
|
|
)
|
|
elif sys.platform.startswith("java"):
|
|
_METADATA["platform"] = " ".join(
|
|
(
|
|
platform.python_implementation(),
|
|
".".join(map(str, sys.version_info)),
|
|
"(%s)" % " ".join((platform.system(), platform.release())),
|
|
)
|
|
)
|
|
else:
|
|
_METADATA["platform"] = " ".join(
|
|
(platform.python_implementation(), ".".join(map(str, sys.version_info)))
|
|
)
|
|
|
|
DOCKER_ENV_PATH = "/.dockerenv"
|
|
ENV_VAR_K8S = "KUBERNETES_SERVICE_HOST"
|
|
|
|
RUNTIME_NAME_DOCKER = "docker"
|
|
ORCHESTRATOR_NAME_K8S = "kubernetes"
|
|
|
|
|
|
def get_container_env_info() -> dict[str, str]:
|
|
"""Returns the runtime and orchestrator of a container.
|
|
If neither value is present, the metadata client.env.container field will be omitted."""
|
|
container = {}
|
|
|
|
if Path(DOCKER_ENV_PATH).exists():
|
|
container["runtime"] = RUNTIME_NAME_DOCKER
|
|
if os.getenv(ENV_VAR_K8S):
|
|
container["orchestrator"] = ORCHESTRATOR_NAME_K8S
|
|
|
|
return container
|
|
|
|
|
|
def _is_lambda() -> bool:
|
|
if os.getenv("AWS_LAMBDA_RUNTIME_API"):
|
|
return True
|
|
env = os.getenv("AWS_EXECUTION_ENV")
|
|
if env:
|
|
return env.startswith("AWS_Lambda_")
|
|
return False
|
|
|
|
|
|
def _is_azure_func() -> bool:
|
|
return bool(os.getenv("FUNCTIONS_WORKER_RUNTIME"))
|
|
|
|
|
|
def _is_gcp_func() -> bool:
|
|
return bool(os.getenv("K_SERVICE") or os.getenv("FUNCTION_NAME"))
|
|
|
|
|
|
def _is_vercel() -> bool:
|
|
return bool(os.getenv("VERCEL"))
|
|
|
|
|
|
def _is_faas() -> bool:
|
|
return _is_lambda() or _is_azure_func() or _is_gcp_func() or _is_vercel()
|
|
|
|
|
|
def _getenv_int(key: str) -> Optional[int]:
|
|
"""Like os.getenv but returns an int, or None if the value is missing/malformed."""
|
|
val = os.getenv(key)
|
|
if not val:
|
|
return None
|
|
try:
|
|
return int(val)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _metadata_env() -> dict[str, Any]:
|
|
env: dict[str, Any] = {}
|
|
container = get_container_env_info()
|
|
if container:
|
|
env["container"] = container
|
|
# Skip if multiple (or no) envs are matched.
|
|
if (_is_lambda(), _is_azure_func(), _is_gcp_func(), _is_vercel()).count(True) != 1:
|
|
return env
|
|
if _is_lambda():
|
|
env["name"] = "aws.lambda"
|
|
region = os.getenv("AWS_REGION")
|
|
if region:
|
|
env["region"] = region
|
|
memory_mb = _getenv_int("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
|
|
if memory_mb is not None:
|
|
env["memory_mb"] = memory_mb
|
|
elif _is_azure_func():
|
|
env["name"] = "azure.func"
|
|
elif _is_gcp_func():
|
|
env["name"] = "gcp.func"
|
|
region = os.getenv("FUNCTION_REGION")
|
|
if region:
|
|
env["region"] = region
|
|
memory_mb = _getenv_int("FUNCTION_MEMORY_MB")
|
|
if memory_mb is not None:
|
|
env["memory_mb"] = memory_mb
|
|
timeout_sec = _getenv_int("FUNCTION_TIMEOUT_SEC")
|
|
if timeout_sec is not None:
|
|
env["timeout_sec"] = timeout_sec
|
|
elif _is_vercel():
|
|
env["name"] = "vercel"
|
|
region = os.getenv("VERCEL_REGION")
|
|
if region:
|
|
env["region"] = region
|
|
return env
|
|
|
|
|
|
_MAX_METADATA_SIZE = 512
|
|
|
|
|
|
# See: https://github.com/mongodb/specifications/blob/5112bcc/source/mongodb-handshake/handshake.rst#limitations
|
|
def _truncate_metadata(metadata: MutableMapping[str, Any]) -> None:
|
|
"""Perform metadata truncation."""
|
|
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
|
|
return
|
|
# 1. Omit fields from env except env.name.
|
|
env_name = metadata.get("env", {}).get("name")
|
|
if env_name:
|
|
metadata["env"] = {"name": env_name}
|
|
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
|
|
return
|
|
# 2. Omit fields from os except os.type.
|
|
os_type = metadata.get("os", {}).get("type")
|
|
if os_type:
|
|
metadata["os"] = {"type": os_type}
|
|
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
|
|
return
|
|
# 3. Omit the env document entirely.
|
|
metadata.pop("env", None)
|
|
encoded_size = len(bson.encode(metadata))
|
|
if encoded_size <= _MAX_METADATA_SIZE:
|
|
return
|
|
# 4. Truncate platform.
|
|
overflow = encoded_size - _MAX_METADATA_SIZE
|
|
plat = metadata.get("platform", "")
|
|
if plat:
|
|
plat = plat[:-overflow]
|
|
if plat:
|
|
metadata["platform"] = plat
|
|
else:
|
|
metadata.pop("platform", None)
|
|
encoded_size = len(bson.encode(metadata))
|
|
if encoded_size <= _MAX_METADATA_SIZE:
|
|
return
|
|
# 5. Truncate driver info.
|
|
overflow = encoded_size - _MAX_METADATA_SIZE
|
|
driver = metadata.get("driver", {})
|
|
if driver:
|
|
# Truncate driver version.
|
|
driver_version = driver.get("version")[:-overflow]
|
|
if len(driver_version) >= len(_METADATA["driver"]["version"]):
|
|
metadata["driver"]["version"] = driver_version
|
|
else:
|
|
metadata["driver"]["version"] = _METADATA["driver"]["version"]
|
|
encoded_size = len(bson.encode(metadata))
|
|
if encoded_size <= _MAX_METADATA_SIZE:
|
|
return
|
|
# Truncate driver name.
|
|
overflow = encoded_size - _MAX_METADATA_SIZE
|
|
driver_name = driver.get("name")[:-overflow]
|
|
if len(driver_name) >= len(_METADATA["driver"]["name"]):
|
|
metadata["driver"]["name"] = driver_name
|
|
else:
|
|
metadata["driver"]["name"] = _METADATA["driver"]["name"]
|
|
|
|
|
|
# If the first getaddrinfo call of this interpreter's life is on a thread,
|
|
# while the main thread holds the import lock, getaddrinfo deadlocks trying
|
|
# to import the IDNA codec. Import it here, where presumably we're on the
|
|
# main thread, to avoid the deadlock. See PYTHON-607.
|
|
"foo".encode("idna")
|
|
|
|
|
|
class PoolOptions:
|
|
"""Read only connection pool options for an AsyncMongoClient/MongoClient.
|
|
|
|
Should not be instantiated directly by application developers. Access
|
|
a client's pool options via
|
|
:attr:`~pymongo.client_options.ClientOptions.pool_options` instead::
|
|
|
|
pool_opts = client.options.pool_options
|
|
pool_opts.max_pool_size
|
|
pool_opts.min_pool_size
|
|
|
|
"""
|
|
|
|
__slots__ = (
|
|
"__max_pool_size",
|
|
"__min_pool_size",
|
|
"__max_idle_time_seconds",
|
|
"__connect_timeout",
|
|
"__socket_timeout",
|
|
"__wait_queue_timeout",
|
|
"__ssl_context",
|
|
"__tls_allow_invalid_hostnames",
|
|
"__event_listeners",
|
|
"__appname",
|
|
"__driver",
|
|
"__metadata",
|
|
"__compression_settings",
|
|
"__max_connecting",
|
|
"__pause_enabled",
|
|
"__server_api",
|
|
"__load_balanced",
|
|
"__credentials",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
max_pool_size: int = MAX_POOL_SIZE,
|
|
min_pool_size: int = MIN_POOL_SIZE,
|
|
max_idle_time_seconds: Optional[int] = MAX_IDLE_TIME_SEC,
|
|
connect_timeout: Optional[float] = None,
|
|
socket_timeout: Optional[float] = None,
|
|
wait_queue_timeout: Optional[int] = WAIT_QUEUE_TIMEOUT,
|
|
ssl_context: Optional[SSLContext] = None,
|
|
tls_allow_invalid_hostnames: bool = False,
|
|
event_listeners: Optional[_EventListeners] = None,
|
|
appname: Optional[str] = None,
|
|
driver: Optional[DriverInfo] = None,
|
|
compression_settings: Optional[CompressionSettings] = None,
|
|
max_connecting: int = MAX_CONNECTING,
|
|
pause_enabled: bool = True,
|
|
server_api: Optional[ServerApi] = None,
|
|
load_balanced: Optional[bool] = None,
|
|
credentials: Optional[MongoCredential] = None,
|
|
is_sync: Optional[bool] = True,
|
|
):
|
|
self.__max_pool_size = max_pool_size
|
|
self.__min_pool_size = min_pool_size
|
|
self.__max_idle_time_seconds = max_idle_time_seconds
|
|
self.__connect_timeout = connect_timeout
|
|
self.__socket_timeout = socket_timeout
|
|
self.__wait_queue_timeout = wait_queue_timeout
|
|
self.__ssl_context = ssl_context
|
|
self.__tls_allow_invalid_hostnames = tls_allow_invalid_hostnames
|
|
self.__event_listeners = event_listeners
|
|
self.__appname = appname
|
|
self.__driver = driver
|
|
self.__compression_settings = compression_settings
|
|
self.__max_connecting = max_connecting
|
|
self.__pause_enabled = pause_enabled
|
|
self.__server_api = server_api
|
|
self.__load_balanced = load_balanced
|
|
self.__credentials = credentials
|
|
self.__metadata = copy.deepcopy(_METADATA)
|
|
|
|
if appname:
|
|
self.__metadata["application"] = {"name": appname}
|
|
|
|
# Combine the "driver" AsyncMongoClient option with PyMongo's info, like:
|
|
# {
|
|
# 'driver': {
|
|
# 'name': 'PyMongo|MyDriver',
|
|
# 'version': '4.2.0|1.2.3',
|
|
# },
|
|
# 'platform': 'CPython 3.8.0|MyPlatform'
|
|
# }
|
|
if not is_sync:
|
|
self.__metadata["driver"]["name"] = "{}|{}".format(
|
|
self.__metadata["driver"]["name"],
|
|
"async",
|
|
)
|
|
if driver:
|
|
if driver.name:
|
|
self.__metadata["driver"]["name"] = "{}|{}".format(
|
|
self.__metadata["driver"]["name"],
|
|
driver.name,
|
|
)
|
|
if driver.version:
|
|
self.__metadata["driver"]["version"] = "{}|{}".format(
|
|
_METADATA["driver"]["version"],
|
|
driver.version,
|
|
)
|
|
if driver.platform:
|
|
self.__metadata["platform"] = "{}|{}".format(_METADATA["platform"], driver.platform)
|
|
|
|
env = _metadata_env()
|
|
if env:
|
|
self.__metadata["env"] = env
|
|
|
|
_truncate_metadata(self.__metadata)
|
|
|
|
@property
|
|
def _credentials(self) -> Optional[MongoCredential]:
|
|
"""A :class:`~pymongo.auth.MongoCredentials` instance or None."""
|
|
return self.__credentials
|
|
|
|
@property
|
|
def non_default_options(self) -> dict[str, Any]:
|
|
"""The non-default options this pool was created with.
|
|
|
|
Added for CMAP's :class:`PoolCreatedEvent`.
|
|
"""
|
|
opts = {}
|
|
if self.__max_pool_size != MAX_POOL_SIZE:
|
|
opts["maxPoolSize"] = self.__max_pool_size
|
|
if self.__min_pool_size != MIN_POOL_SIZE:
|
|
opts["minPoolSize"] = self.__min_pool_size
|
|
if self.__max_idle_time_seconds != MAX_IDLE_TIME_SEC:
|
|
assert self.__max_idle_time_seconds is not None
|
|
opts["maxIdleTimeMS"] = self.__max_idle_time_seconds * 1000
|
|
if self.__wait_queue_timeout != WAIT_QUEUE_TIMEOUT:
|
|
assert self.__wait_queue_timeout is not None
|
|
opts["waitQueueTimeoutMS"] = self.__wait_queue_timeout * 1000
|
|
if self.__max_connecting != MAX_CONNECTING:
|
|
opts["maxConnecting"] = self.__max_connecting
|
|
return opts
|
|
|
|
@property
|
|
def max_pool_size(self) -> float:
|
|
"""The maximum allowable number of concurrent connections to each
|
|
connected server. Requests to a server will block if there are
|
|
`maxPoolSize` outstanding connections to the requested server.
|
|
Defaults to 100. Cannot be 0.
|
|
|
|
When a server's pool has reached `max_pool_size`, operations for that
|
|
server block waiting for a socket to be returned to the pool. If
|
|
``waitQueueTimeoutMS`` is set, a blocked operation will raise
|
|
:exc:`~pymongo.errors.ConnectionFailure` after a timeout.
|
|
By default ``waitQueueTimeoutMS`` is not set.
|
|
"""
|
|
return self.__max_pool_size
|
|
|
|
@property
|
|
def min_pool_size(self) -> int:
|
|
"""The minimum required number of concurrent connections that the pool
|
|
will maintain to each connected server. Default is 0.
|
|
"""
|
|
return self.__min_pool_size
|
|
|
|
@property
|
|
def max_connecting(self) -> int:
|
|
"""The maximum number of concurrent connection creation attempts per
|
|
pool. Defaults to 2.
|
|
"""
|
|
return self.__max_connecting
|
|
|
|
@property
|
|
def pause_enabled(self) -> bool:
|
|
return self.__pause_enabled
|
|
|
|
@property
|
|
def max_idle_time_seconds(self) -> Optional[int]:
|
|
"""The maximum number of seconds that a connection can remain
|
|
idle in the pool before being removed and replaced. Defaults to
|
|
`None` (no limit).
|
|
"""
|
|
return self.__max_idle_time_seconds
|
|
|
|
@property
|
|
def connect_timeout(self) -> Optional[float]:
|
|
"""How long a connection can take to be opened before timing out."""
|
|
return self.__connect_timeout
|
|
|
|
@property
|
|
def socket_timeout(self) -> Optional[float]:
|
|
"""How long a send or receive on a socket can take before timing out."""
|
|
return self.__socket_timeout
|
|
|
|
@property
|
|
def wait_queue_timeout(self) -> Optional[int]:
|
|
"""How long a thread will wait for a socket from the pool if the pool
|
|
has no free sockets.
|
|
"""
|
|
return self.__wait_queue_timeout
|
|
|
|
@property
|
|
def _ssl_context(self) -> Optional[SSLContext]:
|
|
"""An SSLContext instance or None."""
|
|
return self.__ssl_context
|
|
|
|
@property
|
|
def tls_allow_invalid_hostnames(self) -> bool:
|
|
"""If True skip ssl.match_hostname."""
|
|
return self.__tls_allow_invalid_hostnames
|
|
|
|
@property
|
|
def _event_listeners(self) -> Optional[_EventListeners]:
|
|
"""An instance of pymongo.monitoring._EventListeners."""
|
|
return self.__event_listeners
|
|
|
|
@property
|
|
def appname(self) -> Optional[str]:
|
|
"""The application name, for sending with hello in server handshake."""
|
|
return self.__appname
|
|
|
|
@property
|
|
def driver(self) -> Optional[DriverInfo]:
|
|
"""Driver name and version, for sending with hello in handshake."""
|
|
return self.__driver
|
|
|
|
@property
|
|
def _compression_settings(self) -> Optional[CompressionSettings]:
|
|
return self.__compression_settings
|
|
|
|
@property
|
|
def metadata(self) -> dict[str, Any]:
|
|
"""A dict of metadata about the application, driver, os, and platform."""
|
|
return self.__metadata.copy()
|
|
|
|
@property
|
|
def server_api(self) -> Optional[ServerApi]:
|
|
"""A pymongo.server_api.ServerApi or None."""
|
|
return self.__server_api
|
|
|
|
@property
|
|
def load_balanced(self) -> Optional[bool]:
|
|
"""True if this Pool is configured in load balanced mode."""
|
|
return self.__load_balanced
|