yellowstone-fumarole-client 0.2.2__py3-none-any.whl → 0.3.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.
@@ -33,6 +33,7 @@ from yellowstone_fumarole_proto.fumarole_pb2 import (
33
33
  CreateConsumerGroupResponse,
34
34
  )
35
35
  from yellowstone_fumarole_proto.fumarole_pb2_grpc import FumaroleStub
36
+ from yellowstone_fumarole_client.error import SubscribeError
36
37
  import grpc
37
38
 
38
39
  from yellowstone_fumarole_client import config
@@ -54,6 +55,7 @@ DEFAULT_COMMIT_INTERVAL = 5.0 # seconds
54
55
  DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT = 3
55
56
  DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP = 10
56
57
 
58
+ LOGGER = logging.getLogger(__name__)
57
59
  # Error classes
58
60
 
59
61
 
@@ -262,18 +264,30 @@ class FumaroleClient:
262
264
  max_concurrent_download=config.concurrent_download_limit,
263
265
  )
264
266
 
265
- async def rt_run(rt):
266
- async with rt as rt:
267
- await rt.run()
268
-
269
- rt_task = asyncio.create_task(rt_run(rt))
270
-
267
+ rt_task = asyncio.create_task(rt.run())
268
+ rt_task.set_name("rt_task")
269
+ control_plane_src_task.set_name("control_plane_src_task")
271
270
  async def fumarole_overseer():
272
- done, pending = await asyncio.wait(
273
- [rt_task, control_plane_src_task], return_when=asyncio.FIRST_COMPLETED
274
- )
275
- for t in pending:
276
- t.cancel()
271
+ try:
272
+ done, pending = await asyncio.wait(
273
+ [rt_task, control_plane_src_task], return_when=asyncio.FIRST_COMPLETED
274
+ )
275
+ for t in done:
276
+ try:
277
+ exc = t.exception()
278
+ LOGGER.error(f"Fumarole task '{t.get_name()}' failed: {exc}")
279
+ except asyncio.CancelledError:
280
+ pass
281
+ if exc is not None:
282
+ try:
283
+ await dragonsmouth_outlet.put(exc)
284
+ except asyncio.QueueShutdown:
285
+ pass
286
+ for t in pending:
287
+ t.cancel()
288
+ finally:
289
+ await rt.aclose()
290
+
277
291
 
278
292
  fumarole_handle = asyncio.create_task(fumarole_overseer())
279
293
 
@@ -281,6 +295,8 @@ class FumaroleClient:
281
295
  try:
282
296
  while True:
283
297
  update = await dragonsmouth_outlet.get()
298
+ if issubclass(type(update), Exception):
299
+ raise update
284
300
  yield update
285
301
  except (asyncio.CancelledError, asyncio.QueueShutDown):
286
302
  pass
@@ -0,0 +1,19 @@
1
+ from typing import Mapping
2
+
3
+
4
+ class FumaroleClientError(Exception):
5
+ """Base class for all Fumarole-related exceptions."""
6
+ pass
7
+
8
+
9
+ class SubscribeError(FumaroleClientError):
10
+ """Base class for all errors related to subscription."""
11
+ pass
12
+
13
+
14
+ class DownloadSlotError(SubscribeError):
15
+ """Exception raised for errors in the download slot process."""
16
+
17
+ def __init__(self, message: str, ctx: Mapping[str, any]):
18
+ super().__init__(message)
19
+ self.ctx = ctx
@@ -2,7 +2,7 @@
2
2
  from abc import abstractmethod, ABC
3
3
  import asyncio
4
4
  import grpc
5
- from typing import Optional
5
+ from typing import Literal, Mapping, Optional
6
6
  from collections import deque
7
7
  from dataclasses import dataclass
8
8
  import time
@@ -31,6 +31,7 @@ from yellowstone_fumarole_proto.fumarole_pb2_grpc import (
31
31
  )
32
32
  from yellowstone_fumarole_client.utils.aio import Interval
