This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new c512ac9e0b Consolidate gremlin-python read timeout into a single 
ReadTimeoutError (#3507)
c512ac9e0b is described below

commit c512ac9e0bd794b945d1763c9fd2706ca43c4f67
Author: Guian Gumpac <[email protected]>
AuthorDate: Fri Jul 10 11:38:27 2026 -0700

    Consolidate gremlin-python read timeout into a single ReadTimeoutError 
(#3507)
    
    gremlin-python armed two read-timeout mechanisms at the same value - the 
driver's
    async_timeout wrapper (raising asyncio.TimeoutError) and aiohttp's sock_read
    (raising SocketTimeoutError). Whichever fired first was a race, so a read 
timeout
    surfaced nondeterministically as one type or the other, which also made
    test_client_side_timeout_set_for_aiohttp flaky (it asserted on the 
exception message).
    
    Change: remove the redundant async_timeout read wrapper and keep aiohttp 
sock_read
    as the single read-timeout source (it also covers the initial-response wait 
and is
    streaming-safe). Normalize its SocketTimeoutError/ServerTimeoutError into a 
new
    driver-owned ReadTimeoutError so a read timeout always surfaces as one 
deterministic,
    transport-agnostic type. ReadTimeoutError subclasses the builtin 
TimeoutError, so it
    stays catchable via except TimeoutError while leaking neither asyncio nor 
aiohttp types
    (addresses review feedback). It is documented as temporary: once the driver 
is fully
    async it should revert to asyncio.TimeoutError (TINKERPOP-2774).
    
    Assisted-by: Kiro: Claude Opus 4.8
---
 CHANGELOG.asciidoc                                 |  1 +
 .../gremlin_python/driver/aiohttp/transport.py     | 25 +++++++++++------
 .../python/gremlin_python/driver/exceptions.py     | 31 ++++++++++++++++++++++
 .../python/tests/integration/driver/test_client.py | 11 +++++---
 4 files changed, 56 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 9df6d199c4..61a65871e0 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -40,6 +40,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Fixed `gremlin-javascript` `Client.submit()` so that an explicit 
`bulkResults: false` request option is forwarded to the server instead of being 
silently dropped.
 * Fixed `gremlin-dotnet` deflate response decompression, which threw on the 
server's zlib-framed output because it used `DeflateStream` (raw DEFLATE, RFC 
1951) instead of `ZLibStream` (zlib, RFC 1950); the bug was previously masked 
because compression was off by default.
 * Fixed `gremlin-dotnet` SSL options cloning (used on the 
skip-certificate-validation path) to copy `ClientCertificateContext` and 
`AllowTlsResume`, which were previously dropped, breaking mTLS client 
certificates and silently re-enabling TLS resumption.
+* Fixed `gremlin-python` read timeout to derive from a single source (aiohttp 
`sock_read`), removing a redundant `async_timeout` read wrapper that could race 
it; a read timeout now deterministically raises `ReadTimeoutError` (a builtin 
`TimeoutError` subclass).
 * Removed `Transaction.open()` in favor of `begin()`, which is now the single 
transaction-start primitive across embedded and remote contexts.
 * Changed `begin()` and `close()` to be idempotent and calling it when a 
transaction is already in that state no longer throws.
 * Added `maxTransactionLifetime` setting to Gremlin Server, an absolute cap on 
the total age of an HTTP transaction that interrupts a running operation and 
rolls the transaction back when it fires (default 600000ms, set to `0` to 
disable).
diff --git 
a/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py 
b/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
index 2367f8aaaa..447069ffd3 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
@@ -21,6 +21,8 @@ import asyncio
 import socket
 import sys
 
+from gremlin_python.driver.exceptions import ReadTimeoutError
+
 if sys.version_info >= (3, 11):
     import asyncio as async_timeout
 else:
@@ -71,6 +73,18 @@ def _normalize_compression(compression):
     raise TypeError("compression must be a str ('none'|'deflate'), got %s" % 
type(compression).__name__)
 
 
+def _run_read(loop, read_timeout, coro):
+    """Run a response-read coroutine on ``loop``, normalizing aiohttp's read 
timeout
+    (SocketTimeoutError / ServerTimeoutError) into a ``ReadTimeoutError`` so a 
read
+    timeout always surfaces as one deterministic, transport-agnostic type. It 
subclasses
+    the builtin ``TimeoutError`` so callers can still ``except 
TimeoutError``."""
+    try:
+        return loop.run_until_complete(coro)
+    except aiohttp.ServerTimeoutError as e:
+        raise ReadTimeoutError(
+            f"Read timed out after {read_timeout}s waiting for response 
data.") from e
+
+
 def _keep_alive_socket_options(keep_alive_time):
     """Build the list of socket options that enable TCP keep-alive with the
     given idle time before probes begin. TCP_KEEPIDLE is platform dependent
@@ -145,10 +159,8 @@ class AiohttpSyncStream:
         return out
 
     def _read_chunk(self):
-        async def _read():
-            async with async_timeout.timeout(self._read_timeout):
-                return await self._response.content.read(self._FILL_SIZE)
-        return self._loop.run_until_complete(_read())
+        return _run_read(self._loop, self._read_timeout,
+                         self._response.content.read(self._FILL_SIZE))
 
 
 class AiohttpHTTPTransport:
@@ -311,10 +323,7 @@ class AiohttpHTTPTransport:
 
     def read_body(self):
         """Read the entire HTTP response body as bytes."""
-        async def _read():
-            async with async_timeout.timeout(self._read_timeout):
-                return await self._http_req_resp.read()
-        return self._loop.run_until_complete(_read())
+        return _run_read(self._loop, self._read_timeout, 
self._http_req_resp.read())
 
     def close(self):
         # Inner function to perform async close.
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/exceptions.py 
b/gremlin-python/src/main/python/gremlin_python/driver/exceptions.py
new file mode 100644
index 0000000000..dd9b7109cc
--- /dev/null
+++ b/gremlin-python/src/main/python/gremlin_python/driver/exceptions.py
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+
+class ReadTimeoutError(TimeoutError):
+    """Raised when the client-side read timeout elapses while waiting for 
response
+    data from the server.
+
+    Subclasses the builtin :class:`TimeoutError` so it can be caught with
+    ``except TimeoutError`` without depending on the underlying transport.
+
+    This driver-owned type exists only because the driver is currently 
synchronous and
+    should not surface the transport's asyncio/aiohttp timeout types. Once 
gremlin-python
+    is fully asynchronous this should be reverted to raising 
``asyncio.TimeoutError``
+    directly. See TINKERPOP-2774: 
https://issues.apache.org/jira/browse/TINKERPOP-2774
+    """
diff --git 
a/gremlin-python/src/main/python/tests/integration/driver/test_client.py 
b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
index f0390ed86b..5afefa1197 100644
--- a/gremlin-python/src/main/python/tests/integration/driver/test_client.py
+++ b/gremlin-python/src/main/python/tests/integration/driver/test_client.py
@@ -24,6 +24,7 @@ import uuid
 import pytest
 from gremlin_python.driver.client import Client
 from gremlin_python.driver.connection import GremlinServerError
+from gremlin_python.driver.exceptions import ReadTimeoutError
 from gremlin_python.driver.request import RequestMessage
 from gremlin_python.driver.serializer import GraphBinarySerializersV4
 from gremlin_python.structure.graph import CompositePDT, PrimitivePDT
@@ -33,7 +34,6 @@ from gremlin_python.process.strategies import OptionsStrategy
 from gremlin_python.structure.graph import Graph, Vertex
 from gremlin_python.driver.aiohttp.transport import AiohttpHTTPTransport
 from gremlin_python.statics import *
-from asyncio import TimeoutError
 
 __author__ = 'David M. Brown ([email protected])'
 
@@ -191,9 +191,12 @@ def test_client_side_timeout_set_for_aiohttp(client):
         # should fire an exception
         client.submit('Thread.sleep(2000);1', request_options={'language': 
'gremlin-groovy'}).all().result()
         assert False
-    except TimeoutError as err:
-        # asyncio TimeoutError has no message.
-        assert str(err) == ""
+    except ReadTimeoutError as err:
+        # The driver normalizes a read timeout to a single ReadTimeoutError (a 
builtin
+        # TimeoutError subclass) with a deterministic message, so we assert on 
both the
+        # type (still catchable via `except TimeoutError`) and the message.
+        assert isinstance(err, TimeoutError)
+        assert str(err) == "Read timed out after 1.0s waiting for response 
data."
 
     # still can submit after failure
     assert client.submit('g.V(x).values("age")', {'x': 1}).all().result()[0] 
== 29

Reply via email to