yellowstone-fumarole-client 0.2.2__tar.gz → 0.3.1__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.
Files changed (23) hide show
  1. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/PKG-INFO +1 -1
  2. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/pyproject.toml +1 -1
  3. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/__init__.py +27 -11
  4. yellowstone_fumarole_client-0.3.1/yellowstone_fumarole_client/error.py +19 -0
  5. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/aio.py +66 -23
  6. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/README.md +0 -0
  7. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/config.py +0 -0
  8. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/grpc_connectivity.py +0 -0
  9. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/__init__.py +0 -0
  10. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/state_machine.py +0 -0
  11. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/__init__.py +0 -0
  12. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/aio.py +0 -0
  13. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/collections.py +0 -0
  14. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/__init__.py +0 -0
  15. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2.py +0 -0
  16. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2.pyi +0 -0
  17. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2_grpc.py +0 -0
  18. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2.py +0 -0
  19. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2.pyi +0 -0
  20. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2_grpc.py +0 -0
  21. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/solana_storage_pb2.py +0 -0
  22. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/solana_storage_pb2.pyi +0 -0
  23. {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +0 -0
@@ -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.1
4
4
  Summary: Yellowstone Fumarole Python Client
5
5
  Home-page: https://github.com/rpcpool/yellowstone-fumarole
6
6
  Author: Louis-Vincent
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "yellowstone-fumarole-client"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  homepage = "https://github.com/rpcpool/yellowstone-fumarole"
5
5
  repository = "https://github.com/rpcpool/yellowstone-fumarole"
6
6
  description = "Yellowstone Fumarole Python Client"
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
+ from contextlib import suppress
4
+
3
5
  from yellowstone_fumarole_client.grpc_connectivity import (
4
6
  FumaroleGrpcConnector,
5
7
  )
@@ -33,6 +35,7 @@ from yellowstone_fumarole_proto.fumarole_pb2 import (
33
35
  CreateConsumerGroupResponse,
34
36
  )
35
37
  from yellowstone_fumarole_proto.fumarole_pb2_grpc import FumaroleStub
38
+ from yellowstone_fumarole_client.error import SubscribeError
36
39
  import grpc
37
40
 
38
41
  from yellowstone_fumarole_client import config
@@ -54,6 +57,7 @@ DEFAULT_COMMIT_INTERVAL = 5.0 # seconds
54
57
  DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT = 3
55
58
  DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP = 10
56
59
 
60
+ LOGGER = logging.getLogger(__name__)
57
61
  # Error classes
58
62
 
59
63
 
@@ -262,18 +266,28 @@ class FumaroleClient:
262
266
  max_concurrent_download=config.concurrent_download_limit,
263
267
  )
264
268
 
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
-
269
+ rt_task = asyncio.create_task(rt.run())
270
+ rt_task.set_name("rt_task")
271
+ control_plane_src_task.set_name("control_plane_src_task")
271
272
  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()