33
33
  from yellowstone_fumarole_client.grpc_connectivity import FumaroleGrpcConnector
34
+ from yellowstone_fumarole_client.error import SubscribeError, DownloadSlotError
34
35
  import logging
35
36
 
36
37
 
@@ -51,13 +52,28 @@ class CompletedDownloadBlockTask:
51
52
  total_event_downloaded: int
52
53
 
53
54
 
55
+ DownloadBlockErrorKind = Literal[
56
+ "Disconnected",
57
+ "OutletDisconnected",
58
+ "BlockShardNotFound",
59
+ "FailedDownload",
60
+ "Fatal",
61
+ "SessionDeprecated"
62
+ ]
63
+
54
64
  @dataclass
55
65
  class DownloadBlockError:
56
66
  """Represents an error that occurred during the download of a block."""
57
-
58
- kind: str # 'Disconnected', 'OutletDisconnected', 'BlockShardNotFound', 'FailedDownload', 'Fatal'
67
+ kind: DownloadBlockErrorKind
59
68
  message: str
69
+ ctx: Mapping[str, any]
60
70
 
71
+ def into_subscribe_error(self) -> SubscribeError:
72
+ """Convert the error into a SubscribeError."""
73
+ return DownloadSlotError(
74
+ f"{self.kind}: {self.message}",
75
+ ctx=self.ctx
76
+ )
61
77
 
62
78
  @dataclass
63
79
  class DownloadTaskResult:
@@ -139,6 +155,8 @@ class AsyncioFumeDragonsmouthRuntime:
139
155
  # holds metadata about the download task
140
156
  self.download_tasks = dict()
141
157
  self.inflight_tasks = dict()
158
+ self.successful_download_cnt = 0
159
+ self.is_closed = False
142
160
 
143
161
  async def __aenter__(self):
144
162
  return self
@@ -227,18 +245,26 @@ class AsyncioFumeDragonsmouthRuntime:
227
245
  self.inflight_tasks[download_task] = DOWNLOAD_TASK_TYPE_MARKER
228
246
  LOGGER.debug(f"Scheduling download task for slot {download_request.slot}")
229
247
 
230
- def _handle_download_result(self, download_result: DownloadTaskResult):
248
+ async def _handle_download_result(self, download_result: DownloadTaskResult):
231
249
  """Handles the result of a download task."""
232
250
  if download_result.kind == "Ok":
251
+ self.successful_download_cnt += 1
233
252
  completed = download_result.completed
234
253
  LOGGER.debug(
235
- f"Download completed for slot {completed.slot}, shard {completed.shard_idx}, {completed.total_event_downloaded} total events"
254
+ f"Download({self.successful_download_cnt}) completed for slot {completed.slot}, shard {completed.shard_idx}, {completed.total_event_downloaded} total events"
236
255
  )
237
256
  self.sm.make_slot_download_progress(completed.slot, completed.shard_idx)
238
257
  else:
239
258
  slot = download_result.slot
240
- err = download_result.err
241
- raise RuntimeError(f"Failed to download slot {slot}: {err.message}")
259
+ err_kind = download_result.err.kind
260
+ LOGGER.error(f"Download error for slot {slot}: {download_result.err}")
261
+ # If the client queue is disconnected, we don't do anything, next run iteration will close anyway.
262
+ self.is_closed = True
263
+ if err_kind != "OutletDisconnected":
264
+ try:
265
+ await self.dragonsmouth_outlet.put(download_result.err.into_subscribe_error())
266
+ except asyncio.QueueShutDown:
267
+ pass
242
268
 
243
269
  async def _force_commit_offset(self):
244
270
  LOGGER.debug(f"Force committing offset {self.sm.committable_offset}")
@@ -328,7 +354,7 @@ class AsyncioFumeDragonsmouthRuntime:
328
354
  ): COMMIT_TICK_TYPE_MARKER,
329
355
  }
330
356
 
331
- while self.inflight_tasks:
357
+ while self.inflight_tasks and not self.is_closed:
332
358
  ticks += 1
