valkey-glide 2.0.0rc3__cp310-cp310-macosx_11_0_arm64.whl → 2.2.3__cp310-cp310-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.
Files changed (45) hide show
  1. glide/__init__.py +160 -106
  2. glide/async_commands/cluster_commands.py +108 -105
  3. glide/async_commands/core.py +637 -444
  4. glide/async_commands/{server_modules/ft.py → ft.py} +8 -7
  5. glide/async_commands/{server_modules/glide_json.py → glide_json.py} +15 -92
  6. glide/async_commands/standalone_commands.py +27 -58
  7. glide/glide.cpython-310-darwin.so +0 -0
  8. glide/glide.pyi +26 -1
  9. glide/glide_client.py +269 -125
  10. glide/logger.py +33 -21
  11. glide/opentelemetry.py +185 -0
  12. glide_shared/__init__.py +330 -0
  13. glide_shared/commands/__init__.py +0 -0
  14. {glide/async_commands → glide_shared/commands}/batch.py +476 -64
  15. glide_shared/commands/batch_options.py +261 -0
  16. glide_shared/commands/core_options.py +407 -0
  17. {glide/async_commands → glide_shared/commands}/server_modules/ft_options/ft_aggregate_options.py +3 -3
  18. {glide/async_commands → glide_shared/commands}/server_modules/ft_options/ft_create_options.py +4 -2
  19. {glide/async_commands → glide_shared/commands}/server_modules/ft_options/ft_profile_options.py +4 -4
  20. {glide/async_commands → glide_shared/commands}/server_modules/ft_options/ft_search_options.py +4 -2
  21. {glide/async_commands → glide_shared/commands}/server_modules/json_batch.py +4 -4
  22. glide_shared/commands/server_modules/json_options.py +93 -0
  23. {glide/async_commands → glide_shared/commands}/sorted_set.py +2 -2
  24. {glide/async_commands → glide_shared/commands}/stream.py +1 -1
  25. {glide → glide_shared}/config.py +386 -61
  26. {glide → glide_shared}/constants.py +3 -3
  27. {glide → glide_shared}/exceptions.py +27 -1
  28. glide_shared/protobuf/command_request_pb2.py +56 -0
  29. glide_shared/protobuf/connection_request_pb2.py +56 -0
  30. {glide → glide_shared}/protobuf/response_pb2.py +6 -6
  31. {glide → glide_shared}/routes.py +54 -15
  32. valkey_glide-2.2.3.dist-info/METADATA +211 -0
  33. valkey_glide-2.2.3.dist-info/RECORD +40 -0
  34. glide/protobuf/command_request_pb2.py +0 -54
  35. glide/protobuf/command_request_pb2.pyi +0 -1187
  36. glide/protobuf/connection_request_pb2.py +0 -54
  37. glide/protobuf/connection_request_pb2.pyi +0 -320
  38. glide/protobuf/response_pb2.pyi +0 -100
  39. valkey_glide-2.0.0rc3.dist-info/METADATA +0 -127
  40. valkey_glide-2.0.0rc3.dist-info/RECORD +0 -37
  41. {glide/async_commands → glide_shared/commands}/bitmap.py +0 -0
  42. {glide/async_commands → glide_shared/commands}/command_args.py +0 -0
  43. {glide/async_commands → glide_shared/commands}/server_modules/ft_options/ft_constants.py +0 -0
  44. {glide → glide_shared}/protobuf_codec.py +0 -0
  45. {valkey_glide-2.0.0rc3.dist-info → valkey_glide-2.2.3.dist-info}/WHEEL +0 -0
glide/glide_client.py CHANGED
@@ -1,62 +1,121 @@
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 Any, Dict, List, Optional, Tuple, Type, Union, cast
7
-
8
- from glide.async_commands.cluster_commands import ClusterCommands
9
- from glide.async_commands.command_args import ObjectType
10
- from glide.async_commands.core import CoreCommands
11
- from glide.async_commands.standalone_commands import StandaloneCommands
12
- from glide.config import BaseClientConfiguration, ServerCredentials
13
- from glide.constants import DEFAULT_READ_BYTES_SIZE, OK, TEncodable, TRequest, TResult
14
- from glide.exceptions import (
15
- ClosingError,
16
- ConfigurationError,
17
- ConnectionError,
18
- ExecAbortError,
19
- RequestError,
20
- TimeoutError,
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Awaitable,
9
+ Dict,
10
+ List,
11
+ Optional,
12
+ Set,
13
+ Tuple,
14
+ Union,
15
+ cast,
21
16
  )
