Skip to content

Commit 3cf2855

Browse files
committed
fix(sqlserver): enable READ_COMMITTED_SNAPSHOT at session start to prevent CI hang
The previous per-module autouse fixture ran ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT when the connection tests began, by which point earlier worker/concurrency tests had opened connections against the database. Because this ALTER requires an exclusive database lock, the fixture blocked forever (CI observed >50 minutes with no progress) and the missing MVCC semantics also caused parallel-CRUD worker tests to fail. Move the enabling logic to a session-level autouse fixture in the root tests conftest, which runs before any test opens a connection, uses a LOCK_TIMEOUT and retries, and skips databases already configured.
1 parent 2371f02 commit 3cf2855

2 files changed

Lines changed: 87 additions & 31 deletions

File tree

‎tests/conftest.py‎

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,100 @@
11
# tests/conftest.py
22
"""Root conftest for SQL Server backend tests."""
33
import os
4+
import time
5+
46
import pytest
7+
import yaml
58

69
os.environ.setdefault(
710
'TESTSUITE_PROVIDER_REGISTRY',
811
'providers.registry:provider_registry'
912
)
1013

1114

15+
def _load_scenario_map():
16+
"""Load scenario configs from the environment or the default file."""
17+
config_path = os.getenv("SQLSERVER_SCENARIOS_CONFIG_PATH")
18+
if config_path and not os.path.exists(config_path):
19+
print(f"Warning: scenario file not found: {config_path}")
20+
return {}
21+
if not config_path:
22+
config_path = os.path.join(os.path.dirname(__file__), "config", "sqlserver_scenarios.yaml")
23+
if not os.path.exists(config_path):
24+
return {}
25+
try:
26+
with open(config_path, 'r', encoding='utf-8') as f:
27+
data = yaml.safe_load(f)
28+
except Exception:
29+
return {}
30+
return data.get("scenarios", {}) or {}
31+
32+
33+
@pytest.fixture(scope="session", autouse=True)
34+
def _prepare_read_committed_snapshot():
35+
"""Enable READ_COMMITTED_SNAPSHOT on every scenario database up front.
36+
37+
SQL Server's default READ COMMITTED takes shared locks, so a transaction
38+
that updates a row and then reads a full table blocks (deadlocks) on
39+
other transactions' uncommitted rows. MySQL/InnoDB instead uses MVCC
40+
consistent reads. This session-level fixture enables READ_COMMITTED_SNAPSHOT
41+
for every configured database in the session as a whole, before any
42+
test (including concurrent-pool/worker tests) runs and while no other
43+
connections are open against those databases.
44+
45+
Enabling this option requires an exclusive database lock, so it must run
46+
early with a lock timeout instead of blocking forever inside a test module
47+
conftest (which runs after many tests have opened connections).
48+
"""
49+
from rhosocial.activerecord.backend.impl.sqlserver import SQLServerBackend
50+
from rhosocial.activerecord.backend.impl.sqlserver.config import SQLServerConnectionConfig
51+
52+
scenarios = _load_scenario_map()
53+
if not scenarios:
54+
return
55+
56+
for _scenario_name, config in scenarios.items():
57+
if not config:
58+
continue
59+
db_name = config.get("database")
60+
if not db_name:
61+
continue
62+
try:
63+
backend = SQLServerBackend(connection_config=SQLServerConnectionConfig(**config))
64+
backend.connect()
65+
except Exception:
66+
continue
67+
try:
68+
already_on = False
69+
cursor = backend._connection.cursor()
70+
cursor.execute(
71+
"SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = ?",
72+
(db_name,),
73+
)
74+
row = cursor.fetchone()
75+
if row and (row[0] if isinstance(row, (list, tuple)) else getattr(row, "is_read_committed_snapshot_on", False)):
76+
already_on = True
77+
cursor.close()
78+
if already_on:
79+
continue
80+
for _attempt in range(5):
81+
try:
82+
backend.execute("SET LOCK_TIMEOUT 5000")
83+
backend.execute(
84+
f"ALTER DATABASE [{db_name}] SET READ_COMMITTED_SNAPSHOT ON"
85+
)
86+
break
87+
except Exception:
88+
time.sleep(1)
89+
except Exception:
90+
pass
91+
finally:
92+
try:
93+
backend.disconnect()
94+
except Exception:
95+
pass
96+
97+
1298
def pytest_configure(config):
1399
"""Configure custom markers for SQL Server tests."""
14100
config.addinivalue_line(
@@ -25,4 +111,4 @@ def pytest_configure(config):
25111
)
26112
config.addinivalue_line(
27113
"markers", "requires_inner_join: Mark test as requiring INNER JOIN support"
28-
)
114+
)

‎tests/rhosocial/activerecord_sqlserver_test/feature/connection/conftest.py‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import asyncio
99
import sys
1010
import os
11-
import time
1211
from typing import Dict, Any, Generator
1312

1413
import pytest
@@ -83,35 +82,6 @@ def _load_scenarios_from_config():
8382
_load_scenarios_from_config()
8483

8584

86-
@pytest.fixture(scope="session", autouse=True)
87-
def _enable_read_committed_snapshot():
88-
"""Enable READ_COMMITTED_SNAPSHOT so plain SELECTs use row versioning.
89-
90-
SQL Server's default READ COMMITTED takes shared locks, so a transaction
91-
that updates a row and then reads the full table blocks on other
92-
transactions' uncommitted rows (deadlock). MySQL/InnoDB instead uses
93-
MVCC consistent reads. Enabling READ_COMMITTED_SNAPSHOT aligns SQL Server
94-
with that behavior, which the concurrent-pool tests rely on.
95-
"""
96-
if not SCENARIO_MAP:
97-
return
98-
config = get_scenario_config(next(iter(SCENARIO_MAP)))
99-
db_name = config.get("database", "test_db_c")
100-
backend = SQLServerBackend(connection_config=SQLServerConnectionConfig(**config))
101-
backend.connect()
102-
try:
103-
for _ in range(5):
104-
try:
105-
backend.execute(f"ALTER DATABASE [{db_name}] SET READ_COMMITTED_SNAPSHOT ON")
106-
break
107-
except Exception:
108-
time.sleep(0.5)
109-
except Exception:
110-
pass
111-
finally:
112-
backend.disconnect()
113-
114-
11585
def get_scenario_config(name: str) -> Dict[str, Any]:
11686
if name not in SCENARIO_MAP:
11787
if SCENARIO_MAP:

0 commit comments

Comments
 (0)