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.
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/PKG-INFO +1 -1
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/pyproject.toml +1 -1
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/__init__.py +27 -11
- yellowstone_fumarole_client-0.3.1/yellowstone_fumarole_client/error.py +19 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/aio.py +66 -23
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/README.md +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/config.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/grpc_connectivity.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/runtime/state_machine.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/aio.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_client/utils/collections.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/__init__.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2.pyi +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/fumarole_pb2_grpc.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2.pyi +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/geyser_pb2_grpc.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/solana_storage_pb2.py +0 -0
- {yellowstone_fumarole_client-0.2.2 → yellowstone_fumarole_client-0.3.1}/yellowstone_fumarole_proto/solana_storage_pb2.pyi +0 -0
- {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
|
[tool.poetry]
|
2
2
|
name = "yellowstone-fumarole-client"
|
3
|
-
version = "0.
|
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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
t
|
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.
|
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
|
-
|
241
|
-
|
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,
|
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
|
-
|
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=
|
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(
|
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
|
)
|
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
|