22
- from glide.logger import Level as LogLevel
23
- from glide.logger import Logger as ClientLogger
24
- from glide.protobuf.command_request_pb2 import Command, CommandRequest, RequestType
25
- from glide.protobuf.connection_request_pb2 import ConnectionRequest
26
- from glide.protobuf.response_pb2 import RequestErrorType, Response
27
- from glide.protobuf_codec import PartialMessageException, ProtobufCodec
28
- from glide.routes import Route, set_protobuf_route
29
-
30
- from .glide import (
17
+
18
+ import anyio
19
+ import sniffio
20
+ from anyio import to_thread
21
+ from glide.glide import (
31
22
  DEFAULT_TIMEOUT_IN_MILLISECONDS,
32
23
  MAX_REQUEST_ARGS_LEN,
33
24
  ClusterScanCursor,
34
25
  create_leaked_bytes_vec,
26
+ create_otel_span,
27
+ drop_otel_span,
35
28
  get_statistics,
36
29
  start_socket_listener_external,
37
30
  value_from_pointer,
38
31
  )
32
+ from glide_shared.commands.command_args import ObjectType
33
+ from glide_shared.commands.core_options import PubSubMsg
34
+ from glide_shared.config import BaseClientConfiguration, ServerCredentials
35
+ from glide_shared.constants import (
36
+ DEFAULT_READ_BYTES_SIZE,
37
+ OK,
38
+ TEncodable,
39
+ TRequest,
40
+ TResult,
41
+ )
42
+ from glide_shared.exceptions import (
43
+ ClosingError,
44
+ ConfigurationError,
45
+ ConnectionError,
46
+ get_request_error_class,
47
+ )
48
+ from glide_shared.protobuf.command_request_pb2 import (
49
+ Command,
50
+ CommandRequest,
51
+ RefreshIamToken,
52
+ RequestType,
53
+ )
54
+ from glide_shared.protobuf.connection_request_pb2 import ConnectionRequest
55
+ from glide_shared.protobuf.response_pb2 import Response
56
+ from glide_shared.protobuf_codec import PartialMessageException, ProtobufCodec
57
+ from glide_shared.routes import Route, set_protobuf_route
58
+
59
+ from .async_commands.cluster_commands import ClusterCommands
60
+ from .async_commands.core import CoreCommands
61
+ from .async_commands.standalone_commands import StandaloneCommands
62
+ from .logger import Level as LogLevel
63
+ from .logger import Logger as ClientLogger
64
+ from .opentelemetry import OpenTelemetry
39
65
 
40
66
  if sys.version_info >= (3, 11):
41
- import asyncio as async_timeout
42
67
  from typing import Self
43
68
  else:
44
- import async_timeout
45
69
  from typing_extensions import Self
46
70
 
71
+ if TYPE_CHECKING:
72
+ import asyncio
73
+
74
+ import trio
75
+
76
+ TTask = Union[asyncio.Task[None], trio.lowlevel.Task]
77
+ TFuture = Union[asyncio.Future[Any], "_CompatFuture"]
78
+
79
+
80
+ class _CompatFuture:
81
+ """anyio shim for asyncio.Future-like functionality"""
82
+
83
+ def __init__(self) -> None:
84
+ self._is_done = anyio.Event()
85
+ self._result: Any = None
86
+ self._exception: Optional[Exception] = None
87
+
88
+ def set_result(self, result: Any) -> None:
89
+ self._result = result
90
+ self._is_done.set()
91
+
92
+ def set_exception(self, exception: Exception) -> None:
93
+ self._exception = exception
94
+ self._is_done.set()
95
+
96
+ def done(self) -> bool:
97
+ return self._is_done.is_set()
98
+
99
+ def __await__(self):
100
+ return self._is_done.wait().__await__()
47
101
 
48
- def get_request_error_class(
49
- error_type: Optional[RequestErrorType.ValueType],
50
- ) -> Type[RequestError]:
51
- if error_type == RequestErrorType.Disconnect:
52
- return ConnectionError
53
- if error_type == RequestErrorType.ExecAbort:
54
- return ExecAbortError
55
- if error_type == RequestErrorType.Timeout:
56
- return TimeoutError
57
- if error_type == RequestErrorType.Unspecified:
58
- return RequestError
59
- return RequestError
102
+ def result(self) -> Any:
103
+ if self._exception:
104
+ raise self._exception
105
+
106
+ return self._result
107
+
108
+
109
+ def _get_new_future_instance() -> "TFuture":
110
+ if sniffio.current_async_library() == "asyncio":
111
+ import asyncio
112
+
113
+ return asyncio.get_running_loop().create_future()
114
+
115
+ # _CompatFuture is also compatible with asyncio, but is not as closely integrated
116
+ # into the asyncio event loop and thus introduces a noticeable performance
117
+ # degradation. so we only use it for trio
118
+ return _CompatFuture()
60
119
 
61
120
 
62
121
  class BaseClient(CoreCommands):
@@ -65,17 +124,47 @@ class BaseClient(CoreCommands):
65
124
  To create a new client, use the `create` classmethod
66
125
  """
67
126
  self.config: BaseClientConfiguration = config
68
- self._available_futures: Dict[int, asyncio.Future] = {}
127
+ self._available_futures: Dict[int, "TFuture"] = {}
69
128
  self._available_callback_indexes: List[int] = list()
70
129
  self._buffered_requests: List[TRequest] = list()
71
130
  self._writer_lock = threading.Lock()
72
131
  self.socket_path: Optional[str] = None
73
- self._reader_task: Optional[asyncio.Task] = None
132
+ self._reader_task: Optional["TTask"] = None
74
133
  self._is_closed: bool = False
75
- self._pubsub_futures: List[asyncio.Future] = []
134
+ self._pubsub_futures: List["TFuture"] = []
76
135
  self._pubsub_lock = threading.Lock()
77
136
  self._pending_push_notifications: List[Response] = list()
78
137
 
138
+ self._pending_tasks: Optional[Set[Awaitable[None]]] = None
139
+ """asyncio-only to avoid gc on pending write tasks"""
140
+
141
+ def _create_task(self, task, *args, **kwargs):
142
+ """framework agnostic free-floating task shim"""
143
+ framework = sniffio.current_async_library()
144
+ if framework == "trio":
145
+ from functools import partial
146
+
147
+ import trio
148
+
149
+ return trio.lowlevel.spawn_system_task(partial(task, **kwargs), *args)
150
+ elif framework == "asyncio":
151
+ import asyncio
152
+
153
+ # the asyncio event loop holds weak refs to tasks, so it's recommended to
154
+ # hold strong refs to them during their lifetime to prevent garbage
155
+ # collection
156
+ t = asyncio.create_task(task(*args, **kwargs))
157
+
158
+ if self._pending_tasks is None:
159
+ self._pending_tasks = set()
160
+
161
+ self._pending_tasks.add(t)
162
+ t.add_done_callback(self._pending_tasks.discard)
163
+
164
+ return t
165
+
166
+ raise RuntimeError(f"Unsupported async framework {framework}")
167
+
79
168
  @classmethod
80
169
  async def create(cls, config: BaseClientConfiguration) -> Self:
81
170
  """Creates a Glide client.
@@ -148,8 +237,8 @@ class BaseClient(CoreCommands):
148
237
  """
149
238
  config = config
150
239
  self = cls(config)
151
- init_future: asyncio.Future = asyncio.Future()
152
- loop = asyncio.get_event_loop()
240
+
241
+ init_event: threading.Event = threading.Event()
153
242
 
154
243
  def init_callback(socket_path: Optional[str], err: Optional[str]):
155
244
  if err is not None:
@@ -161,7 +250,7 @@ class BaseClient(CoreCommands):
161
250
  else:
162
251
  # Received socket path
163
252
  self.socket_path = socket_path
164
- loop.call_soon_threadsafe(init_future.set_result, True)
253
+ init_event.set()
165
254
 
166
255
  start_socket_listener_external(init_callback=init_callback)
167
256
 
@@ -169,36 +258,27 @@ class BaseClient(CoreCommands):
169
258
  # level or higher
170
259
  ClientLogger.log(LogLevel.INFO, "connection info", "new connection established")
171
260
  # Wait for the socket listener to complete its initialization
172
- await init_future
261
+ await to_thread.run_sync(init_event.wait)
173
262
  # Create UDS connection
174
263
  await self._create_uds_connection()
264
+
175
265
  # Start the reader loop as a background task
176
- self._reader_task = asyncio.create_task(self._reader_loop())
266
+ self._reader_task = self._create_task(self._reader_loop)
267
+
177
268
  # Set the client configurations
178
269
  await self._set_connection_configurations()
270
+
179
271
  return self
180
272
 
181
273
  async def _create_uds_connection(self) -> None:
182
274
  try:
183
275
  # Open an UDS connection
184
- async with async_timeout.timeout(DEFAULT_TIMEOUT_IN_MILLISECONDS):
185
- reader, writer = await asyncio.open_unix_connection(
186
- path=self.socket_path
276
+ with anyio.fail_after(DEFAULT_TIMEOUT_IN_MILLISECONDS):
277
+ self._stream = await anyio.connect_unix(
278
+ path=cast(str, self.socket_path)
187
279
  )
188
- self._reader = reader
189
- self._writer = writer
190
280
  except Exception as e:
191
- await self.close(f"Failed to create UDS connection: {e}")
192
- raise
193
-
194
- def __del__(self) -> None:
195
- try:
196
- if self._reader_task:
197
- self._reader_task.cancel()
198
- except RuntimeError as e:
199
- if "no running event loop" in str(e):
200
- # event loop already closed
201
- pass
281
+ raise ClosingError("Failed to create UDS connection") from e
202
282
 
203
283
  async def close(self, err_message: Optional[str] = None) -> None:
204
284
  """
@@ -210,25 +290,24 @@ class BaseClient(CoreCommands):
210
290
  closing all open futures.
211
291
  Defaults to None.
212
292
  """
213
- self._is_closed = True
214
- for response_future in self._available_futures.values():
215
- if not response_future.done():
216
- err_message = "" if err_message is None else err_message
217
- response_future.set_exception(ClosingError(err_message))
218
- try:
219
- self._pubsub_lock.acquire()
220
- for pubsub_future in self._pubsub_futures:
221
- if not pubsub_future.done() and not pubsub_future.cancelled():
222
- pubsub_future.set_exception(ClosingError(""))
223
- finally:
224
- self._pubsub_lock.release()
293
+ if not self._is_closed:
294
+ self._is_closed = True
295
+ err_message = "" if err_message is None else err_message
296
+ for response_future in self._available_futures.values():
297
+ if not response_future.done():
298
+ response_future.set_exception(ClosingError(err_message))
299
+ try:
300
+ self._pubsub_lock.acquire()
301
+ for pubsub_future in self._pubsub_futures:
302
+ if not pubsub_future.done():
303
+ pubsub_future.set_exception(ClosingError(err_message))
304
+ finally:
305
+ self._pubsub_lock.release()
225
306
 
226
- self._writer.close()
227
- await self._writer.wait_closed()
228
- self.__del__()
307
+ await self._stream.aclose()
229
308
 
230
- def _get_future(self, callback_idx: int) -> asyncio.Future:
231
- response_future: asyncio.Future = asyncio.Future()
309
+ def _get_future(self, callback_idx: int) -> "TFuture":
310
+ response_future: "TFuture" = _get_new_future_instance()
232
311
  self._available_futures.update({callback_idx: response_future})
233
312
  return response_future
234
313
 
@@ -237,14 +316,15 @@ class BaseClient(CoreCommands):
237
316
 
238
317
  async def _set_connection_configurations(self) -> None:
239
318
  conn_request = self._get_protobuf_conn_request()
240
- response_future: asyncio.Future = self._get_future(0)
241
- await self._write_or_buffer_request(conn_request)
319
+ response_future: "TFuture" = self._get_future(0)
320
+ self._create_write_task(conn_request)
242
321
  await response_future
243
- if response_future.result() is not OK:
244
- raise ClosingError(response_future.result())
322
+ res = response_future.result()
323
+ if res is not OK:
324
+ raise ClosingError(res)
245
325
 
246
326
  def _create_write_task(self, request: TRequest):
247
- asyncio.create_task(self._write_or_buffer_request(request))
327
+ self._create_task(self._write_or_buffer_request, request)
248
328
 
249
329
  async def _write_or_buffer_request(self, request: TRequest):
250
330
  self._buffered_requests.append(request)
@@ -252,7 +332,21 @@ class BaseClient(CoreCommands):
252
332
  try:
253
333
  while len(self._buffered_requests) > 0:
254
334
  await self._write_buffered_requests_to_socket()
255
-
335
+ except Exception as e:
336
+ # trio system tasks cannot raise exceptions, so gracefully propagate
337
+ # any error to the pending future instead
338
+ callback_idx = (
339
+ request.callback_idx if isinstance(request, CommandRequest) else 0
340
+ )
341
+ res_future = self._available_futures.pop(callback_idx, None)
342
+ if res_future and not res_future.done():
343
+ res_future.set_exception(e)
344
+ else:
345
+ ClientLogger.log(
346
+ LogLevel.WARN,
347
+ "unhandled response error",
348
+ f"Unhandled response error for unknown request: {callback_idx}",
349
+ )
256
350
  finally:
257
351
  self._writer_lock.release()
258
352
 
@@ -262,8 +356,10 @@ class BaseClient(CoreCommands):
262
356
  b_arr = bytearray()
263
357
  for request in requests:
264
358
  ProtobufCodec.encode_delimited(b_arr, request)
265
- self._writer.write(b_arr)
266
- await self._writer.drain()
359
+ try:
360
+ await self._stream.send(b_arr)
361
+ except (anyio.ClosedResourceError, anyio.EndOfStream):
362
+ raise ClosingError("The communication layer was unexpectedly closed.")
267
363
 
268
364
  def _encode_arg(self, arg: TEncodable) -> bytes:
269
365
  """
@@ -314,6 +410,13 @@ class BaseClient(CoreCommands):
314
410
  raise ClosingError(
315
411
  "Unable to execute requests; the client is closed. Please create a new client."
316
412
  )
413
+
414
+ # Create span if OpenTelemetry is configured and sampling indicates we should trace
415
+ span = None
416
+ if OpenTelemetry.should_sample():
417
+ command_name = RequestType.Name(request_type)
418
+ span = create_otel_span(command_name)
419
+
317
420
  request = CommandRequest()
318
421
  request.callback_idx = self._get_callback_index()
319
422
  request.single_command.request_type = request_type
@@ -328,6 +431,11 @@ class BaseClient(CoreCommands):
328
431
  request.single_command.args_vec_pointer = create_leaked_bytes_vec(
329
432
  encoded_args
330
433
  )
434
+
435
+ # Add span pointer to request if span was created
436
+ if span:
437
+ request.root_span_ptr = span
438
+
331
439
  set_protobuf_route(request, route)
332
440
  return await self._write_request_await_response(request)
333
441
 
@@ -345,6 +453,14 @@ class BaseClient(CoreCommands):
345
453
  raise ClosingError(
346
454
  "Unable to execute requests; the client is closed. Please create a new client."
347
455
  )
456
+
457
+ # Create span if OpenTelemetry is configured and sampling indicates we should trace
458
+ span = None
459
+
460
+ if OpenTelemetry.should_sample():
461
+ # Use "Batch" as span name for batches
462
+ span = create_otel_span("Batch")
463
+
348
464
  request = CommandRequest()
349
465
  request.callback_idx = self._get_callback_index()
350
466
  batch_commands = []
@@ -366,6 +482,11 @@ class BaseClient(CoreCommands):
366
482
  request.batch.timeout = timeout
367
483
  request.batch.retry_server_error = retry_server_error
368
484
  request.batch.retry_connection_error = retry_connection_error
485
+
486
+ # Add span pointer to request if span was created
487
+ if span:
488
+ request.root_span_ptr = span
489
+
369
490
  set_protobuf_route(request, route)
370
491
  return await self._write_request_await_response(request)
371
492
 
@@ -400,7 +521,7 @@ class BaseClient(CoreCommands):
400
521
  set_protobuf_route(request, route)
401
522
  return await self._write_request_await_response(request)
402
523
 
403
- async def get_pubsub_message(self) -> CoreCommands.PubSubMsg:
524
+ async def get_pubsub_message(self) -> PubSubMsg:
404
525
  if self._is_closed:
405
526
  raise ClosingError(
406
527
  "Unable to execute requests; the client is closed. Please create a new client."
@@ -417,16 +538,17 @@ class BaseClient(CoreCommands):
417
538
  )
418
539
 
419
540
  # locking might not be required
420
- response_future: asyncio.Future = asyncio.Future()
541
+ response_future: "TFuture" = _get_new_future_instance()
421
542
  try:
422
543
  self._pubsub_lock.acquire()
423
544
  self._pubsub_futures.append(response_future)
424
545
  self._complete_pubsub_futures_safe()
425
546
  finally:
426
547
  self._pubsub_lock.release()
427
- return await response_future
548
+ await response_future
549
+ return response_future.result()
428
550
 
429
- def try_get_pubsub_message(self) -> Optional[CoreCommands.PubSubMsg]:
551
+ def try_get_pubsub_message(self) -> Optional[PubSubMsg]:
430
552
  if self._is_closed:
431
553
  raise ClosingError(
432
554
  "Unable to execute requests; the client is closed. Please create a new client."
@@ -443,7 +565,7 @@ class BaseClient(CoreCommands):
443
565
  )
444
566
 
445
567
  # locking might not be required
446
- msg: Optional[CoreCommands.PubSubMsg] = None
568
+ msg: Optional[PubSubMsg] = None
447
569
  try:
448
570
  self._pubsub_lock.acquire()
449
571
  self._complete_pubsub_futures_safe()
@@ -457,12 +579,11 @@ class BaseClient(CoreCommands):
457
579
  def _cancel_pubsub_futures_with_exception_safe(self, exception: ConnectionError):
458
580
  while len(self._pubsub_futures):
459
581
  next_future = self._pubsub_futures.pop(0)
460
- if not next_future.cancelled():
461
- next_future.set_exception(exception)
582
+ next_future.set_exception(exception)
462
583
 
463
584
  def _notification_to_pubsub_message_safe(
464
585
  self, response: Response
465
- ) -> Optional[CoreCommands.PubSubMsg]:
586
+ ) -> Optional[PubSubMsg]:
466
587
  pubsub_message = None
