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