273
+ try:
274
+ done, pending = await asyncio.wait(
275
+ [rt_task, control_plane_src_task], return_when=asyncio.FIRST_COMPLETED
276
+ )
277
+ for t in done:
278
+ try:
279
+ exc = t.exception()
280
+ LOGGER.error(f"Fumarole task '{t.get_name()}' failed: {exc}")
281
+ except asyncio.CancelledError:
282
+ pass
283
+ if exc is not None:
284
+ with suppress(asyncio.QueueShutDown):
285
+ await dragonsmouth_outlet.put(exc)
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 isinstance(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 Any, 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
@@ -1,8 +1,11 @@
1
1
  # DataPlaneConn
2
2
  from abc import abstractmethod, ABC
3
3
  import asyncio
4
+ from contextlib import suppress
5
+ from typing import Any
6
+
4
7
  import grpc
5
- from typing import Optional
8
+ from typing import Literal, Mapping, Optional
6
9
  from collections import deque
7
10
  from dataclasses import dataclass
8
11
  import time
@@ -16,7 +19,6 @@ from yellowstone_fumarole_proto.geyser_pb2 import (
16
19
  SubscribeRequest,
17
20
  SubscribeUpdate,
18
21
  SubscribeUpdateSlot,
19
- CommitmentLevel as ProtoCommitmentLevel,
20
22
  )
21
23
  from yellowstone_fumarole_proto.fumarole_pb2 import (
22
24
  ControlCommand,
@@ -30,7 +32,7 @@ from yellowstone_fumarole_proto.fumarole_pb2_grpc import (
30
32
  FumaroleStub,
31
33
  )
32
34
  from yellowstone_fumarole_client.utils.aio import Interval
33
- from yellowstone_fumarole_client.grpc_connectivity import FumaroleGrpcConnector
35
+ from yellowstone_fumarole_client.error import SubscribeError, DownloadSlotError
34
36
  import logging
35
37
 
36
38
 
@@ -51,13 +53,28 @@ class CompletedDownloadBlockTask:
51
53
  total_event_downloaded: int
52
54
 
53
55
 
56
+ DownloadBlockErrorKind = Literal[
57
+ "Disconnected",
58
+ "OutletDisconnected",
59
+ "BlockShardNotFound",
60
+ "FailedDownload",
61
+ "Fatal",
62
+ "SessionDeprecated"
63
+ ]
64
+
54
65
  @dataclass
55
66
  class DownloadBlockError:
56
67
  """Represents an error that occurred during the download of a block."""
57
-
58
- kind: str # 'Disconnected', 'OutletDisconnected', 'BlockShardNotFound', 'FailedDownload', 'Fatal'
68
+ kind: DownloadBlockErrorKind
59
69
  message: str
70
+ ctx: Mapping[str, Any]
60
71
 
72
+ def into_subscribe_error(self) -> SubscribeError:
73
+ """Convert the error into a SubscribeError."""
74
+ return DownloadSlotError(
75
+ f"{self.kind}: {self.message}",
76
+ ctx=self.ctx
77
+ )
61
78
 
62
79
  @dataclass
63
80
  class DownloadTaskResult:
@@ -139,6 +156,8 @@ class AsyncioFumeDragonsmouthRuntime:
139
156
  # holds metadata about the download task
140
157
  self.download_tasks = dict()
141
158
  self.inflight_tasks = dict()
159
+ self.successful_download_cnt = 0
160
+ self.is_closed = False
142
161
 
143
162
  async def __aenter__(self):
144
163
  return self
@@ -227,18 +246,24 @@ class AsyncioFumeDragonsmouthRuntime:
227
246
  self.inflight_tasks[download_task] = DOWNLOAD_TASK_TYPE_MARKER
228
247
  LOGGER.debug(f"Scheduling download task for slot {download_request.slot}")
229
248
 
230
- def _handle_download_result(self, download_result: DownloadTaskResult):
249
+ async def _handle_download_result(self, download_result: DownloadTaskResult):
231
250
  """Handles the result of a download task."""
232
251
  if download_result.kind == "Ok":
252
+ self.successful_download_cnt += 1
233
253
  completed = download_result.completed
234
254
  LOGGER.debug(
235
- f"Download completed for slot {completed.slot}, shard {completed.shard_idx}, {completed.total_event_downloaded} total events"
255
+ f"Download({self.successful_download_cnt}) completed for slot {completed.slot}, shard {completed.shard_idx}, {completed.total_event_downloaded} total events"
236
256
  )
237
257
  self.sm.make_slot_download_progress(completed.slot, completed.shard_idx)
238
258
  else:
239
259
  slot = download_result.slot
240
- err = download_result.err
241
- raise RuntimeError(f"Failed to download slot {slot}: {err.message}")
260
+ err_kind = download_result.err.kind
261
+ LOGGER.error(f"Download error for slot {slot}: {download_result.err}")
262
+ # If the client queue is disconnected, we don't do anything, next run iteration will close anyway.
263
+ self.is_closed = True
264
+ if err_kind != "OutletDisconnected":
265
+ with suppress(asyncio.QueueShutDown):
266
+ await self.dragonsmouth_outlet.put(download_result.err.into_subscribe_error())
242
267
 
243
268
  async def _force_commit_offset(self):
244
269
  LOGGER.debug(f"Force committing offset {self.sm.committable_offset}")
@@ -328,7 +353,7 @@ class AsyncioFumeDragonsmouthRuntime:
328
353
  ): COMMIT_TICK_TYPE_MARKER,
329
354
  }
330
355
 
331
- while self.inflight_tasks:
356
+ while self.inflight_tasks and not self.is_closed:
332
357
  ticks += 1
333
358
  LOGGER.debug(f"Runtime loop tick")
334
359
  if ticks % self.gc_interval == 0:
@@ -370,7 +395,7 @@ class AsyncioFumeDragonsmouthRuntime:
370
395
  elif sigcode == DOWNLOAD_TASK_TYPE_MARKER:
371
396
  LOGGER.debug("Download task result received")
372
397
  assert self.download_tasks.pop(t)
373
- self._handle_download_result(result)
398
+ await self._handle_download_result(result)
374
399
  elif sigcode == COMMIT_TICK_TYPE_MARKER:
375
400
  LOGGER.debug("Commit tick reached")