467
588
  push_notification = cast(
468
589
  Dict[str, Any], value_from_pointer(response.resp_pointer)
@@ -481,11 +602,11 @@ class BaseClient(CoreCommands):
481
602
  ):
482
603
  values: List = push_notification["values"]
483
604
  if message_kind == "PMessage":
484
- pubsub_message = BaseClient.PubSubMsg(
605
+ pubsub_message = PubSubMsg(
485
606
  message=values[2], channel=values[1], pattern=values[0]
486
607
  )
487
608
  else:
488
- pubsub_message = BaseClient.PubSubMsg(
609
+ pubsub_message = PubSubMsg(
489
610
  message=values[1], channel=values[0], pattern=None
490
611
  )
491
612
  elif (
@@ -538,10 +659,16 @@ class BaseClient(CoreCommands):
538
659
  if response.HasField("closing_error")
539
660
  else f"Client Error - closing due to unknown error. callback index: {response.callback_idx}"
540
661
  )
662
+ exc = ClosingError(err_msg)
541
663
  if res_future is not None:
542
- res_future.set_exception(ClosingError(err_msg))
543
- await self.close(err_msg)
544
- raise ClosingError(err_msg)
664
+ res_future.set_exception(exc)
665
+ else:
666
+ ClientLogger.log(
667
+ LogLevel.WARN,
668
+ "unhandled response error",
669
+ f"Unhandled response error for unknown request: {response.callback_idx}",
670
+ )
671
+ raise exc
545
672
  else:
546
673
  self._available_callback_indexes.append(response.callback_idx)
547
674
  if response.HasField("request_error"):
@@ -554,6 +681,10 @@ class BaseClient(CoreCommands):
554
681
  else:
555
682
  res_future.set_result(None)
556
683
 
684
+ # Clean up span if it was created
685
+ if response.HasField("root_span_ptr"):
686
+ drop_otel_span(response.root_span_ptr)
687
+
557
688
  async def _process_push(self, response: Response) -> None:
558
689
  if response.HasField("closing_error") or not response.HasField("resp_pointer"):
559
690
  err_msg = (
@@ -561,9 +692,7 @@ class BaseClient(CoreCommands):
561
692
  if response.HasField("closing_error")
562
693
  else "Client Error - push notification without resp_pointer"
563
694
  )
564
- await self.close(err_msg)
565
695
  raise ClosingError(err_msg)
566
-
567
696
  try:
568
697
  self._pubsub_lock.acquire()
569
698
  callback, context = self.config._get_pubsub_callback_and_context()
@@ -579,30 +708,36 @@ class BaseClient(CoreCommands):
579
708
 
580
709
  async def _reader_loop(self) -> None:
581
710
  # Socket reader loop
582
- remaining_read_bytes = bytearray()
583
- while True:
584
- read_bytes = await self._reader.read(DEFAULT_READ_BYTES_SIZE)
585
- if len(read_bytes) == 0:
586
- err_msg = "The communication layer was unexpectedly closed."
587
- await self.close(err_msg)
588
- raise ClosingError(err_msg)
589
- read_bytes = remaining_read_bytes + bytearray(read_bytes)
590
- read_bytes_view = memoryview(read_bytes)
591
- offset = 0
592
- while offset <= len(read_bytes):
711
+ try:
712
+ remaining_read_bytes = bytearray()
713
+ while True:
593
714
  try:
594
- response, offset = ProtobufCodec.decode_delimited(
595
- read_bytes, read_bytes_view, offset, Response
715
+ read_bytes = await self._stream.receive(DEFAULT_READ_BYTES_SIZE)
716
+ except (anyio.ClosedResourceError, anyio.EndOfStream):
717
+ raise ClosingError(
718
+ "The communication layer was unexpectedly closed."
596
719
  )
597
- except PartialMessageException:
598
- # Received only partial response, break the inner loop
599
- remaining_read_bytes = read_bytes[offset:]
600
- break
601
- response = cast(Response, response)
602
- if response.is_push:
603
- await self._process_push(response=response)
604
- else:
605
- await self._process_response(response=response)
720
+ read_bytes = remaining_read_bytes + bytearray(read_bytes)
721
+ read_bytes_view = memoryview(read_bytes)
722
+ offset = 0
723
+ while offset <= len(read_bytes):
724
+ try:
725
+ response, offset = ProtobufCodec.decode_delimited(
726
+ read_bytes, read_bytes_view, offset, Response
727
+ )
728
+ except PartialMessageException:
729
+ # Received only partial response, break the inner loop
730
+ remaining_read_bytes = read_bytes[offset:]
731
+ break
732
+ response = cast(Response, response)
733
+ if response.is_push:
734
+ await self._process_push(response=response)
735
+ else:
736
+ await self._process_response(response=response)
737
+ except Exception as e:
738
+ # close and stop reading at terminal exceptions from incoming responses or
739
+ # stream closures
740
+ await self.close(str(e))
606
741
 
607
742
  async def get_statistics(self) -> dict:
608
743
  return get_statistics()
@@ -623,6 +758,15 @@ class BaseClient(CoreCommands):
623
758
  self.config.credentials.password = password or ""
624
759
  return response
625
760
 
761
+ async def _refresh_iam_token(self) -> TResult:
762
+ request = CommandRequest()
763
+ request.callback_idx = self._get_callback_index()
764
+ request.refresh_iam_token.CopyFrom(
765
+ RefreshIamToken()
766
+ ) # Empty message, just triggers the refresh
767
+ response = await self._write_request_await_response(request)
768
+ return response
769
+
626
770
 
627
771
  class GlideClusterClient(BaseClient, ClusterCommands):
628
772
  """