Skip to content

Commit a94ba81

Browse files
Move node pessimation to connection pools
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f1de3df commit a94ba81

4 files changed

Lines changed: 71 additions & 43 deletions

File tree

tests/query/test_query_session_hints.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import asyncio
12
from unittest import mock
23

4+
import pytest
5+
36
from ydb._grpc.common.protos import ydb_query_pb2
7+
from ydb.aio.pool import ConnectionPool as AsyncConnectionPool
8+
from ydb.pool import ConnectionPool
49
from ydb.query.session import QuerySession
510

611

712
def _make_session(node_id=42):
813
driver = mock.Mock()
9-
driver._store = mock.Mock()
10-
driver._store.connections_by_node_id = {node_id: mock.Mock()}
11-
driver._on_disconnected = mock.Mock(return_value=None)
14+
driver._pessimize_node = mock.Mock()
1215
session = QuerySession(driver)
1316
session._session_id = "test-session"
1417
session._node_id = node_id
@@ -18,7 +21,6 @@ def _make_session(node_id=42):
1821
class TestQuerySessionAttachHints:
1922
def test_node_shutdown_pessimizes_node_and_invalidates_session(self):
2023
session, driver = _make_session(node_id=42)
21-
connection = driver._store.connections_by_node_id[42]
2224

2325
session._handle_attach_session_state(
2426
ydb_query_pb2.SessionState(
@@ -27,7 +29,7 @@ def test_node_shutdown_pessimizes_node_and_invalidates_session(self):
2729
)
2830
)
2931

30-
driver._on_disconnected.assert_called_once_with(connection)
32+
driver._pessimize_node.assert_called_once_with(42)
3133
assert session._invalidated
3234
assert session._closed
3335

@@ -41,11 +43,11 @@ def test_session_shutdown_invalidates_without_pessimizing_node(self):
4143
)
4244
)
4345

44-
driver._on_disconnected.assert_not_called()
46+
driver._pessimize_node.assert_not_called()
4547
assert session._invalidated
4648
assert session._closed
4749

48-
def test_node_shutdown_with_zero_node_id_skips_pessimization(self):
50+
def test_node_shutdown_with_zero_node_id_delegates_to_driver(self):
4951
session, driver = _make_session(node_id=0)
5052

5153
session._handle_attach_session_state(
@@ -55,7 +57,7 @@ def test_node_shutdown_with_zero_node_id_skips_pessimization(self):
5557
)
5658
)
5759

58-
driver._on_disconnected.assert_not_called()
60+
driver._pessimize_node.assert_called_once_with(0)
5961
assert session._invalidated
6062

6163
def test_no_hint_does_not_invalidate(self):
@@ -65,6 +67,46 @@ def test_no_hint_does_not_invalidate(self):
6567
ydb_query_pb2.SessionState(status=0),
6668
)
6769

68-
driver._on_disconnected.assert_not_called()
70+
driver._pessimize_node.assert_not_called()
6971
assert not session._invalidated
7072
assert not session._closed
73+
74+
75+
class TestConnectionPoolAttachHintPessimization:
76+
def test_sync_pool_pessimizes_node_connection(self):
77+
pool = ConnectionPool.__new__(ConnectionPool)
78+
connection = mock.Mock()
79+
pool._store = mock.Mock()
80+
pool._store.connections_by_node_id = {42: connection}
81+
pool._on_disconnected = mock.Mock()
82+
83+
pool._pessimize_node(42)
84+
85+
pool._on_disconnected.assert_called_once_with(connection)
86+
87+
def test_sync_pool_ignores_missing_node_connection(self):
88+
pool = ConnectionPool.__new__(ConnectionPool)
89+
pool._store = mock.Mock()
90+
pool._store.connections_by_node_id = {}
91+
pool._on_disconnected = mock.Mock()
92+
93+
pool._pessimize_node(42)
94+
pool._pessimize_node(0)
95+
pool._pessimize_node(None)
96+
97+
pool._on_disconnected.assert_not_called()
98+
99+
@pytest.mark.asyncio
100+
async def test_async_pool_pessimizes_node_connection(self):
101+
pool = AsyncConnectionPool.__new__(AsyncConnectionPool)
102+
connection = mock.Mock()
103+
disconnect = mock.AsyncMock()
104+
pool._store = mock.Mock()
105+
pool._store.connections_by_node_id = {42: connection}
106+
pool._on_disconnected = mock.Mock(return_value=disconnect)
107+
108+
pool._pessimize_node(42)
109+
await asyncio.sleep(0)
110+
111+
pool._on_disconnected.assert_called_once_with(connection)
112+
disconnect.assert_awaited_once_with()

