@@ -29,68 +29,78 @@ def test_multi_statements_false():
2929 assert rows == ((17 ,),)
3030
3131
32- def _assert_thread_id_blocked_during_operation (conn , func , min_wait = 0.15 ):
33- error = None
34- done = threading .Event ()
35- thread_id_done = threading .Event ()
36- results = {}
37-
38- def run ():
39- nonlocal error
40- try :
41- func ()
42- except Exception as exc : # pragma: no cover - error checked below
43- error = exc
44- finally :
45- done .set ()
46-
47- def read_thread_id ():
48- try :
49- results ["thread_id" ] = conn .thread_id ()
50- except Exception as exc : # pragma: no cover - assertion checked below
51- results ["error" ] = exc
52- finally :
53- thread_id_done .set ()
54-
55- thread = threading .Thread (target = run )
56- thread .start ()
57- time .sleep (0.05 )
58- assert not done .is_set ()
59-
60- blocker = threading .Thread (target = read_thread_id )
61- blocker .start ()
62-
63- assert not thread_id_done .wait (min_wait )
64- thread .join ()
65- blocker .join ()
66- assert error is None
67- assert done .is_set ()
68- assert "error" not in results
69- assert isinstance (results ["thread_id" ], int )
70-
71-
72- def test_connection_methods_are_serialized ():
32+ def test_connection_concurrent_use_raises ():
33+ """While a slow query holds the connection lock, any other access from
34+ a second thread must raise ProgrammingError immediately (not block)."""
7335 conn = connection_factory ()
7436 try :
75- def run_query ():
76- conn .query ("SELECT SLEEP(0.2)" )
77- result = conn .store_result ()
78- assert result .fetch_row () == ((0 ,),)
79-
80- _assert_thread_id_blocked_during_operation (conn , run_query )
37+ thread_error = None
38+ done = threading .Event ()
39+
40+ def run_slow_query ():
41+ nonlocal thread_error
42+ try :
43+ conn .query ("SELECT SLEEP(0.5)" )
44+ result = conn .store_result ()
45+ result .fetch_row ()
46+ except Exception as exc : # pragma: no cover
47+ thread_error = exc
48+ finally :
49+ done .set ()
50+
51+ thread = threading .Thread (target = run_slow_query )
52+ thread .start ()
53+
54+ # Give the background thread time to acquire the lock and enter SLEEP.
55+ time .sleep (0.1 )
56+
57+ start = time .monotonic ()
58+ with pytest .raises (ProgrammingError , match = "already in use" ):
59+ conn .thread_id ()
60+ # Should fail immediately, not wait for the SLEEP to finish.
61+ assert time .monotonic () - start < 0.1
62+
63+ done .wait ()
64+ thread .join ()
65+ assert thread_error is None
8166 finally :
8267 conn .close ()
8368
8469
85- def test_result_methods_share_connection_lock ():
70+ def test_result_concurrent_use_raises ():
71+ """While fetch_row holds the connection lock streaming a slow result,
72+ any other access from a second thread must raise ProgrammingError immediately."""
8673 conn = connection_factory ()
8774 try :
88- conn .query ("SELECT 1 UNION ALL SELECT SLEEP(0.2 )" )
75+ conn .query ("SELECT 1 UNION ALL SELECT SLEEP(0.5 )" )
8976 result = conn .use_result ()
9077
91- def fetch_all_rows ():
92- assert result . fetch_row ( maxrows = 0 ) == (( 1 ,), ( 0 ,) )
78+ thread_error = None
79+ done = threading . Event ( )
9380
94- _assert_thread_id_blocked_during_operation (conn , fetch_all_rows )
81+ def fetch_all_rows ():
82+ nonlocal thread_error
83+ try :
84+ assert result .fetch_row (maxrows = 0 ) == ((1 ,), (0 ,))
85+ except Exception as exc : # pragma: no cover
86+ thread_error = exc
87+ finally :
88+ done .set ()
89+
90+ thread = threading .Thread (target = fetch_all_rows )
91+ thread .start ()
92+
93+ # Give the background thread time to acquire the lock and enter SLEEP.
94+ time .sleep (0.1 )
95+
96+ start = time .monotonic ()
97+ with pytest .raises (ProgrammingError , match = "already in use" ):
98+ conn .thread_id ()
99+ # Should fail immediately, not wait for the SLEEP to finish.
100+ assert time .monotonic () - start < 0.1
101+
102+ done .wait ()
103+ thread .join ()
104+ assert thread_error is None
95105 finally :
96106 conn .close ()
0 commit comments