Skip to content
6 changes: 6 additions & 0 deletions pymongo/driver_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class DriverInfo(namedtuple("DriverInfo", ["name", "version", "platform"])):
can add its own info to this log message. Initialize with three strings
like 'MyDriver', '1.2.3', 'some platform info'. Any of these strings may be
None to accept PyMongo's default.

The ``|`` character is the reserved delimiter used to join appended
metadata, so it must not appear in any of the fields. A
:class:`ValueError` is raised if it does.
"""

def __new__(
Expand All @@ -42,5 +46,7 @@ def __new__(
raise TypeError(
f"Wrong type for DriverInfo {key} option, value must be an instance of str, not {type(value)}"
)
if value and "|" in value:
raise ValueError(f"DriverInfo {key} must not contain the '|' delimiter")
Comment thread
blink1073 marked this conversation as resolved.

return self
93 changes: 60 additions & 33 deletions pymongo/pool_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
WAIT_QUEUE_TIMEOUT,
has_c,
)
from pymongo.lock import _create_lock

if TYPE_CHECKING:
from pymongo.auth_shared import MongoCredential
Expand Down Expand Up @@ -234,26 +235,33 @@ def _truncate_metadata(metadata: MutableMapping[str, Any]) -> None:
encoded_size = len(bson.encode(metadata))
if encoded_size <= _MAX_METADATA_SIZE:
return
# 5. Truncate driver info.
overflow = encoded_size - _MAX_METADATA_SIZE
# 5. Truncate driver info, keeping name and version 1:1 index-aligned.
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"]
# Trim wrapper version content first, dropping paired segments only as
# a last resort, so name and version stay 1:1 aligned.
while True:
encoded_size = len(bson.encode(metadata))
if encoded_size <= _MAX_METADATA_SIZE:
break
overflow = encoded_size - _MAX_METADATA_SIZE
previous = (driver.get("name"), driver.get("version"))
n_parts = driver.get("name", "").split("|")
v_parts = driver.get("version", "").split("|")

if len(v_parts) > 1 and v_parts[-1]:
v_parts[-1] = v_parts[-1][:-overflow]
driver["version"] = "|".join(v_parts)
elif len(n_parts) > 1:
n_parts.pop()
v_parts.pop()
driver["name"] = "|".join(n_parts)
driver["version"] = "|".join(v_parts)
else:
break

if previous == (driver.get("name"), driver.get("version")):
break


# If the first getaddrinfo call of this interpreter's life is on a thread,
Expand All @@ -277,6 +285,7 @@ class PoolOptions:
"""

