yellowstone-fumarole-client 0.1.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.1.0/PKG-INFO +110 -0
- yellowstone_fumarole_client-0.1.0/README.md +94 -0
- yellowstone_fumarole_client-0.1.0/pyproject.toml +32 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/__init__.py +297 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/config.py +26 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/grpc_connectivity.py +197 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/runtime/__init__.py +0 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/runtime/aio.py +525 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/runtime/state_machine.py +326 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/utils/__init__.py +0 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/utils/aio.py +29 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_client/utils/collections.py +37 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/__init__.py +0 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/fumarole_v2_pb2.py +122 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/fumarole_v2_pb2.pyi +328 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/fumarole_v2_pb2_grpc.py +400 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/geyser_pb2.py +144 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/geyser_pb2.pyi +501 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/geyser_pb2_grpc.py +355 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/solana_storage_pb2.py +75 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/solana_storage_pb2.pyi +238 -0
- yellowstone_fumarole_client-0.1.0/yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +24 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: yellowstone-fumarole-client
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Yellowstone Fumarole Python Client
|
5
|
+
Home-page: https://github.com/rpcpool/yellowstone-fumarole
|
6
|
+
Author: Louis-Vincent
|
7
|
+
Author-email: louis-vincent@triton.one
|
8
|
+
Requires-Python: >=3.13,<4.0
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
11
|
+
Requires-Dist: grpcio (>=1.71.1,<2.0.0)
|
12
|
+
Requires-Dist: protobuf (>=5.29.1,<6.0.0)
|
13
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
14
|
+
Project-URL: Repository, https://github.com/rpcpool/yellowstone-fumarole
|
15
|
+
Description-Content-Type: text/markdown
|
16
|
+
|
17
|
+
# Fumarole Python SDK
|
18
|
+
|
19
|
+
This module contains Fumarole SDK for `python` programming language.
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
```yaml
|
24
|
+
endpoint: <"https://fumarole.endpoint.rpcpool.com">
|
25
|
+
x-token: <YOUR X-TOKEN secret here>
|
26
|
+
```
|
27
|
+
|
28
|
+
## Manage consumer group
|
29
|
+
|
30
|
+
Refer to [fume CLI](https://crates.io/crates/yellowstone-fumarole-cli) to manage your consumer groups.
|
31
|
+
|
32
|
+
## Examples
|
33
|
+
|
34
|
+
```python
|
35
|
+
|
36
|
+
from typing import Optional
|
37
|
+
import uuid
|
38
|
+
import asyncio
|
39
|
+
import logging
|
40
|
+
from os import environ
|
41
|
+
from collections import defaultdict
|
42
|
+
from yellowstone_fumarole_client.config import FumaroleConfig
|
43
|
+
from yellowstone_fumarole_client import FumaroleClient
|
44
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2 import CreateConsumerGroupRequest
|
45
|
+
from yellowstone_fumarole_proto.geyser_pb2 import (
|
46
|
+
SubscribeRequest,
|
47
|
+
SubscribeRequestFilterAccounts,
|
48
|
+
SubscribeRequestFilterTransactions,
|
49
|
+
SubscribeRequestFilterBlocksMeta,
|
50
|
+
SubscribeRequestFilterEntry,
|
51
|
+
SubscribeRequestFilterSlots,
|
52
|
+
)
|
53
|
+
from yellowstone_fumarole_proto.geyser_pb2 import (
|
54
|
+
SubscribeUpdate,
|
55
|
+
SubscribeUpdateTransaction,
|
56
|
+
SubscribeUpdateBlockMeta,
|
57
|
+
SubscribeUpdateAccount,
|
58
|
+
SubscribeUpdateEntry,
|
59
|
+
SubscribeUpdateSlot,
|
60
|
+
)
|
61
|
+
|
62
|
+
async def dragonsmouth_like_session(fumarole_config):
|
63
|
+
with open("~/.fumarole/config.yaml") as f:
|
64
|
+
fumarole_config = FumaroleConfig.from_yaml(f)
|
65
|
+
|
66
|
+
client: FumaroleClient = await FumaroleClient.connect(fumarole_config)
|
67
|
+
await client.delete_all_consumer_groups()
|
68
|
+
|
69
|
+
# --- This is optional ---
|
70
|
+
resp = await client.create_consumer_group(
|
71
|
+
CreateConsumerGroupRequest(
|
72
|
+
consumer_group_name="test",
|
73
|
+
)
|
74
|
+
)
|
75
|
+
assert resp.consumer_group_id, "Failed to create consumer group"
|
76
|
+
# --- END OF OPTIONAL BLOCK ---
|
77
|
+
|
78
|
+
session = await client.dragonsmouth_subscribe(
|
79
|
+
consumer_group_name="test",
|
80
|
+
request=SubscribeRequest(
|
81
|
+
# accounts={"fumarole": SubscribeRequestFilterAccounts()},
|
82
|
+
transactions={"fumarole": SubscribeRequestFilterTransactions()},
|
83
|
+
blocks_meta={"fumarole": SubscribeRequestFilterBlocksMeta()},
|
84
|
+
entry={"fumarole": SubscribeRequestFilterEntry()},
|
85
|
+
slots={"fumarole": SubscribeRequestFilterSlots()},
|
86
|
+
),
|
87
|
+
)
|
88
|
+
dragonsmouth_source = session.source
|
89
|
+
handle = session.fumarole_handle
|
90
|
+
block_map = defaultdict(BlockConstruction)
|
91
|
+
while True:
|
92
|
+
tasks = [asyncio.create_task(dragonsmouth_source.get()), handle]
|
93
|
+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
94
|
+
for t in done:
|
95
|
+
if tasks[0] == t:
|
96
|
+
result: SubscribeUpdate = t.result()
|
97
|
+
if result.HasField("block_meta"):
|
98
|
+
block_meta: SubscribeUpdateBlockMeta = result.block_meta
|
99
|
+
elif result.HasField("transaction"):
|
100
|
+
tx: SubscribeUpdateTransaction = result.transaction
|
101
|
+
elif result.HasField("account"):
|
102
|
+
account: SubscribeUpdateAccount = result.account
|
103
|
+
elif result.HasField("entry"):
|
104
|
+
entry: SubscribeUpdateEntry = result.entry
|
105
|
+
elif result.HasField("slot"):
|
106
|
+
result: SubscribeUpdateSlot = result.slot
|
107
|
+
else:
|
108
|
+
result = t.result()
|
109
|
+
raise RuntimeError("failed to get dragonsmouth source: %s" % result)
|
110
|
+
```
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Fumarole Python SDK
|
2
|
+
|
3
|
+
This module contains Fumarole SDK for `python` programming language.
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
```yaml
|
8
|
+
endpoint: <"https://fumarole.endpoint.rpcpool.com">
|
9
|
+
x-token: <YOUR X-TOKEN secret here>
|
10
|
+
```
|
11
|
+
|
12
|
+
## Manage consumer group
|
13
|
+
|
14
|
+
Refer to [fume CLI](https://crates.io/crates/yellowstone-fumarole-cli) to manage your consumer groups.
|
15
|
+
|
16
|
+
## Examples
|
17
|
+
|
18
|
+
```python
|
19
|
+
|
20
|
+
from typing import Optional
|
21
|
+
import uuid
|
22
|
+
import asyncio
|
23
|
+
import logging
|
24
|
+
from os import environ
|
25
|
+
from collections import defaultdict
|
26
|
+
from yellowstone_fumarole_client.config import FumaroleConfig
|
27
|
+
from yellowstone_fumarole_client import FumaroleClient
|
28
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2 import CreateConsumerGroupRequest
|
29
|
+
from yellowstone_fumarole_proto.geyser_pb2 import (
|
30
|
+
SubscribeRequest,
|
31
|
+
SubscribeRequestFilterAccounts,
|
32
|
+
SubscribeRequestFilterTransactions,
|
33
|
+
SubscribeRequestFilterBlocksMeta,
|
34
|
+
SubscribeRequestFilterEntry,
|
35
|
+
SubscribeRequestFilterSlots,
|
36
|
+
)
|
37
|
+
from yellowstone_fumarole_proto.geyser_pb2 import (
|
38
|
+
SubscribeUpdate,
|
39
|
+
SubscribeUpdateTransaction,
|
40
|
+
SubscribeUpdateBlockMeta,
|
41
|
+
SubscribeUpdateAccount,
|
42
|
+
SubscribeUpdateEntry,
|
43
|
+
SubscribeUpdateSlot,
|
44
|
+
)
|
45
|
+
|
46
|
+
async def dragonsmouth_like_session(fumarole_config):
|
47
|
+
with open("~/.fumarole/config.yaml") as f:
|
48
|
+
fumarole_config = FumaroleConfig.from_yaml(f)
|
49
|
+
|
50
|
+
client: FumaroleClient = await FumaroleClient.connect(fumarole_config)
|
51
|
+
await client.delete_all_consumer_groups()
|
52
|
+
|
53
|
+
# --- This is optional ---
|
54
|
+
resp = await client.create_consumer_group(
|
55
|
+
CreateConsumerGroupRequest(
|
56
|
+
consumer_group_name="test",
|
57
|
+
)
|
58
|
+
)
|
59
|
+
assert resp.consumer_group_id, "Failed to create consumer group"
|
60
|
+
# --- END OF OPTIONAL BLOCK ---
|
61
|
+
|
62
|
+
session = await client.dragonsmouth_subscribe(
|
63
|
+
consumer_group_name="test",
|
64
|
+
request=SubscribeRequest(
|
65
|
+
# accounts={"fumarole": SubscribeRequestFilterAccounts()},
|
66
|
+
transactions={"fumarole": SubscribeRequestFilterTransactions()},
|
67
|
+
blocks_meta={"fumarole": SubscribeRequestFilterBlocksMeta()},
|
68
|
+
entry={"fumarole": SubscribeRequestFilterEntry()},
|
69
|
+
slots={"fumarole": SubscribeRequestFilterSlots()},
|
70
|
+
),
|
71
|
+
)
|
72
|
+
dragonsmouth_source = session.source
|
73
|
+
handle = session.fumarole_handle
|
74
|
+
block_map = defaultdict(BlockConstruction)
|
75
|
+
while True:
|
76
|
+
tasks = [asyncio.create_task(dragonsmouth_source.get()), handle]
|
77
|
+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
78
|
+
for t in done:
|
79
|
+
if tasks[0] == t:
|
80
|
+
result: SubscribeUpdate = t.result()
|
81
|
+
if result.HasField("block_meta"):
|
82
|
+
block_meta: SubscribeUpdateBlockMeta = result.block_meta
|
83
|
+
elif result.HasField("transaction"):
|
84
|
+
tx: SubscribeUpdateTransaction = result.transaction
|
85
|
+
elif result.HasField("account"):
|
86
|
+
account: SubscribeUpdateAccount = result.account
|
87
|
+
elif result.HasField("entry"):
|
88
|
+
entry: SubscribeUpdateEntry = result.entry
|
89
|
+
elif result.HasField("slot"):
|
90
|
+
result: SubscribeUpdateSlot = result.slot
|
91
|
+
else:
|
92
|
+
result = t.result()
|
93
|
+
raise RuntimeError("failed to get dragonsmouth source: %s" % result)
|
94
|
+
```
|
@@ -0,0 +1,32 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "yellowstone-fumarole-client"
|
3
|
+
version = "0.1.0"
|
4
|
+
homepage = "https://github.com/rpcpool/yellowstone-fumarole"
|
5
|
+
repository = "https://github.com/rpcpool/yellowstone-fumarole"
|
6
|
+
description = "Yellowstone Fumarole Python Client"
|
7
|
+
authors = ["Louis-Vincent <louis-vincent@triton.one>", "Triton One <help@triton.one>"]
|
8
|
+
readme = "README.md"
|
9
|
+
|
10
|
+
packages = [
|
11
|
+
{ include = "yellowstone_fumarole_proto" },
|
12
|
+
{ include = "yellowstone_fumarole_client" },
|
13
|
+
]
|
14
|
+
|
15
|
+
[tool.poetry.dependencies]
|
16
|
+
python = "^3.13"
|
17
|
+
grpcio = "^1.71.1"
|
18
|
+
protobuf = "^5.29.1"
|
19
|
+
pyyaml = "^6.0.2"
|
20
|
+
|
21
|
+
[tool.poetry.group.test.dependencies]
|
22
|
+
pytest = "^8.3.4"
|
23
|
+
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
25
|
+
grpcio-tools = "^1.68.1"
|
26
|
+
black = "^24.10.0"
|
27
|
+
pytest-asyncio = "^0.26.0"
|
28
|
+
deptry = "^0.23.1"
|
29
|
+
|
30
|
+
[build-system]
|
31
|
+
requires = ["poetry-core"]
|
32
|
+
build-backend = "poetry.core.masonry.api"
|
@@ -0,0 +1,297 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from yellowstone_fumarole_client.grpc_connectivity import (
|
4
|
+
FumaroleGrpcConnector,
|
5
|
+
)
|
6
|
+
from typing import Dict, Optional
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from yellowstone_fumarole_client.config import FumaroleConfig
|
9
|
+
from yellowstone_fumarole_client.runtime.aio import (
|
10
|
+
AsyncioFumeDragonsmouthRuntime,
|
11
|
+
FumaroleSM,
|
12
|
+
DEFAULT_GC_INTERVAL,
|
13
|
+
DEFAULT_SLOT_MEMORY_RETENTION,
|
14
|
+
GrpcSlotDownloader,
|
15
|
+
)
|
16
|
+
from yellowstone_fumarole_proto.geyser_pb2 import SubscribeRequest, SubscribeUpdate
|
17
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2 import (
|
18
|
+
ControlResponse,
|
19
|
+
VersionRequest,
|
20
|
+
VersionResponse,
|
21
|
+
JoinControlPlane,
|
22
|
+
ControlCommand,
|
23
|
+
ListConsumerGroupsRequest,
|
24
|
+
ListConsumerGroupsResponse,
|
25
|
+
GetConsumerGroupInfoRequest,
|
26
|
+
ConsumerGroupInfo,
|
27
|
+
DeleteConsumerGroupRequest,
|
28
|
+
DeleteConsumerGroupResponse,
|
29
|
+
CreateConsumerGroupRequest,
|
30
|
+
CreateConsumerGroupResponse,
|
31
|
+
)
|
32
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2_grpc import FumaroleStub
|
33
|
+
import grpc
|
34
|
+
|
35
|
+
__all__ = [
|
36
|
+
"FumaroleClient",
|
37
|
+
"FumaroleConfig",
|
38
|
+
"FumaroleSubscribeConfig",
|
39
|
+
"DragonsmouthAdapterSession",
|
40
|
+
"DEFAULT_DRAGONSMOUTH_CAPACITY",
|
41
|
+
"DEFAULT_COMMIT_INTERVAL",
|
42
|
+
"DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT",
|
43
|
+
"DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP",
|
44
|
+
]
|
45
|
+
|
46
|
+
# Constants
|
47
|
+
DEFAULT_DRAGONSMOUTH_CAPACITY = 10000
|
48
|
+
DEFAULT_COMMIT_INTERVAL = 5.0 # seconds
|
49
|
+
DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT = 3
|
50
|
+
DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP = 10
|
51
|
+
|
52
|
+
# Error classes
|
53
|
+
|
54
|
+
|
55
|
+
# FumaroleSubscribeConfig
|
56
|
+
@dataclass
|
57
|
+
class FumaroleSubscribeConfig:
|
58
|
+
"""Configuration for subscribing to a dragonsmouth stream."""
|
59
|
+
|
60
|
+
# The maximum number of concurrent download tasks per TCP connection.
|
61
|
+
concurrent_download_limit: int = DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP
|
62
|
+
|
63
|
+
# The interval at which to commit the slot memory.
|
64
|
+
commit_interval: float = DEFAULT_COMMIT_INTERVAL
|
65
|
+
|
66
|
+
# The maximum number of failed slot download attempts before giving up.
|
67
|
+
max_failed_slot_download_attempt: int = DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT
|
68
|
+
|
69
|
+
# The maximum number of slots to download concurrently.
|
70
|
+
data_channel_capacity: int = DEFAULT_DRAGONSMOUTH_CAPACITY
|
71
|
+
|
72
|
+
# The interval at which to perform garbage collection on the slot memory.
|
73
|
+
gc_interval: int = DEFAULT_GC_INTERVAL
|
74
|
+
|
75
|
+
# The retention period for slot memory in seconds.
|
76
|
+
slot_memory_retention: int = DEFAULT_SLOT_MEMORY_RETENTION
|
77
|
+
|
78
|
+
|
79
|
+
# DragonsmouthAdapterSession
|
80
|
+
@dataclass
|
81
|
+
class DragonsmouthAdapterSession:
|
82
|
+
"""Session for interacting with the dragonsmouth-like stream."""
|
83
|
+
|
84
|
+
# The queue for sending SubscribeRequest update to the dragonsmouth stream.
|
85
|
+
sink: asyncio.Queue
|
86
|
+
|
87
|
+
# The queue for receiving SubscribeUpdate from the dragonsmouth stream.
|
88
|
+
source: asyncio.Queue
|
89
|
+
|
90
|
+
# The task handle for the fumarole runtime.
|
91
|
+
fumarole_handle: asyncio.Task
|
92
|
+
|
93
|
+
|
94
|
+
# FumaroleClient
|
95
|
+
class FumaroleClient:
|
96
|
+
"""Fumarole client for interacting with the Fumarole server."""
|
97
|
+
|
98
|
+
logger = logging.getLogger(__name__)
|
99
|
+
|
100
|
+
def __init__(self, connector: FumaroleGrpcConnector, stub: FumaroleStub):
|
101
|
+
self.connector = connector
|
102
|
+
self.stub = stub
|
103
|
+
|
104
|
+
@staticmethod
|
105
|
+
async def connect(config: config.FumaroleConfig) -> "FumaroleClient":
|
106
|
+
"""Connect to the Fumarole server using the provided configuration.
|
107
|
+
Args:
|
108
|
+
config (FumaroleConfig): Configuration for the Fumarole client.
|
109
|
+
"""
|
110
|
+
endpoint = config.endpoint
|
111
|
+
connector = FumaroleGrpcConnector(config=config, endpoint=endpoint)
|
112
|
+
FumaroleClient.logger.debug(f"Connecting to {endpoint}")
|
113
|
+
client = await connector.connect()
|
114
|
+
FumaroleClient.logger.debug(f"Connected to {endpoint}")
|
115
|
+
return FumaroleClient(connector=connector, stub=client)
|
116
|
+
|
117
|
+
async def version(self) -> VersionResponse:
|
118
|
+
"""Get the version of the Fumarole server."""
|
119
|
+
request = VersionRequest()
|
120
|
+
response = await self.stub.Version(request)
|
121
|
+
return response
|
122
|
+
|
123
|
+
async def dragonsmouth_subscribe(
|
124
|
+
self, consumer_group_name: str, request: SubscribeRequest
|
125
|
+
) -> DragonsmouthAdapterSession:
|
126
|
+
"""Subscribe to a dragonsmouth stream with default configuration.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
consumer_group_name (str): The name of the consumer group.
|
130
|
+
request (SubscribeRequest): The request to subscribe to the dragonsmouth stream.
|
131
|
+
"""
|
132
|
+
return await self.dragonsmouth_subscribe_with_config(
|
133
|
+
consumer_group_name, request, FumaroleSubscribeConfig()
|
134
|
+
)
|
135
|
+
|
136
|
+
async def dragonsmouth_subscribe_with_config(
|
137
|
+
self,
|
138
|
+
consumer_group_name: str,
|
139
|
+
request: SubscribeRequest,
|
140
|
+
config: FumaroleSubscribeConfig,
|
141
|
+
) -> DragonsmouthAdapterSession:
|
142
|
+
"""Subscribe to a dragonsmouth stream with custom configuration.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
consumer_group_name (str): The name of the consumer group.
|
146
|
+
request (SubscribeRequest): The request to subscribe to the dragonsmouth stream.
|
147
|
+
config (FumaroleSubscribeConfig): The configuration for the dragonsmouth subscription.
|
148
|
+
"""
|
149
|
+
dragonsmouth_outlet = asyncio.Queue(maxsize=config.data_channel_capacity)
|
150
|
+
fume_control_plane_q = asyncio.Queue(maxsize=100)
|
151
|
+
|
152
|
+
initial_join = JoinControlPlane(consumer_group_name=consumer_group_name)
|
153
|
+
initial_join_command = ControlCommand(initial_join=initial_join)
|
154
|
+
await fume_control_plane_q.put(initial_join_command)
|
155
|
+
|
156
|
+
FumaroleClient.logger.debug(
|
157
|
+
f"Sent initial join command: {initial_join_command}"
|
158
|
+
)
|
159
|
+
|
160
|
+
async def control_plane_sink():
|
161
|
+
while True:
|
162
|
+
try:
|
163
|
+
update = await fume_control_plane_q.get()
|
164
|
+
yield update
|
165
|
+
except asyncio.QueueShutDown:
|
166
|
+
break
|
167
|
+
|
168
|
+
fume_control_plane_stream_rx: grpc.aio.StreamStreamCall = self.stub.Subscribe(
|
169
|
+
control_plane_sink()
|
170
|
+
) # it's actually InterceptedStreamStreamCall, but grpc lib doesn't export it
|
171
|
+
|
172
|
+
control_response: ControlResponse = await fume_control_plane_stream_rx.read()
|
173
|
+
init = control_response.init
|
174
|
+
if init is None:
|
175
|
+
raise ValueError(f"Unexpected initial response: {control_response}")
|
176
|
+
|
177
|
+
# Once we have the initial response, we can spin a task to read from the stream
|
178
|
+
# and put the updates into the queue.
|
179
|
+
# This is a bit of a hack, but we need a Queue not a StreamStreamMultiCallable
|
180
|
+
# because Queue are cancel-safe, while Stream are not, or at least didn't find any docs about it.
|
181
|
+
fume_control_plane_rx_q = asyncio.Queue(maxsize=100)
|
182
|
+
|
183
|
+
async def control_plane_source():
|
184
|
+
while True:
|
185
|
+
try:
|
186
|
+
async for update in fume_control_plane_stream_rx:
|
187
|
+
await fume_control_plane_rx_q.put(update)
|
188
|
+
except asyncio.QueueShutDown:
|
189
|
+
break
|
190
|
+
|
191
|
+
_cp_src_task = asyncio.create_task(control_plane_source())
|
192
|
+
|
193
|
+
FumaroleClient.logger.debug(f"Control response: {control_response}")
|
194
|
+
|
195
|
+
last_committed_offset = init.last_committed_offsets.get(0)
|
196
|
+
if last_committed_offset is None:
|
197
|
+
raise ValueError("No last committed offset")
|
198
|
+
|
199
|
+
sm = FumaroleSM(last_committed_offset, config.slot_memory_retention)
|
200
|
+
subscribe_request_queue = asyncio.Queue(maxsize=100)
|
201
|
+
|
202
|
+
data_plane_client = await self.connector.connect()
|
203
|
+
|
204
|
+
grpc_slot_downloader = GrpcSlotDownloader(
|
205
|
+
client=data_plane_client,
|
206
|
+
)
|
207
|
+
|
208
|
+
rt = AsyncioFumeDragonsmouthRuntime(
|
209
|
+
sm=sm,
|
210
|
+
slot_downloader=grpc_slot_downloader,
|
211
|
+
subscribe_request_update_q=subscribe_request_queue,
|
212
|
+
subscribe_request=request,
|
213
|
+
consumer_group_name=consumer_group_name,
|
214
|
+
control_plane_tx_q=fume_control_plane_q,
|
215
|
+
control_plane_rx_q=fume_control_plane_rx_q,
|
216
|
+
dragonsmouth_outlet=dragonsmouth_outlet,
|
217
|
+
commit_interval=config.commit_interval,
|
218
|
+
gc_interval=config.gc_interval,
|
219
|
+
max_concurrent_download=config.concurrent_download_limit,
|
220
|
+
)
|
221
|
+
|
222
|
+
fumarole_handle = asyncio.create_task(rt.run())
|
223
|
+
FumaroleClient.logger.debug(f"Fumarole handle created: {fumarole_handle}")
|
224
|
+
return DragonsmouthAdapterSession(
|
225
|
+
sink=subscribe_request_queue,
|
226
|
+
source=dragonsmouth_outlet,
|
227
|
+
fumarole_handle=fumarole_handle,
|
228
|
+
)
|
229
|
+
|
230
|
+
async def list_consumer_groups(
|
231
|
+
self,
|
232
|
+
) -> ListConsumerGroupsResponse:
|
233
|
+
"""Lists all consumer groups."""
|
234
|
+
return await self.stub.ListConsumerGroups(ListConsumerGroupsRequest())
|
235
|
+
|
236
|
+
async def get_consumer_group_info(
|
237
|
+
self, consumer_group_name: str
|
238
|
+
) -> Optional[ConsumerGroupInfo]:
|
239
|
+
"""Gets information about a consumer group by name.
|
240
|
+
Returns None if the consumer group does not exist.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
consumer_group_name (str): The name of the consumer group to retrieve information for.
|
244
|
+
"""
|
245
|
+
try:
|
246
|
+
return await self.stub.GetConsumerGroupInfo(
|
247
|
+
GetConsumerGroupInfoRequest(consumer_group_name=consumer_group_name)
|
248
|
+
)
|
249
|
+
except grpc.aio.AioRpcError as e:
|
250
|
+
if e.code() == grpc.StatusCode.NOT_FOUND:
|
251
|
+
return None
|
252
|
+
else:
|
253
|
+
raise
|
254
|
+
|
255
|
+
async def delete_consumer_group(
|
256
|
+
self, consumer_group_name: str
|
257
|
+
) -> DeleteConsumerGroupResponse:
|
258
|
+
"""Delete a consumer group by name.
|
259
|
+
|
260
|
+
NOTE: this operation is idempotent, meaning that if the consumer group does not exist, it will not raise an error.
|
261
|
+
Args:
|
262
|
+
consumer_group_name (str): The name of the consumer group to delete.
|
263
|
+
"""
|
264
|
+
return await self.stub.DeleteConsumerGroup(
|
265
|
+
DeleteConsumerGroupRequest(consumer_group_name=consumer_group_name)
|
266
|
+
)
|
267
|
+
|
268
|
+
async def delete_all_consumer_groups(
|
269
|
+
self,
|
270
|
+
) -> DeleteConsumerGroupResponse:
|
271
|
+
"""Deletes all consumer groups."""
|
272
|
+
consumer_group_list = await self.list_consumer_groups()
|
273
|
+
|
274
|
+
tasks = []
|
275
|
+
|
276
|
+
async with asyncio.TaskGroup() as tg:
|
277
|
+
for group in consumer_group_list.consumer_groups:
|
278
|
+
cg_name = group.consumer_group_name
|
279
|
+
task = tg.create_task(self.delete_consumer_group(cg_name))
|
280
|
+
tasks.append((cg_name, task))
|
281
|
+
|
282
|
+
# Raise an error if any task fails
|
283
|
+
for cg_name, task in tasks:
|
284
|
+
result = task.result()
|
285
|
+
if not result.success:
|
286
|
+
raise RuntimeError(
|
287
|
+
f"Failed to delete consumer group {cg_name}: {result.error}"
|
288
|
+
)
|
289
|
+
|
290
|
+
async def create_consumer_group(
|
291
|
+
self, request: CreateConsumerGroupRequest
|
292
|
+
) -> CreateConsumerGroupResponse:
|
293
|
+
"""Creates a new consumer group.
|
294
|
+
Args:
|
295
|
+
request (CreateConsumerGroupRequest): The request to create a consumer group.
|
296
|
+
"""
|
297
|
+
return await self.stub.CreateConsumerGroup(request)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Dict, Optional
|
3
|
+
import yaml
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class FumaroleConfig:
|
8
|
+
endpoint: str
|
9
|
+
x_token: Optional[str] = None
|
10
|
+
max_decoding_message_size_bytes: int = 512_000_000
|
11
|
+
x_metadata: Dict[str, str] = None
|
12
|
+
|
13
|
+
def __post_init__(self):
|
14
|
+
self.x_metadata = self.x_metadata or {}
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
def from_yaml(cls, fileobj) -> "FumaroleConfig":
|
18
|
+
data = yaml.safe_load(fileobj)
|
19
|
+
return cls(
|
20
|
+
endpoint=data["endpoint"],
|
21
|
+
x_token=data.get("x-token") or data.get("x_token"),
|
22
|
+
max_decoding_message_size_bytes=data.get(
|
23
|
+
"max_decoding_message_size_bytes", cls.max_decoding_message_size_bytes
|
24
|
+
),
|
25
|
+
x_metadata=data.get("x-metadata", {}),
|
26
|
+
)
|