valkey-glide 1.3.5rc5__cp312-cp312-macosx_11_0_arm64.whl → 2.0.0__cp312-cp312-macosx_11_0_arm64.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.
Potentially problematic release.
This version of valkey-glide might be problematic. Click here for more details.
- glide/__init__.py +32 -8
- glide/async_commands/{transaction.py → batch.py} +1420 -992
- glide/async_commands/batch_options.py +261 -0
- glide/async_commands/bitmap.py +94 -85
- glide/async_commands/cluster_commands.py +293 -126
- glide/async_commands/command_args.py +7 -6
- glide/async_commands/core.py +1313 -721
- glide/async_commands/server_modules/ft.py +83 -14
- glide/async_commands/server_modules/ft_options/ft_aggregate_options.py +15 -8
- glide/async_commands/server_modules/ft_options/ft_create_options.py +23 -11
- glide/async_commands/server_modules/ft_options/ft_profile_options.py +12 -7
- glide/async_commands/server_modules/ft_options/ft_search_options.py +12 -6
- glide/async_commands/server_modules/glide_json.py +134 -43
- glide/async_commands/server_modules/json_batch.py +157 -127
- glide/async_commands/sorted_set.py +39 -29
- glide/async_commands/standalone_commands.py +202 -95
- glide/async_commands/stream.py +94 -87
- glide/config.py +253 -112
- glide/constants.py +8 -4
- glide/glide.cpython-312-darwin.so +0 -0
- glide/glide.pyi +25 -0
- glide/glide_client.py +305 -94
- glide/logger.py +31 -19
- glide/opentelemetry.py +181 -0
- glide/protobuf/command_request_pb2.py +15 -15
- glide/protobuf/command_request_pb2.pyi +75 -46
- glide/protobuf/connection_request_pb2.py +12 -12
- glide/protobuf/connection_request_pb2.pyi +36 -29
- glide/protobuf/response_pb2.py +6 -6
- glide/protobuf/response_pb2.pyi +14 -9
- glide/protobuf_codec.py +7 -6
- glide/routes.py +41 -8
- {valkey_glide-1.3.5rc5.dist-info → valkey_glide-2.0.0.dist-info}/METADATA +38 -14
- valkey_glide-2.0.0.dist-info/RECORD +39 -0
- valkey_glide-1.3.5rc5.dist-info/RECORD +0 -37
- {valkey_glide-1.3.5rc5.dist-info → valkey_glide-2.0.0.dist-info}/WHEEL +0 -0
glide/glide.pyi
CHANGED
|
@@ -27,6 +27,28 @@ class ClusterScanCursor:
|
|
|
27
27
|
def get_cursor(self) -> str: ...
|
|
28
28
|
def is_finished(self) -> bool: ...
|
|
29
29
|
|
|
30
|
+
class OpenTelemetryConfig:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
traces: Optional[OpenTelemetryTracesConfig] = None,
|
|
34
|
+
metrics: Optional[OpenTelemetryMetricsConfig] = None,
|
|
35
|
+
flush_interval_ms: Optional[int] = None,
|
|
36
|
+
) -> None: ...
|
|
37
|
+
def get_traces(self) -> Optional[OpenTelemetryTracesConfig]: ...
|
|
38
|
+
def set_traces(self, traces: OpenTelemetryTracesConfig) -> None: ...
|
|
39
|
+
def get_metrics(self) -> Optional[OpenTelemetryMetricsConfig]: ...
|
|
40
|
+
|
|
41
|
+
class OpenTelemetryTracesConfig:
|
|
42
|
+
def __init__(
|
|
43
|
+
self, endpoint: str, sample_percentage: Optional[int] = None
|
|
44
|
+
) -> None: ...
|
|
45
|
+
def get_endpoint(self) -> str: ...
|
|
46
|
+
def get_sample_percentage(self) -> Optional[int]: ...
|
|
47
|
+
|
|
48
|
+
class OpenTelemetryMetricsConfig:
|
|
49
|
+
def __init__(self, endpoint: str) -> None: ...
|
|
50
|
+
def get_endpoint(self) -> str: ...
|
|
51
|
+
|
|
30
52
|
def start_socket_listener_external(init_callback: Callable) -> None: ...
|
|
31
53
|
def value_from_pointer(pointer: int) -> TResult: ...
|
|
32
54
|
def create_leaked_value(message: str) -> int: ...
|
|
@@ -34,3 +56,6 @@ def create_leaked_bytes_vec(args_vec: List[bytes]) -> int: ...
|
|
|
34
56
|
def get_statistics() -> dict: ...
|
|
35
57
|
def py_init(level: Optional[Level], file_name: Optional[str]) -> Level: ...
|
|
36
58
|
def py_log(log_level: Level, log_identifier: str, message: str) -> None: ...
|
|
59
|
+
def create_otel_span(name: str) -> int: ...
|
|
60
|
+
def drop_otel_span(span_ptr: int) -> None: ...
|
|
61
|
+
def init_opentelemetry(config: OpenTelemetryConfig) -> None: ...
|
glide/glide_client.py
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import sys
|
|
5
4
|
import threading
|
|
6
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Any,
|
|
8
|
+
Awaitable,
|
|
9
|
+
Dict,
|
|
10
|
+
List,
|
|
11
|
+
Optional,
|
|
12
|
+
Set,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
cast,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import anyio
|
|
20
|
+
import sniffio
|
|
21
|
+
from anyio import to_thread
|
|
7
22
|
|
|
8
23
|
from glide.async_commands.cluster_commands import ClusterCommands
|
|
9
24
|
from glide.async_commands.command_args import ObjectType
|
|
@@ -21,6 +36,7 @@ from glide.exceptions import (
|
|
|
21
36
|
)
|
|
22
37
|
from glide.logger import Level as LogLevel
|
|
23
38
|
from glide.logger import Logger as ClientLogger
|
|
39
|
+
from glide.opentelemetry import OpenTelemetry
|
|
24
40
|
from glide.protobuf.command_request_pb2 import Command, CommandRequest, RequestType
|
|
25
41
|
from glide.protobuf.connection_request_pb2 import ConnectionRequest
|
|
26
42
|
from glide.protobuf.response_pb2 import RequestErrorType, Response
|
|
@@ -32,18 +48,26 @@ from .glide import (
|
|
|
32
48
|
MAX_REQUEST_ARGS_LEN,
|
|
33
49
|
ClusterScanCursor,
|
|
34
50
|
create_leaked_bytes_vec,
|
|
51
|
+
create_otel_span,
|
|
52
|
+
drop_otel_span,
|
|
35
53
|
get_statistics,
|
|
36
54
|
start_socket_listener_external,
|
|
37
55
|
value_from_pointer,
|
|
38
56
|
)
|
|
39
57
|
|
|
40
58
|
if sys.version_info >= (3, 11):
|
|
41
|
-
import asyncio as async_timeout
|
|
42
59
|
from typing import Self
|
|
43
60
|
else:
|
|
44
|
-
import async_timeout
|
|
45
61
|
from typing_extensions import Self
|
|
46
62
|
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
import asyncio
|
|
65
|
+
|
|
66
|
+
import trio
|
|
67
|
+
|
|
68
|
+
TTask = Union[asyncio.Task[None], trio.lowlevel.Task]
|
|
69
|
+
TFuture = Union[asyncio.Future[Any], "_CompatFuture"]
|
|
70
|
+
|
|
47
71
|
|
|
48
72
|
def get_request_error_class(
|
|
49
73
|
error_type: Optional[RequestErrorType.ValueType],
|
|
@@ -59,38 +83,168 @@ def get_request_error_class(
|
|
|
59
83
|
return RequestError
|
|
60
84
|
|
|
61
85
|
|
|
86
|
+
class _CompatFuture:
|
|
87
|
+
"""anyio shim for asyncio.Future-like functionality"""
|
|
88
|
+
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
self._is_done = anyio.Event()
|
|
91
|
+
self._result: Any = None
|
|
92
|
+
self._exception: Optional[Exception] = None
|
|
93
|
+
|
|
94
|
+
def set_result(self, result: Any) -> None:
|
|
95
|
+
self._result = result
|
|
96
|
+
self._is_done.set()
|
|
97
|
+
|
|
98
|
+
def set_exception(self, exception: Exception) -> None:
|
|
99
|
+
self._exception = exception
|
|
100
|
+
self._is_done.set()
|
|
101
|
+
|
|
102
|
+
def done(self) -> bool:
|
|
103
|
+
return self._is_done.is_set()
|
|
104
|
+
|
|
105
|
+
def __await__(self):
|
|
106
|
+
return self._is_done.wait().__await__()
|
|
107
|
+
|
|
108
|
+
def result(self) -> Any:
|
|
109
|
+
if self._exception:
|
|
110
|
+
raise self._exception
|
|
111
|
+
|
|
112
|
+
return self._result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_new_future_instance() -> "TFuture":
|
|
116
|
+
if sniffio.current_async_library() == "asyncio":
|
|
117
|
+
import asyncio
|
|
118
|
+
|
|
119
|
+
return asyncio.get_running_loop().create_future()
|
|
120
|
+
|
|
121
|
+
# _CompatFuture is also compatible with asyncio, but is not as closely integrated
|
|
122
|
+
# into the asyncio event loop and thus introduces a noticeable performance
|
|
123
|
+
# degradation. so we only use it for trio
|
|
124
|
+
return _CompatFuture()
|
|
125
|
+
|
|
126
|
+
|
|
62
127
|
class BaseClient(CoreCommands):
|
|
63
128
|
def __init__(self, config: BaseClientConfiguration):
|
|
64
129
|
"""
|
|
65
130
|
To create a new client, use the `create` classmethod
|
|
66
131
|
"""
|
|
67
132
|
self.config: BaseClientConfiguration = config
|
|
68
|
-
self._available_futures: Dict[int,
|
|
133
|
+
self._available_futures: Dict[int, "TFuture"] = {}
|
|
69
134
|
self._available_callback_indexes: List[int] = list()
|
|
70
135
|
self._buffered_requests: List[TRequest] = list()
|
|
71
136
|
self._writer_lock = threading.Lock()
|
|
72
137
|
self.socket_path: Optional[str] = None
|
|
73
|
-
self._reader_task: Optional[
|
|
138
|
+
self._reader_task: Optional["TTask"] = None
|
|
74
139
|
self._is_closed: bool = False
|
|
75
|
-
self._pubsub_futures: List[
|
|
140
|
+
self._pubsub_futures: List["TFuture"] = []
|
|
76
141
|
self._pubsub_lock = threading.Lock()
|
|
77
142
|
self._pending_push_notifications: List[Response] = list()
|
|
78
143
|
|
|
144
|
+
self._pending_tasks: Optional[Set[Awaitable[None]]] = None
|
|
145
|
+
"""asyncio-only to avoid gc on pending write tasks"""
|
|
146
|
+
|
|
147
|
+
def _create_task(self, task, *args, **kwargs):
|
|
148
|
+
"""framework agnostic free-floating task shim"""
|
|
149
|
+
framework = sniffio.current_async_library()
|
|
150
|
+
if framework == "trio":
|
|
151
|
+
from functools import partial
|
|
152
|
+
|
|
153
|
+
import trio
|
|
154
|
+
|
|
155
|
+
return trio.lowlevel.spawn_system_task(partial(task, **kwargs), *args)
|
|
156
|
+
elif framework == "asyncio":
|
|
157
|
+
import asyncio
|
|
158
|
+
|
|
159
|
+
# the asyncio event loop holds weak refs to tasks, so it's recommended to
|
|
160
|
+
# hold strong refs to them during their lifetime to prevent garbage
|
|
161
|
+
# collection
|
|
162
|
+
t = asyncio.create_task(task(*args, **kwargs))
|
|
163
|
+
|
|
164
|
+
if self._pending_tasks is None:
|
|
165
|
+
self._pending_tasks = set()
|
|
166
|
+
|
|
167
|
+
self._pending_tasks.add(t)
|
|
168
|
+
t.add_done_callback(self._pending_tasks.discard)
|
|
169
|
+
|
|
170
|
+
return t
|
|
171
|
+
|
|
172
|
+
raise RuntimeError(f"Unsupported async framework {framework}")
|
|
173
|
+
|
|
79
174
|
@classmethod
|
|
80
175
|
async def create(cls, config: BaseClientConfiguration) -> Self:
|
|
81
176
|
"""Creates a Glide client.
|
|
82
177
|
|
|
83
178
|
Args:
|
|
84
|
-
config (ClientConfiguration): The client
|
|
85
|
-
|
|
179
|
+
config (ClientConfiguration): The configuration options for the client, including cluster addresses,
|
|
180
|
+
authentication credentials, TLS settings, periodic checks, and Pub/Sub subscriptions.
|
|
86
181
|
|
|
87
182
|
Returns:
|
|
88
|
-
Self: a
|
|
183
|
+
Self: A promise that resolves to a connected client instance.
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
# Connecting to a Standalone Server
|
|
187
|
+
>>> from glide import GlideClientConfiguration, NodeAddress, GlideClient, ServerCredentials, BackoffStrategy
|
|
188
|
+
>>> config = GlideClientConfiguration(
|
|
189
|
+
... [
|
|
190
|
+
... NodeAddress('primary.example.com', 6379),
|
|
191
|
+
... NodeAddress('replica1.example.com', 6379),
|
|
192
|
+
... ],
|
|
193
|
+
... use_tls = True,
|
|
194
|
+
... database_id = 1,
|
|
195
|
+
... credentials = ServerCredentials(username = 'user1', password = 'passwordA'),
|
|
196
|
+
... reconnect_strategy = BackoffStrategy(num_of_retries = 5, factor = 1000, exponent_base = 2),
|
|
197
|
+
... pubsub_subscriptions = GlideClientConfiguration.PubSubSubscriptions(
|
|
198
|
+
... channels_and_patterns = {GlideClientConfiguration.PubSubChannelModes.Exact: {'updates'}},
|
|
199
|
+
... callback = lambda message,context : print(message),
|
|
200
|
+
... ),
|
|
201
|
+
... )
|
|
202
|
+
>>> client = await GlideClient.create(config)
|
|
203
|
+
|
|
204
|
+
# Connecting to a Cluster
|
|
205
|
+
>>> from glide import GlideClusterClientConfiguration, NodeAddress, GlideClusterClient,
|
|
206
|
+
... PeriodicChecksManualInterval
|
|
207
|
+
>>> config = GlideClusterClientConfiguration(
|
|
208
|
+
... [
|
|
209
|
+
... NodeAddress('address1.example.com', 6379),
|
|
210
|
+
... NodeAddress('address2.example.com', 6379),
|
|
211
|
+
... ],
|
|
212
|
+
... use_tls = True,
|
|
213
|
+
... periodic_checks = PeriodicChecksManualInterval(duration_in_sec = 30),
|
|
214
|
+
... credentials = ServerCredentials(username = 'user1', password = 'passwordA'),
|
|
215
|
+
... reconnect_strategy = BackoffStrategy(num_of_retries = 5, factor = 1000, exponent_base = 2),
|
|
216
|
+
... pubsub_subscriptions = GlideClusterClientConfiguration.PubSubSubscriptions(
|
|
217
|
+
... channels_and_patterns = {
|
|
218
|
+
... GlideClusterClientConfiguration.PubSubChannelModes.Exact: {'updates'},
|
|
219
|
+
... GlideClusterClientConfiguration.PubSubChannelModes.Sharded: {'sharded_channel'},
|
|
220
|
+
... },
|
|
221
|
+
... callback = lambda message,context : print(message),
|
|
222
|
+
... ),
|
|
223
|
+
... )
|
|
224
|
+
>>> client = await GlideClusterClient.create(config)
|
|
225
|
+
|
|
226
|
+
Remarks:
|
|
227
|
+
Use this static method to create and connect a client to a Valkey server.
|
|
228
|
+
The client will automatically handle connection establishment, including cluster topology discovery and
|
|
229
|
+
handling of authentication and TLS configurations.
|
|
230
|
+
|
|
231
|
+
- **Cluster Topology Discovery**: The client will automatically discover the cluster topology based
|
|
232
|
+
on the seed addresses provided.
|
|
233
|
+
- **Authentication**: If `ServerCredentials` are provided, the client will attempt to authenticate
|
|
234
|
+
using the specified username and password.
|
|
235
|
+
- **TLS**: If `use_tls` is set to `true`, the client will establish secure connections using TLS.
|
|
236
|
+
- **Periodic Checks**: The `periodic_checks` setting allows you to configure how often the client
|
|
237
|
+
checks for cluster topology changes.
|
|
238
|
+
- **Reconnection Strategy**: The `BackoffStrategy` settings define how the client will attempt to
|
|
239
|
+
reconnect in case of disconnections.
|
|
240
|
+
- **Pub/Sub Subscriptions**: Any channels or patterns specified in `PubSubSubscriptions` will be
|
|
241
|
+
subscribed to upon connection.
|
|
242
|
+
|
|
89
243
|
"""
|
|
90
244
|
config = config
|
|
91
245
|
self = cls(config)
|
|
92
|
-
|
|
93
|
-
|
|
246
|
+
|
|
247
|
+
init_event: threading.Event = threading.Event()
|
|
94
248
|
|
|
95
249
|
def init_callback(socket_path: Optional[str], err: Optional[str]):
|
|
96
250
|
if err is not None:
|
|
@@ -102,7 +256,7 @@ class BaseClient(CoreCommands):
|
|
|
102
256
|
else:
|
|
103
257
|
# Received socket path
|
|
104
258
|
self.socket_path = socket_path
|
|
105
|
-
|
|
259
|
+
init_event.set()
|
|
106
260
|
|
|
107
261
|
start_socket_listener_external(init_callback=init_callback)
|
|
108
262
|
|
|
@@ -110,36 +264,27 @@ class BaseClient(CoreCommands):
|
|
|
110
264
|
# level or higher
|
|
111
265
|
ClientLogger.log(LogLevel.INFO, "connection info", "new connection established")
|
|
112
266
|
# Wait for the socket listener to complete its initialization
|
|
113
|
-
await
|
|
267
|
+
await to_thread.run_sync(init_event.wait)
|
|
114
268
|
# Create UDS connection
|
|
115
269
|
await self._create_uds_connection()
|
|
270
|
+
|
|
116
271
|
# Start the reader loop as a background task
|
|
117
|
-
self._reader_task =
|
|
272
|
+
self._reader_task = self._create_task(self._reader_loop)
|
|
273
|
+
|
|
118
274
|
# Set the client configurations
|
|
119
275
|
await self._set_connection_configurations()
|
|
276
|
+
|
|
120
277
|
return self
|
|
121
278
|
|
|
122
279
|
async def _create_uds_connection(self) -> None:
|
|
123
280
|
try:
|
|
124
281
|
# Open an UDS connection
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
path=self.socket_path
|
|
282
|
+
with anyio.fail_after(DEFAULT_TIMEOUT_IN_MILLISECONDS):
|
|
283
|
+
self._stream = await anyio.connect_unix(
|
|
284
|
+
path=cast(str, self.socket_path)
|
|
128
285
|
)
|
|
129
|
-
self._reader = reader
|
|
130
|
-
self._writer = writer
|
|
131
286
|
except Exception as e:
|
|
132
|
-
|
|
133
|
-
raise
|
|
134
|
-
|
|
135
|
-
def __del__(self) -> None:
|
|
136
|
-
try:
|
|
137
|
-
if self._reader_task:
|
|
138
|
-
self._reader_task.cancel()
|
|
139
|
-
except RuntimeError as e:
|
|
140
|
-
if "no running event loop" in str(e):
|
|
141
|
-
# event loop already closed
|
|
142
|
-
pass
|
|
287
|
+
raise ClosingError("Failed to create UDS connection") from e
|
|
143
288
|
|
|
144
289
|
async def close(self, err_message: Optional[str] = None) -> None:
|
|
145
290
|
"""
|
|
@@ -147,28 +292,28 @@ class BaseClient(CoreCommands):
|
|
|
147
292
|
All open futures will be closed with an exception.
|
|
148
293
|
|
|
149
294
|
Args:
|
|
150
|
-
err_message (Optional[str]): If not None, this error message will be passed along with the exceptions when
|
|
295
|
+
err_message (Optional[str]): If not None, this error message will be passed along with the exceptions when
|
|
296
|
+
closing all open futures.
|
|
151
297
|
Defaults to None.
|
|
152
298
|
"""
|
|
153
|
-
self._is_closed
|
|
154
|
-
|
|
155
|
-
if
|
|
156
|
-
|
|
157
|
-
response_future.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
pubsub_future.
|
|
163
|
-
|
|
164
|
-
|
|
299
|
+
if not self._is_closed:
|
|
300
|
+
self._is_closed = True
|
|
301
|
+
err_message = "" if err_message is None else err_message
|
|
302
|
+
for response_future in self._available_futures.values():
|
|
303
|
+
if not response_future.done():
|
|
304
|
+
response_future.set_exception(ClosingError(err_message))
|
|
305
|
+
try:
|
|
306
|
+
self._pubsub_lock.acquire()
|
|
307
|
+
for pubsub_future in self._pubsub_futures:
|
|
308
|
+
if not pubsub_future.done():
|
|
309
|
+
pubsub_future.set_exception(ClosingError(err_message))
|
|
310
|
+
finally:
|
|
311
|
+
self._pubsub_lock.release()
|
|
165
312
|
|
|
166
|
-
|
|
167
|
-
await self._writer.wait_closed()
|
|
168
|
-
self.__del__()
|
|
313
|
+
await self._stream.aclose()
|
|
169
314
|
|
|
170
|
-
def _get_future(self, callback_idx: int) ->
|
|
171
|
-
response_future:
|
|
315
|
+
def _get_future(self, callback_idx: int) -> "TFuture":
|
|
316
|
+
response_future: "TFuture" = _get_new_future_instance()
|
|
172
317
|
self._available_futures.update({callback_idx: response_future})
|
|
173
318
|
return response_future
|
|
174
319
|
|
|
@@ -177,14 +322,15 @@ class BaseClient(CoreCommands):
|
|
|
177
322
|
|
|
178
323
|
async def _set_connection_configurations(self) -> None:
|
|
179
324
|
conn_request = self._get_protobuf_conn_request()
|
|
180
|
-
response_future:
|
|
181
|
-
|
|
325
|
+
response_future: "TFuture" = self._get_future(0)
|
|
326
|
+
self._create_write_task(conn_request)
|
|
182
327
|
await response_future
|
|
183
|
-
|
|
184
|
-
|
|
328
|
+
res = response_future.result()
|
|
329
|
+
if res is not OK:
|
|
330
|
+
raise ClosingError(res)
|
|
185
331
|
|
|
186
332
|
def _create_write_task(self, request: TRequest):
|
|
187
|
-
|
|
333
|
+
self._create_task(self._write_or_buffer_request, request)
|
|
188
334
|
|
|
189
335
|
async def _write_or_buffer_request(self, request: TRequest):
|
|
190
336
|
self._buffered_requests.append(request)
|
|
@@ -192,7 +338,21 @@ class BaseClient(CoreCommands):
|
|
|
192
338
|
try:
|
|
193
339
|
while len(self._buffered_requests) > 0:
|
|
194
340
|
await self._write_buffered_requests_to_socket()
|
|
195
|
-
|
|
341
|
+
except Exception as e:
|
|
342
|
+
# trio system tasks cannot raise exceptions, so gracefully propagate
|
|
343
|
+
# any error to the pending future instead
|
|
344
|
+
callback_idx = (
|
|
345
|
+
request.callback_idx if isinstance(request, CommandRequest) else 0
|
|
346
|
+
)
|
|
347
|
+
res_future = self._available_futures.pop(callback_idx, None)
|
|
348
|
+
if res_future:
|
|
349
|
+
res_future.set_exception(e)
|
|
350
|
+
else:
|
|
351
|
+
ClientLogger.log(
|
|
352
|
+
LogLevel.WARN,
|
|
353
|
+
"unhandled response error",
|
|
354
|
+
f"Unhandled response error for unknown request: {callback_idx}",
|
|
355
|
+
)
|
|
196
356
|
finally:
|
|
197
357
|
self._writer_lock.release()
|
|
198
358
|
|
|
@@ -202,8 +362,7 @@ class BaseClient(CoreCommands):
|
|
|
202
362
|
b_arr = bytearray()
|
|
203
363
|
for request in requests:
|
|
204
364
|
ProtobufCodec.encode_delimited(b_arr, request)
|
|
205
|
-
self.
|
|
206
|
-
await self._writer.drain()
|
|
365
|
+
await self._stream.send(b_arr)
|
|
207
366
|
|
|
208
367
|
def _encode_arg(self, arg: TEncodable) -> bytes:
|
|
209
368
|
"""
|
|
@@ -254,6 +413,13 @@ class BaseClient(CoreCommands):
|
|
|
254
413
|
raise ClosingError(
|
|
255
414
|
"Unable to execute requests; the client is closed. Please create a new client."
|
|
256
415
|
)
|
|
416
|
+
|
|
417
|
+
# Create span if OpenTelemetry is configured and sampling indicates we should trace
|
|
418
|
+
span = None
|
|
419
|
+
if OpenTelemetry.should_sample():
|
|
420
|
+
command_name = RequestType.Name(request_type)
|
|
421
|
+
span = create_otel_span(command_name)
|
|
422
|
+
|
|
257
423
|
request = CommandRequest()
|
|
258
424
|
request.callback_idx = self._get_callback_index()
|
|
259
425
|
request.single_command.request_type = request_type
|
|
@@ -268,21 +434,39 @@ class BaseClient(CoreCommands):
|
|
|
268
434
|
request.single_command.args_vec_pointer = create_leaked_bytes_vec(
|
|
269
435
|
encoded_args
|
|
270
436
|
)
|
|
437
|
+
|
|
438
|
+
# Add span pointer to request if span was created
|
|
439
|
+
if span:
|
|
440
|
+
request.root_span_ptr = span
|
|
441
|
+
|
|
271
442
|
set_protobuf_route(request, route)
|
|
272
443
|
return await self._write_request_await_response(request)
|
|
273
444
|
|
|
274
|
-
async def
|
|
445
|
+
async def _execute_batch(
|
|
275
446
|
self,
|
|
276
447
|
commands: List[Tuple[RequestType.ValueType, List[TEncodable]]],
|
|
448
|
+
is_atomic: bool,
|
|
449
|
+
raise_on_error: bool = False,
|
|
450
|
+
retry_server_error: bool = False,
|
|
451
|
+
retry_connection_error: bool = False,
|
|
277
452
|
route: Optional[Route] = None,
|
|
453
|
+
timeout: Optional[int] = None,
|
|
278
454
|
) -> List[TResult]:
|
|
279
455
|
if self._is_closed:
|
|
280
456
|
raise ClosingError(
|
|
281
457
|
"Unable to execute requests; the client is closed. Please create a new client."
|
|
282
458
|
)
|
|
459
|
+
|
|
460
|
+
# Create span if OpenTelemetry is configured and sampling indicates we should trace
|
|
461
|
+
span = None
|
|
462
|
+
|
|
463
|
+
if OpenTelemetry.should_sample():
|
|
464
|
+
# Use "Batch" as span name for batches
|
|
465
|
+
span = create_otel_span("Batch")
|
|
466
|
+
|
|
283
467
|
request = CommandRequest()
|
|
284
468
|
request.callback_idx = self._get_callback_index()
|
|
285
|
-
|
|
469
|
+
batch_commands = []
|
|
286
470
|
for requst_type, args in commands:
|
|
287
471
|
command = Command()
|
|
288
472
|
command.request_type = requst_type
|
|
@@ -293,8 +477,19 @@ class BaseClient(CoreCommands):
|
|
|
293
477
|
command.args_array.args[:] = encoded_args
|
|
294
478
|
else:
|
|
295
479
|
command.args_vec_pointer = create_leaked_bytes_vec(encoded_args)
|
|
296
|
-
|
|
297
|
-
request.
|
|
480
|
+
batch_commands.append(command)
|
|
481
|
+
request.batch.commands.extend(batch_commands)
|
|
482
|
+
request.batch.is_atomic = is_atomic
|
|
483
|
+
request.batch.raise_on_error = raise_on_error
|
|
484
|
+
if timeout is not None:
|
|
485
|
+
request.batch.timeout = timeout
|
|
486
|
+
request.batch.retry_server_error = retry_server_error
|
|
487
|
+
request.batch.retry_connection_error = retry_connection_error
|
|
488
|
+
|
|
489
|
+
# Add span pointer to request if span was created
|
|
490
|
+
if span:
|
|
491
|
+
request.root_span_ptr = span
|
|
492
|
+
|
|
298
493
|
set_protobuf_route(request, route)
|
|
299
494
|
return await self._write_request_await_response(request)
|
|
300
495
|
|
|
@@ -346,14 +541,15 @@ class BaseClient(CoreCommands):
|
|
|
346
541
|
)
|
|
347
542
|
|
|
348
543
|
# locking might not be required
|
|
349
|
-
response_future:
|
|
544
|
+
response_future: "TFuture" = _get_new_future_instance()
|
|
350
545
|
try:
|
|
351
546
|
self._pubsub_lock.acquire()
|
|
352
547
|
self._pubsub_futures.append(response_future)
|
|
353
548
|
self._complete_pubsub_futures_safe()
|
|
354
549
|
finally:
|
|
355
550
|
self._pubsub_lock.release()
|
|
356
|
-
|
|
551
|
+
await response_future
|
|
552
|
+
return response_future.result()
|
|
357
553
|
|
|
358
554
|
def try_get_pubsub_message(self) -> Optional[CoreCommands.PubSubMsg]:
|
|
359
555
|
if self._is_closed:
|
|
@@ -386,8 +582,7 @@ class BaseClient(CoreCommands):
|
|
|
386
582
|
def _cancel_pubsub_futures_with_exception_safe(self, exception: ConnectionError):
|
|
387
583
|
while len(self._pubsub_futures):
|
|
388
584
|
next_future = self._pubsub_futures.pop(0)
|
|
389
|
-
|
|
390
|
-
next_future.set_exception(exception)
|
|
585
|
+
next_future.set_exception(exception)
|
|
391
586
|
|
|
392
587
|
def _notification_to_pubsub_message_safe(
|
|
393
588
|
self, response: Response
|
|
@@ -467,10 +662,16 @@ class BaseClient(CoreCommands):
|
|
|
467
662
|
if response.HasField("closing_error")
|
|
468
663
|
else f"Client Error - closing due to unknown error. callback index: {response.callback_idx}"
|
|
469
664
|
)
|
|
665
|
+
exc = ClosingError(err_msg)
|
|
470
666
|
if res_future is not None:
|
|
471
|
-
res_future.set_exception(
|
|
472
|
-
|
|
473
|
-
|
|
667
|
+
res_future.set_exception(exc)
|
|
668
|
+
else:
|
|
669
|
+
ClientLogger.log(
|
|
670
|
+
LogLevel.WARN,
|
|
671
|
+
"unhandled response error",
|
|
672
|
+
f"Unhandled response error for unknown request: {response.callback_idx}",
|
|
673
|
+
)
|
|
674
|
+
raise exc
|
|
474
675
|
else:
|
|
475
676
|
self._available_callback_indexes.append(response.callback_idx)
|
|
476
677
|
if response.HasField("request_error"):
|
|
@@ -483,6 +684,10 @@ class BaseClient(CoreCommands):
|
|
|
483
684
|
else:
|
|
484
685
|
res_future.set_result(None)
|
|
485
686
|
|
|
687
|
+
# Clean up span if it was created
|
|
688
|
+
if response.HasField("root_span_ptr"):
|
|
689
|
+
drop_otel_span(response.root_span_ptr)
|
|
690
|
+
|
|
486
691
|
async def _process_push(self, response: Response) -> None:
|
|
487
692
|
if response.HasField("closing_error") or not response.HasField("resp_pointer"):
|
|
488
693
|
err_msg = (
|
|
@@ -490,9 +695,7 @@ class BaseClient(CoreCommands):
|
|
|
490
695
|
if response.HasField("closing_error")
|
|
491
696
|
else "Client Error - push notification without resp_pointer"
|
|
492
697
|
)
|
|
493
|
-
await self.close(err_msg)
|
|
494
698
|
raise ClosingError(err_msg)
|
|
495
|
-
|
|
496
699
|
try:
|
|
497
700
|
self._pubsub_lock.acquire()
|
|
498
701
|
callback, context = self.config._get_pubsub_callback_and_context()
|
|
@@ -508,30 +711,36 @@ class BaseClient(CoreCommands):
|
|
|
508
711
|
|
|
509
712
|
async def _reader_loop(self) -> None:
|
|
510
713
|
# Socket reader loop
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if len(read_bytes) == 0:
|
|
515
|
-
err_msg = "The communication layer was unexpectedly closed."
|
|
516
|
-
await self.close(err_msg)
|
|
517
|
-
raise ClosingError(err_msg)
|
|
518
|
-
read_bytes = remaining_read_bytes + bytearray(read_bytes)
|
|
519
|
-
read_bytes_view = memoryview(read_bytes)
|
|
520
|
-
offset = 0
|
|
521
|
-
while offset <= len(read_bytes):
|
|
714
|
+
try:
|
|
715
|
+
remaining_read_bytes = bytearray()
|
|
716
|
+
while True:
|
|
522
717
|
try:
|
|
523
|
-
|
|
524
|
-
|
|
718
|
+
read_bytes = await self._stream.receive(DEFAULT_READ_BYTES_SIZE)
|
|
719
|
+
except (anyio.ClosedResourceError, anyio.EndOfStream):
|
|
720
|
+
raise ClosingError(
|
|
721
|
+
"The communication layer was unexpectedly closed."
|
|
525
722
|
)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
723
|
+
read_bytes = remaining_read_bytes + bytearray(read_bytes)
|
|
724
|
+
read_bytes_view = memoryview(read_bytes)
|
|
725
|
+
offset = 0
|
|
726
|
+
while offset <= len(read_bytes):
|
|
727
|
+
try:
|
|
728
|
+
response, offset = ProtobufCodec.decode_delimited(
|
|
729
|
+
read_bytes, read_bytes_view, offset, Response
|
|
730
|
+
)
|
|
731
|
+
except PartialMessageException:
|
|
732
|
+
# Received only partial response, break the inner loop
|
|
733
|
+
remaining_read_bytes = read_bytes[offset:]
|
|
734
|
+
break
|
|
735
|
+
response = cast(Response, response)
|
|
736
|
+
if response.is_push:
|
|
737
|
+
await self._process_push(response=response)
|
|
738
|
+
else:
|
|
739
|
+
await self._process_response(response=response)
|
|
740
|
+
except Exception as e:
|
|
741
|
+
# close and stop reading at terminal exceptions from incoming responses or
|
|
742
|
+
# stream closures
|
|
743
|
+
await self.close(str(e))
|
|
535
744
|
|
|
536
745
|
async def get_statistics(self) -> dict:
|
|
537
746
|
return get_statistics()
|
|
@@ -556,8 +765,9 @@ class BaseClient(CoreCommands):
|
|
|
556
765
|
class GlideClusterClient(BaseClient, ClusterCommands):
|
|
557
766
|
"""
|
|
558
767
|
Client used for connection to cluster servers.
|
|
768
|
+
Use :func:`~BaseClient.create` to request a client.
|
|
559
769
|
For full documentation, see
|
|
560
|
-
https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#cluster
|
|
770
|
+
[Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#cluster)
|
|
561
771
|
"""
|
|
562
772
|
|
|
563
773
|
async def _cluster_scan(
|
|
@@ -596,8 +806,9 @@ class GlideClusterClient(BaseClient, ClusterCommands):
|
|
|
596
806
|
class GlideClient(BaseClient, StandaloneCommands):
|
|
597
807
|
"""
|
|
598
808
|
Client used for connection to standalone servers.
|
|
809
|
+
Use :func:`~BaseClient.create` to request a client.
|
|
599
810
|
For full documentation, see
|
|
600
|
-
https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#standalone
|
|
811
|
+
[Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#standalone)
|
|
601
812
|
"""
|
|
602
813
|
|
|
603
814
|
|