__slots__ = (
"__appended_drivers",
"__appname",
"__compression_settings",
"__connect_timeout",
Expand All @@ -288,6 +297,7 @@ class PoolOptions:
"__max_idle_time_seconds",
"__max_pool_size",
"__metadata",
"__metadata_lock",
"__min_pool_size",
"__pause_enabled",
"__server_api",
Expand Down Expand Up @@ -336,6 +346,8 @@ def __init__(
self.__load_balanced = load_balanced
self.__credentials = credentials
self.__metadata = copy.deepcopy(_METADATA)
self.__appended_drivers: list[DriverInfo] = []
self.__metadata_lock = _create_lock()

if appname:
self.__metadata["application"] = {"name": appname}
Expand All @@ -353,11 +365,19 @@ def __init__(
self.__metadata["driver"]["name"],
"c",
)
self.__metadata["driver"]["version"] = "{}|{}".format(
self.__metadata["driver"]["version"],
"",
)
if not is_sync:
self.__metadata["driver"]["name"] = "{}|{}".format(
self.__metadata["driver"]["name"],
"async",
)
self.__metadata["driver"]["version"] = "{}|{}".format(
self.__metadata["driver"]["version"],
"",
)
if driver:
self._update_metadata(driver)

Expand All @@ -368,28 +388,35 @@ def __init__(
_truncate_metadata(self.__metadata)

def _update_metadata(self, driver: DriverInfo) -> None:
"""Updates the client's metadata"""
if driver.name and driver.name.lower() in self.__metadata["driver"]["name"].lower().split(
"|"
):
return
"""Updates the client's metadata."""
with self.__metadata_lock:
if driver in self.__appended_drivers:
return

metadata = copy.deepcopy(self.__metadata)
name_delims = self.__metadata["driver"]["name"].count("|")
metadata = copy.deepcopy(self.__metadata)

if driver.name:
metadata["driver"]["name"] = "{}|{}".format(
metadata["driver"]["name"],
driver.name,
metadata["driver"]["name"], driver.name or ""
)
if driver.version:
metadata["driver"]["version"] = "{}|{}".format(
metadata["driver"]["version"],
driver.version,
metadata["driver"]["version"], driver.version or ""
)
if driver.platform:
metadata["platform"] = "{}|{}".format(metadata["platform"], driver.platform)
if driver.platform:
if "platform" in metadata:
metadata["platform"] = "{}|{}".format(metadata["platform"], driver.platform)
else:
metadata["platform"] = driver.platform

_truncate_metadata(metadata)

self.__metadata = metadata

self.__metadata = metadata
# Only track drivers whose appended name/version pair survived
# truncation (i.e. the name gained a segment), so __appended_drivers
# stays bounded and the dedup membership check stays fast.
if metadata["driver"]["name"].count("|") > name_delims:
self.__appended_drivers.append(driver)

@property
def _credentials(self) -> Optional[MongoCredential]:
Expand Down
66 changes: 65 additions & 1 deletion test/asynchronous/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@
_IS_SYNC = False


def _driver_version(base_version: str, name: str, last_version: str | None = None) -> str:
"""Build a metadata driver version aligned 1:1 with ``name`` segments.

The ``|c`` and ``|async`` name segments always have an empty version entry,
so the version string has one delimiter per name delimiter. ``last_version``
is used when the final segment carries a wrapped driver's version.
"""
segments = [""] * name.count("|")
if last_version is not None:
segments[-1] = last_version
return "|".join([base_version, *segments])


class AsyncClientUnitTest(AsyncUnitTest):
"""AsyncMongoClient tests that don't require a server."""

Expand Down Expand Up @@ -386,6 +399,9 @@ async def test_metadata(self):
metadata["driver"]["name"] = "PyMongo|c|async"
else:
metadata["driver"]["name"] = "PyMongo|async"
metadata["driver"]["version"] = _driver_version(
_METADATA["driver"]["version"], metadata["driver"]["name"]
)
metadata["application"] = {"name": "foobar"}
client = self.simple_client("mongodb://foo:27017/?appname=foobar&connect=false")
options = client.options
Expand All @@ -412,7 +428,9 @@ async def test_metadata(self):
metadata["driver"]["name"] = "PyMongo|c|async|FooDriver"
else:
metadata["driver"]["name"] = "PyMongo|async|FooDriver"
metadata["driver"]["version"] = "{}|1.2.3".format(_METADATA["driver"]["version"])
metadata["driver"]["version"] = _driver_version(
_METADATA["driver"]["version"], metadata["driver"]["name"], last_version="1.2.3"
)
client = self.simple_client(
"foo",
27017,
Expand All @@ -422,6 +440,13 @@ async def test_metadata(self):
)
options = client.options
self.assertEqual(options.pool_options.metadata, metadata)
if has_c():
metadata["driver"]["name"] = "PyMongo|c|async|FooDriver"
else:
metadata["driver"]["name"] = "PyMongo|async|FooDriver"
metadata["driver"]["version"] = _driver_version(
_METADATA["driver"]["version"], metadata["driver"]["name"], last_version="1.2.3"
)
metadata["platform"] = "{}|FooPlatform".format(_METADATA["platform"])
client = self.simple_client(
"foo",
Expand All @@ -438,19 +463,55 @@ async def test_metadata(self):
connect=False,
)
options = client.options
truncated = options.pool_options.metadata["driver"]
self.assertLessEqual(
len(bson.encode(options.pool_options.metadata)),
_MAX_METADATA_SIZE,
)
self.assertEqual(
truncated["name"].count("|"),
truncated["version"].count("|"),
)
client = self.simple_client(
driver=DriverInfo(name="s" * _MAX_METADATA_SIZE, version="s" * _MAX_METADATA_SIZE),
connect=False,
)
options = client.options
truncated = options.pool_options.metadata["driver"]
self.assertLessEqual(
len(bson.encode(options.pool_options.metadata)),
_MAX_METADATA_SIZE,
)
self.assertEqual(
truncated["name"].count("|"),
truncated["version"].count("|"),
)
# Successive appends must stay within the limit and keep name and
# version index-aligned after truncation. Once the metadata saturates,
# further appends must not grow the dedup tracking list.
client = self.simple_client(connect=False)
for i in range(300):
client.append_metadata(DriverInfo(name=f"D{i}", version=f"1.{i}"))
pool = client.options.pool_options
self.assertLessEqual(len(bson.encode(pool.metadata)), _MAX_METADATA_SIZE)
self.assertEqual(
pool.metadata["driver"]["name"].count("|"),
pool.metadata["driver"]["version"].count("|"),
)
count = len(pool._PoolOptions__appended_drivers)
for i in range(300, 600):
client.append_metadata(DriverInfo(name=f"D{i}", version=f"1.{i}"))
self.assertEqual(len(pool._PoolOptions__appended_drivers), count)
# Platform-only appends (empty name/version) stay bounded the same way.
client = self.simple_client(connect=False)
for i in range(300):
client.append_metadata(DriverInfo(name="", version="", platform=f"P{i}"))
pool = client.options.pool_options
self.assertLessEqual(len(bson.encode(pool.metadata)), _MAX_METADATA_SIZE)
count = len(pool._PoolOptions__appended_drivers)
for i in range(300, 600):
client.append_metadata(DriverInfo(name="", version="", platform=f"P{i}"))
self.assertEqual(len(pool._PoolOptions__appended_drivers), count)

@mock.patch.dict("os.environ", {ENV_VAR_K8S: "1"})
def test_container_metadata(self):
Expand Down Expand Up @@ -2224,6 +2285,9 @@ async def _test_handshake(self, env_vars, expected_env):
metadata["driver"]["name"] = "PyMongo|c|async"
else:
metadata["driver"]["name"] = "PyMongo|async"
metadata["driver"]["version"] = _driver_version(
_METADATA["driver"]["version"], metadata["driver"]["name"]
)
if expected_env is not None:
metadata["env"] = expected_env

Expand Down
Loading
Loading