queues/venv/lib/python3.11/site-packages/pymongo/pool_options.py
Egor Matveev 6c6a549aff
All checks were successful
Deploy Prod / Build (pull_request) Successful in 9s
Deploy Prod / Push (pull_request) Successful in 12s
Deploy Prod / Deploy prod (pull_request) Successful in 10s
fix
2024-12-28 22:48:16 +03:00

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