yellowstone-fumarole-client 0.1.0rc2__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.
- yellowstone_fumarole_client/__init__.py +297 -0
- yellowstone_fumarole_client/config.py +26 -0
- yellowstone_fumarole_client/grpc_connectivity.py +197 -0
- yellowstone_fumarole_client/runtime/__init__.py +0 -0
- yellowstone_fumarole_client/runtime/aio.py +525 -0
- yellowstone_fumarole_client/runtime/state_machine.py +324 -0
- yellowstone_fumarole_client/utils/__init__.py +0 -0
- yellowstone_fumarole_client/utils/aio.py +30 -0
- yellowstone_fumarole_client-0.1.0rc2.dist-info/METADATA +115 -0
- yellowstone_fumarole_client-0.1.0rc2.dist-info/RECORD +21 -0
- yellowstone_fumarole_client-0.1.0rc2.dist-info/WHEEL +4 -0
- yellowstone_fumarole_proto/__init__.py +0 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2.py +122 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2.pyi +328 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2_grpc.py +400 -0
- yellowstone_fumarole_proto/geyser_pb2.py +144 -0
- yellowstone_fumarole_proto/geyser_pb2.pyi +501 -0
- yellowstone_fumarole_proto/geyser_pb2_grpc.py +355 -0
- yellowstone_fumarole_proto/solana_storage_pb2.py +75 -0
- yellowstone_fumarole_proto/solana_storage_pb2.pyi +238 -0
- yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +24 -0
@@ -0,0 +1,525 @@
|
|
1
|
+
# DataPlaneConn
|
2
|
+
from abc import abstractmethod, ABC
|
3
|
+
import asyncio
|
4
|
+
import uuid
|
5
|
+
import grpc
|
6
|
+
from typing import Optional, List
|
7
|
+
from collections import abc, deque
|
8
|
+
from dataclasses import dataclass
|
9
|
+
import time
|
10
|
+
from yellowstone_fumarole_client.runtime.state_machine import (
|
11
|
+
FumaroleSM,
|
12
|
+
FumeDownloadRequest,
|
13
|
+
FumeOffset,
|
14
|
+
FumeShardIdx,
|
15
|
+
)
|
16
|
+
from yellowstone_fumarole_proto.geyser_pb2 import (
|
17
|
+
SubscribeRequest,
|
18
|
+
SubscribeUpdate,
|
19
|
+
SubscribeUpdateSlot,
|
20
|
+
CommitmentLevel as ProtoCommitmentLevel,
|
21
|
+
)
|
22
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2 import (
|
23
|
+
ControlCommand,
|
24
|
+
PollBlockchainHistory,
|
25
|
+
CommitOffset,
|
26
|
+
ControlResponse,
|
27
|
+
DownloadBlockShard,
|
28
|
+
BlockFilters,
|
29
|
+
)
|
30
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2_grpc import (
|
31
|
+
Fumarole as GrpcFumaroleClient,
|
32
|
+
)
|
33
|
+
from yellowstone_fumarole_client.utils.aio import Interval
|
34
|
+
from yellowstone_fumarole_client.grpc_connectivity import FumaroleGrpcConnector
|
35
|
+
import logging
|
36
|
+
|
37
|
+
|
38
|
+
# Constants
|
39
|
+
DEFAULT_GC_INTERVAL = 5
|
40
|
+
|
41
|
+
DEFAULT_SLOT_MEMORY_RETENTION = 10000
|
42
|
+
|
43
|
+
|
44
|
+
# DownloadTaskResult
|
45
|
+
@dataclass
|
46
|
+
class CompletedDownloadBlockTask:
|
47
|
+
"""Represents a completed download block task."""
|
48
|
+
|
49
|
+
slot: int
|
50
|
+
block_uid: bytes
|
51
|
+
shard_idx: FumeShardIdx
|
52
|
+
total_event_downloaded: int
|
53
|
+
|
54
|
+
|
55
|
+
@dataclass
|
56
|
+
class DownloadBlockError:
|
57
|
+
"""Represents an error that occurred during the download of a block."""
|
58
|
+
|
59
|
+
kind: str # 'Disconnected', 'OutletDisconnected', 'BlockShardNotFound', 'FailedDownload', 'Fatal'
|
60
|
+
message: str
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class DownloadTaskResult:
|
65
|
+
"""Represents the result of a download task."""
|
66
|
+
|
67
|
+
kind: str # 'Ok' or 'Err'
|
68
|
+
completed: Optional[CompletedDownloadBlockTask] = None
|
69
|
+
slot: Optional[int] = None
|
70
|
+
err: Optional[DownloadBlockError] = None
|
71
|
+
|
72
|
+
|
73
|
+
LOGGER = logging.getLogger(__name__)
|
74
|
+
|
75
|
+
|
76
|
+
class AsyncSlotDownloader(ABC):
|
77
|
+
"""Abstract base class for slot downloaders."""
|
78
|
+
|
79
|
+
@abstractmethod
|
80
|
+
async def run_download(
|
81
|
+
self, subscribe_request: SubscribeRequest, spec: "DownloadTaskArgs"
|
82
|
+
) -> DownloadTaskResult:
|
83
|
+
"""Run the download task for a given slot."""
|
84
|
+
pass
|
85
|
+
|
86
|
+
|
87
|
+
# TokioFumeDragonsmouthRuntime
|
88
|
+
class AsyncioFumeDragonsmouthRuntime:
|
89
|
+
"""Asynchronous runtime for Fumarole with Dragonsmouth-like stream support."""
|
90
|
+
|
91
|
+
def __init__(
|
92
|
+
self,
|
93
|
+
sm: FumaroleSM,
|
94
|
+
slot_downloader: AsyncSlotDownloader,
|
95
|
+
subscribe_request_update_q: asyncio.Queue,
|
96
|
+
subscribe_request: SubscribeRequest,
|
97
|
+
consumer_group_name: str,
|
98
|
+
control_plane_tx_q: asyncio.Queue,
|
99
|
+
control_plane_rx_q: asyncio.Queue,
|
100
|
+
dragonsmouth_outlet: asyncio.Queue,
|
101
|
+
commit_interval: float, # in seconds
|
102
|
+
gc_interval: int,
|
103
|
+
max_concurrent_download: int = 10,
|
104
|
+
):
|
105
|
+
"""Initialize the runtime with the given parameters.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
sm (FumaroleSM): The state machine managing the Fumarole state.
|
109
|
+
slot_downloader (AsyncSlotDownloader): The downloader for slots.
|
110
|
+
subscribe_request_update_q (asyncio.Queue): The queue for subscribe request updates.
|
111
|
+
subscribe_request (SubscribeRequest): The initial subscribe request.
|
112
|
+
consumer_group_name (str): The name of the consumer group.
|
113
|
+
control_plane_tx_q (asyncio.Queue): The queue for sending control commands.
|
114
|
+
control_plane_rx_q (asyncio.Queue): The queue for receiving control responses.
|
115
|
+
dragonsmouth_outlet (asyncio.Queue): The outlet for Dragonsmouth updates.
|
116
|
+
commit_interval (float): The interval for committing offsets, in seconds.
|
117
|
+
gc_interval (int): The interval for garbage collection, in seconds.
|
118
|
+
max_concurrent_download (int): The maximum number of concurrent download tasks.
|
119
|
+
"""
|
120
|
+
self.sm = sm
|
121
|
+
self.slot_downloader: AsyncSlotDownloader = slot_downloader
|
122
|
+
self.subscribe_request_update_q = subscribe_request_update_q
|
123
|
+
self.subscribe_request = subscribe_request
|
124
|
+
self.consumer_group_name = consumer_group_name
|
125
|
+
self.control_plane_tx = control_plane_tx_q
|
126
|
+
self.control_plane_rx = control_plane_rx_q
|
127
|
+
self.dragonsmouth_outlet = dragonsmouth_outlet
|
128
|
+
self.commit_interval = commit_interval
|
129
|
+
self.gc_interval = gc_interval
|
130
|
+
self.max_concurrent_download = max_concurrent_download
|
131
|
+
self.download_tasks = dict()
|
132
|
+
self.inner_runtime_channel: asyncio.Queue = asyncio.Queue()
|
133
|
+
|
134
|
+
def _build_poll_history_cmd(
|
135
|
+
self, from_offset: Optional[FumeOffset]
|
136
|
+
) -> ControlCommand:
|
137
|
+
"""Build a command to poll the blockchain history."""
|
138
|
+
return ControlCommand(poll_hist=PollBlockchainHistory(shard_id=0, limit=None))
|
139
|
+
|
140
|
+
def _build_commit_offset_cmd(self, offset: FumeOffset) -> ControlCommand:
|
141
|
+
return ControlCommand(commit_offset=CommitOffset(offset=offset, shard_id=0))
|
142
|
+
|
143
|
+
def _handle_control_response(self, control_response: ControlResponse):
|
144
|
+
"""Handle the control response received from the control plane."""
|
145
|
+
response_field = control_response.WhichOneof("response")
|
146
|
+
assert response_field is not None, "Control response is empty"
|
147
|
+
|
148
|
+
match response_field:
|
149
|
+
case "poll_hist":
|
150
|
+
poll_hist = control_response.poll_hist
|
151
|
+
LOGGER.debug(f"Received poll history {len(poll_hist.events)} events")
|
152
|
+
self.sm.queue_blockchain_event(poll_hist.events)
|
153
|
+
case "commit_offset":
|
154
|
+
commit_offset = control_response.commit_offset
|
155
|
+
LOGGER.debug(f"Received commit offset: {commit_offset}")
|
156
|
+
self.sm.update_committed_offset(commit_offset.offset)
|
157
|
+
case "pong":
|
158
|
+
LOGGER.debug("Received pong")
|
159
|
+
case _:
|
160
|
+
raise ValueError("Unexpected control response")
|
161
|
+
|
162
|
+
async def poll_history_if_needed(self):
|
163
|
+
"""Poll the history if the state machine needs new events."""
|
164
|
+
if self.sm.need_new_blockchain_events():
|
165
|
+
cmd = self._build_poll_history_cmd(self.sm.committable_offset)
|
166
|
+
await self.control_plane_tx.put(cmd)
|
167
|
+
|
168
|
+
def commitment_level(self):
|
169
|
+
"""Gets the commitment level from the subscribe request."""
|
170
|
+
return self.subscribe_request.commitment
|
171
|
+
|
172
|
+
def _schedule_download_task_if_any(self):
|
173
|
+
"""Schedules download tasks if there are any available slots."""
|
174
|
+
while True:
|
175
|
+
LOGGER.debug("Checking for download tasks to schedule")
|
176
|
+
if len(self.download_tasks) >= self.max_concurrent_download:
|
177
|
+
break
|
178
|
+
|
179
|
+
# Pop a slot to download from the state machine
|
180
|
+
LOGGER.debug("Popping slot to download")
|
181
|
+
download_request = self.sm.pop_slot_to_download(self.commitment_level())
|
182
|
+
if not download_request:
|
183
|
+
LOGGER.debug("No download request available")
|
184
|
+
break
|
185
|
+
|
186
|
+
LOGGER.debug(f"Download request for slot {download_request.slot} popped")
|
187
|
+
assert (
|
188
|
+
download_request.blockchain_id
|
189
|
+
), "Download request must have a blockchain ID"
|
190
|
+
download_task_args = DownloadTaskArgs(
|
191
|
+
download_request=download_request,
|
192
|
+
dragonsmouth_outlet=self.dragonsmouth_outlet,
|
193
|
+
)
|
194
|
+
|
195
|
+
coro = self.slot_downloader.run_download(
|
196
|
+
self.subscribe_request, download_task_args
|
197
|
+
)
|
198
|
+
donwload_task = asyncio.create_task(coro)
|
199
|
+
self.download_tasks[donwload_task] = download_request
|
200
|
+
LOGGER.debug(f"Scheduling download task for slot {download_request.slot}")
|
201
|
+
|
202
|
+
def _handle_download_result(self, download_result: DownloadTaskResult):
|
203
|
+
"""Handles the result of a download task."""
|
204
|
+
if download_result.kind == "Ok":
|
205
|
+
completed = download_result.completed
|
206
|
+
LOGGER.debug(
|
207
|
+
f"Download completed for slot {completed.slot}, shard {completed.shard_idx}, {completed.total_event_downloaded} total events"
|
208
|
+
)
|
209
|
+
self.sm.make_slot_download_progress(completed.slot, completed.shard_idx)
|
210
|
+
else:
|
211
|
+
slot = download_result.slot
|
212
|
+
err = download_result.err
|
213
|
+
raise RuntimeError(f"Failed to download slot {slot}: {err.message}")
|
214
|
+
|
215
|
+
async def _force_commit_offset(self):
|
216
|
+
LOGGER.debug(f"Force committing offset {self.sm.committable_offset}")
|
217
|
+
await self.control_plane_tx.put(
|
218
|
+
self._build_commit_offset_cmd(self.sm.committable_offset)
|
219
|
+
)
|
220
|
+
|
221
|
+
async def _commit_offset(self):
|
222
|
+
if self.sm.last_committed_offset < self.sm.committable_offset:
|
223
|
+
LOGGER.debug(f"Committing offset {self.sm.committable_offset}")
|
224
|
+
await self._force_commit_offset()
|
225
|
+
self.last_commit = time.time()
|
226
|
+
|
227
|
+
async def _drain_slot_status(self):
|
228
|
+
"""Drains the slot status from the state machine and sends updates to the Dragonsmouth outlet."""
|
229
|
+
commitment = self.subscribe_request.commitment
|
230
|
+
slot_status_vec = deque()
|
231
|
+
while slot_status := self.sm.pop_next_slot_status():
|
232
|
+
slot_status_vec.append(slot_status)
|
233
|
+
|
234
|
+
if not slot_status_vec:
|
235
|
+
return
|
236
|
+
|
237
|
+
LOGGER.debug(f"Draining {len(slot_status_vec)} slot status")
|
238
|
+
for slot_status in slot_status_vec:
|
239
|
+
matched_filters = []
|
240
|
+
for filter_name, filter in self.subscribe_request.slots.items():
|
241
|
+
if (
|
242
|
+
filter.filter_by_commitment
|
243
|
+
and slot_status.commitment_level == commitment
|
244
|
+
):
|
245
|
+
matched_filters.append(filter_name)
|
246
|
+
elif not filter.filter_by_commitment:
|
247
|
+
matched_filters.append(filter_name)
|
248
|
+
|
249
|
+
if matched_filters:
|
250
|
+
update = SubscribeUpdate(
|
251
|
+
filters=matched_filters,
|
252
|
+
created_at=None,
|
253
|
+
slot=SubscribeUpdateSlot(
|
254
|
+
slot=slot_status.slot,
|
255
|
+
parent=slot_status.parent_slot,
|
256
|
+
status=slot_status.commitment_level,
|
257
|
+
dead_error=slot_status.dead_error,
|
258
|
+
),
|
259
|
+
)
|
260
|
+
try:
|
261
|
+
await self.dragonsmouth_outlet.put(update)
|
262
|
+
except asyncio.QueueFull:
|
263
|
+
return
|
264
|
+
|
265
|
+
self.sm.mark_event_as_processed(slot_status.session_sequence)
|
266
|
+
|
267
|
+
async def _handle_control_plane_resp(
|
268
|
+
self, result: ControlResponse | Exception
|
269
|
+
) -> bool:
|
270
|
+
"""Handles the control plane response."""
|
271
|
+
if isinstance(result, Exception):
|
272
|
+
await self.dragonsmouth_outlet.put(result)
|
273
|
+
return False
|
274
|
+
self._handle_control_response(result)
|
275
|
+
return True
|
276
|
+
|
277
|
+
def handle_new_subscribe_request(self, subscribe_request: SubscribeRequest):
|
278
|
+
self.subscribe_request = subscribe_request
|
279
|
+
|
280
|
+
async def run(self):
|
281
|
+
"""Runs the Fumarole asyncio runtime."""
|
282
|
+
LOGGER.debug(f"Fumarole runtime starting...")
|
283
|
+
await self.control_plane_tx.put(self._build_poll_history_cmd(None))
|
284
|
+
LOGGER.debug("Initial poll history command sent")
|
285
|
+
await self._force_commit_offset()
|
286
|
+
LOGGER.debug("Initial commit offset command sent")
|
287
|
+
ticks = 0
|
288
|
+
|
289
|
+
task_map = {
|
290
|
+
asyncio.create_task(
|
291
|
+
self.subscribe_request_update_q.get()
|
292
|
+
): "dragonsmouth_bidi",
|
293
|
+
asyncio.create_task(self.control_plane_rx.get()): "control_plane_rx",
|
294
|
+
asyncio.create_task(Interval(self.commit_interval).tick()): "commit_tick",
|
295
|
+
}
|
296
|
+
|
297
|
+
pending = set(task_map.keys())
|
298
|
+
while pending:
|
299
|
+
ticks += 1
|
300
|
+
LOGGER.debug(f"Runtime loop tick")
|
301
|
+
if ticks % self.gc_interval == 0:
|
302
|
+
LOGGER.debug("Running garbage collection")
|
303
|
+
self.sm.gc()
|
304
|
+
ticks = 0
|
305
|
+
LOGGER.debug(f"Polling history if needed")
|
306
|
+
await self.poll_history_if_needed()
|
307
|
+
LOGGER.debug("Scheduling download tasks if any")
|
308
|
+
self._schedule_download_task_if_any()
|
309
|
+
for t in self.download_tasks.keys():
|
310
|
+
pending.add(t)
|
311
|
+
task_map[t] = "download_task"
|
312
|
+
|
313
|
+
download_task_inflight = len(self.download_tasks)
|
314
|
+
LOGGER.debug(
|
315
|
+
f"Current download tasks in flight: {download_task_inflight} / {self.max_concurrent_download}"
|
316
|
+
)
|
317
|
+
done, pending = await asyncio.wait(
|
318
|
+
pending, return_when=asyncio.FIRST_COMPLETED
|
319
|
+
)
|
320
|
+
for t in done:
|
321
|
+
result = t.result()
|
322
|
+
name = task_map.pop(t)
|
323
|
+
match name:
|
324
|
+
case "dragonsmouth_bidi":
|
325
|
+
LOGGER.debug("Dragonsmouth subscribe request received")
|
326
|
+
assert isinstance(
|
327
|
+
result, SubscribeRequest
|
328
|
+
), "Expected SubscribeRequest"
|
329
|
+
self.handle_new_subscribe_request(result)
|
330
|
+
new_task = asyncio.create_task(
|
331
|
+
self.subscribe_request_update_q.get()
|
332
|
+
)
|
333
|
+
task_map[new_task] = "dragonsmouth_bidi"
|
334
|
+
pending.add(new_task)
|
335
|
+
pass
|
336
|
+
case "control_plane_rx":
|
337
|
+
LOGGER.debug("Control plane response received")
|
338
|
+
if not await self._handle_control_plane_resp(result):
|
339
|
+
LOGGER.debug("Control plane error")
|
340
|
+
return
|
341
|
+
new_task = asyncio.create_task(self.control_plane_rx.get())
|
342
|
+
task_map[new_task] = "control_plane_rx"
|
343
|
+
pending.add(new_task)
|
344
|
+
case "download_task":
|
345
|
+
LOGGER.debug("Download task result received")
|
346
|
+
assert self.download_tasks.pop(t)
|
347
|
+
self._handle_download_result(result)
|
348
|
+
case "commit_tick":
|
349
|
+
LOGGER.debug("Commit tick reached")
|
350
|
+
await self._commit_offset()
|
351
|
+
new_task = asyncio.create_task(
|
352
|
+
Interval(self.commit_interval).tick()
|
353
|
+
)
|
354
|
+
task_map[new_task] = "commit_tick"
|
355
|
+
pending.add(new_task)
|
356
|
+
case unknown:
|
357
|
+
raise RuntimeError(f"Unexpected task name: {unknown}")
|
358
|
+
|
359
|
+
await self._drain_slot_status()
|
360
|
+
|
361
|
+
LOGGER.debug("Fumarole runtime exiting")
|
362
|
+
|
363
|
+
|
364
|
+
# DownloadTaskRunnerChannels
|
365
|
+
@dataclass
|
366
|
+
class DownloadTaskRunnerChannels:
|
367
|
+
download_task_queue_tx: asyncio.Queue
|
368
|
+
cnc_tx: asyncio.Queue
|
369
|
+
download_result_rx: asyncio.Queue
|
370
|
+
|
371
|
+
|
372
|
+
# DownloadTaskRunnerCommand
|
373
|
+
@dataclass
|
374
|
+
class DownloadTaskRunnerCommand:
|
375
|
+
kind: str
|
376
|
+
subscribe_request: Optional[SubscribeRequest] = None
|
377
|
+
|
378
|
+
@classmethod
|
379
|
+
def UpdateSubscribeRequest(cls, subscribe_request: SubscribeRequest):
|
380
|
+
return cls(kind="UpdateSubscribeRequest", subscribe_request=subscribe_request)
|
381
|
+
|
382
|
+
|
383
|
+
# DownloadTaskArgs
|
384
|
+
@dataclass
|
385
|
+
class DownloadTaskArgs:
|
386
|
+
download_request: FumeDownloadRequest
|
387
|
+
dragonsmouth_outlet: asyncio.Queue
|
388
|
+
|
389
|
+
|
390
|
+
class GrpcSlotDownloader(AsyncSlotDownloader):
|
391
|
+
|
392
|
+
def __init__(
|
393
|
+
self,
|
394
|
+
client: GrpcFumaroleClient,
|
395
|
+
):
|
396
|
+
self.client = client
|
397
|
+
|
398
|
+
async def run_download(
|
399
|
+
self, subscribe_request: SubscribeRequest, spec: DownloadTaskArgs
|
400
|
+
) -> DownloadTaskResult:
|
401
|
+
|
402
|
+
download_task = GrpcDownloadBlockTaskRun(
|
403
|
+
download_request=spec.download_request,
|
404
|
+
client=self.client,
|
405
|
+
filters=BlockFilters(
|
406
|
+
accounts=subscribe_request.accounts,
|
407
|
+
transactions=subscribe_request.transactions,
|
408
|
+
entries=subscribe_request.entry,
|
409
|
+
blocks_meta=subscribe_request.blocks_meta,
|
410
|
+
),
|
411
|
+
dragonsmouth_oulet=spec.dragonsmouth_outlet,
|
412
|
+
)
|
413
|
+
|
414
|
+
LOGGER.debug(f"Running download task for slot {spec.download_request.slot}")
|
415
|
+
return await download_task.run()
|
416
|
+
|
417
|
+
|
418
|
+
# GrpcDownloadBlockTaskRun
|
419
|
+
class GrpcDownloadBlockTaskRun:
|
420
|
+
def __init__(
|
421
|
+
self,
|
422
|
+
download_request: FumeDownloadRequest,
|
423
|
+
client: GrpcFumaroleClient,
|
424
|
+
filters: Optional[BlockFilters],
|
425
|
+
dragonsmouth_oulet: asyncio.Queue,
|
426
|
+
):
|
427
|
+
self.download_request = download_request
|
428
|
+
self.client = client
|
429
|
+
self.filters = filters
|
430
|
+
self.dragonsmouth_oulet = dragonsmouth_oulet
|
431
|
+
|
432
|
+
def map_tonic_error_code_to_download_block_error(
|
433
|
+
self, e: grpc.aio.AioRpcError
|
434
|
+
) -> DownloadBlockError:
|
435
|
+
code = e.code()
|
436
|
+
if code == grpc.StatusCode.NOT_FOUND:
|
437
|
+
return DownloadBlockError(
|
438
|
+
kind="BlockShardNotFound", message="Block shard not found"
|
439
|
+
)
|
440
|
+
elif code == grpc.StatusCode.UNAVAILABLE:
|
441
|
+
return DownloadBlockError(kind="Disconnected", message="Disconnected")
|
442
|
+
elif code in (
|
443
|
+
grpc.StatusCode.INTERNAL,
|
444
|
+
grpc.StatusCode.ABORTED,
|
445
|
+
grpc.StatusCode.DATA_LOSS,
|
446
|
+
grpc.StatusCode.RESOURCE_EXHAUSTED,
|
447
|
+
grpc.StatusCode.UNKNOWN,
|
448
|
+
grpc.StatusCode.CANCELLED,
|
449
|
+
grpc.StatusCode.DEADLINE_EXCEEDED,
|
450
|
+
):
|
451
|
+
return DownloadBlockError(kind="FailedDownload", message="Failed download")
|
452
|
+
elif code == grpc.StatusCode.INVALID_ARGUMENT:
|
453
|
+
raise ValueError("Invalid argument")
|
454
|
+
else:
|
455
|
+
return DownloadBlockError(kind="Fatal", message=f"Unknown error: {code}")
|
456
|
+
|
457
|
+
async def run(self) -> DownloadTaskResult:
|
458
|
+
request = DownloadBlockShard(
|
459
|
+
blockchain_id=self.download_request.blockchain_id,
|
460
|
+
block_uid=self.download_request.block_uid,
|
461
|
+
shard_idx=0,
|
462
|
+
blockFilters=self.filters,
|
463
|
+
)
|
464
|
+
try:
|
465
|
+
LOGGER.debug(
|
466
|
+
f"Requesting download for block {self.download_request.block_uid.hex()} at slot {self.download_request.slot}"
|
467
|
+
)
|
468
|
+
resp = self.client.DownloadBlock(request)
|
469
|
+
except grpc.aio.AioRpcError as e:
|
470
|
+
LOGGER.error(f"Download block error: {e}")
|
471
|
+
return DownloadTaskResult(
|
472
|
+
kind="Err",
|
473
|
+
slot=self.download_request.slot,
|
474
|
+
err=self.map_tonic_error_code_to_download_block_error(e),
|
475
|
+
)
|
476
|
+
|
477
|
+
total_event_downloaded = 0
|
478
|
+
try:
|
479
|
+
async for data in resp:
|
480
|
+
kind = data.WhichOneof("response")
|
481
|
+
match kind:
|
482
|
+
case "update":
|
483
|
+
update = data.update
|
484
|
+
assert update is not None, "Update is None"
|
485
|
+
total_event_downloaded += 1
|
486
|
+
try:
|
487
|
+
await self.dragonsmouth_oulet.put(update)
|
488
|
+
except asyncio.QueueShutDown:
|
489
|
+
LOGGER.error("Dragonsmouth outlet is disconnected")
|
490
|
+
return DownloadTaskResult(
|
491
|
+
kind="Err",
|
492
|
+
slot=self.download_request.slot,
|
493
|
+
err=DownloadBlockError(
|
494
|
+
kind="OutletDisconnected",
|
495
|
+
message="Outlet disconnected",
|
496
|
+
),
|
497
|
+
)
|
498
|
+
case "block_shard_download_finish":
|
499
|
+
LOGGER.debug(
|
500
|
+
f"Download finished for block {self.download_request.block_uid.hex()} at slot {self.download_request.slot}"
|
501
|
+
)
|
502
|
+
return DownloadTaskResult(
|
503
|
+
kind="Ok",
|
504
|
+
completed=CompletedDownloadBlockTask(
|
505
|
+
slot=self.download_request.slot,
|
506
|
+
block_uid=self.download_request.block_uid,
|
507
|
+
shard_idx=0,
|
508
|
+
total_event_downloaded=total_event_downloaded,
|
509
|
+
),
|
510
|
+
)
|
511
|
+
case unknown:
|
512
|
+
raise RuntimeError("Unexpected response kind: {unknown}")
|
513
|
+
except grpc.aio.AioRpcError as e:
|
514
|
+
LOGGER.error(f"Download block error: {e}")
|
515
|
+
return DownloadTaskResult(
|
516
|
+
kind="Err",
|
517
|
+
slot=self.download_request.slot,
|
518
|
+
err=self.map_tonic_error_code_to_download_block_error(e),
|
519
|
+
)
|
520
|
+
|
521
|
+
return DownloadTaskResult(
|
522
|
+
kind="Err",
|
523
|
+
slot=self.download_request.slot,
|
524
|
+
err=DownloadBlockError(kind="FailedDownload", message="Failed download"),
|
525
|
+
)
|