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

isapego pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 21ddb58b994 IGNITE-27373 DB API Driver 3: Add tests with GIL disabled 
(#7683)
21ddb58b994 is described below

commit 21ddb58b99449987bc5372be7d682de5e23e5caf
Author: Igor Sapego <[email protected]>
AuthorDate: Sun Mar 1 04:58:31 2026 +0100

    IGNITE-27373 DB API Driver 3: Add tests with GIL disabled (#7683)
---
 .teamcity/test/build_types/RunPlatformTests.kt     |   2 +-
 .teamcity/test/platform_tests/Project.kt           |   4 +-
 .teamcity/test/platform_tests/RunPythonTests.kt    |  17 ++
 .../test/platform_tests/python_tests/Project.kt    |  27 +++
 .../PythonDbApiToxTest.kt}                         |  40 +++--
 .../python/dbapi/pyignite_dbapi/__init__.py        |   4 +-
 .../python/dbapi/requirements/install.txt          |   2 -
 .../platforms/python/dbapi/requirements/tests.txt  |   6 +-
 modules/platforms/python/dbapi/tests/conftest.py   |  13 +-
 .../python/dbapi/tests/test_concurrency.py         | 189 +++++++++++++++++++++
 modules/platforms/python/dbapi/tox.ini             |   9 +-
 11 files changed, 284 insertions(+), 29 deletions(-)

diff --git a/.teamcity/test/build_types/RunPlatformTests.kt 
b/.teamcity/test/build_types/RunPlatformTests.kt
index 40d246e198d..ee2d2aece0f 100644
--- a/.teamcity/test/build_types/RunPlatformTests.kt
+++ b/.teamcity/test/build_types/RunPlatformTests.kt
@@ -18,6 +18,6 @@ object RunPlatformTests : BuildType({
 //        snapshot(PlatformCppTestsWindows) {}  // Always falling, under 
investigation
         snapshot(PlatformDotnetTestsWindows) {}
         snapshot(PlatformDotnetTestsLinux) {}
-        snapshot(PlatformPythonTestsLinux) {}
+        snapshot(RunPythonTests) {}
     }
 })
