Skip to content

Commit 3d3e4dc

Browse files
authored
PYTHON-3464 Add FaaS platform to handshake metadata (#1204)
Truncate metadata env, os, and platform fields if needed.
1 parent d340710 commit 3d3e4dc

File tree

2 files changed

+272
-83
lines changed

2 files changed

+272
-83
lines changed

pymongo/pool.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
import threading
2424
import time
2525
import weakref
26-
from typing import Any, NoReturn, Optional
26+
from typing import Any, Dict, NoReturn, Optional
2727

28+
import bson
2829
from bson import DEFAULT_CODEC_OPTIONS
2930
from bson.son import SON
3031
from pymongo import __version__, _csot, auth, helpers
@@ -231,6 +232,108 @@ def _set_keepalive_times(sock):
231232
)
232233

233234

235+
def _is_lambda() -> bool:
236+
if os.getenv("AWS_LAMBDA_RUNTIME_API"):
237+
return True
238+
env = os.getenv("AWS_EXECUTION_ENV")
239+
if env:
240+
return env.startswith("AWS_Lambda_")
241+
return False
242+
243+
244+
def _is_azure_func() -> bool:
245+
return bool(os.getenv("FUNCTIONS_WORKER_RUNTIME"))
246+
247+
248+
def _is_gcp_func() -> bool:
249+
return bool(os.getenv("K_SERVICE") or os.getenv("FUNCTION_NAME"))
250+
251+
252+
def _is_vercel() -> bool:
253+
return bool(os.getenv("VERCEL"))
254+
255+
256+
def _getenv_int(key: str) -> Optional[int]:
257+
"""Like os.getenv but returns an int, or None if the value is missing/malformed."""
258+
val = os.getenv(key)
259+
if not val:
260+
return None
261+
try:
262+
return int(val)
263+
except ValueError:
264+
return None
265+
266+
267+
def _metadata_env() -> Dict[str, Any]:
268+
env: Dict[str, Any] = {}
269+
# Skip if multiple (or no) envs are matched.
270+
if (_is_lambda(), _is_azure_func(), _is_gcp_func(), _is_vercel()).count(True) != 1:
271+
return env
272+
if _is_lambda():
273+
env["name"] = "aws.lambda"
274+
region = os.getenv("AWS_REGION")
275+
if region:
276+
env["region"] = region
277+
memory_mb = _getenv_int("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")
278+
if memory_mb is not None:
279+
env["memory_mb"] = memory_mb
280+
elif _is_azure_func():
281+
env["name"] = "azure.func"
282+
elif _is_gcp_func():
283+
env["name"] = "gcp.func"
284+
region = os.getenv("FUNCTION_REGION")
285+
if region:
286+
env["region"] = region
287+
memory_mb = _getenv_int("FUNCTION_MEMORY_MB")
288+
if memory_mb is not None:
289+
env["memory_mb"] = memory_mb
290+
timeout_sec = _getenv_int("FUNCTION_TIMEOUT_SEC")
291+
if timeout_sec is not None:
292+
env["timeout_sec"] = timeout_sec
293+
elif _is_vercel():
294+
env["name"] = "vercel"
295+
region = os.getenv("VERCEL_REGION")
296+
if region:
297+
env["region"] = region
298+
return env
299+
300+
301+
_MAX_METADATA_SIZE = 512
302+
303+
304+
# See: https://github.com/mongodb/specifications/blob/5112bcc/source/mongodb-handshake/handshake.rst#limitations
305+
def _truncate_metadata(metadata):
306+
"""Perform metadata truncation."""
307+
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
308+
return
309+
# 1. Omit fields from env except env.name.
310+
env_name = metadata.get("env", {}).get("name")
311+
if env_name:
312+
metadata["env"] = {"name": env_name}
313+
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
314+
return
315+
# 2. Omit fields from os except os.type.
316+
os_type = metadata.get("os", {}).get("type")
317+
if os_type:
318+
metadata["os"] = {"type": os_type}
319+
if len(bson.encode(metadata)) <= _MAX_METADATA_SIZE:
320+
return
321+
# 3. Omit the env document entirely.
322+
metadata.pop("env", None)
323+
encoded_size = len(bson.encode(metadata))
324+
if encoded_size <= _MAX_METADATA_SIZE:
325+
return
326+
# 4. Truncate platform.
327+
overflow = encoded_size - _MAX_METADATA_SIZE
328+
plat = metadata.get("platform", "")
329+
if plat:
330+
plat = plat[:-overflow]
331+
if plat:
332+
metadata["platform"] = plat
333+
else:
334+
metadata.pop("platform", None)
335+
336+
234337
# If the first getaddrinfo call of this interpreter's life is on a thread,
235338
# while the main thread holds the import lock, getaddrinfo deadlocks trying
236339
# to import the IDNA codec. Import it here, where presumably we're on the
@@ -364,6 +467,12 @@ def __init__(
364467
if driver.platform:
365468
self.__metadata["platform"] = "%s|%s" % (_METADATA["platform"], driver.platform)
366469

470+
env = _metadata_env()
471+
if env:
472+
self.__metadata["env"] = env
473+
474+
_truncate_metadata(self.__metadata)
475+
367476
@property
368477
def _credentials(self):
369478
"""A :class:`~pymongo.auth.MongoCredentials` instance or None."""

0 commit comments

Comments
 (0)