Uploaded image for project: 'Python Driver'
  1. Python Driver
  2. PYTHON-5158

Async client mishandles EAGAIN when pyopenssl is installed

    • Type: Icon: Bug Bug
    • Resolution: Won't Do
    • Priority: Icon: Unknown Unknown
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Python Drivers
    • Hide

      1. What would you like to communicate to the user about this feature?
      2. Would you like the user to see examples of the syntax and/or executable code and its output?
      3. Which versions of the driver/connector does this apply to?

      Show
      1. What would you like to communicate to the user about this feature? 2. Would you like the user to see examples of the syntax and/or executable code and its output? 3. Which versions of the driver/connector does this apply to?
    • None
    • None
    • None
    • None
    • None
    • None

      test.asynchronous.test_encryption.TestKmsTLSOptions.test_06_named_kms_providers_apply_tls_options_gcp failed with an EAGAIN error which indicates we are improperly handling async pyopenssl connections:

       [2025/02/26 12:36:40.564] FAILURE: AssertionError: "HTTP status=404" does not match "KMS request failed after 3 retries due to a network error, last attempt failed with: 127.0.0.1:9002: (11, 'EAGAIN') (configured timeouts: socketTimeoutMS: 20000.0ms, connectTimeoutMS: 20000.0ms)" ()
       [2025/02/26 12:36:40.564] self = <pymongo.asynchronous.encryption._EncryptionIO object at 0x7fd146023da0>
       [2025/02/26 12:36:40.564] kms_context = <pymongocrypt.mongocrypt.MongoCryptKmsContext object at 0x7fd125d9d2a0>
       [2025/02/26 12:36:40.564]     async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
       [2025/02/26 12:36:40.564]         """Complete a KMS request.
       [2025/02/26 12:36:40.564]     
       [2025/02/26 12:36:40.564]         :param kms_context: A :class:`MongoCryptKmsContext`.
       [2025/02/26 12:36:40.564]     
       [2025/02/26 12:36:40.564]         :return: None
       [2025/02/26 12:36:40.564]         """
       [2025/02/26 12:36:40.564]         endpoint = kms_context.endpoint
       [2025/02/26 12:36:40.564]         message = kms_context.message
       [2025/02/26 12:36:40.564]         provider = kms_context.kms_provider
       [2025/02/26 12:36:40.564]         ctx = self.opts._kms_ssl_contexts.get(provider)
       [2025/02/26 12:36:40.564]         if ctx is None:
       [2025/02/26 12:36:40.564]             # Enable strict certificate verification, OCSP, match hostname, and
       [2025/02/26 12:36:40.564]             # SNI using the system default CA certificates.
       [2025/02/26 12:36:40.564]             ctx = get_ssl_context(
       [2025/02/26 12:36:40.564]                 None,  # certfile
       [2025/02/26 12:36:40.564]                 None,  # passphrase
       [2025/02/26 12:36:40.564]                 None,  # ca_certs
       [2025/02/26 12:36:40.564]                 None,  # crlfile
       [2025/02/26 12:36:40.564]                 False,  # allow_invalid_certificates
       [2025/02/26 12:36:40.564]                 False,  # allow_invalid_hostnames
       [2025/02/26 12:36:40.564]                 False,  # disable_ocsp_endpoint_check
       [2025/02/26 12:36:40.564]             )
       [2025/02/26 12:36:40.564]         # CSOT: set timeout for socket creation.
       [2025/02/26 12:36:40.564]         connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)
       [2025/02/26 12:36:40.564]         opts = PoolOptions(
       [2025/02/26 12:36:40.564]             connect_timeout=connect_timeout,
       [2025/02/26 12:36:40.564]             socket_timeout=connect_timeout,
       [2025/02/26 12:36:40.564]             ssl_context=ctx,
       [2025/02/26 12:36:40.564]         )
       [2025/02/26 12:36:40.564]         address = parse_host(endpoint, _HTTPS_PORT)
       [2025/02/26 12:36:40.564]         sleep_u = kms_context.usleep
       [2025/02/26 12:36:40.564]         if sleep_u:
       [2025/02/26 12:36:40.564]             sleep_sec = float(sleep_u) / 1e6
       [2025/02/26 12:36:40.564]             await asyncio.sleep(sleep_sec)
       [2025/02/26 12:36:40.564]         try:
       [2025/02/26 12:36:40.564]             conn = await _connect_kms(address, opts)
       [2025/02/26 12:36:40.564]             try:
       [2025/02/26 12:36:40.564]                 await async_sendall(conn, message)
       [2025/02/26 12:36:40.564]                 while kms_context.bytes_needed > 0:
       [2025/02/26 12:36:40.564]                     # CSOT: update timeout.
       [2025/02/26 12:36:40.564]                     conn.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0))
       [2025/02/26 12:36:40.564]                     if _IS_SYNC:
       [2025/02/26 12:36:40.564]                         data = conn.recv(kms_context.bytes_needed)
       [2025/02/26 12:36:40.564]                     else:
       [2025/02/26 12:36:40.564]                         from pymongo.network_layer import (  # type: ignore[attr-defined]
       [2025/02/26 12:36:40.564]                             async_receive_data_socket,
       [2025/02/26 12:36:40.564]                         )
       [2025/02/26 12:36:40.564]     
       [2025/02/26 12:36:40.564] >                       data = await async_receive_data_socket(conn, kms_context.bytes_needed)
       [2025/02/26 12:36:40.564] pymongo/asynchronous/encryption.py:210: 
       [2025/02/26 12:36:40.564] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
       [2025/02/26 12:36:40.564] pymongo/network_layer.py:303: in async_receive_data_socket
       [2025/02/26 12:36:40.564]     return await asyncio.wait_for(
       [2025/02/26 12:36:40.564] /opt/python/3.12/lib/python3.12/asyncio/tasks.py:520: in wait_for
       [2025/02/26 12:36:40.564]     return await fut
       [2025/02/26 12:36:40.564] pymongo/network_layer.py:146: in _async_receive_ssl
       [2025/02/26 12:36:40.564]     read = conn.recv_into(mv[total_read:])
       [2025/02/26 12:36:40.564] pymongo/pyopenssl_context.py:165: in recv_into
       [2025/02/26 12:36:40.564]     return self._call(super().recv_into, *args, **kwargs)
       [2025/02/26 12:36:40.564] pymongo/pyopenssl_context.py:126: in _call
       [2025/02/26 12:36:40.564]     return call(*args, **kwargs)
       [2025/02/26 12:36:40.564] .local/uv/cache/builds-v0/.tmp56qHKz/lib/python3.12/site-packages/OpenSSL/SSL.py:2268: in recv_into
       [2025/02/26 12:36:40.564]     self._raise_ssl_error(self._ssl, result)
       [2025/02/26 12:36:40.564] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
       [2025/02/26 12:36:40.564] self = <pymongo.pyopenssl_context._sslConn object at 0x7fd125d9c4a0>
       [2025/02/26 12:36:40.564] ssl = <cdata 'SSL *' 0x4bdda40>, result = -1
       [2025/02/26 12:36:40.564]     def _raise_ssl_error(self, ssl: Any, result: int) -> None:
       [2025/02/26 12:36:40.564]         if self._context._verify_helper is not None:
       [2025/02/26 12:36:40.564]             self._context._verify_helper.raise_if_problem()
       [2025/02/26 12:36:40.564]         if self._context._alpn_select_helper is not None:
       [2025/02/26 12:36:40.564]             self._context._alpn_select_helper.raise_if_problem()
       [2025/02/26 12:36:40.564]         if self._context._ocsp_helper is not None:
       [2025/02/26 12:36:40.564]             self._context._ocsp_helper.raise_if_problem()
       [2025/02/26 12:36:40.564]     
       [2025/02/26 12:36:40.564]         error = _lib.SSL_get_error(ssl, result)
       [2025/02/26 12:36:40.564]         if error == _lib.SSL_ERROR_WANT_READ:
       [2025/02/26 12:36:40.564]             raise WantReadError()
       [2025/02/26 12:36:40.564]         elif error == _lib.SSL_ERROR_WANT_WRITE:
       [2025/02/26 12:36:40.564]             raise WantWriteError()
       [2025/02/26 12:36:40.564]         elif error == _lib.SSL_ERROR_ZERO_RETURN:
       [2025/02/26 12:36:40.564]             raise ZeroReturnError()
       [2025/02/26 12:36:40.564]         elif error == _lib.SSL_ERROR_WANT_X509_LOOKUP:
       [2025/02/26 12:36:40.564]             # TODO: This is untested.
       [2025/02/26 12:36:40.564]             raise WantX509LookupError()
       [2025/02/26 12:36:40.564]         elif error == _lib.SSL_ERROR_SYSCALL:
       [2025/02/26 12:36:40.564]             if platform == "win32":
       [2025/02/26 12:36:40.564]                 errno = _ffi.getwinerror()[0]
       [2025/02/26 12:36:40.564]             else:
       [2025/02/26 12:36:40.564]                 errno = _ffi.errno
       [2025/02/26 12:36:40.564]             if _lib.ERR_peek_error() == 0 or errno != 0:
       [2025/02/26 12:36:40.564]                 if result < 0 and errno != 0:
       [2025/02/26 12:36:40.564] >                   raise SysCallError(errno, errorcode.get(errno))
       [2025/02/26 12:36:40.564] E                   OpenSSL.SSL.SysCallError: (11, 'EAGAIN')
       [2025/02/26 12:36:40.564] .local/uv/cache/builds-v0/.tmp56qHKz/lib/python3.12/site-packages/OpenSSL/SSL.py:1962: SysCallError
       [2025/02/26 12:36:40.564] The above exception was the direct cause of the following exception:
      ...
      

      https://spruce.mongodb.com/task/mongo_python_driver_encryption_pyopenssl_rhel8_python3.12_test_8.0_standalone_noauth_nossl_sync_async_61feccacfefaf342ad673bf320b68d839bbdf66c_25_02_26_19_23_02/tests?execution=0&sortBy=STATUS&sortDir=ASC

      We should retry the recv after EAGAIN similar to how we handle WantReadError and WantWriteError.

            Assignee:
            Unassigned Unassigned
            Reporter:
            shane.harvey@mongodb.com Shane Harvey
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved:
              None
              None
              None
              None