diff --git a/.teamcity/test/platform_tests/Project.kt 
b/.teamcity/test/platform_tests/Project.kt
index 34583900de8..4a2236af6ee 100644
--- a/.teamcity/test/platform_tests/Project.kt
+++ b/.teamcity/test/platform_tests/Project.kt
@@ -9,6 +9,8 @@ object Project : Project({
     id(getId(this::class))
     name = "[Platform Tests]"
 
+    subProject(test.platform_tests.python_tests.Project)
+
     /**
      * List of platform linux tests
      */
@@ -19,7 +21,7 @@ object Project : Project({
         PlatformCppOdbcTestsRpmLinux,
         PlatformCppOdbcTestsTgzLinux,
         PlatformDotnetTestsLinux,
-        PlatformPythonTestsLinux
+        RunPythonTests
     ).forEach {
         buildType(
             ApacheIgnite3CustomBuildType.Builder(it)
diff --git a/.teamcity/test/platform_tests/RunPythonTests.kt 
b/.teamcity/test/platform_tests/RunPythonTests.kt
new file mode 100644
index 00000000000..47cc668d887
--- /dev/null
+++ b/.teamcity/test/platform_tests/RunPythonTests.kt
@@ -0,0 +1,17 @@
+package test.platform_tests
+
+import jetbrains.buildServer.configs.kotlin.BuildType
+import org.apache.ignite.teamcity.Teamcity.Companion.getId
+
+object RunPythonTests : BuildType({
+    id(getId(this::class))
+    name = "> Run :: Python Tests"
+    description = "Run all Python Tests"
+    type = Type.COMPOSITE
+
+    dependencies {
+        test.platform_tests.python_tests.Project.buildTypes.forEach{
+            snapshot(it) {}
+        }
+    }
+})
diff --git a/.teamcity/test/platform_tests/python_tests/Project.kt 
b/.teamcity/test/platform_tests/python_tests/Project.kt
new file mode 100644
index 00000000000..c7d4e84a6f2
--- /dev/null
+++ b/.teamcity/test/platform_tests/python_tests/Project.kt
@@ -0,0 +1,27 @@
+package test.platform_tests.python_tests
+
+import jetbrains.buildServer.configs.kotlin.Project
+import org.apache.ignite.teamcity.ApacheIgnite3CustomBuildType
+import org.apache.ignite.teamcity.Teamcity.Companion.getId
+
+object Project : Project({
+    id(getId(this::class))
+    name = "[Python Tests]"
+
+    listOf(
+            Triple("3.10", "py310", "Python DB API Tests - Python 3.10"),
+            Triple("3.11", "py311", "Python DB API Tests - Python 3.11"),
+            Triple("3.12", "py312", "Python DB API Tests - Python 3.12"),
+            Triple("3.13", "py313", "Python DB API Tests - Python 3.13"),
+            Triple("3.14", "py314", "Python DB API Tests - Python 3.14"),
+            Triple("3.14t", "py314t", "Python DB API Tests (No GIL) - Python 
3.14"),
+    ).forEach { (ver, toxEnv, name) ->
+        buildType(
+            ApacheIgnite3CustomBuildType.Builder(PythonDbApiToxTest(ver, 
toxEnv, name))
+                .ignite3VCS().ignite3BuildDependency().setupMavenProxy()
+                .defaultBuildTypeSettings().requireLinux()
+                .build().buildType
+        )
+    }
+})
+
diff --git a/.teamcity/test/platform_tests/PlatformPythonTestsLinux.kt 
b/.teamcity/test/platform_tests/python_tests/PythonDbApiToxTest.kt
similarity index 81%
rename from .teamcity/test/platform_tests/PlatformPythonTestsLinux.kt
rename to .teamcity/test/platform_tests/python_tests/PythonDbApiToxTest.kt
index 55ca54a7038..06961b9dc21 100644
--- a/.teamcity/test/platform_tests/PlatformPythonTestsLinux.kt
+++ b/.teamcity/test/platform_tests/python_tests/PythonDbApiToxTest.kt
@@ -1,4 +1,4 @@
-package test.platform_tests
+package test.platform_tests.python_tests
 
 import jetbrains.buildServer.configs.kotlin.BuildType
 import jetbrains.buildServer.configs.kotlin.ParameterDisplay
@@ -10,17 +10,22 @@ import 
jetbrains.buildServer.configs.kotlin.failureConditions.failOnText
 import org.apache.ignite.teamcity.CustomBuildSteps.Companion.customGradle
 import org.apache.ignite.teamcity.Teamcity
 
-
-object PlatformPythonTestsLinux : BuildType({
-    id(Teamcity.getId(this::class))
-    name = "Platform Python Tests (Linux)"
+class PythonDbApiToxTest(
+        private val pythonVersion: String,
+        private val toxEnv: String,
+        private val suiteName: String
+) : BuildType({
+    id(Teamcity.getId(this::class, pythonVersion, true))
+    name = suiteName
 
     params {
         text("PATH__WORKING_DIR", 
"""%VCSROOT__IGNITE3%\modules\platforms\python\dbapi""", display = 
ParameterDisplay.HIDDEN, allowEmpty = true)
-        param("env.IGNITE_CPP_TESTS_USE_SINGLE_NODE", "")
-        param("env.CPP_STAGING", """%PATH__WORKING_DIR%\cpp_staging""")
-        param("TOX_ENV", "py310")
-        param("PYTHON_VERSION", "3.10")
+        param("TOX_ENV", toxEnv)
+        param("PYTHON_VERSION", pythonVersion)
+    }
+
+    requirements {
+        equals("env.DIND_ENABLED", "true")
     }
 
     steps {
@@ -29,7 +34,18 @@ object PlatformPythonTestsLinux : BuildType({
             tasks = ":ignite-runner:integrationTestClasses"
         }
         script {
-            name = "Python Client tests"
+            name = "Update pyenv"
+            workingDir = "%PATH__WORKING_DIR%"
+            scriptContent = """
+                set -x
+                eval "${'$'}(pyenv init - dash)"
+
+                cd "${'$'}(pyenv root)"
+                git pull
+            """.trimIndent()
+        }
+        script {
+            name = "Run tox"
             workingDir = "%PATH__WORKING_DIR%"
             scriptContent = """
                 #!/usr/bin/env bash
@@ -84,8 +100,4 @@ object PlatformPythonTestsLinux : BuildType({
             reverse = false
         }
     }
-
-    requirements {
-        equals("env.DIND_ENABLED", "true")
-    }
 })
diff --git a/modules/platforms/python/dbapi/pyignite_dbapi/__init__.py 
b/modules/platforms/python/dbapi/pyignite_dbapi/__init__.py
index 7d6b70afde4..3e59f6f2299 100644
--- a/modules/platforms/python/dbapi/pyignite_dbapi/__init__.py
+++ b/modules/platforms/python/dbapi/pyignite_dbapi/__init__.py
@@ -27,8 +27,8 @@ __version__ = pkgutil.get_data(__name__, 
"_version.txt").decode
 apilevel = '2.0'
 """PEP 249 is supported."""
 
-threadsafety = 1
-"""Threads may share the module, but not connections."""
+threadsafety = 2
+"""Threads may share the module and connections, but not cursors."""
 
 paramstyle = 'qmark'
 """Parameter style is a question mark, e.g. '...WHERE name=?'."""
diff --git a/modules/platforms/python/dbapi/requirements/install.txt 
b/modules/platforms/python/dbapi/requirements/install.txt
index 5f3e1531bd8..5c00eb37a5e 100644
--- a/modules/platforms/python/dbapi/requirements/install.txt
+++ b/modules/platforms/python/dbapi/requirements/install.txt
@@ -1,3 +1 @@
 # these pip packages are necessary for the pyignite_dbapi to run
-
-attrs==23.1.0
\ No newline at end of file
diff --git a/modules/platforms/python/dbapi/requirements/tests.txt 
b/modules/platforms/python/dbapi/requirements/tests.txt
index ad48df9e321..6cd09643561 100644
--- a/modules/platforms/python/dbapi/requirements/tests.txt
+++ b/modules/platforms/python/dbapi/requirements/tests.txt
@@ -1,8 +1,6 @@
 # these packages are used for testing
 
-pytest==6.2.5
-pytest-cov==2.11.1
-teamcity-messages==1.28
+pytest==8.2.2
+teamcity-messages==1.33
 psutil==5.8.0
-flake8==3.8.4
 dbapi-compliance==1.15.0
\ No newline at end of file
diff --git a/modules/platforms/python/dbapi/tests/conftest.py 
b/modules/platforms/python/dbapi/tests/conftest.py
index 16a4cc908cb..ca9b9866982 100644
--- a/modules/platforms/python/dbapi/tests/conftest.py
+++ b/modules/platforms/python/dbapi/tests/conftest.py
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
+import time
 
 import pyignite_dbapi
 import pytest
@@ -24,20 +25,26 @@ logger.setLevel(logging.DEBUG)
 
 TEST_PAGE_SIZE = 32
 
+TEST_CONNECT_KWARGS = {
+    "address": server_addresses_basic,
+    "page_size": TEST_PAGE_SIZE,
+    "heartbeat_interval": 2,
+}
+
 @pytest.fixture()
 def table_name(request):
-    return request.node.originalname
+    return f"{request.node.originalname}_{int(time.monotonic_ns())}"
 
 
 @pytest.fixture()
 def connection():
-    conn = pyignite_dbapi.connect(address=server_addresses_basic, 
page_size=TEST_PAGE_SIZE, heartbeat_interval=2)
+    conn = pyignite_dbapi.connect(**TEST_CONNECT_KWARGS)
     yield conn
     conn.close()
 
 @pytest.fixture()
 def service_connection():
-    conn = pyignite_dbapi.connect(address=server_addresses_basic, 
page_size=TEST_PAGE_SIZE, heartbeat_interval=2)
+    conn = pyignite_dbapi.connect(**TEST_CONNECT_KWARGS)
     yield conn
     conn.close()
 
diff --git a/modules/platforms/python/dbapi/tests/test_concurrency.py 
b/modules/platforms/python/dbapi/tests/test_concurrency.py
new file mode 100644
index 00000000000..59e76cd9249
--- /dev/null
+++ b/modules/platforms/python/dbapi/tests/test_concurrency.py
@@ -0,0 +1,189 @@
+# 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.
+
+import threading
+import time
+
+import pytest
+
+import pyignite_dbapi
+from tests.conftest import TEST_CONNECT_KWARGS
+from tests.util import wait_for_condition
+
+NUM_THREADS = 50
+
+
[email protected]()
+def module_level_threadsafety():
+    assert pyignite_dbapi.threadsafety >= 1, "Module can not be used 
concurrently"
+
+
[email protected]()
+def connection_level_threadsafety(module_level_threadsafety):
+    assert pyignite_dbapi.threadsafety >= 2, "Connections can not be used 
concurrently"
+
+
[email protected]()
+def table(table_name, service_cursor, drop_table_cleanup):
+    service_cursor.execute(f"CREATE TABLE {table_name} (id int primary key, 
data varchar)")
+    yield table_name
+
+
+def run_threads(fn, n=NUM_THREADS, *args):
+    barrier = threading.Barrier(n)
+    errors = []
+    errors_lock = threading.Lock()
+
+    def wrapper(tid):
+        try:
+            barrier.wait()
+            fn(tid, *args)
+        except Exception as e:
+            with errors_lock:
+                errors.append(e)
+
+    threads = [threading.Thread(target=wrapper, args=(i,)) for i in range(n)]
+    for t in threads:
+        t.start()
+    for t in threads:
+        t.join()
+
+    if errors:
+        raise errors[0]
+
+
+def test_concurrent_module_import(module_level_threadsafety):
+    import importlib
+
+    def task(_):
+        m = importlib.import_module(pyignite_dbapi.__name__)
+        assert m.threadsafety > 0, "Module can not be used concurrently"
+
+    run_threads(task)
+
+
+def test_concurrent_connect_use_close(module_level_threadsafety):
+    def task(_):
+        c = pyignite_dbapi.connect(**TEST_CONNECT_KWARGS)
+        with c.cursor() as cur:
+            cur.execute("SELECT 1")
+            assert cur.fetchone() is not None
+        c.close()
+
+    run_threads(task)
+
+
+def test_shared_connection_per_thread_cursors(connection, 
connection_level_threadsafety):
+    def task(_):
+        with connection.cursor() as cur:
+            cur.execute("SELECT 1")
+            row = cur.fetchone()
+            assert row is not None
+
+    run_threads(task)
+
+
+def test_concurrent_inserts_no_lost_writes(table, connection, 
connection_level_threadsafety):
+    rows_per_thread = 50
+
+    def task(thread_id):
+        with connection.cursor() as cur:
+            for i in range(rows_per_thread):
+                cur.execute(f"INSERT INTO {table} (id, data) VALUES (?, ?)", 
(thread_id * rows_per_thread + i, f"v{thread_id}-{i}"))
+
+    run_threads(task)
+
+    with connection.cursor() as cur:
+        cur.execute(f"SELECT COUNT(*) FROM {table}")
+        count = cur.fetchone()[0]
+    assert count == NUM_THREADS * rows_per_thread
+
+
+def test_concurrent_commit_and_rollback(table, module_level_threadsafety):
+    """Half the threads commit, half rollback. Only committed rows appear."""
+    committed_ids = []
+    lock = threading.Lock()
+
+    def task(thread_id):
+        with pyignite_dbapi.connect(**TEST_CONNECT_KWARGS) as conn:
+            conn.autocommit = False
+            with conn.cursor() as cur:
+                cur.execute(f"INSERT INTO {table} (id, data) VALUES (?, ?)", 
(thread_id, "x"))
+            if thread_id % 2 == 0:
+                conn.commit()
+                with lock:
+                    committed_ids.append(thread_id)
+            else:
+                conn.rollback()
+
+    run_threads(task)
+
+    def get_ids():
+        with pyignite_dbapi.connect(**TEST_CONNECT_KWARGS) as conn:
+            with conn.cursor() as cur:
+                cur.execute(f"SELECT id FROM {table} ORDER BY id")
+                return {row[0] for row in cur.fetchall()}
+
+    # There is currently no mechanism to synchronize the observable timestamp 
across
+    # multiple connections, so changes will eventually become visible, but not 
necessarily immediately.
+    wait_for_condition(lambda: get_ids() == set(committed_ids), interval=0.5)
+
+
+def test_concurrent_fetchall_result_integrity(table, connection, 
connection_level_threadsafety):
+    rows_num = 200
+    with connection.cursor() as cur:
+        cur.executemany(f"INSERT INTO {table} (id, data) VALUES (?, ?)", [(i, 
f"val-{i}") for i in range(rows_num)])
+
+    def task(_):
+        with connection.cursor() as cur:
+            cur.execute(f"SELECT id, data FROM {table} ORDER BY id")
+            rows = cur.fetchall()
+            assert len(rows) == rows_num, f"Expected {rows_num} rows, got 
{len(rows)}"
+
+        for idx, (rid, val) in enumerate(rows):
+            assert val == f"val-{rid}", f"Corrupted row: id={rid}, val={val!r}"
+
+    run_threads(task)
+
+
+def test_cursor_description_thread_safety(table, connection, 
connection_level_threadsafety):
+    expected_names = {"ID", "DATA"}
+
+    def task(_):
+        with connection.cursor() as cur:
+            cur.execute(f"SELECT id, data FROM {table} LIMIT 1")
+            desc = cur.description
+            assert desc is not None
+            col_names = {col[0] for col in desc}
+            assert col_names == expected_names, f"Unexpected columns: 
{col_names}"
+
+    run_threads(task)
+
+
+def test_concurrent_executemany(table, connection, 
connection_level_threadsafety):
+    rows_per_thread = 20
+
+    def task(thread_id):
+        rows = [(thread_id * 1000 + i, f"{thread_id}-{i}") for i in 
range(rows_per_thread)]
+        with connection.cursor() as cur:
+            cur.executemany(f"INSERT INTO {table} (id, data) VALUES (?, ?)", 
rows)
+
+    run_threads(task)
+
+    with connection.cursor() as cur:
+        cur.execute(f"SELECT COUNT(*) FROM {table}")
+        count = cur.fetchone()[0]
+
+    assert count == NUM_THREADS * rows_per_thread
diff --git a/modules/platforms/python/dbapi/tox.ini 
b/modules/platforms/python/dbapi/tox.ini
index 30b72d12dea..90060e9b83a 100644
--- a/modules/platforms/python/dbapi/tox.ini
+++ b/modules/platforms/python/dbapi/tox.ini
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 [tox]
-env_list = codestyle,py31{0,1,2,3,4}
+env_list = codestyle,py31{0,1,2,3,4,4t}
 isolated_build = True
 
 [testenv]
@@ -24,5 +24,10 @@ deps =
     -r ./requirements/install.txt
     -r ./requirements/tests.txt
 recreate = True
-commands = pytest -s --teamcity tests
+commands = pytest -s --teamcity {posargs:tests}
 
+[testenv:py314t]
+set_env = PYTHON_GIL=0
+commands =
+    python -c "import sys; assert not sys._is_gil_enabled(), 'GIL is still 
enabled!'"
+    pytest -s --teamcity {posargs:tests}

Reply via email to