ydb/aio/pool.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
import logging
55
import random
6-
from typing import Any, Callable, Optional, Tuple, TYPE_CHECKING
6+
from typing import Any, Callable, Optional, Tuple, TYPE_CHECKING, cast
77

88
from ydb import issues
99
from ydb.opentelemetry.tracing import SpanName, create_ydb_span
@@ -311,6 +311,15 @@ async def __wrapper__() -> None:
311311

312312
return __wrapper__
313313

314+
def _pessimize_node(self, node_id: Optional[int]) -> None:
315+
"""Deprioritize the connection attached to the given YDB node."""
316+
if node_id is None or node_id <= 0:
317+
return
318+
319+
connection = cast(Optional[Connection], self._store.connections_by_node_id.get(node_id))
320+
if connection is not None:
321+
asyncio.get_running_loop().create_task(self._on_disconnected(connection)())
322+
314323
async def wait(self, timeout: Optional[float] = 7.0, fail_fast: bool = False) -> None: # type: ignore[override] # async override of sync method
315324
with create_ydb_span(SpanName.DRIVER_INITIALIZE, self._driver_config, kind="internal").attach_context():
316325
await self._store.get(fast_fail=fail_fast, wait_timeout=timeout if timeout is not None else 7.0)

ydb/pool.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,15 @@ def _on_disconnected(self, connection: Connection) -> None:
494494
if self._discovery_thread:
495495
self._discovery_thread.notify_disconnected()
496496

497+
def _pessimize_node(self, node_id: Optional[int]) -> None:
498+
"""Deprioritize the connection attached to the given YDB node."""
499+
if node_id is None or node_id <= 0:
500+
return
501+
502+
connection = self._store.connections_by_node_id.get(node_id)
503+
if connection is not None:
504+
self._on_disconnected(connection)
505+
497506
def discovery_debug_details(self) -> str:
498507
"""
499508
Returns debug string about last errors

ydb/query/session.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import abc
2-
import asyncio
32
import json
43
import logging
54
import threading
@@ -169,42 +168,11 @@ def _handle_attach_session_state(self, response_pb) -> None:
169168

170169
hint = response_pb.WhichOneof("session_hint")
171170
if hint == "node_shutdown":
172-
self._pessimize_node_by_id(self._node_id)
171+
self._driver._pessimize_node(self._node_id)
173172
self._close_session(invalidate=True)
174173
elif hint == "session_shutdown":
175174
self._close_session(invalidate=True)
176175

177-
def _pessimize_node_by_id(self, node_id: Optional[int]) -> None:
178-
"""Deprioritize the node's connection (same effect as a transport failure)."""
179-
if node_id is None or node_id <= 0:
180-
return
181-
182-
store = getattr(self._driver, "_store", None)
183-
if store is None:
184-
return
185-
186-
connection = getattr(store, "connections_by_node_id", {}).get(node_id)
187-
if connection is None:
188-
return
189-
190-
on_disconnected = getattr(self._driver, "_on_disconnected", None)
191-
if on_disconnected is None:
192-
connection.close()
193-
return
194-
195-
disconnect_cb = on_disconnected(connection)
196-
if disconnect_cb is None:
197-
return
198-
199-
if asyncio.iscoroutinefunction(disconnect_cb):
200-
loop = getattr(self, "_loop", None)
201-
if loop is not None and loop.is_running():
202-
loop.create_task(disconnect_cb())
203-
return
204-
205-
if callable(disconnect_cb):
206-
disconnect_cb()
207-
208176
def _close_session(self, invalidate: bool = False) -> None:
209177
if self._closed:
210178
return

0 commit comments

Comments
 (0)