376
401
  await self._commit_offset()
@@ -382,10 +407,7 @@ class AsyncioFumeDragonsmouthRuntime:
382
407
  raise RuntimeError(f"Unexpected task name: {sigcode}")
383
408
 
384
409
  await self._drain_slot_status()
385
-
386
410
  await self.aclose()
387
- LOGGER.debug("Fumarole runtime exiting")
388
-
389
411
 
390
412
  # DownloadTaskRunnerChannels
391
413
  @dataclass
@@ -456,15 +478,21 @@ class GrpcDownloadBlockTaskRun:
456
478
  self.dragonsmouth_oulet = dragonsmouth_oulet
457
479
 
458
480
  def map_tonic_error_code_to_download_block_error(
459
- self, e: grpc.aio.AioRpcError
481
+ self,
482
+ e: grpc.aio.AioRpcError
460
483
  ) -> DownloadBlockError:
461
484
  code = e.code()
485
+ ctx = {
486
+ "grpc_status_code": code,
487
+ "slot": self.download_request.slot,
488
+ "block_uid": self.download_request.block_uid,
489
+ }
462
490
  if code == grpc.StatusCode.NOT_FOUND:
463
491
  return DownloadBlockError(
464
- kind="BlockShardNotFound", message="Block shard not found"
492
+ kind="BlockShardNotFound", message="Block shard not found", ctx=ctx,
465
493
  )
466
494
  elif code == grpc.StatusCode.UNAVAILABLE:
467
- return DownloadBlockError(kind="Disconnected", message="Disconnected")
495
+ return DownloadBlockError(kind="Disconnected", message="Disconnected", ctx=ctx)
468
496
  elif code in (
469
497
  grpc.StatusCode.INTERNAL,
470
498
  grpc.StatusCode.ABORTED,
@@ -474,11 +502,13 @@ class GrpcDownloadBlockTaskRun:
474
502
  grpc.StatusCode.CANCELLED,
475
503
  grpc.StatusCode.DEADLINE_EXCEEDED,
476
504
  ):
477
- return DownloadBlockError(kind="FailedDownload", message="Failed download")
505
+ return DownloadBlockError(kind="FailedDownload", message="Failed download", ctx=ctx)
478
506
  elif code == grpc.StatusCode.INVALID_ARGUMENT:
479
507
  raise ValueError("Invalid argument")
508
+ elif code == grpc.StatusCode.FAILED_PRECONDITION:
509
+ return DownloadBlockError(kind="SessionDeprecated", message="Session is deprecated", ctx=ctx)
480
510
  else:
481
- return DownloadBlockError(kind="Fatal", message=f"Unknown error: {code}")
511
+ return DownloadBlockError(kind="Fatal", message=f"Unknown error: {code}", ctx=ctx)
482
512
 
483
513
  async def run(self) -> DownloadTaskResult:
484
514
  request = DownloadBlockShard(
@@ -519,6 +549,10 @@ class GrpcDownloadBlockTaskRun:
519
549
  err=DownloadBlockError(
520
550
  kind="OutletDisconnected",
521
551
  message="Outlet disconnected",
552
+ ctx = {
553
+ "slot": self.download_request.slot,
554
+ "block_uid": self.download_request.block_uid,
555
+ }
522
556
  ),
523
557
  )
524
558
  case "block_shard_download_finish":
@@ -535,17 +569,26 @@ class GrpcDownloadBlockTaskRun:
535
569
  ),
536
570
  )
537
571
  case unknown:
538
- raise RuntimeError("Unexpected response kind: {unknown}")
572
+ raise RuntimeError(f"Unexpected response kind: {unknown}")
573
+
539
574
  except grpc.aio.AioRpcError as e:
540
- LOGGER.error(f"Download block error: {e}")
575
+ e2 = self.map_tonic_error_code_to_download_block_error(e)
576
+ LOGGER.error(f"Download block error: {e}, {e2}")
541
577
  return DownloadTaskResult(
542
578
  kind="Err",
543
579
  slot=self.download_request.slot,
544
- err=self.map_tonic_error_code_to_download_block_error(e),
580
+ err=e2,
545
581
  )
546
582
 
547
583
  return DownloadTaskResult(
548
584
  kind="Err",
549
585
  slot=self.download_request.slot,
550
- err=DownloadBlockError(kind="FailedDownload", message="Failed download"),
586
+ err=DownloadBlockError(
587
+ kind="FailedDownload",
588
+ message="Failed download",
589
+ ctx={
590
+ "slot": self.download_request.slot,
591
+ "block_uid": self.download_request.block_uid,
592
+ },
593
+ ),
551
594
  )