yellowstone-fumarole-client 0.2.2__tar.gz → 0.3.0__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.
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/PKG-INFO +1 -1
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/pyproject.toml +1 -1
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/__init__.py +27 -11
- yellowstone_fumarole_client-0.3.0/yellowstone_fumarole_client/error.py +19 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/runtime/aio.py +53 -20
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/README.md +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/config.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/grpc_connectivity.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/runtime/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/runtime/state_machine.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/utils/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/utils/aio.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_client/utils/collections.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/fumarole_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/fumarole_pb2.pyi +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/fumarole_pb2_grpc.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/geyser_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/geyser_pb2.pyi +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/geyser_pb2_grpc.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/solana_storage_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/solana_storage_pb2.pyi +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.0}/yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "yellowstone-fumarole-client"
|
3
|
-
version = "0.
|
3
|
+
version = "0.3.0"
|
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"
|
@@ -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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
t
|
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
|
-
|
241
|
-
|
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,
|
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
|
-
|
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=
|
577
|
+
err=e2,
|
545
578
|
)
|
546
579
|
|
547
580
|
return DownloadTaskResult(
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|