]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: premature ConnectionTimeout 1280/head
authorEric Buehl <eric.buehl@gmail.com>
Wed, 18 Mar 2026 17:30:54 +0000 (10:30 -0700)
committerEric Buehl <eric.buehl@gmail.com>
Wed, 18 Mar 2026 22:36:12 +0000 (15:36 -0700)
When time.monotonic() returns a large value (e.g. on a long-running system, or when patched by a time-mocking library) causes ConnectionTimeout to fire immediately.

docs/news.rst
psycopg_c/psycopg_c/_psycopg/generators.pyx
tests/test_connection.py
tests/test_connection_async.py

index e5836cfe66e91c45b8a1f6c7de9154de1a28b983..bccc0b3cf46824f7f4eff6f7fbc9f55314b163a8 100644 (file)
@@ -7,6 +7,16 @@
 ``psycopg`` release notes
 =========================
 
+Future releases
+---------------
+
+Psycopg 3.3.4 (unreleased)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+- Fix possible spurious connection timeout in systems with very long uptimes
+  in C extension (:ticket:`#1280`).
+
+
 Current release
 ---------------
 
index 0ea40157f805acb4c236c37c31b7a35880066d39..ea308268169840b6f7b82d6c0cc4a1333a02f6bd 100644 (file)
@@ -38,7 +38,7 @@ def connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[abc.PGconn]:
     cdef int conn_status = libpq.PQstatus(pgconn_ptr)
     cdef int poll_status
     cdef object wait, ready
-    cdef float deadline = 0.0
+    cdef double deadline = 0.0
 
     if timeout:
         deadline = monotonic() + timeout
@@ -89,7 +89,7 @@ def connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[abc.PGconn]:
 def cancel(pq.PGcancelConn cancel_conn, *, timeout: float = 0.0) -> PQGenConn[None]:
     cdef libpq.PGcancelConn *pgcancelconn_ptr = cancel_conn.pgcancelconn_ptr
     cdef int status
-    cdef float deadline = 0.0
+    cdef double deadline = 0.0
 
     if timeout:
         deadline = monotonic() + timeout
index 7312a11c7edb70f2aed79e43fbb98e4af8860b0e..940f465e33b75c2bc58328ceb0e928b33563b8af 100644 (file)
@@ -122,6 +122,20 @@ def test_connect_timeout(conn_cls, proxy):
     assert elapsed == pytest.approx(2.0, 0.1)
 
 
+@pytest.mark.skipif(pq.__impl__ == "python", reason="only affects C extension")
+def test_connect_timeout_large_monotonic(conn_cls, dsn, monkeypatch):
+    # Regression: deadline was stored as C float (32-bit). At ~777 days of
+    # process uptime the float32 ULP reaches 8 s, so a 2-second timeout is
+    # silently rounded away, causing ConnectionTimeout to fire immediately.
+    # 2^26 = 67_108_864 s ≈ 777 days is the minimum value where this occurs
+    # with psycopg's enforced 2-second minimum connect_timeout.
+    from psycopg._cmodule import _psycopg
+
+    monkeypatch.setattr(_psycopg, "monotonic", lambda: 67108864.5)
+    with conn_cls.connect(dsn, connect_timeout=2):
+        pass
+
+
 @pytest.mark.slow
 @pytest.mark.timing
 def test_multi_hosts(conn_cls, proxy, dsn, monkeypatch):
index 122228a49cd96ca4bb953494f6ff3f19b03d70e3..d54bf09ea6a850812da5ec29de8e4804f24b29e2 100644 (file)
@@ -117,6 +117,20 @@ async def test_connect_timeout(aconn_cls, proxy):
     assert elapsed == pytest.approx(2.0, 0.1)
 
 
+@pytest.mark.skipif(pq.__impl__ == "python", reason="only affects C extension")
+async def test_connect_timeout_large_monotonic(aconn_cls, dsn, monkeypatch):
+    # Regression: deadline was stored as C float (32-bit). At ~777 days of
+    # process uptime the float32 ULP reaches 8 s, so a 2-second timeout is
+    # silently rounded away, causing ConnectionTimeout to fire immediately.
+    # 2^26 = 67_108_864 s ≈ 777 days is the minimum value where this occurs
+    # with psycopg's enforced 2-second minimum connect_timeout.
+    from psycopg._cmodule import _psycopg
+
+    monkeypatch.setattr(_psycopg, "monotonic", lambda: 67108864.5)
+    async with await aconn_cls.connect(dsn, connect_timeout=2):
+        pass
+
+
 @pytest.mark.slow
 @pytest.mark.timing
 async def test_multi_hosts(aconn_cls, proxy, dsn, monkeypatch):