333
359
  LOGGER.debug(f"Runtime loop tick")
334
360
  if ticks % self.gc_interval == 0:
@@ -370,7 +396,7 @@ class AsyncioFumeDragonsmouthRuntime:
370
396
  elif sigcode == DOWNLOAD_TASK_TYPE_MARKER:
371
397
  LOGGER.debug("Download task result received")
372
398
  assert self.download_tasks.pop(t)
373
- self._handle_download_result(result)
399
+ await self._handle_download_result(result)
374
400
  elif sigcode == COMMIT_TICK_TYPE_MARKER:
375
401
  LOGGER.debug("Commit tick reached")
376
402
  await self._commit_offset()
@@ -382,10 +408,7 @@ class AsyncioFumeDragonsmouthRuntime:
382
408
  raise RuntimeError(f"Unexpected task name: {sigcode}")
383
409
 
384
410
  await self._drain_slot_status()
385
-
386
411
  await self.aclose()
387
- LOGGER.debug("Fumarole runtime exiting")
388
-
389
412
 
390
413
  # DownloadTaskRunnerChannels
391
414
  @dataclass
@@ -456,15 +479,21 @@ class GrpcDownloadBlockTaskRun:
456
479
  self.dragonsmouth_oulet = dragonsmouth_oulet
457
480
 
458
481
  def map_tonic_error_code_to_download_block_error(
459
- self, e: grpc.aio.AioRpcError
482
+ self,
483
+ e: grpc.aio.AioRpcError
460
484
  ) -> DownloadBlockError:
461
485
  code = e.code()
486
+ ctx = {
487
+ "grpc_status_code": code,
488
+ "slot": self.download_request.slot,
489
+ "block_uid": self.download_request.block_uid,
490
+ }
462
491
  if code == grpc.StatusCode.NOT_FOUND:
463
492
  return DownloadBlockError(
464
- kind="BlockShardNotFound", message="Block shard not found"
493
+ kind="BlockShardNotFound", message="Block shard not found", ctx=ctx,
465
494
  )
466
495
  elif code == grpc.StatusCode.UNAVAILABLE:
467
- return DownloadBlockError(kind="Disconnected", message="Disconnected")
496
+ return DownloadBlockError(kind="Disconnected", message="Disconnected", ctx=ctx)
468
497
  elif code in (
469
498
  grpc.StatusCode.INTERNAL,
470
499
  grpc.StatusCode.ABORTED,
@@ -474,11 +503,13 @@ class GrpcDownloadBlockTaskRun:
474
503
  grpc.StatusCode.CANCELLED,
475
504
  grpc.StatusCode.DEADLINE_EXCEEDED,
476
505
  ):
477
- return DownloadBlockError(kind="FailedDownload", message="Failed download")
506
+ return DownloadBlockError(kind="FailedDownload", message="Failed download", ctx=ctx)
478
507
  elif code == grpc.StatusCode.INVALID_ARGUMENT:
479
508
  raise ValueError("Invalid argument")
509
+ elif code == grpc.StatusCode.FAILED_PRECONDITION:
510
+ return DownloadBlockError(kind="SessionDeprecated", message="Session is deprecated", ctx=ctx)
480
511
  else:
481
- return DownloadBlockError(kind="Fatal", message=f"Unknown error: {code}")
512
+ return DownloadBlockError(kind="Fatal", message=f"Unknown error: {code}", ctx=ctx)
482
513
 
483
514
  async def run(self) -> DownloadTaskResult:
484
515
  request = DownloadBlockShard(
@@ -535,13 +566,15 @@ class GrpcDownloadBlockTaskRun:
535
566
  ),
536
567
  )
537
568
  case unknown:
538
- raise RuntimeError("Unexpected response kind: {unknown}")
569
+ raise RuntimeError(f"Unexpected response kind: {unknown}")
570
+
539
571
  except grpc.aio.AioRpcError as e:
