nomosdb 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nomosdb/__init__.py +59 -0
- nomosdb/_async/__init__.py +0 -0
- nomosdb/_async/connection.py +126 -0
- nomosdb/_async/driver.py +75 -0
- nomosdb/_async/pool.py +228 -0
- nomosdb/_async/work.py +504 -0
- nomosdb/_bolt.py +267 -0
- nomosdb/_common.py +235 -0
- nomosdb/_io.py +155 -0
- nomosdb/_packstream.py +237 -0
- nomosdb/_sync/__init__.py +1 -0
- nomosdb/_sync/connection.py +127 -0
- nomosdb/_sync/driver.py +76 -0
- nomosdb/_sync/pool.py +229 -0
- nomosdb/_sync/work.py +505 -0
- nomosdb/_version.py +3 -0
- nomosdb/async_client.py +230 -0
- nomosdb/client.py +266 -0
- nomosdb/connection.py +237 -0
- nomosdb/exceptions.py +134 -0
- nomosdb/graph.py +110 -0
- nomosdb/orm.py +238 -0
- nomosdb/py.typed +0 -0
- nomosdb/query.py +30 -0
- nomosdb/transaction.py +44 -0
- nomosdb/types.py +17 -0
- nomosdb/utils.py +30 -0
- nomosdb-0.16.0.dist-info/METADATA +179 -0
- nomosdb-0.16.0.dist-info/RECORD +32 -0
- nomosdb-0.16.0.dist-info/WHEEL +5 -0
- nomosdb-0.16.0.dist-info/licenses/LICENSE +17 -0
- nomosdb-0.16.0.dist-info/top_level.txt +1 -0
nomosdb/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NomosDB Python driver.
|
|
3
|
+
|
|
4
|
+
Bolt (recommended): real transactions, retries, cluster routing, asyncio.
|
|
5
|
+
|
|
6
|
+
>>> import nomosdb
|
|
7
|
+
>>> drv = nomosdb.driver("bolt://localhost:7687", auth=("nomosdb", "secret"))
|
|
8
|
+
>>> with drv.session() as s:
|
|
9
|
+
... for rec in s.run("MATCH (p:Person) RETURN p.name AS name LIMIT 10"):
|
|
10
|
+
... print(rec["name"])
|
|
11
|
+
>>> drv.close()
|
|
12
|
+
|
|
13
|
+
The earlier client stays available: NomosDB("bolt://...") or NomosDB("http://...")
|
|
14
|
+
with query() returning dict rows, and the ORM.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ._async.driver import AsyncDriver, async_driver
|
|
18
|
+
from ._async.work import AsyncManagedTransaction, AsyncResult, AsyncSession, AsyncTransaction
|
|
19
|
+
from ._common import READ_ACCESS, WRITE_ACCESS, Record, ResultSummary, SummaryCounters
|
|
20
|
+
from ._sync.driver import Driver, driver
|
|
21
|
+
from ._sync.work import ManagedTransaction, Result, Session, Transaction
|
|
22
|
+
from ._version import __version__
|
|
23
|
+
from .client import NomosDB
|
|
24
|
+
from .connection import Result as QueryResult
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
AuthError,
|
|
27
|
+
AuthenticationError,
|
|
28
|
+
ClientError,
|
|
29
|
+
ConfigurationError,
|
|
30
|
+
ConnectionPoolFull,
|
|
31
|
+
DatabaseError,
|
|
32
|
+
NomosConnectionError,
|
|
33
|
+
NomosDBError,
|
|
34
|
+
NotALeader,
|
|
35
|
+
ProtocolError,
|
|
36
|
+
QueryException,
|
|
37
|
+
ResultConsumedError,
|
|
38
|
+
ResultNotSingleError,
|
|
39
|
+
ServerError,
|
|
40
|
+
ServiceUnavailable,
|
|
41
|
+
SessionExpired,
|
|
42
|
+
TransactionCommitUnknown,
|
|
43
|
+
TransactionError,
|
|
44
|
+
TransientError,
|
|
45
|
+
)
|
|
46
|
+
from .orm import Entity, Node, Relationship, node_class, relationship_class
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"driver", "async_driver", "Driver", "AsyncDriver",
|
|
50
|
+
"Session", "AsyncSession", "Transaction", "AsyncTransaction",
|
|
51
|
+
"ManagedTransaction", "AsyncManagedTransaction", "Result", "AsyncResult",
|
|
52
|
+
"Record", "ResultSummary", "SummaryCounters", "READ_ACCESS", "WRITE_ACCESS",
|
|
53
|
+
"NomosDB", "QueryResult", "Node", "Relationship", "Entity", "node_class", "relationship_class",
|
|
54
|
+
"NomosDBError", "NomosConnectionError", "ConnectionPoolFull", "QueryException", "TransactionError",
|
|
55
|
+
"AuthenticationError", "ProtocolError", "ConfigurationError", "ServerError", "ClientError",
|
|
56
|
+
"TransientError", "DatabaseError", "AuthError", "NotALeader", "ServiceUnavailable", "SessionExpired",
|
|
57
|
+
"TransactionCommitUnknown", "ResultConsumedError", "ResultNotSingleError",
|
|
58
|
+
"__version__",
|
|
59
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""One Bolt connection: handshake, authentication, pipelined requests."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from .._bolt import HANDSHAKE, Dechunker, Protocol, Response, parse_version
|
|
6
|
+
from .._common import Address, Config
|
|
7
|
+
from .._io import AsyncBoltSocket, monotonic
|
|
8
|
+
from ..exceptions import ServerError, ServiceUnavailable, SessionExpired
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncConnection:
|
|
12
|
+
def __init__(self, sock: AsyncBoltSocket, address: Address, version: Tuple[int, int]):
|
|
13
|
+
self._sock = sock
|
|
14
|
+
self.address = address
|
|
15
|
+
self.protocol = Protocol(version)
|
|
16
|
+
self._dechunker = Dechunker()
|
|
17
|
+
self._inbox: list = []
|
|
18
|
+
self.server_agent = ""
|
|
19
|
+
self.connection_id = ""
|
|
20
|
+
self.defunct = False
|
|
21
|
+
self.closed = False
|
|
22
|
+
self.last_used = monotonic()
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
async def open(cls, address: Address, config: Config) -> "AsyncConnection":
|
|
26
|
+
sock = await AsyncBoltSocket.connect(address, config.ssl_context, config.connection_timeout)
|
|
27
|
+
try:
|
|
28
|
+
await sock.sendall(HANDSHAKE)
|
|
29
|
+
answer = b""
|
|
30
|
+
while len(answer) < 4:
|
|
31
|
+
part = await sock.recv(config.connection_timeout)
|
|
32
|
+
if not part:
|
|
33
|
+
raise ServiceUnavailable(f"{address[0]}:{address[1]} closed the connection during the Bolt handshake "
|
|
34
|
+
"(is it the Bolt port? nomosdb-server listens for Bolt on 7687)")
|
|
35
|
+
answer += part
|
|
36
|
+
conn = cls(sock, address, parse_version(answer[:4]))
|
|
37
|
+
if len(answer) > 4:
|
|
38
|
+
conn._inbox.extend(conn._dechunker.feed(answer[4:]))
|
|
39
|
+
hello = conn.protocol.hello(config.user_agent, config.auth, config.routing_context)
|
|
40
|
+
await conn.send_all()
|
|
41
|
+
await conn.fetch_all()
|
|
42
|
+
except BaseException as e:
|
|
43
|
+
await sock.close()
|
|
44
|
+
if isinstance(e, OSError):
|
|
45
|
+
raise ServiceUnavailable(f"{address[0]}:{address[1]} did not complete the Bolt handshake "
|
|
46
|
+
f"(is it the Bolt port? nomosdb-server listens for Bolt on 7687): {e}") from None
|
|
47
|
+
raise
|
|
48
|
+
conn.server_agent = str(hello.metadata.get("server", ""))
|
|
49
|
+
conn.connection_id = str(hello.metadata.get("connection_id", ""))
|
|
50
|
+
return conn
|
|
51
|
+
|
|
52
|
+
# -- I/O
|
|
53
|
+
async def send_all(self) -> None:
|
|
54
|
+
data = self.protocol.take_outbox()
|
|
55
|
+
if not data:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
await self._sock.sendall(data)
|
|
59
|
+
except OSError as e:
|
|
60
|
+
await self._broken(e)
|
|
61
|
+
|
|
62
|
+
async def fetch_message(self) -> None:
|
|
63
|
+
"""Reads until at least one answer has been handled."""
|
|
64
|
+
while not self._inbox:
|
|
65
|
+
try:
|
|
66
|
+
data = await self._sock.recv()
|
|
67
|
+
except OSError as e:
|
|
68
|
+
await self._broken(e)
|
|
69
|
+
if not data:
|
|
70
|
+
await self._broken(None)
|
|
71
|
+
self._inbox.extend(self._dechunker.feed(data))
|
|
72
|
+
tag, fields = self._inbox.pop(0)
|
|
73
|
+
self.protocol.handle(tag, fields)
|
|
74
|
+
|
|
75
|
+
async def fetch_all(self) -> None:
|
|
76
|
+
"""Reads the answers to every request sent; raises the first failure."""
|
|
77
|
+
error: Optional[ServerError] = None
|
|
78
|
+
while self.protocol.pending:
|
|
79
|
+
r = self.protocol.pending[0]
|
|
80
|
+
while not r.done:
|
|
81
|
+
await self.fetch_message()
|
|
82
|
+
if r.error is not None and error is None:
|
|
83
|
+
error = r.error
|
|
84
|
+
if error is not None:
|
|
85
|
+
raise error
|
|
86
|
+
|
|
87
|
+
async def wait(self, r: Response) -> Response:
|
|
88
|
+
"""Sends what is queued and reads until r is answered; raises its failure."""
|
|
89
|
+
await self.send_all()
|
|
90
|
+
while not r.done:
|
|
91
|
+
await self.fetch_message()
|
|
92
|
+
if r.error is not None:
|
|
93
|
+
raise r.error
|
|
94
|
+
return r
|
|
95
|
+
|
|
96
|
+
async def _broken(self, e: Optional[BaseException]) -> None:
|
|
97
|
+
self.defunct = True
|
|
98
|
+
await self._sock.close()
|
|
99
|
+
why = f": {e}" if e else ""
|
|
100
|
+
raise SessionExpired(f"the connection to {self.address[0]}:{self.address[1]} broke{why}")
|
|
101
|
+
|
|
102
|
+
# -- state
|
|
103
|
+
async def reset(self) -> None:
|
|
104
|
+
"""Back to READY: ends a transaction or a failure."""
|
|
105
|
+
r = self.protocol.reset(Response("RESET"))
|
|
106
|
+
await self.wait(r)
|
|
107
|
+
await self.fetch_all()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def ready(self) -> bool:
|
|
111
|
+
return not (self.defunct or self.closed or self.protocol.pending or self.protocol.failed)
|
|
112
|
+
|
|
113
|
+
async def close(self) -> None:
|
|
114
|
+
if self.closed:
|
|
115
|
+
return
|
|
116
|
+
self.closed = True
|
|
117
|
+
if not self.defunct:
|
|
118
|
+
try:
|
|
119
|
+
self.protocol.goodbye()
|
|
120
|
+
await self._sock.sendall(self.protocol.take_outbox())
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
await self._sock.close()
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"<AsyncConnection {self.address[0]}:{self.address[1]} bolt/{self.protocol.version[0]}.{self.protocol.version[1]}>"
|
nomosdb/_async/driver.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The driver: configuration, the connection pool, sessions."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from .._common import READ_ACCESS, WRITE_ACCESS, Config, Record, ResultSummary
|
|
6
|
+
from ..exceptions import ConfigurationError
|
|
7
|
+
from .pool import AsyncDirectPool, AsyncRoutingPool
|
|
8
|
+
from .work import AsyncManagedTransaction, AsyncSession
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncDriver:
|
|
12
|
+
"""Holds the connection pool of one server or cluster. Thread safe (the blocking
|
|
13
|
+
Driver) / task safe (AsyncDriver); sessions are not."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, uri: str, auth: Any = None, **config: Any):
|
|
16
|
+
self._config = Config(uri, auth, **config)
|
|
17
|
+
self._pool = AsyncRoutingPool(self._config) if self._config.routing else AsyncDirectPool(self._config)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def encrypted(self) -> bool:
|
|
21
|
+
return self._config.ssl_context is not None
|
|
22
|
+
|
|
23
|
+
def session(self, default_access_mode: str = WRITE_ACCESS, bookmarks: Optional[List[str]] = None,
|
|
24
|
+
fetch_size: Optional[int] = None, database: Optional[str] = None) -> AsyncSession:
|
|
25
|
+
return AsyncSession(self._pool, self._config, default_access_mode, bookmarks, fetch_size, database)
|
|
26
|
+
|
|
27
|
+
async def verify_connectivity(self) -> None:
|
|
28
|
+
"""Raises unless a server answers and accepts the credentials."""
|
|
29
|
+
conn = await self._pool.acquire(READ_ACCESS, [])
|
|
30
|
+
await self._pool.release(conn)
|
|
31
|
+
|
|
32
|
+
async def get_server_info(self) -> Dict[str, Any]:
|
|
33
|
+
conn = await self._pool.acquire(READ_ACCESS, [])
|
|
34
|
+
try:
|
|
35
|
+
return {"address": conn.address, "agent": conn.server_agent,
|
|
36
|
+
"protocol_version": conn.protocol.version, "connection_id": conn.connection_id}
|
|
37
|
+
finally:
|
|
38
|
+
await self._pool.release(conn)
|
|
39
|
+
|
|
40
|
+
async def execute_query(self, query: str, parameters: Optional[Dict[str, Any]] = None, routing: str = "w",
|
|
41
|
+
bookmarks: Optional[List[str]] = None,
|
|
42
|
+
**kwparameters: Any) -> Tuple[List[Record], ResultSummary, List[str]]:
|
|
43
|
+
"""Runs one statement in a retried transaction and returns (records, summary, keys)."""
|
|
44
|
+
if routing not in ("r", "w"):
|
|
45
|
+
raise ConfigurationError("routing is 'r' (a reader) or 'w' (the leader)")
|
|
46
|
+
params = {**(parameters or {}), **kwparameters}
|
|
47
|
+
|
|
48
|
+
async def work(tx: AsyncManagedTransaction) -> Tuple[List[Record], ResultSummary, List[str]]:
|
|
49
|
+
res = await tx.run(query, params)
|
|
50
|
+
records = await res.to_list()
|
|
51
|
+
return records, await res.consume(), await res.keys()
|
|
52
|
+
|
|
53
|
+
async with self.session(bookmarks=bookmarks) as s:
|
|
54
|
+
if routing == "r":
|
|
55
|
+
return await s.execute_read(work)
|
|
56
|
+
return await s.execute_write(work)
|
|
57
|
+
|
|
58
|
+
async def close(self) -> None:
|
|
59
|
+
await self._pool.close()
|
|
60
|
+
|
|
61
|
+
async def __aenter__(self) -> "AsyncDriver":
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
65
|
+
await self.close()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def async_driver(uri: str, auth: Any = None, **config: Any) -> AsyncDriver:
|
|
69
|
+
"""A driver for bolt://host:7687 (one server) or nomosdb://host:7687 (a cluster),
|
|
70
|
+
with +s (TLS, verified) or +ssc (TLS, self-signed) for encryption.
|
|
71
|
+
|
|
72
|
+
auth is (user, password). Options: max_connection_pool_size, connection_timeout,
|
|
73
|
+
connection_acquisition_timeout, max_transaction_retry_time, fetch_size,
|
|
74
|
+
trusted_certificates (a CA file for +s), user_agent."""
|
|
75
|
+
return AsyncDriver(uri, auth, **config)
|
nomosdb/_async/pool.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Connection pools: one server (bolt://) or a cluster through its routing table (nomosdb://)."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import Deque, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .._bolt import Response
|
|
7
|
+
from .._common import READ_ACCESS, Address, Config, parse_address
|
|
8
|
+
from .._io import AsyncCondition, monotonic
|
|
9
|
+
from ..exceptions import ClientError, ServiceUnavailable
|
|
10
|
+
from .connection import AsyncConnection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncPool:
|
|
14
|
+
"""Connections per address, at most max_connection_pool_size each."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Config):
|
|
17
|
+
self.config = config
|
|
18
|
+
self._idle: Dict[Address, Deque[AsyncConnection]] = {}
|
|
19
|
+
self._in_use: Dict[Address, int] = {}
|
|
20
|
+
self._cond = AsyncCondition()
|
|
21
|
+
self.closed = False
|
|
22
|
+
|
|
23
|
+
async def acquire_at(self, address: Address) -> AsyncConnection:
|
|
24
|
+
if self.closed:
|
|
25
|
+
raise ServiceUnavailable("the driver is closed")
|
|
26
|
+
deadline = monotonic() + self.config.connection_acquisition_timeout
|
|
27
|
+
while True:
|
|
28
|
+
async with self._cond:
|
|
29
|
+
idle = self._idle.setdefault(address, deque())
|
|
30
|
+
conn = idle.pop() if idle else None
|
|
31
|
+
if conn is None:
|
|
32
|
+
if self._in_use.get(address, 0) < self.config.max_connection_pool_size:
|
|
33
|
+
self._in_use[address] = self._in_use.get(address, 0) + 1
|
|
34
|
+
opening = True
|
|
35
|
+
else:
|
|
36
|
+
opening = False
|
|
37
|
+
left = deadline - monotonic()
|
|
38
|
+
if left <= 0 or not await self._cond.wait_for(
|
|
39
|
+
lambda: self._idle.get(address) or
|
|
40
|
+
self._in_use.get(address, 0) < self.config.max_connection_pool_size, left):
|
|
41
|
+
raise ServiceUnavailable(
|
|
42
|
+
f"no connection to {address[0]}:{address[1]} became free within "
|
|
43
|
+
f"{self.config.connection_acquisition_timeout} s (max_connection_pool_size="
|
|
44
|
+
f"{self.config.max_connection_pool_size})")
|
|
45
|
+
continue
|
|
46
|
+
else:
|
|
47
|
+
self._in_use[address] = self._in_use.get(address, 0) + 1
|
|
48
|
+
opening = False
|
|
49
|
+
if opening:
|
|
50
|
+
try:
|
|
51
|
+
return await AsyncConnection.open(address, self.config)
|
|
52
|
+
except BaseException:
|
|
53
|
+
await self._forget(address)
|
|
54
|
+
raise
|
|
55
|
+
if await self._alive(conn):
|
|
56
|
+
return conn
|
|
57
|
+
await self._forget(address)
|
|
58
|
+
|
|
59
|
+
async def _alive(self, conn: AsyncConnection) -> bool:
|
|
60
|
+
t = self.config.liveness_check_timeout
|
|
61
|
+
if conn.defunct or conn.closed:
|
|
62
|
+
return False
|
|
63
|
+
if t is not None and monotonic() - conn.last_used > t:
|
|
64
|
+
try:
|
|
65
|
+
await conn.reset()
|
|
66
|
+
except (ServiceUnavailable, ClientError):
|
|
67
|
+
await conn.close()
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
async def _forget(self, address: Address) -> None:
|
|
72
|
+
async with self._cond:
|
|
73
|
+
self._in_use[address] -= 1
|
|
74
|
+
self._cond.notify_all()
|
|
75
|
+
|
|
76
|
+
async def release(self, conn: AsyncConnection) -> None:
|
|
77
|
+
"""Takes a connection back; one in a transaction or a failure is reset first."""
|
|
78
|
+
if not (conn.defunct or conn.closed) and not conn.ready:
|
|
79
|
+
try:
|
|
80
|
+
await conn.reset()
|
|
81
|
+
except Exception:
|
|
82
|
+
await conn.close()
|
|
83
|
+
async with self._cond:
|
|
84
|
+
self._in_use[conn.address] -= 1
|
|
85
|
+
if conn.defunct or conn.closed or self.closed:
|
|
86
|
+
keep = False
|
|
87
|
+
else:
|
|
88
|
+
conn.last_used = monotonic()
|
|
89
|
+
self._idle.setdefault(conn.address, deque()).append(conn)
|
|
90
|
+
keep = True
|
|
91
|
+
self._cond.notify_all()
|
|
92
|
+
if not keep:
|
|
93
|
+
await conn.close()
|
|
94
|
+
|
|
95
|
+
async def deactivate(self, address: Address) -> None:
|
|
96
|
+
"""Closes the idle connections to a server that failed."""
|
|
97
|
+
async with self._cond:
|
|
98
|
+
idle = self._idle.pop(address, deque())
|
|
99
|
+
for c in idle:
|
|
100
|
+
await c.close()
|
|
101
|
+
|
|
102
|
+
async def close(self) -> None:
|
|
103
|
+
self.closed = True
|
|
104
|
+
async with self._cond:
|
|
105
|
+
idle = [c for q in self._idle.values() for c in q]
|
|
106
|
+
self._idle.clear()
|
|
107
|
+
for c in idle:
|
|
108
|
+
await c.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AsyncDirectPool(AsyncPool):
|
|
112
|
+
"""bolt://: every session goes to the one server."""
|
|
113
|
+
|
|
114
|
+
async def acquire(self, access_mode: str, bookmarks: List[str]) -> AsyncConnection:
|
|
115
|
+
return await self.acquire_at(self.config.address)
|
|
116
|
+
|
|
117
|
+
async def on_write_failure(self, address: Address) -> None:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
async def on_unavailable(self, address: Address) -> None:
|
|
121
|
+
await self.deactivate(address)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _RoutingTable:
|
|
125
|
+
def __init__(self, seed: Address):
|
|
126
|
+
self.routers: List[Address] = [seed]
|
|
127
|
+
self.readers: List[Address] = []
|
|
128
|
+
self.writers: List[Address] = []
|
|
129
|
+
self.expires = 0.0
|
|
130
|
+
self._next_reader = 0
|
|
131
|
+
self._next_writer = 0
|
|
132
|
+
|
|
133
|
+
def fresh(self, access_mode: str) -> bool:
|
|
134
|
+
servers = self.readers if access_mode == READ_ACCESS else self.writers
|
|
135
|
+
return monotonic() < self.expires and bool(servers)
|
|
136
|
+
|
|
137
|
+
def update(self, rt: Dict) -> None:
|
|
138
|
+
ttl = float(rt.get("ttl", 300))
|
|
139
|
+
readers, writers, routers = [], [], []
|
|
140
|
+
for s in rt.get("servers", []):
|
|
141
|
+
addrs = [parse_address(a) for a in s.get("addresses", [])]
|
|
142
|
+
role = s.get("role")
|
|
143
|
+
if role == "READ":
|
|
144
|
+
readers += addrs
|
|
145
|
+
elif role == "WRITE":
|
|
146
|
+
writers += addrs
|
|
147
|
+
elif role == "ROUTE":
|
|
148
|
+
routers += addrs
|
|
149
|
+
if not routers:
|
|
150
|
+
raise ServiceUnavailable("the routing table names no routers")
|
|
151
|
+
self.routers, self.readers, self.writers = routers, readers, writers
|
|
152
|
+
self.expires = monotonic() + ttl
|
|
153
|
+
|
|
154
|
+
def pick(self, access_mode: str) -> Optional[Address]:
|
|
155
|
+
if access_mode == READ_ACCESS:
|
|
156
|
+
if not self.readers:
|
|
157
|
+
return None
|
|
158
|
+
self._next_reader = (self._next_reader + 1) % len(self.readers)
|
|
159
|
+
return self.readers[self._next_reader]
|
|
160
|
+
if not self.writers:
|
|
161
|
+
return None
|
|
162
|
+
self._next_writer = (self._next_writer + 1) % len(self.writers)
|
|
163
|
+
return self.writers[self._next_writer]
|
|
164
|
+
|
|
165
|
+
def remove(self, address: Address) -> None:
|
|
166
|
+
for lst in (self.routers, self.readers, self.writers):
|
|
167
|
+
while address in lst:
|
|
168
|
+
lst.remove(address)
|
|
169
|
+
|
|
170
|
+
def remove_writer(self, address: Address) -> None:
|
|
171
|
+
while address in self.writers:
|
|
172
|
+
self.writers.remove(address)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AsyncRoutingPool(AsyncPool):
|
|
176
|
+
"""nomosdb:// / neo4j://: asks the cluster which members write and read (ROUTE)."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, config: Config):
|
|
179
|
+
super().__init__(config)
|
|
180
|
+
self.table = _RoutingTable(config.address)
|
|
181
|
+
self._refreshing = AsyncCondition()
|
|
182
|
+
|
|
183
|
+
async def acquire(self, access_mode: str, bookmarks: List[str]) -> AsyncConnection:
|
|
184
|
+
last: Optional[BaseException] = None
|
|
185
|
+
for _ in range(3):
|
|
186
|
+
if not self.table.fresh(access_mode):
|
|
187
|
+
await self._refresh(bookmarks)
|
|
188
|
+
address = self.table.pick(access_mode)
|
|
189
|
+
if address is None:
|
|
190
|
+
raise ServiceUnavailable(
|
|
191
|
+
f"the cluster has no {'reader' if access_mode == READ_ACCESS else 'leader'} at the moment")
|
|
192
|
+
try:
|
|
193
|
+
return await self.acquire_at(address)
|
|
194
|
+
except ServiceUnavailable as e:
|
|
195
|
+
last = e
|
|
196
|
+
await self.on_unavailable(address)
|
|
197
|
+
raise last if last else ServiceUnavailable("no cluster member could be reached")
|
|
198
|
+
|
|
199
|
+
async def _refresh(self, bookmarks: List[str]) -> None:
|
|
200
|
+
async with self._refreshing:
|
|
201
|
+
routers = list(self.table.routers)
|
|
202
|
+
if self.config.address not in routers:
|
|
203
|
+
routers.append(self.config.address) # the seed, in case every router moved
|
|
204
|
+
errors = []
|
|
205
|
+
for address in routers:
|
|
206
|
+
try:
|
|
207
|
+
conn = await self.acquire_at(address)
|
|
208
|
+
except ServiceUnavailable as e:
|
|
209
|
+
errors.append(str(e))
|
|
210
|
+
continue
|
|
211
|
+
try:
|
|
212
|
+
r = conn.protocol.route(self.config.routing_context or {}, list(bookmarks), Response("ROUTE"))
|
|
213
|
+
await conn.wait(r)
|
|
214
|
+
self.table.update(r.metadata.get("rt", {}))
|
|
215
|
+
return
|
|
216
|
+
except (ServiceUnavailable, ClientError) as e:
|
|
217
|
+
errors.append(f"{address[0]}:{address[1]}: {e}")
|
|
218
|
+
finally:
|
|
219
|
+
await self.release(conn)
|
|
220
|
+
raise ServiceUnavailable("no routing table: " + "; ".join(errors))
|
|
221
|
+
|
|
222
|
+
async def on_write_failure(self, address: Address) -> None:
|
|
223
|
+
"""A write reached a member that is not the leader: ask again."""
|
|
224
|
+
self.table.remove_writer(address)
|
|
225
|
+
|
|
226
|
+
async def on_unavailable(self, address: Address) -> None:
|
|
227
|
+
self.table.remove(address)
|
|
228
|
+
await self.deactivate(address)
|