yellowstone-fumarole-client 0.1.0__py3-none-any.whl → 0.2.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.
- yellowstone_fumarole_client/__init__.py +83 -14
- yellowstone_fumarole_client/config.py +17 -1
- yellowstone_fumarole_client/grpc_connectivity.py +11 -28
- yellowstone_fumarole_client/runtime/aio.py +83 -67
- yellowstone_fumarole_client/runtime/state_machine.py +2 -3
- yellowstone_fumarole_client/utils/aio.py +0 -1
- {yellowstone_fumarole_client-0.1.0.dist-info → yellowstone_fumarole_client-0.2.0.dist-info}/METADATA +35 -25
- yellowstone_fumarole_client-0.2.0.dist-info/RECORD +22 -0
- yellowstone_fumarole_proto/fumarole_pb2.py +134 -0
- yellowstone_fumarole_proto/{fumarole_v2_pb2.pyi → fumarole_pb2.pyi} +51 -5
- yellowstone_fumarole_proto/{fumarole_v2_pb2_grpc.py → fumarole_pb2_grpc.py} +155 -69
- yellowstone_fumarole_proto/geyser_pb2.py +39 -35
- yellowstone_fumarole_proto/geyser_pb2.pyi +15 -2
- yellowstone_fumarole_proto/geyser_pb2_grpc.py +44 -1
- yellowstone_fumarole_proto/solana_storage_pb2.py +32 -32
- yellowstone_fumarole_proto/solana_storage_pb2.pyi +6 -3
- yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +1 -1
- yellowstone_fumarole_client-0.1.0.dist-info/RECORD +0 -22
- yellowstone_fumarole_proto/fumarole_v2_pb2.py +0 -122
- {yellowstone_fumarole_client-0.1.0.dist-info → yellowstone_fumarole_client-0.2.0.dist-info}/WHEEL +0 -0
@@ -3,18 +3,21 @@ import logging
|
|
3
3
|
from yellowstone_fumarole_client.grpc_connectivity import (
|
4
4
|
FumaroleGrpcConnector,
|
5
5
|
)
|
6
|
-
from typing import
|
6
|
+
from typing import AsyncGenerator, Optional
|
7
7
|
from dataclasses import dataclass
|
8
8
|
from yellowstone_fumarole_client.config import FumaroleConfig
|
9
9
|
from yellowstone_fumarole_client.runtime.aio import (
|
10
10
|
AsyncioFumeDragonsmouthRuntime,
|
11
|
-
FumaroleSM,
|
12
11
|
DEFAULT_GC_INTERVAL,
|
13
12
|
DEFAULT_SLOT_MEMORY_RETENTION,
|
14
13
|
GrpcSlotDownloader,
|
15
14
|
)
|
15
|
+
from yellowstone_fumarole_client.runtime.state_machine import (
|
16
|
+
FumaroleSM,
|
17
|
+
FumeOffset,
|
18
|
+
)
|
16
19
|
from yellowstone_fumarole_proto.geyser_pb2 import SubscribeRequest, SubscribeUpdate
|
17
|
-
from yellowstone_fumarole_proto.
|
20
|
+
from yellowstone_fumarole_proto.fumarole_pb2 import (
|
18
21
|
ControlResponse,
|
19
22
|
VersionRequest,
|
20
23
|
VersionResponse,
|
@@ -29,9 +32,11 @@ from yellowstone_fumarole_proto.fumarole_v2_pb2 import (
|
|
29
32
|
CreateConsumerGroupRequest,
|
30
33
|
CreateConsumerGroupResponse,
|
31
34
|
)
|
32
|
-
from yellowstone_fumarole_proto.
|
35
|
+
from yellowstone_fumarole_proto.fumarole_pb2_grpc import FumaroleStub
|
33
36
|
import grpc
|
34
37
|
|
38
|
+
from yellowstone_fumarole_client import config
|
39
|
+
|
35
40
|
__all__ = [
|
36
41
|
"FumaroleClient",
|
37
42
|
"FumaroleConfig",
|
@@ -44,7 +49,7 @@ __all__ = [
|
|
44
49
|
]
|
45
50
|
|
46
51
|
# Constants
|
47
|
-
DEFAULT_DRAGONSMOUTH_CAPACITY =
|
52
|
+
DEFAULT_DRAGONSMOUTH_CAPACITY = 100000
|
48
53
|
DEFAULT_COMMIT_INTERVAL = 5.0 # seconds
|
49
54
|
DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT = 3
|
50
55
|
DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP = 10
|
@@ -72,10 +77,23 @@ class FumaroleSubscribeConfig:
|
|
72
77
|
# The interval at which to perform garbage collection on the slot memory.
|
73
78
|
gc_interval: int = DEFAULT_GC_INTERVAL
|
74
79
|
|
75
|
-
#
|
80
|
+
# How many processed slot numbers to retain in memory to avoid duplication.
|
76
81
|
slot_memory_retention: int = DEFAULT_SLOT_MEMORY_RETENTION
|
77
82
|
|
78
83
|
|
84
|
+
@dataclass
|
85
|
+
class FumaroleSubscribeStats:
|
86
|
+
"""Commit/slot statistics for the Fumarole subscribe session."""
|
87
|
+
|
88
|
+
# Last committed log offset in Fumarole -- this is a low-level, implementation detail.
|
89
|
+
# NOTE: this should not be part as business logic, can change any time.
|
90
|
+
log_committed_offset: FumeOffset
|
91
|
+
# NOTE:: this is a low-level information, can change any time.
|
92
|
+
log_committable_offset: FumeOffset
|
93
|
+
# Max slot seen by the in the current session - does not mean it has been processed.
|
94
|
+
max_slot_seen: int
|
95
|
+
|
96
|
+
|
79
97
|
# DragonsmouthAdapterSession
|
80
98
|
@dataclass
|
81
99
|
class DragonsmouthAdapterSession:
|
@@ -85,10 +103,31 @@ class DragonsmouthAdapterSession:
|
|
85
103
|
sink: asyncio.Queue
|
86
104
|
|
87
105
|
# The queue for receiving SubscribeUpdate from the dragonsmouth stream.
|
88
|
-
source:
|
106
|
+
source: AsyncGenerator[SubscribeUpdate, None]
|
89
107
|
|
90
108
|
# The task handle for the fumarole runtime.
|
91
|
-
|
109
|
+
_fumarole_handle: asyncio.Task
|
110
|
+
|
111
|
+
_sm: FumaroleSM
|
112
|
+
|
113
|
+
async def __aenter__(self):
|
114
|
+
"""Enter the session context."""
|
115
|
+
return self
|
116
|
+
|
117
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
118
|
+
self.sink.shutdown()
|
119
|
+
self._fumarole_handle.cancel()
|
120
|
+
|
121
|
+
def stats(self) -> FumaroleSubscribeStats:
|
122
|
+
"""Get low-level statistics of the Fumarole state-machine."""
|
123
|
+
commitable = self._sm.committable_offset
|
124
|
+
committed = self._sm.last_committed_offset
|
125
|
+
max_slot = self._sm.max_slot_detected
|
126
|
+
return FumaroleSubscribeStats(
|
127
|
+
log_committed_offset=committed,
|
128
|
+
log_committable_offset=commitable,
|
129
|
+
max_slot_seen=max_slot,
|
130
|
+
)
|
92
131
|
|
93
132
|
|
94
133
|
# FumaroleClient
|
@@ -162,7 +201,7 @@ class FumaroleClient:
|
|
162
201
|
try:
|
163
202
|
update = await fume_control_plane_q.get()
|
164
203
|
yield update
|
165
|
-
except asyncio.QueueShutDown:
|
204
|
+
except (asyncio.CancelledError, asyncio.QueueShutDown):
|
166
205
|
break
|
167
206
|
|
168
207
|
fume_control_plane_stream_rx: grpc.aio.StreamStreamCall = self.stub.Subscribe(
|
@@ -187,8 +226,12 @@ class FumaroleClient:
|
|
187
226
|
await fume_control_plane_rx_q.put(update)
|
188
227
|
except asyncio.QueueShutDown:
|
189
228
|
break
|
229
|
+
except asyncio.CancelledError:
|
230
|
+
break
|
231
|
+
finally:
|
232
|
+
fume_control_plane_rx_q.shutdown()
|
190
233
|
|
191
|
-
|
234
|
+
control_plane_src_task = asyncio.create_task(control_plane_source())
|
192
235
|
|
193
236
|
FumaroleClient.logger.debug(f"Control response: {control_response}")
|
194
237
|
|
@@ -219,12 +262,38 @@ class FumaroleClient:
|
|
219
262
|
max_concurrent_download=config.concurrent_download_limit,
|
220
263
|
)
|
221
264
|
|
222
|
-
|
223
|
-
|
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
|
+
|
271
|
+
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()
|
277
|
+
|
278
|
+
fumarole_handle = asyncio.create_task(fumarole_overseer())
|
279
|
+
|
280
|
+
async def source_gen() -> AsyncGenerator[SubscribeUpdate, None]:
|
281
|
+
try:
|
282
|
+
while True:
|
283
|
+
update = await dragonsmouth_outlet.get()
|
284
|
+
yield update
|
285
|
+
except asyncio.CancelledError:
|
286
|
+
pass
|
287
|
+
except asyncio.Queue:
|
288
|
+
pass
|
289
|
+
finally:
|
290
|
+
dragonsmouth_outlet.shutdown()
|
291
|
+
|
224
292
|
return DragonsmouthAdapterSession(
|
225
293
|
sink=subscribe_request_queue,
|
226
|
-
source=
|
227
|
-
|
294
|
+
source=source_gen(),
|
295
|
+
_fumarole_handle=fumarole_handle,
|
296
|
+
_sm=sm,
|
228
297
|
)
|
229
298
|
|
230
299
|
async def list_consumer_groups(
|
@@ -1,14 +1,19 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from typing import Dict, Optional
|
2
|
+
from typing import Dict, Literal, Optional
|
3
3
|
import yaml
|
4
4
|
|
5
5
|
|
6
|
+
SUPPORTED_COMPRESSION = ["gzip"]
|
7
|
+
SupportedCompression = Literal["gzip"]
|
8
|
+
|
9
|
+
|
6
10
|
@dataclass
|
7
11
|
class FumaroleConfig:
|
8
12
|
endpoint: str
|
9
13
|
x_token: Optional[str] = None
|
10
14
|
max_decoding_message_size_bytes: int = 512_000_000
|
11
15
|
x_metadata: Dict[str, str] = None
|
16
|
+
response_compression: Optional[SupportedCompression] = None
|
12
17
|
|
13
18
|
def __post_init__(self):
|
14
19
|
self.x_metadata = self.x_metadata or {}
|
@@ -16,6 +21,14 @@ class FumaroleConfig:
|
|
16
21
|
@classmethod
|
17
22
|
def from_yaml(cls, fileobj) -> "FumaroleConfig":
|
18
23
|
data = yaml.safe_load(fileobj)
|
24
|
+
response_compression = data.get(
|
25
|
+
"response_compression", cls.response_compression
|
26
|
+
)
|
27
|
+
if (
|
28
|
+
response_compression is not None
|
29
|
+
and response_compression not in SUPPORTED_COMPRESSION
|
30
|
+
):
|
31
|
+
raise ValueError(f"response_compression must be in {SUPPORTED_COMPRESSION}")
|
19
32
|
return cls(
|
20
33
|
endpoint=data["endpoint"],
|
21
34
|
x_token=data.get("x-token") or data.get("x_token"),
|
@@ -23,4 +36,7 @@ class FumaroleConfig:
|
|
23
36
|
"max_decoding_message_size_bytes", cls.max_decoding_message_size_bytes
|
24
37
|
),
|
25
38
|
x_metadata=data.get("x-metadata", {}),
|
39
|
+
response_compression=data.get(
|
40
|
+
"response_compression", cls.response_compression
|
41
|
+
),
|
26
42
|
)
|
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
from typing import Optional
|
3
3
|
import grpc
|
4
4
|
from yellowstone_fumarole_client.config import FumaroleConfig
|
5
|
-
from yellowstone_fumarole_proto.
|
5
|
+
from yellowstone_fumarole_proto.fumarole_pb2_grpc import FumaroleStub
|
6
6
|
|
7
7
|
X_TOKEN_HEADER = "x-token"
|
8
8
|
|
@@ -31,34 +31,8 @@ class TritonAuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
|
31
31
|
return _triton_sign_request(callback, self.x_token, None)
|
32
32
|
|
33
33
|
|
34
|
-
def grpc_channel(endpoint: str, x_token=None, compression=None, *grpc_options):
|
35
|
-
options = [("grpc.max_receive_message_length", 111111110), *grpc_options]
|
36
|
-
if x_token is not None:
|
37
|
-
auth = TritonAuthMetadataPlugin(x_token)
|
38
|
-
# ssl_creds allow you to use our https endpoint
|
39
|
-
# grpc.ssl_channel_credentials with no arguments will look through your CA trust store.
|
40
|
-
ssl_creds = grpc.ssl_channel_credentials()
|
41
|
-
|
42
|
-
# call credentials will be sent on each request if setup with composite_channel_credentials.
|
43
|
-
call_creds: grpc.CallCredentials = grpc.metadata_call_credentials(auth)
|
44
|
-
|
45
|
-
# Combined creds will store the channel creds aswell as the call credentials
|
46
|
-
combined_creds = grpc.composite_channel_credentials(ssl_creds, call_creds)
|
47
|
-
|
48
|
-
return grpc.secure_channel(
|
49
|
-
endpoint,
|
50
|
-
credentials=combined_creds,
|
51
|
-
compression=compression,
|
52
|
-
options=options,
|
53
|
-
)
|
54
|
-
else:
|
55
|
-
return grpc.insecure_channel(endpoint, compression=compression, options=options)
|
56
|
-
|
57
|
-
|
58
34
|
# Because of a bug in grpcio library, multiple inheritance of ClientInterceptor subclasses does not work.
|
59
35
|
# You have to create a new class for each type of interceptor you want to use.
|
60
|
-
|
61
|
-
|
62
36
|
class MetadataInterceptor(
|
63
37
|
grpc.aio.UnaryStreamClientInterceptor,
|
64
38
|
grpc.aio.StreamUnaryClientInterceptor,
|
@@ -166,6 +140,11 @@ class FumaroleGrpcConnector:
|
|
166
140
|
async def connect(self, *grpc_options) -> FumaroleStub:
|
167
141
|
options = [("grpc.max_receive_message_length", 111111110), *grpc_options]
|
168
142
|
interceptors = MetadataInterceptor(self.config.x_metadata).interceptors()
|
143
|
+
compression = (
|
144
|
+
grpc.Compression.Gzip
|
145
|
+
if self.config.response_compression == "gzip"
|
146
|
+
else None
|
147
|
+
)
|
169
148
|
if self.config.x_token is not None:
|
170
149
|
auth = TritonAuthMetadataPlugin(self.config.x_token)
|
171
150
|
# ssl_creds allow you to use our https endpoint
|
@@ -184,6 +163,7 @@ class FumaroleGrpcConnector:
|
|
184
163
|
self.endpoint,
|
185
164
|
credentials=combined_creds,
|
186
165
|
options=options,
|
166
|
+
compression=compression,
|
187
167
|
interceptors=interceptors,
|
188
168
|
)
|
189
169
|
else:
|
@@ -191,7 +171,10 @@ class FumaroleGrpcConnector:
|
|
191
171
|
"Using insecure channel without authentication"
|
192
172
|
)
|
193
173
|
channel = grpc.aio.insecure_channel(
|
194
|
-
self.endpoint,
|
174
|
+
self.endpoint,
|
175
|
+
options=options,
|
176
|
+
interceptors=interceptors,
|
177
|
+
compression=compression,
|
195
178
|
)
|
196
179
|
|
197
180
|
return FumaroleStub(channel)
|
@@ -1,10 +1,9 @@
|
|
1
1
|
# DataPlaneConn
|
2
2
|
from abc import abstractmethod, ABC
|
3
3
|
import asyncio
|
4
|
-
import uuid
|
5
4
|
import grpc
|
6
|
-
from typing import Optional
|
7
|
-
from collections import
|
5
|
+
from typing import Optional
|
6
|
+
from collections import deque
|
8
7
|
from dataclasses import dataclass
|
9
8
|
import time
|
10
9
|
from yellowstone_fumarole_client.runtime.state_machine import (
|
@@ -19,7 +18,7 @@ from yellowstone_fumarole_proto.geyser_pb2 import (
|
|
19
18
|
SubscribeUpdateSlot,
|
20
19
|
CommitmentLevel as ProtoCommitmentLevel,
|
21
20
|
)
|
22
|
-
from yellowstone_fumarole_proto.
|
21
|
+
from yellowstone_fumarole_proto.fumarole_pb2 import (
|
23
22
|
ControlCommand,
|
24
23
|
PollBlockchainHistory,
|
25
24
|
CommitOffset,
|
@@ -27,7 +26,7 @@ from yellowstone_fumarole_proto.fumarole_v2_pb2 import (
|
|
27
26
|
DownloadBlockShard,
|
28
27
|
BlockFilters,
|
29
28
|
)
|
30
|
-
from yellowstone_fumarole_proto.
|
29
|
+
from yellowstone_fumarole_proto.fumarole_pb2_grpc import (
|
31
30
|
FumaroleStub,
|
32
31
|
)
|
33
32
|
from yellowstone_fumarole_client.utils.aio import Interval
|
@@ -84,6 +83,12 @@ class AsyncSlotDownloader(ABC):
|
|
84
83
|
pass
|
85
84
|
|
86
85
|
|
86
|
+
SUBSCRIBE_REQ_UPDATE_TYPE_MARKER: int = 1
|
87
|
+
CONTROL_PLANE_RESP_TYPE_MARKER: int = 2
|
88
|
+
COMMIT_TICK_TYPE_MARKER: int = 3
|
89
|
+
DOWNLOAD_TASK_TYPE_MARKER: int = 4
|
90
|
+
|
91
|
+
|
87
92
|
# TokioFumeDragonsmouthRuntime
|
88
93
|
class AsyncioFumeDragonsmouthRuntime:
|
89
94
|
"""Asynchronous runtime for Fumarole with Dragonsmouth-like stream support."""
|
@@ -119,17 +124,32 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
119
124
|
"""
|
120
125
|
self.sm = sm
|
121
126
|
self.slot_downloader: AsyncSlotDownloader = slot_downloader
|
122
|
-
self.
|
127
|
+
self.subscribe_request_update_rx: asyncio.Queue = subscribe_request_update_q
|
123
128
|
self.subscribe_request = subscribe_request
|
124
129
|
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
|
130
|
+
self.control_plane_tx: asyncio.Queue = control_plane_tx_q
|
131
|
+
self.control_plane_rx: asyncio.Queue = control_plane_rx_q
|
132
|
+
self.dragonsmouth_outlet: asyncio.Queue = dragonsmouth_outlet
|
128
133
|
self.commit_interval = commit_interval
|
129
134
|
self.gc_interval = gc_interval
|
130
135
|
self.max_concurrent_download = max_concurrent_download
|
136
|
+
|
137
|
+
# holds metadata about the download task
|
131
138
|
self.download_tasks = dict()
|
132
|
-
self.
|
139
|
+
self.inflight_tasks = dict()
|
140
|
+
|
141
|
+
async def __aenter__(self):
|
142
|
+
return self
|
143
|
+
|
144
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
145
|
+
await self.aclose()
|
146
|
+
|
147
|
+
async def aclose(self):
|
148
|
+
self.control_plane_tx.shutdown()
|
149
|
+
self.dragonsmouth_outlet.shutdown()
|
150
|
+
for t, kind in self.inflight_tasks.items():
|
151
|
+
LOGGER.debug(f"closing {kind} task")
|
152
|
+
t.cancel()
|
133
153
|
|
134
154
|
def _build_poll_history_cmd(
|
135
155
|
self, from_offset: Optional[FumeOffset]
|
@@ -195,8 +215,9 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
195
215
|
coro = self.slot_downloader.run_download(
|
196
216
|
self.subscribe_request, download_task_args
|
197
217
|
)
|
198
|
-
|
199
|
-
self.download_tasks[
|
218
|
+
download_task = asyncio.create_task(coro)
|
219
|
+
self.download_tasks[download_task] = download_request
|
220
|
+
self.inflight_tasks[download_task] = DOWNLOAD_TASK_TYPE_MARKER
|
200
221
|
LOGGER.debug(f"Scheduling download task for slot {download_request.slot}")
|
201
222
|
|
202
223
|
def _handle_download_result(self, download_result: DownloadTaskResult):
|
@@ -219,10 +240,10 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
219
240
|
)
|
220
241
|
|
221
242
|
async def _commit_offset(self):
|
243
|
+
self.last_commit = time.time()
|
222
244
|
if self.sm.last_committed_offset < self.sm.committable_offset:
|
223
245
|
LOGGER.debug(f"Committing offset {self.sm.committable_offset}")
|
224
246
|
await self._force_commit_offset()
|
225
|
-
self.last_commit = time.time()
|
226
247
|
|
227
248
|
async def _drain_slot_status(self):
|
228
249
|
"""Drains the slot status from the state machine and sends updates to the Dragonsmouth outlet."""
|
@@ -245,8 +266,10 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
245
266
|
matched_filters.append(filter_name)
|
246
267
|
elif not filter.filter_by_commitment:
|
247
268
|
matched_filters.append(filter_name)
|
248
|
-
|
249
269
|
if matched_filters:
|
270
|
+
LOGGER.debug(
|
271
|
+
f"Matched {len(matched_filters)} filters for SlotStatus Update"
|
272
|
+
)
|
250
273
|
update = SubscribeUpdate(
|
251
274
|
filters=matched_filters,
|
252
275
|
created_at=None,
|
@@ -257,10 +280,7 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
257
280
|
dead_error=slot_status.dead_error,
|
258
281
|
),
|
259
282
|
)
|
260
|
-
|
261
|
-
await self.dragonsmouth_outlet.put(update)
|
262
|
-
except asyncio.QueueFull:
|
263
|
-
return
|
283
|
+
await self.dragonsmouth_outlet.put(update)
|
264
284
|
|
265
285
|
self.sm.mark_event_as_processed(slot_status.session_sequence)
|
266
286
|
|
@@ -286,16 +306,19 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
286
306
|
LOGGER.debug("Initial commit offset command sent")
|
287
307
|
ticks = 0
|
288
308
|
|
289
|
-
|
309
|
+
self.inflight_tasks = {
|
310
|
+
asyncio.create_task(
|
311
|
+
self.subscribe_request_update_rx.get()
|
312
|
+
): SUBSCRIBE_REQ_UPDATE_TYPE_MARKER,
|
313
|
+
asyncio.create_task(
|
314
|
+
self.control_plane_rx.get()
|
315
|
+
): CONTROL_PLANE_RESP_TYPE_MARKER,
|
290
316
|
asyncio.create_task(
|
291
|
-
self.
|
292
|
-
):
|
293
|
-
asyncio.create_task(self.control_plane_rx.get()): "control_plane_rx",
|
294
|
-
asyncio.create_task(Interval(self.commit_interval).tick()): "commit_tick",
|
317
|
+
Interval(self.commit_interval).tick()
|
318
|
+
): COMMIT_TICK_TYPE_MARKER,
|
295
319
|
}
|
296
320
|
|
297
|
-
|
298
|
-
while pending:
|
321
|
+
while self.inflight_tasks:
|
299
322
|
ticks += 1
|
300
323
|
LOGGER.debug(f"Runtime loop tick")
|
301
324
|
if ticks % self.gc_interval == 0:
|
@@ -306,58 +329,51 @@ class AsyncioFumeDragonsmouthRuntime:
|
|
306
329
|
await self.poll_history_if_needed()
|
307
330
|
LOGGER.debug("Scheduling download tasks if any")
|
308
331
|
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
332
|
download_task_inflight = len(self.download_tasks)
|
314
333
|
LOGGER.debug(
|
315
334
|
f"Current download tasks in flight: {download_task_inflight} / {self.max_concurrent_download}"
|
316
335
|
)
|
317
|
-
done,
|
318
|
-
|
336
|
+
done, _pending = await asyncio.wait(
|
337
|
+
self.inflight_tasks.keys(), return_when=asyncio.FIRST_COMPLETED
|
319
338
|
)
|
320
339
|
for t in done:
|
321
340
|
result = t.result()
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
LOGGER.debug("Control plane
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
task_map[new_task] = "commit_tick"
|
355
|
-
pending.add(new_task)
|
356
|
-
case unknown:
|
357
|
-
raise RuntimeError(f"Unexpected task name: {unknown}")
|
341
|
+
sigcode = self.inflight_tasks.pop(t)
|
342
|
+
if sigcode == SUBSCRIBE_REQ_UPDATE_TYPE_MARKER:
|
343
|
+
LOGGER.debug("Dragonsmouth subscribe request received")
|
344
|
+
assert isinstance(
|
345
|
+
result, SubscribeRequest
|
346
|
+
), "Expected SubscribeRequest"
|
347
|
+
self.handle_new_subscribe_request(result)
|
348
|
+
new_task = asyncio.create_task(
|
349
|
+
self.subscribe_request_update_rx.get()
|
350
|
+
)
|
351
|
+
self.inflight_tasks[new_task] = SUBSCRIBE_REQ_UPDATE_TYPE_MARKER
|
352
|
+
pass
|
353
|
+
elif sigcode == CONTROL_PLANE_RESP_TYPE_MARKER:
|
354
|
+
LOGGER.debug("Control plane response received")
|
355
|
+
if not await self._handle_control_plane_resp(result):
|
356
|
+
LOGGER.debug("Control plane error")
|
357
|
+
return
|
358
|
+
new_task = asyncio.create_task(self.control_plane_rx.get())
|
359
|
+
self.inflight_tasks[new_task] = CONTROL_PLANE_RESP_TYPE_MARKER
|
360
|
+
elif sigcode == DOWNLOAD_TASK_TYPE_MARKER:
|
361
|
+
LOGGER.debug("Download task result received")
|
362
|
+
assert self.download_tasks.pop(t)
|
363
|
+
self._handle_download_result(result)
|
364
|
+
elif sigcode == COMMIT_TICK_TYPE_MARKER:
|
365
|
+
LOGGER.debug("Commit tick reached")
|
366
|
+
await self._commit_offset()
|
367
|
+
new_task = asyncio.create_task(
|
368
|
+
Interval(self.commit_interval).tick()
|
369
|
+
)
|
370
|
+
self.inflight_tasks[new_task] = COMMIT_TICK_TYPE_MARKER
|
371
|
+
else:
|
372
|
+
raise RuntimeError(f"Unexpected task name: {sigcode}")
|
358
373
|
|
359
374
|
await self._drain_slot_status()
|
360
375
|
|
376
|
+
self.aclose()
|
361
377
|
LOGGER.debug("Fumarole runtime exiting")
|
362
378
|
|
363
379
|
|
@@ -1,12 +1,11 @@
|
|
1
|
-
from typing import Optional,
|
1
|
+
from typing import Optional, Set, Deque, Sequence
|
2
2
|
from collections import deque, defaultdict
|
3
|
-
from yellowstone_fumarole_proto.
|
3
|
+
from yellowstone_fumarole_proto.fumarole_pb2 import (
|
4
4
|
CommitmentLevel,
|
5
5
|
BlockchainEvent,
|
6
6
|
)
|
7
7
|
from yellowstone_fumarole_client.utils.collections import OrderedSet
|
8
8
|
import heapq
|
9
|
-
import uuid
|
10
9
|
from enum import Enum
|
11
10
|
|
12
11
|
__all__ = [
|
{yellowstone_fumarole_client-0.1.0.dist-info → yellowstone_fumarole_client-0.2.0.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: yellowstone-fumarole-client
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Yellowstone Fumarole Python Client
|
5
5
|
Home-page: https://github.com/rpcpool/yellowstone-fumarole
|
6
6
|
Author: Louis-Vincent
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.13,<4.0
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
11
|
Requires-Dist: grpcio (>=1.71.1,<2.0.0)
|
12
|
-
Requires-Dist: protobuf (>=
|
12
|
+
Requires-Dist: protobuf (>=6.32.0,<7.0.0)
|
13
13
|
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
14
14
|
Project-URL: Repository, https://github.com/rpcpool/yellowstone-fumarole
|
15
15
|
Description-Content-Type: text/markdown
|
@@ -78,33 +78,43 @@ async def dragonsmouth_like_session(fumarole_config):
|
|
78
78
|
session = await client.dragonsmouth_subscribe(
|
79
79
|
consumer_group_name="test",
|
80
80
|
request=SubscribeRequest(
|
81
|
-
|
81
|
+
accounts={"fumarole": SubscribeRequestFilterAccounts()},
|
82
82
|
transactions={"fumarole": SubscribeRequestFilterTransactions()},
|
83
83
|
blocks_meta={"fumarole": SubscribeRequestFilterBlocksMeta()},
|
84
84
|
entry={"fumarole": SubscribeRequestFilterEntry()},
|
85
85
|
slots={"fumarole": SubscribeRequestFilterSlots()},
|
86
86
|
),
|
87
87
|
)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
88
|
+
async with session:
|
89
|
+
dragonsmouth_like_source = session.source
|
90
|
+
# result: SubscribeUpdate
|
91
|
+
async for result in dragonsmouth_like_source:
|
92
|
+
if result.HasField("block_meta"):
|
93
|
+
block_meta: SubscribeUpdateBlockMeta = result.block_meta
|
94
|
+
elif result.HasField("transaction"):
|
95
|
+
tx: SubscribeUpdateTransaction = result.transaction
|
96
|
+
elif result.HasField("account"):
|
97
|
+
account: SubscribeUpdateAccount = result.account
|
98
|
+
elif result.HasField("entry"):
|
99
|
+
entry: SubscribeUpdateEntry = result.entry
|
100
|
+
elif result.HasField("slot"):
|
101
|
+
result: SubscribeUpdateSlot = result.slot
|
102
|
+
|
103
|
+
# OUTSIDE THE SCOPE, YOU SHOULD NEVER USE `session` again.
|
104
|
+
```
|
105
|
+
|
106
|
+
|
107
|
+
At any point you can get a rough estimate if you are progression through the slot using `DragonsmouthAdapterSession.stats()` call:
|
108
|
+
|
109
|
+
```python
|
110
|
+
|
111
|
+
async with session:
|
112
|
+
stats: FumaroleSubscribeStats = session.stats()
|
113
|
+
print(f"{stats.log_committed_offset}, {stats.log_committable_offset}, {stats.max_slot_seen}")
|
110
114
|
```
|
115
|
+
|
116
|
+
`log_committed_offset` : what have been ACK so for to fumarole remote service.
|
117
|
+
`log_committable_offset` : what can be ACK to next commit call.
|
118
|
+
`max_slot_seen` : maximum slot seen in the inner fumarole client state -- not yet processed by your code.
|
119
|
+
|
120
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
yellowstone_fumarole_client/__init__.py,sha256=-UQcnKGG7D7cTcMMXE98y8nIqc-Xqm0qXA3R0iQ6uks,13381
|
2
|
+
yellowstone_fumarole_client/config.py,sha256=aclhCPUy6RO-xtXR9w8otmt1RzFZyFnbF28jk115C2g,1394
|
3
|
+
yellowstone_fumarole_client/grpc_connectivity.py,sha256=Sex_x6_Bha0wGD7rRqr-V_slsohX1tDFeiHdqahLJ4Q,6639
|
4
|
+
yellowstone_fumarole_client/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
yellowstone_fumarole_client/runtime/aio.py,sha256=ydcfIP-FpQ4Cgd5drd35Zm9fd_g6qEhpKkdPQHO7gJM,21364
|
6
|
+
yellowstone_fumarole_client/runtime/state_machine.py,sha256=d4blPv62UcqZ0HigRK2IXTd_8MeRrRjhclDVpE0PMnQ,12634
|
7
|
+
yellowstone_fumarole_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
yellowstone_fumarole_client/utils/aio.py,sha256=lm_BNkPiw5CJ6FjDlQUPoCKAqY3eYFAedAJB8mhNbzE,639
|
9
|
+
yellowstone_fumarole_client/utils/collections.py,sha256=BO0kADUKIRkpQ-fRpBtmn5rA7Xu4P4MkJ2rsU2FxfBc,979
|
10
|
+
yellowstone_fumarole_proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
yellowstone_fumarole_proto/fumarole_pb2.py,sha256=aF869yXAjiTE5hqpKatzql42N1saAO0ayVQOJJ8sxVs,14315
|
12
|
+
yellowstone_fumarole_proto/fumarole_pb2.pyi,sha256=eJgwihIU5wzJVlEZvYlG_F0vaW-z-axONwOoCQtnI-s,20092
|
13
|
+
yellowstone_fumarole_proto/fumarole_pb2_grpc.py,sha256=t-d3wKHd8xbHjct7h0IVmeFN3PTP7WPinYwxgDs6mO0,19117
|
14
|
+
yellowstone_fumarole_proto/geyser_pb2.py,sha256=95QrgJGuWll2t7XQiLunY20a9prhK4F1tclzXIQS0I0,20169
|
15
|
+
yellowstone_fumarole_proto/geyser_pb2.pyi,sha256=7P9cFmkhK05NhTBlPtzPitzXZeAS_kGBbt6teu2PyGk,27187
|
16
|
+
yellowstone_fumarole_proto/geyser_pb2_grpc.py,sha256=JCEz0KM_jg_610HyQI_F1K4kJlRGkGsP192XEDTCoYM,15342
|
17
|
+
yellowstone_fumarole_proto/solana_storage_pb2.py,sha256=LS-P5EPyS0n1pO9_U73rA6SPlbGSTEC2qYhuS3skzA8,8443
|
18
|
+
yellowstone_fumarole_proto/solana_storage_pb2.pyi,sha256=HivhoN4VEe_W7kB4lc2Un5AeTAv3xiKR_HLI096qmyA,13040
|
19
|
+
yellowstone_fumarole_proto/solana_storage_pb2_grpc.py,sha256=-rb9Dr0HXohIrHrnxukYrmUYl6OCVbkQYh2-pCO6740,895
|
20
|
+
yellowstone_fumarole_client-0.2.0.dist-info/METADATA,sha256=P90eNzQ2clW8LALfQZfvye9h5YtxZYB1sEdwIo-7uQk,4156
|
21
|
+
yellowstone_fumarole_client-0.2.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
22
|
+
yellowstone_fumarole_client-0.2.0.dist-info/RECORD,,
|