540
- LOGGER.error(f"Download block error: {e}")
572
+ e2 = self.map_tonic_error_code_to_download_block_error(e)
573
+ LOGGER.error(f"Download block error: {e}, {e2}")
541
574
  return DownloadTaskResult(
542
575
  kind="Err",
543
576
  slot=self.download_request.slot,
544
- err=self.map_tonic_error_code_to_download_block_error(e),
577
+ err=e2,
545
578
  )
546
579
 
547
580
  return DownloadTaskResult(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: yellowstone-fumarole-client
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Yellowstone Fumarole Python Client
5
5
  Home-page: https://github.com/rpcpool/yellowstone-fumarole
6
6
  Author: Louis-Vincent
@@ -1,8 +1,9 @@
1
- yellowstone_fumarole_client/__init__.py,sha256=QIfks8mTmyOywbGLG4ZMXrp4LDBNq5s_SV_1P0PKH6E,13351
1
+ yellowstone_fumarole_client/__init__.py,sha256=Ngm4D8PMKKifrGyjg82XRfdrqOXrjtxLH6sg0YoW6Eg,14128
2
2
  yellowstone_fumarole_client/config.py,sha256=aclhCPUy6RO-xtXR9w8otmt1RzFZyFnbF28jk115C2g,1394
3
+ yellowstone_fumarole_client/error.py,sha256=TFoy72LyzgHRwx9J7uvurB0jsnnmxbJwJaioSlUpcds,483
3
4
  yellowstone_fumarole_client/grpc_connectivity.py,sha256=Sex_x6_Bha0wGD7rRqr-V_slsohX1tDFeiHdqahLJ4Q,6639
4
5
  yellowstone_fumarole_client/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- yellowstone_fumarole_client/runtime/aio.py,sha256=CmZZMvmZ1DmzTQAv-Vbm4DA7N4GXjHJtI_TWTW2FD5k,21758
6
+ yellowstone_fumarole_client/runtime/aio.py,sha256=DLdsTv9hwXov4irOTnESYENFwWX6_NpwT8xEj3yZMW4,23143
6
7
  yellowstone_fumarole_client/runtime/state_machine.py,sha256=eH2ARUOBdBJqk1zs9WLXD1WixqtgZX4bIdDkg1cWttA,12781
7
8
  yellowstone_fumarole_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
9
  yellowstone_fumarole_client/utils/aio.py,sha256=lm_BNkPiw5CJ6FjDlQUPoCKAqY3eYFAedAJB8mhNbzE,639
@@ -17,6 +18,6 @@ yellowstone_fumarole_proto/geyser_pb2_grpc.py,sha256=JCEz0KM_jg_610HyQI_F1K4kJlR
17
18
  yellowstone_fumarole_proto/solana_storage_pb2.py,sha256=LS-P5EPyS0n1pO9_U73rA6SPlbGSTEC2qYhuS3skzA8,8443
18
19
  yellowstone_fumarole_proto/solana_storage_pb2.pyi,sha256=HivhoN4VEe_W7kB4lc2Un5AeTAv3xiKR_HLI096qmyA,13040
19
20
  yellowstone_fumarole_proto/solana_storage_pb2_grpc.py,sha256=-rb9Dr0HXohIrHrnxukYrmUYl6OCVbkQYh2-pCO6740,895
20
- yellowstone_fumarole_client-0.2.2.dist-info/METADATA,sha256=v8IJrufEwWAEZTAnllvkg3h-DuaitgWVJm9dzZNxvbI,4156
21
- yellowstone_fumarole_client-0.2.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
22
- yellowstone_fumarole_client-0.2.2.dist-info/RECORD,,
21
+ yellowstone_fumarole_client-0.3.0.dist-info/METADATA,sha256=lzwP6_lZMW8HXwBgVmNBmXJq3S48CsoOkTcfQ2xsMQ4,4156
22
+ yellowstone_fumarole_client-0.3.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
+ yellowstone_fumarole_client-0.3.0.dist-info/RECORD,,