kurrentdbclient 1.3.3__tar.gz → 1.3.5__tar.gz
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.
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/PKG-INFO +7 -6
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/README.md +5 -5
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/__init__.py +1 -1
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/asyncio_client.py +18 -6
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/client.py +183 -14
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/common.py +134 -32
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/connection.py +2 -2
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/exceptions.py +8 -2
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/spanners.py +8 -12
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/persistent.py +29 -19
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/streams.py +48 -24
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/pyproject.toml +3 -2
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/LICENSE +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/connection_spec.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/events.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/gossip.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/__init__.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/__init__.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/attributes.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/package.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/utils.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/instrumentation/opentelemetry/version.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/projections.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/google/rpc/code_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/errors_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/kurrent/rpc/rpc_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/cluster_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/gossip_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/persistent_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/projections_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/shared_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/status_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v1/streams_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/errors_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2.pyi +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/protos/v2/streams/streams_pb2_grpc.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/py.typed +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/unpack_error_status.py +0 -0
- {kurrentdbclient-1.3.3 → kurrentdbclient-1.3.5}/kurrentdbclient/v2streams.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kurrentdbclient
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: Python gRPC Client for KurrentDB
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
20
|
Classifier: Programming Language :: Python
|
|
20
21
|
Classifier: Topic :: Database
|
|
21
22
|
Classifier: Topic :: Database :: Front-Ends
|
|
@@ -56,10 +57,10 @@ with the KurrentDB team, and are officially supported by Kurrent Inc.
|
|
|
56
57
|
Although not all aspects of the KurrentDB gRPC API are implemented, most
|
|
57
58
|
features are presented in an easy-to-use interface.
|
|
58
59
|
|
|
59
|
-
These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0,
|
|
60
|
-
and 26.
|
|
60
|
+
These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0, 26.1,
|
|
61
|
+
and 26.2, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
|
|
61
62
|
in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12,
|
|
62
|
-
3.13, and 3.
|
|
63
|
+
3.13, 3.14, and 3.15.
|
|
63
64
|
|
|
64
65
|
The test suite has 100% line and branch coverage. The code has typing annotations
|
|
65
66
|
checked strictly with mypy. The code is formatted with black and isort, and checked
|
|
@@ -572,8 +573,8 @@ The table below describes the query string field-values supported by this client
|
|
|
572
573
|
| GossipTimeout | integer (default: 5) | The default value (in seconds) of the `timeout` argument of gossip read methods, such as `read_gossip()`. |
|
|
573
574
|
| MaxDiscoverAttempts | integer (default: 10) | The number of attempts to read gossip when connecting or reconnecting to a cluster member. |
|
|
574
575
|
| DiscoveryInterval | integer (default: 100) | How long to wait (in milliseconds) between gossip retries. |
|
|
575
|
-
| KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.
|
|
576
|
-
| KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" gRPC channel
|
|
576
|
+
| KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_time_ms" gRPC channel option. |
|
|
577
|
+
| KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" and "grpc.http2.ping_timeout_ms" gRPC channel options. |
|
|
577
578
|
| TlsCaFile | string (default: `None`) | Absolute filesystem path to file containing the CA certicate in PEM format. This will be used to verify the server's certificate. |
|
|
578
579
|
| UserCertFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate in PEM format. |
|
|
579
580
|
| UserKeyFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate's private key in PEM format. |
|
|
@@ -22,10 +22,10 @@ with the KurrentDB team, and are officially supported by Kurrent Inc.
|
|
|
22
22
|
Although not all aspects of the KurrentDB gRPC API are implemented, most
|
|
23
23
|
features are presented in an easy-to-use interface.
|
|
24
24
|
|
|
25
|
-
These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0,
|
|
26
|
-
and 26.
|
|
25
|
+
These clients have been tested to work with KurrentDB versions 25.0, 25.1, 26.0, 26.1,
|
|
26
|
+
and 26.2, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS,
|
|
27
27
|
in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12,
|
|
28
|
-
3.13, and 3.
|
|
28
|
+
3.13, 3.14, and 3.15.
|
|
29
29
|
|
|
30
30
|
The test suite has 100% line and branch coverage. The code has typing annotations
|
|
31
31
|
checked strictly with mypy. The code is formatted with black and isort, and checked
|
|
@@ -538,8 +538,8 @@ The table below describes the query string field-values supported by this client
|
|
|
538
538
|
| GossipTimeout | integer (default: 5) | The default value (in seconds) of the `timeout` argument of gossip read methods, such as `read_gossip()`. |
|
|
539
539
|
| MaxDiscoverAttempts | integer (default: 10) | The number of attempts to read gossip when connecting or reconnecting to a cluster member. |
|
|
540
540
|
| DiscoveryInterval | integer (default: 100) | How long to wait (in milliseconds) between gossip retries. |
|
|
541
|
-
| KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.
|
|
542
|
-
| KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" gRPC channel
|
|
541
|
+
| KeepAliveInterval | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_time_ms" gRPC channel option. |
|
|
542
|
+
| KeepAliveTimeout | integer (default: `None`) | The value (in milliseconds) of the "grpc.keepalive_timeout_ms" and "grpc.http2.ping_timeout_ms" gRPC channel options. |
|
|
543
543
|
| TlsCaFile | string (default: `None`) | Absolute filesystem path to file containing the CA certicate in PEM format. This will be used to verify the server's certificate. |
|
|
544
544
|
| UserCertFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate in PEM format. |
|
|
545
545
|
| UserKeyFile | string (default: `None`) | Absolute filesystem path to file containing the X.509 user certificate's private key in PEM format. |
|
|
@@ -65,6 +65,7 @@ from kurrentdbclient.exceptions import (
|
|
|
65
65
|
|
|
66
66
|
if TYPE_CHECKING:
|
|
67
67
|
from collections.abc import Iterable, Sequence
|
|
68
|
+
from uuid import UUID
|
|
68
69
|
|
|
69
70
|
from kurrentdbclient.persistent import ConsumerStrategy, SubscriptionInfo
|
|
70
71
|
from kurrentdbclient.projections import ProjectionState, ProjectionStatistics
|
|
@@ -80,6 +81,7 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
80
81
|
client: AsyncKurrentDBClient, *args: Any, **kwargs: Any
|
|
81
82
|
) -> Any:
|
|
82
83
|
await client.connect()
|
|
84
|
+
connection_id = client.connection_id
|
|
83
85
|
try:
|
|
84
86
|
return await f(client, *args, **kwargs)
|
|
85
87
|
|
|
@@ -92,7 +94,8 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
92
94
|
)
|
|
93
95
|
):
|
|
94
96
|
await client.reconnect(
|
|
95
|
-
grpc_target(e.host, e.port) if e.host and e.port else None
|
|
97
|
+
grpc_target(e.host, e.port) if e.host and e.port else None,
|
|
98
|
+
connection_id=connection_id,
|
|
96
99
|
)
|
|
97
100
|
await asyncio.sleep(0.1)
|
|
98
101
|
return await f(client, *args, **kwargs)
|
|
@@ -100,13 +103,13 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
100
103
|
|
|
101
104
|
except grpc.aio.UsageError as e:
|
|
102
105
|
if "Channel is closed" in str(e):
|
|
103
|
-
await client.reconnect()
|
|
106
|
+
await client.reconnect(connection_id=connection_id)
|
|
104
107
|
await asyncio.sleep(0.1)
|
|
105
108
|
return await f(client, *args, **kwargs)
|
|
106
109
|
raise # pragma: no cover
|
|
107
110
|
|
|
108
111
|
except ServiceUnavailableError:
|
|
109
|
-
await client.reconnect()
|
|
112
|
+
await client.reconnect(connection_id=connection_id)
|
|
110
113
|
await asyncio.sleep(0.1)
|
|
111
114
|
return await f(client, *args, **kwargs)
|
|
112
115
|
|
|
@@ -119,7 +122,7 @@ def retrygrpc(f: _TCallable) -> _TCallable:
|
|
|
119
122
|
try:
|
|
120
123
|
return await f(*args, **kwargs)
|
|
121
124
|
except GrpcError:
|
|
122
|
-
await asyncio.sleep(0.
|
|
125
|
+
await asyncio.sleep(0.5)
|
|
123
126
|
return await f(*args, **kwargs)
|
|
124
127
|
|
|
125
128
|
return cast(_TCallable, retrygrpc_decorator)
|
|
@@ -143,6 +146,11 @@ class AsyncKurrentDBClient(BaseKurrentDBClient):
|
|
|
143
146
|
self._is_reconnection_required = Event()
|
|
144
147
|
self._connection_lock = Lock()
|
|
145
148
|
|
|
149
|
+
@property
|
|
150
|
+
def connection_id(self) -> UUID:
|
|
151
|
+
assert self._connection is not None
|
|
152
|
+
return self._connection.connection_id
|
|
153
|
+
|
|
146
154
|
async def connect(self) -> None:
|
|
147
155
|
if self._connection is None:
|
|
148
156
|
async with self._connection_lock:
|
|
@@ -163,8 +171,12 @@ class AsyncKurrentDBClient(BaseKurrentDBClient):
|
|
|
163
171
|
def connection_target(self) -> str:
|
|
164
172
|
return self.connection.grpc_target
|
|
165
173
|
|
|
166
|
-
async def reconnect(
|
|
167
|
-
self
|
|
174
|
+
async def reconnect(
|
|
175
|
+
self, grpc_target: str | None = None, connection_id: UUID | None = None
|
|
176
|
+
) -> None:
|
|
177
|
+
# Avoid reconnecting if client already has a new connection.
|
|
178
|
+
if connection_id is None or connection_id == self.connection_id:
|
|
179
|
+
self._is_reconnection_required.set()
|
|
168
180
|
async with self._connection_lock:
|
|
169
181
|
if self._is_reconnection_required.is_set():
|
|
170
182
|
new = await self._connect(grpc_target)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import json
|
|
4
5
|
import random
|
|
5
6
|
import sys
|
|
@@ -62,6 +63,7 @@ from kurrentdbclient.events import (
|
|
|
62
63
|
StreamStateCheck,
|
|
63
64
|
)
|
|
64
65
|
from kurrentdbclient.exceptions import (
|
|
66
|
+
AccessDeniedError,
|
|
65
67
|
DiscoveryFailedError,
|
|
66
68
|
FollowerNotFoundError,
|
|
67
69
|
GrpcError,
|
|
@@ -81,6 +83,7 @@ from kurrentdbclient.gossip import (
|
|
|
81
83
|
|
|
82
84
|
if TYPE_CHECKING:
|
|
83
85
|
from collections.abc import Iterable, Sequence
|
|
86
|
+
from uuid import UUID
|
|
84
87
|
|
|
85
88
|
from kurrentdbclient.persistent import (
|
|
86
89
|
ConsumerStrategy,
|
|
@@ -116,9 +119,14 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
116
119
|
def autoreconnect_decorator(*args: Any, **kwargs: Any) -> Any:
|
|
117
120
|
client = args[0]
|
|
118
121
|
assert isinstance(client, KurrentDBClient)
|
|
122
|
+
connection_id = client.connection_id
|
|
119
123
|
try:
|
|
120
124
|
return f(*args, **kwargs)
|
|
121
125
|
|
|
126
|
+
# TODO: Maybe refine the retry so that we do reconnect and try again when
|
|
127
|
+
# there is connection failure after retrying without reconnecting because
|
|
128
|
+
# the connection has changed. Need to think about this a bit more...
|
|
129
|
+
|
|
122
130
|
except NodeIsNotLeaderError as e:
|
|
123
131
|
if (
|
|
124
132
|
client.connection_spec.options.node_preference == NODE_PREFERENCE_LEADER
|
|
@@ -128,7 +136,8 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
128
136
|
)
|
|
129
137
|
):
|
|
130
138
|
client.reconnect(
|
|
131
|
-
grpc_target(e.host, e.port) if e.host and e.port else None
|
|
139
|
+
grpc_target(e.host, e.port) if e.host and e.port else None,
|
|
140
|
+
connection_id=connection_id,
|
|
132
141
|
)
|
|
133
142
|
sleep(0.1)
|
|
134
143
|
return f(*args, **kwargs)
|
|
@@ -137,13 +146,13 @@ def autoreconnect(f: _TCallable) -> _TCallable:
|
|
|
137
146
|
except ValueError as e:
|
|
138
147
|
s = str(e)
|
|
139
148
|
if "Channel closed!" in s or "Cannot invoke RPC on closed channel!" in s:
|
|
140
|
-
client.reconnect()
|
|
149
|
+
client.reconnect(connection_id=connection_id)
|
|
141
150
|
sleep(0.1)
|
|
142
151
|
return f(*args, **kwargs)
|
|
143
152
|
raise # pragma: no cover
|
|
144
153
|
|
|
145
154
|
except ServiceUnavailableError:
|
|
146
|
-
client.reconnect()
|
|
155
|
+
client.reconnect(connection_id=connection_id)
|
|
147
156
|
sleep(0.1)
|
|
148
157
|
return f(*args, **kwargs)
|
|
149
158
|
|
|
@@ -208,12 +217,16 @@ class BaseKurrentDBClient(ABC):
|
|
|
208
217
|
|
|
209
218
|
self._default_deadline = self.connection_spec.options.default_deadline
|
|
210
219
|
|
|
220
|
+
# GRPC options ref: https://grpc.github.io/grpc/core/group__grpc__arg__keys.html
|
|
211
221
|
self.grpc_options: GrpcOptions = (
|
|
212
222
|
("grpc.max_receive_message_length", 17 * 1024 * 1024),
|
|
213
223
|
)
|
|
214
224
|
if self.connection_spec.options.keep_alive_interval is not None:
|
|
215
225
|
self.grpc_options += (
|
|
216
|
-
(
|
|
226
|
+
(
|
|
227
|
+
"grpc.keepalive_time_ms",
|
|
228
|
+
self.connection_spec.options.keep_alive_interval,
|
|
229
|
+
),
|
|
217
230
|
)
|
|
218
231
|
if self.connection_spec.options.keep_alive_timeout is not None:
|
|
219
232
|
self.grpc_options += (
|
|
@@ -221,6 +234,12 @@ class BaseKurrentDBClient(ABC):
|
|
|
221
234
|
"grpc.keepalive_timeout_ms",
|
|
222
235
|
self.connection_spec.options.keep_alive_timeout,
|
|
223
236
|
),
|
|
237
|
+
# gRPC closes a connection whose ping is not answered after
|
|
238
|
+
# this time (default 60s), not after keepalive_timeout_ms.
|
|
239
|
+
(
|
|
240
|
+
"grpc.http2.ping_timeout_ms",
|
|
241
|
+
self.connection_spec.options.keep_alive_timeout,
|
|
242
|
+
),
|
|
224
243
|
)
|
|
225
244
|
|
|
226
245
|
self._call_metadata = (
|
|
@@ -312,6 +331,10 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
312
331
|
def connection_target(self) -> str:
|
|
313
332
|
return self._connection.grpc_target
|
|
314
333
|
|
|
334
|
+
@property
|
|
335
|
+
def connection_id(self) -> UUID:
|
|
336
|
+
return self._connection.connection_id
|
|
337
|
+
|
|
315
338
|
@property
|
|
316
339
|
def streams(self) -> StreamsService:
|
|
317
340
|
return self._connection.streams
|
|
@@ -401,14 +424,22 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
401
424
|
|
|
402
425
|
return connection
|
|
403
426
|
|
|
404
|
-
def reconnect(
|
|
405
|
-
self
|
|
427
|
+
def reconnect(
|
|
428
|
+
self, grpc_target: str | None = None, *, connection_id: UUID | None = None
|
|
429
|
+
) -> None:
|
|
430
|
+
# Avoid reconnecting if client already has a new connection.
|
|
431
|
+
if connection_id is None or connection_id == self.connection_id:
|
|
432
|
+
self._is_reconnection_required.set()
|
|
433
|
+
# Single reconnection.
|
|
406
434
|
with self._reconnection_lock:
|
|
407
435
|
if self._is_reconnection_required.is_set():
|
|
408
436
|
new_conn = self._connect(grpc_target)
|
|
409
437
|
old_conn, self._connection = self._connection, new_conn
|
|
410
438
|
old_conn.close()
|
|
411
439
|
self._is_reconnection_required.clear()
|
|
440
|
+
# Exercise the connection (avoids delayed errors due to lazy channel)
|
|
441
|
+
with contextlib.suppress(AccessDeniedError):
|
|
442
|
+
self._get_commit_position()
|
|
412
443
|
else: # pragma: no cover
|
|
413
444
|
# Todo: Test with concurrent writes to wrong node state.
|
|
414
445
|
pass
|
|
@@ -668,17 +699,19 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
668
699
|
"""
|
|
669
700
|
Returns a sequence of recorded events from the named stream.
|
|
670
701
|
"""
|
|
671
|
-
with self.
|
|
702
|
+
with self._read_stream(
|
|
672
703
|
stream_name=stream_name,
|
|
673
704
|
stream_position=stream_position,
|
|
674
705
|
backwards=backwards,
|
|
675
706
|
resolve_links=resolve_links,
|
|
676
707
|
limit=limit,
|
|
677
708
|
timeout=timeout,
|
|
678
|
-
credentials=credentials
|
|
709
|
+
credentials=credentials,
|
|
679
710
|
) as events:
|
|
680
711
|
return tuple(events)
|
|
681
712
|
|
|
713
|
+
@retrygrpc
|
|
714
|
+
@autoreconnect
|
|
682
715
|
def read_stream(
|
|
683
716
|
self,
|
|
684
717
|
stream_name: str,
|
|
@@ -689,6 +722,30 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
689
722
|
limit: int = sys.maxsize,
|
|
690
723
|
timeout: float | None = None,
|
|
691
724
|
credentials: grpc.CallCredentials | None = None,
|
|
725
|
+
) -> AbstractReadResponse:
|
|
726
|
+
"""
|
|
727
|
+
Reads recorded events from the named stream.
|
|
728
|
+
"""
|
|
729
|
+
return self._read_stream(
|
|
730
|
+
stream_name=stream_name,
|
|
731
|
+
stream_position=stream_position,
|
|
732
|
+
backwards=backwards,
|
|
733
|
+
resolve_links=resolve_links,
|
|
734
|
+
limit=limit,
|
|
735
|
+
timeout=timeout,
|
|
736
|
+
credentials=credentials,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def _read_stream(
|
|
740
|
+
self,
|
|
741
|
+
stream_name: str,
|
|
742
|
+
*,
|
|
743
|
+
stream_position: int | None = None,
|
|
744
|
+
backwards: bool = False,
|
|
745
|
+
resolve_links: bool = False,
|
|
746
|
+
limit: int = sys.maxsize,
|
|
747
|
+
timeout: float | None = None,
|
|
748
|
+
credentials: grpc.CallCredentials | None = None,
|
|
692
749
|
) -> AbstractReadResponse:
|
|
693
750
|
"""
|
|
694
751
|
Reads recorded events from the named stream.
|
|
@@ -704,6 +761,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
704
761
|
credentials=credentials or self._call_credentials,
|
|
705
762
|
)
|
|
706
763
|
|
|
764
|
+
@retrygrpc
|
|
765
|
+
@autoreconnect
|
|
707
766
|
def read_all(
|
|
708
767
|
self,
|
|
709
768
|
*,
|
|
@@ -717,6 +776,36 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
717
776
|
limit: int = sys.maxsize,
|
|
718
777
|
timeout: float | None = None,
|
|
719
778
|
credentials: grpc.CallCredentials | None = None,
|
|
779
|
+
) -> AbstractReadResponse:
|
|
780
|
+
"""
|
|
781
|
+
Reads recorded events in "all streams" in the database.
|
|
782
|
+
"""
|
|
783
|
+
return self._read_all(
|
|
784
|
+
commit_position=commit_position,
|
|
785
|
+
backwards=backwards,
|
|
786
|
+
resolve_links=resolve_links,
|
|
787
|
+
filter_exclude=filter_exclude,
|
|
788
|
+
filter_include=filter_include,
|
|
789
|
+
filter_by_stream_name=filter_by_stream_name,
|
|
790
|
+
filter_by_prefix=filter_by_prefix,
|
|
791
|
+
limit=limit,
|
|
792
|
+
timeout=timeout,
|
|
793
|
+
credentials=credentials,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
def _read_all(
|
|
797
|
+
self,
|
|
798
|
+
*,
|
|
799
|
+
commit_position: int | None = None,
|
|
800
|
+
backwards: bool = False,
|
|
801
|
+
resolve_links: bool = False,
|
|
802
|
+
filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
|
|
803
|
+
filter_include: Sequence[str] = (),
|
|
804
|
+
filter_by_stream_name: bool = False,
|
|
805
|
+
filter_by_prefix: bool = False,
|
|
806
|
+
limit: int = sys.maxsize,
|
|
807
|
+
timeout: float | None = None,
|
|
808
|
+
credentials: grpc.CallCredentials | None = None,
|
|
720
809
|
) -> AbstractReadResponse:
|
|
721
810
|
"""
|
|
722
811
|
Reads recorded events in "all streams" in the database.
|
|
@@ -735,6 +824,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
735
824
|
credentials=credentials or self._call_credentials,
|
|
736
825
|
)
|
|
737
826
|
|
|
827
|
+
@retrygrpc
|
|
828
|
+
@autoreconnect
|
|
738
829
|
def read_index(
|
|
739
830
|
self,
|
|
740
831
|
index_name: str,
|
|
@@ -748,7 +839,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
748
839
|
if not index_name.startswith("$idx-"):
|
|
749
840
|
index_name = f"$idx-{index_name}"
|
|
750
841
|
|
|
751
|
-
return self.
|
|
842
|
+
return self._read_all(
|
|
752
843
|
commit_position=commit_position,
|
|
753
844
|
filter_include=(index_name,),
|
|
754
845
|
filter_by_stream_name=True,
|
|
@@ -807,15 +898,34 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
807
898
|
"""
|
|
808
899
|
Returns the current commit position of the database.
|
|
809
900
|
"""
|
|
810
|
-
|
|
901
|
+
return self._get_commit_position(
|
|
902
|
+
filter_exclude=filter_exclude,
|
|
903
|
+
filter_include=filter_include,
|
|
904
|
+
filter_by_stream_name=filter_by_stream_name,
|
|
905
|
+
timeout=timeout,
|
|
906
|
+
credentials=credentials,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
def _get_commit_position(
|
|
910
|
+
self,
|
|
911
|
+
*,
|
|
912
|
+
filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
|
|
913
|
+
filter_include: Sequence[str] = (),
|
|
914
|
+
filter_by_stream_name: bool = False,
|
|
915
|
+
timeout: float | None = None,
|
|
916
|
+
credentials: grpc.CallCredentials | None = None,
|
|
917
|
+
) -> int:
|
|
918
|
+
"""
|
|
919
|
+
Returns the current commit position of the database.
|
|
920
|
+
"""
|
|
921
|
+
read_response = self._read_all(
|
|
811
922
|
backwards=True,
|
|
812
923
|
filter_exclude=filter_exclude,
|
|
813
924
|
filter_include=filter_include,
|
|
814
925
|
filter_by_stream_name=filter_by_stream_name,
|
|
815
926
|
limit=1,
|
|
816
927
|
timeout=timeout,
|
|
817
|
-
|
|
818
|
-
credentials=credentials or self._call_credentials,
|
|
928
|
+
credentials=credentials,
|
|
819
929
|
)
|
|
820
930
|
recorded_events = tuple(read_response)
|
|
821
931
|
if recorded_events:
|
|
@@ -895,6 +1005,45 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
895
1005
|
include_fell_behind: bool = False,
|
|
896
1006
|
timeout: float | None = None,
|
|
897
1007
|
credentials: grpc.CallCredentials | None = None,
|
|
1008
|
+
) -> AbstractCatchupSubscription:
|
|
1009
|
+
"""
|
|
1010
|
+
Starts a catch-up subscription, from which all
|
|
1011
|
+
recorded events in the database can be received.
|
|
1012
|
+
"""
|
|
1013
|
+
return self._subscribe_to_all(
|
|
1014
|
+
commit_position=commit_position,
|
|
1015
|
+
from_end=from_end,
|
|
1016
|
+
resolve_links=resolve_links,
|
|
1017
|
+
filter_exclude=filter_exclude,
|
|
1018
|
+
filter_include=filter_include,
|
|
1019
|
+
filter_by_stream_name=filter_by_stream_name,
|
|
1020
|
+
filter_by_prefix=filter_by_prefix,
|
|
1021
|
+
include_checkpoints=include_checkpoints,
|
|
1022
|
+
window_size=window_size,
|
|
1023
|
+
checkpoint_interval_multiplier=checkpoint_interval_multiplier,
|
|
1024
|
+
include_caught_up=include_caught_up,
|
|
1025
|
+
include_fell_behind=include_fell_behind,
|
|
1026
|
+
timeout=timeout,
|
|
1027
|
+
credentials=credentials,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
def _subscribe_to_all(
|
|
1031
|
+
self,
|
|
1032
|
+
*,
|
|
1033
|
+
commit_position: int | None = None,
|
|
1034
|
+
from_end: bool = False,
|
|
1035
|
+
resolve_links: bool = False,
|
|
1036
|
+
filter_exclude: Sequence[str] = DEFAULT_EXCLUDE_FILTER,
|
|
1037
|
+
filter_include: Sequence[str] = (),
|
|
1038
|
+
filter_by_stream_name: bool = False,
|
|
1039
|
+
filter_by_prefix: bool = False,
|
|
1040
|
+
include_checkpoints: bool = False,
|
|
1041
|
+
window_size: int = DEFAULT_WINDOW_SIZE,
|
|
1042
|
+
checkpoint_interval_multiplier: int = DEFAULT_CHECKPOINT_INTERVAL_MULTIPLIER,
|
|
1043
|
+
include_caught_up: bool = False,
|
|
1044
|
+
include_fell_behind: bool = False,
|
|
1045
|
+
timeout: float | None = None,
|
|
1046
|
+
credentials: grpc.CallCredentials | None = None,
|
|
898
1047
|
) -> AbstractCatchupSubscription:
|
|
899
1048
|
"""
|
|
900
1049
|
Starts a catch-up subscription, from which all
|
|
@@ -919,6 +1068,8 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
919
1068
|
credentials=credentials or self._call_credentials,
|
|
920
1069
|
)
|
|
921
1070
|
|
|
1071
|
+
@retrygrpc
|
|
1072
|
+
@autoreconnect
|
|
922
1073
|
def subscribe_to_index(
|
|
923
1074
|
self,
|
|
924
1075
|
index_name: str,
|
|
@@ -931,7 +1082,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
931
1082
|
if not index_name.startswith("$idx-"):
|
|
932
1083
|
index_name = f"$idx-{index_name}"
|
|
933
1084
|
|
|
934
|
-
return self.
|
|
1085
|
+
return self._subscribe_to_all(
|
|
935
1086
|
commit_position=commit_position,
|
|
936
1087
|
filter_include=(index_name,),
|
|
937
1088
|
filter_by_stream_name=True,
|
|
@@ -1411,6 +1562,24 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
1411
1562
|
*,
|
|
1412
1563
|
timeout: float | None = None,
|
|
1413
1564
|
credentials: grpc.CallCredentials | None = None,
|
|
1565
|
+
) -> SubscriptionInfo:
|
|
1566
|
+
"""
|
|
1567
|
+
Gets info for a persistent subscription.
|
|
1568
|
+
"""
|
|
1569
|
+
return self._get_subscription_info(
|
|
1570
|
+
group_name=group_name,
|
|
1571
|
+
stream_name=stream_name,
|
|
1572
|
+
timeout=timeout,
|
|
1573
|
+
credentials=credentials,
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
def _get_subscription_info(
|
|
1577
|
+
self,
|
|
1578
|
+
group_name: str,
|
|
1579
|
+
stream_name: str | None = None,
|
|
1580
|
+
*,
|
|
1581
|
+
timeout: float | None = None,
|
|
1582
|
+
credentials: grpc.CallCredentials | None = None,
|
|
1414
1583
|
) -> SubscriptionInfo:
|
|
1415
1584
|
"""
|
|
1416
1585
|
Gets info for a persistent subscription.
|
|
@@ -1744,7 +1913,7 @@ class KurrentDBClient(BaseKurrentDBClient):
|
|
|
1744
1913
|
Updates a persistent subscription on one stream.
|
|
1745
1914
|
"""
|
|
1746
1915
|
|
|
1747
|
-
info = self.
|
|
1916
|
+
info = self._get_subscription_info(
|
|
1748
1917
|
group_name=group_name,
|
|
1749
1918
|
stream_name=stream_name,
|
|
1750
1919
|
timeout=timeout,
|